From 4eb5712e36e9214ff18a930f531f4bc2731ce361 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 21 Jun 2026 22:31:37 +0100 Subject: [PATCH] Move equipped item projection into runtime --- Aetheria.State.Verify/Program.cs | 63 ++- Assets/Scripts/UI/Menu/InventoryMenu.cs | 321 +---------- ...untimeEquippedItemDetailsSurfaceBuilder.cs | 509 ++++++++++++++++++ 3 files changed, 584 insertions(+), 309 deletions(-) diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 1769f1888..8e554ee77 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -4289,8 +4289,8 @@ static void RequireInventoryCargoItemDetailsUseEveSurface(string root) if (!source.Contains("ResolveInventorySurfaceStateRef", StringComparison.Ordinal) || !source.Contains("AetheriaRuntimeDaemonItemStatQueries.TryReadItemStatRef(", StringComparison.Ordinal) || - !source.Contains("AetheriaRuntimeDaemonItemStatQueries.ItemStatRef(", StringComparison.Ordinal) || !source.Contains("AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat(", StringComparison.Ordinal) || + !cargoItemSurfaceBuilder.Contains("AetheriaRuntimeDaemonItemStatQueries.ItemStatRef(", StringComparison.Ordinal) || source.Contains("private static bool TryReadItemStatRef(", StringComparison.Ordinal) || source.Contains("DecodeRefToken", StringComparison.Ordinal) || source.Contains("GameManager.ItemManager.GetTier", StringComparison.Ordinal) || @@ -4419,10 +4419,10 @@ static void RequireInventoryEquippedItemDetailsUseEveSurface(string root) "HandleEquippedItemDetailsSurfaceCommand(", "AetheriaEveUnitySurfaceHost.RenderRuntime(", "AetheriaEveUnitySurfaceHost.Hide(_equippedItemDetailsSurfaceDocument)", - "AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.Build(ProjectEquippedItemDetailsSurfaceState(", - "ProjectEquippedItemDetailsSurfaceState(", + "AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.Build(ProjectEquippedItemDetailsSurface(", + "AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.Project(", + "ProjectEquippedItemObservation(", "ProjectEquippedItemTemperatureControls(", - "ProjectEquippedItemBehaviorSections(", "ProjectEquippedItemWeaponGroupControls(", "ProjectEquippedItemActionBarSlots(", "AetheriaRuntimeEquippedItemDetailsSurfaceCommands.TryRead(request, out var command)", @@ -4464,6 +4464,12 @@ static void RequireInventoryEquippedItemDetailsUseEveSurface(string root) "ResolveEquippedItemDetailsSurfaceDocument(", "new EveUiToolkitSurfaceLowerer()", "host.AddComponent", + "new AetheriaRuntimeEquippedItemDetailsSurfaceState(", + "ProjectEquippedItemDetailsSurfaceState(", + "ProjectEquippedItemBehaviorSections(", + "ProjectEquippedItemBehaviorMetric(", + "new AetheriaRuntimeEquippedItemSection(", + "new AetheriaRuntimeEquippedItemMetric(", "private static EveSurfaceComponent Card(", "private static EveSurfaceComponent Metric(", "private static EveSurfaceComponent Text(", @@ -4516,6 +4522,7 @@ static void RequireInventoryEquippedItemDetailsUseEveSurface(string root) var requiredBuilderSymbols = new[] { "public sealed class AetheriaRuntimeEquippedItemDetailsSurfaceState", + "public sealed class AetheriaRuntimeEquippedItemObservation", "public sealed class AetheriaRuntimeEquippedItemSection", "public sealed class AetheriaRuntimeEquippedItemMetric", "public sealed class AetheriaRuntimeEquippedItemControl", @@ -4532,6 +4539,10 @@ static void RequireInventoryEquippedItemDetailsUseEveSurface(string root) "public const string ToggleWeaponGroup = \"aetheria.inventory.equipped_item_details.weapon_group.toggle\"", "public const string BindWeaponGroup = \"aetheria.inventory.equipped_item_details.weapon_group.bind\"", "public const string ClearActionBarBinding = \"aetheria.inventory.equipped_item_details.action_bar.clear\"", + "public static AetheriaRuntimeEquippedItemDetailsSurfaceState Project(", + "ProjectBehaviorSections(", + "ProjectBehaviorMetric(", + "AetheriaRuntimeDaemonItemStatQueries.ItemStatRef(", "public static AetheriaRuntimeSurfaceDocument Build(", "new AetheriaRuntimeSurfaceCommandTemplate(Close", "new AetheriaRuntimeSurfaceCommandTemplate(ToggleOverrideShutdown", @@ -9505,6 +9516,33 @@ static void RequireTypedBehaviorMetadataCoverage(string root) var tradeMenu = File.Exists(tradeMenuPath) ? File.ReadAllText(tradeMenuPath) : throw new InvalidOperationException("Cannot verify typed behavior metadata coverage; TradeMenu.cs is missing."); + var cargoBuilderPath = Path.Combine( + root, + "Packages", + "org.gamecult.aetheria.state", + "Runtime", + "AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs"); + var equippedBuilderPath = Path.Combine( + root, + "Packages", + "org.gamecult.aetheria.state", + "Runtime", + "AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs"); + var tradeItemBuilderPath = Path.Combine( + root, + "Packages", + "org.gamecult.aetheria.state", + "Runtime", + "AetheriaRuntimeTradeItemDetailsSurfaceBuilder.cs"); + var cargoBuilder = File.Exists(cargoBuilderPath) + ? File.ReadAllText(cargoBuilderPath) + : throw new InvalidOperationException("Cannot verify typed behavior metadata coverage; cargo item details builder is missing."); + var equippedBuilder = File.Exists(equippedBuilderPath) + ? File.ReadAllText(equippedBuilderPath) + : throw new InvalidOperationException("Cannot verify typed behavior metadata coverage; equipped item details builder is missing."); + var tradeItemBuilder = File.Exists(tradeItemBuilderPath) + ? File.ReadAllText(tradeItemBuilderPath) + : throw new InvalidOperationException("Cannot verify typed behavior metadata coverage; trade item details builder is missing."); var requiredMetadataSymbols = new[] { @@ -9534,25 +9572,34 @@ static void RequireTypedBehaviorMetadataCoverage(string root) "FormatTemperature" }; + var inventoryBehaviorProjection = cargoBuilder + "\n" + equippedBuilder; var missingInventorySymbols = requiredUiSymbols - .Where(symbol => !inventoryMenu.Contains(symbol, StringComparison.Ordinal)) + .Where(symbol => !inventoryBehaviorProjection.Contains(symbol, StringComparison.Ordinal)) .ToArray(); if (missingInventorySymbols.Length > 0) { throw new InvalidOperationException( - "InventoryMenu no longer renders typed temperature-bearing behavior metadata: " + + "Inventory item surface builders no longer render typed temperature-bearing behavior metadata: " + string.Join(", ", missingInventorySymbols)); } var missingTradeSymbols = requiredUiSymbols - .Where(symbol => !tradeMenu.Contains(symbol, StringComparison.Ordinal)) + .Where(symbol => !tradeItemBuilder.Contains(symbol, StringComparison.Ordinal)) .ToArray(); if (missingTradeSymbols.Length > 0) { throw new InvalidOperationException( - "TradeMenu no longer renders typed temperature-bearing behavior metadata: " + + "Trade item surface builder no longer renders typed temperature-bearing behavior metadata: " + string.Join(", ", missingTradeSymbols)); } + + if (inventoryMenu.Contains("ProjectCargoItemBehaviorMetric(", StringComparison.Ordinal) || + inventoryMenu.Contains("ProjectEquippedItemBehaviorMetric(", StringComparison.Ordinal) || + tradeMenu.Contains("ProjectTradeItemBehaviorMetric(", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "Unity menus must not own typed behavior metric projection after runtime surface builders take that role."); + } } static void RequireNameToolsUsesUiToolkit(string root) diff --git a/Assets/Scripts/UI/Menu/InventoryMenu.cs b/Assets/Scripts/UI/Menu/InventoryMenu.cs index d7e4d6b7a..b8236b67e 100644 --- a/Assets/Scripts/UI/Menu/InventoryMenu.cs +++ b/Assets/Scripts/UI/Menu/InventoryMenu.cs @@ -301,7 +301,7 @@ public class InventoryMenu : MonoBehaviour transform, _equippedItemDetailsSurfaceDocument, "Aetheria Inventory Equipped Item Details Surface", - AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.Build(ProjectEquippedItemDetailsSurfaceState(item, typedItem)), + AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.Build(ProjectEquippedItemDetailsSurface(item, typedItem)), HandleEquippedItemDetailsSurfaceCommand, _equippedItemDetailsSurfaceChrome, ResolveInventorySurfaceStateRef, @@ -407,30 +407,33 @@ public class InventoryMenu : MonoBehaviour }; } - private AetheriaRuntimeEquippedItemDetailsSurfaceState ProjectEquippedItemDetailsSurfaceState( + private AetheriaRuntimeEquippedItemDetailsSurfaceState ProjectEquippedItemDetailsSurface( EquippedItem item, AetheriaRuntimeCatalogItem typedItem) { - var durability = item.EquippableItem.Durability < .01f - ? "Item Destroyed!" - : $"{(int)(item.EquippableItem.Durability / GetMaxDurability(typedItem, item.EquippableItem) * 100)}%"; var hasWeapon = item.GetBehavior() != null; - return new AetheriaRuntimeEquippedItemDetailsSurfaceState( + return AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.Project( + typedItem, + ProjectEquippedItemObservation(item), BuildEquippedItemTitle(item, typedItem), - typedItem.Description ?? "", ActionGameManager.RuntimeCatalog?.GetManufacturer(typedItem)?.Name ?? "GameCult", - ActionGameManager.RuntimePlayerSettings.Format((float)typedItem.Mass), - durability, - ActionGameManager.RuntimePlayerSettings.FormatTemperature(item.Temperature), - FormatTemperatureRange(typedItem), - item.EquippableItem.OverrideShutdown ? "Enabled" : "Disabled", - item.EquippableItem.OverrideShutdown ? "Disable Override" : "Enable Override", + ActionGameManager.RuntimePlayerSettings.Format, + ActionGameManager.RuntimePlayerSettings.FormatTemperature, ProjectEquippedItemTemperatureControls(item).ToArray(), - ProjectEquippedItemBehaviorSections(typedItem, item.EquippableItem).ToArray(), hasWeapon ? ProjectEquippedItemWeaponGroupControls(item).ToArray() : Array.Empty(), - hasWeapon ? ProjectEquippedItemActionBarSlots(item).ToArray() : Array.Empty(), - DateTime.UtcNow.ToString("O")); + hasWeapon ? ProjectEquippedItemActionBarSlots(item).ToArray() : Array.Empty()); + } + + private static AetheriaRuntimeEquippedItemObservation ProjectEquippedItemObservation(EquippedItem item) + { + var equippableItem = item?.EquippableItem; + return new AetheriaRuntimeEquippedItemObservation( + equippableItem?.ItemKey ?? "", + equippableItem?.Quality ?? 1, + equippableItem?.Durability ?? 1, + item?.Temperature ?? 0, + equippableItem != null && equippableItem.OverrideShutdown); } private IEnumerable ProjectEquippedItemTemperatureControls( @@ -520,98 +523,7 @@ public class InventoryMenu : MonoBehaviour equippableItem != null && equippableItem.OverrideShutdown); } - private IEnumerable ProjectEquippedItemBehaviorSections( - AetheriaRuntimeCatalogItem typedItem, - EquippableItem equippableItem) - { - foreach (var behavior in typedItem.BehaviorPayloads ?? Array.Empty()) - { - if (string.Equals(behavior.Kind, AetheriaRuntimeBehaviorKinds.StatModifier, StringComparison.Ordinal)) - { - var statReference = AetheriaRuntimeBehaviorValueReader.ReadStatReference(FindTypedBehaviorField(behavior, 1)?.Value); - var modifier = AetheriaRuntimeBehaviorValueReader.ReadPerformanceStat(FindTypedBehaviorField(behavior, 2)?.Value); - var modifierType = AetheriaRuntimeBehaviorValueReader.ReadEnum( - FindTypedBehaviorField(behavior, 3)?.Value, - StatModifierType.Constant); - yield return new AetheriaRuntimeEquippedItemSection( - $"{AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.SurfaceId}.behavior.{behavior.Kind}.stat_modifier", - "Stat Modifier", - new[] - { - new AetheriaRuntimeEquippedItemMetric( - $"{AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.SurfaceId}.behavior.{behavior.Kind}.target", - $"{statReference.Target.SplitCamelCase()}:{statReference.Stat.SplitCamelCase()}", - $"{(modifierType == StatModifierType.Constant ? "+" : "x")}{FormatCurrentItemStat(modifier, equippableItem)}", - AetheriaRuntimeDaemonItemStatQueries.ItemStatRef( - equippableItem.ItemKey, - behavior.Kind, - behavior.Group, - 2)) - }); - continue; - } - - var metadata = AetheriaRuntimeBehaviorMetadataCatalog.Get(behavior.Kind); - if (metadata == null) - continue; - - var fields = metadata.DisplayFields - .Select(field => ProjectEquippedItemBehaviorMetric(behavior, field, equippableItem)) - .Where(metric => metric != null) - .ToArray(); - - if (fields.Length == 0) - continue; - - yield return new AetheriaRuntimeEquippedItemSection( - $"{AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.SurfaceId}.behavior.{behavior.Kind}", - behavior.Kind.FormatTypeName(), - fields); - } - } - - private AetheriaRuntimeEquippedItemMetric ProjectEquippedItemBehaviorMetric( - AetheriaRuntimeBehaviorPayload behavior, - AetheriaRuntimeBehaviorFieldMetadata field, - EquippableItem equippableItem) - { - var payloadField = FindTypedBehaviorField(behavior, field.Key); - if (payloadField == null) - return null; - - string value; - switch (field.ValueKind) - { - case AetheriaRuntimeBehaviorFieldValueKind.Number: - value = ActionGameManager.RuntimePlayerSettings.Format((float)payloadField.Value.NumberValue); - break; - case AetheriaRuntimeBehaviorFieldValueKind.Temperature: - value = ActionGameManager.RuntimePlayerSettings.FormatTemperature((float)payloadField.Value.NumberValue); - break; - case AetheriaRuntimeBehaviorFieldValueKind.Integer: - value = ((int)payloadField.Value.NumberValue).ToString(); - break; - case AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat: - value = FormatCurrentItemStat(payloadField.Value, equippableItem); - break; - default: - return null; - } - - return new AetheriaRuntimeEquippedItemMetric( - $"{AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.SurfaceId}.behavior.{behavior.Kind}.{field.Key}", - field.Name.SplitCamelCase(), - value, - field.ValueKind == AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat - ? AetheriaRuntimeDaemonItemStatQueries.ItemStatRef( - equippableItem.ItemKey, - behavior.Kind, - behavior.Group, - field.Key) - : ""); - } - -private string BuildEquippedItemTitle(EquippedItem item, AetheriaRuntimeCatalogItem typedItem) + private string BuildEquippedItemTitle(EquippedItem item, AetheriaRuntimeCatalogItem typedItem) { return $"{typedItem?.Name ?? "Unknown Item"} ({FormatItemTier(typedItem, item.EquippableItem)})"; } @@ -627,11 +539,6 @@ private string BuildEquippedItemTitle(EquippedItem item, AetheriaRuntimeCatalogI : ""; } - private static string FormatCurrentItemStat(PerformanceStat stat, EquippableItem item) - { - return FormatCurrentItemStat(ToRuntimeBehaviorValue(stat), item); - } - private static string FormatCurrentItemStat(AetheriaRuntimeBehaviorValue stat, EquippableItem item) { return ActionGameManager.RuntimePlayerSettings.Format( @@ -696,157 +603,6 @@ private string BuildEquippedItemTitle(EquippedItem item, AetheriaRuntimeCatalogI string.Equals(item.ItemKey ?? "", itemKey ?? "", StringComparison.Ordinal); } - private static AetheriaRuntimeBehaviorValue ToRuntimeBehaviorValue(PerformanceStat stat) - { - return new AetheriaRuntimeBehaviorValue( - "performance-stat", - "", - 0, - false, - "", - "", - new[] - { - Number(stat?.Min ?? 0), - Number(stat?.Max ?? 0), - Number(stat?.HeatExponentMultiplier ?? 0), - Number(stat?.DurabilityExponentMultiplier ?? 0), - Number(stat?.QualityExponent ?? 0), - ToRuntimeStatRecipeValue(stat?.Recipe) - }, - Array.Empty()); - } - - private static AetheriaRuntimeBehaviorValue ToRuntimeStatRecipeValue(StatRecipe recipe) - { - if (recipe == null) - return Number(0); - - return new AetheriaRuntimeBehaviorValue( - "stat-recipe", - "", - 0, - false, - "", - "", - new[] - { - Number(recipe.BaseValue), - new AetheriaRuntimeBehaviorValue( - "stat-recipe-modifiers", - "", - 0, - false, - "", - "", - (recipe.Modifiers ?? Array.Empty()) - .Where(modifier => modifier != null) - .Select(ToRuntimeStatRecipeModifierValue) - .ToArray(), - Array.Empty()) - }, - Array.Empty()); - } - - private static AetheriaRuntimeBehaviorValue ToRuntimeStatRecipeModifierValue(StatRecipeModifier modifier) - { - return new AetheriaRuntimeBehaviorValue( - "stat-recipe-modifier", - "", - 0, - false, - "", - "", - new[] - { - Text(ConditionToken(modifier.Condition)), - Text(OperationToken(modifier.Operation)), - Number(modifier.Amount), - Number(0), - Bool(modifier.Enabled) - }, - Array.Empty()); - } - - private static string ConditionToken(StatConditionMask condition) - { - switch (condition) - { - case StatConditionMask.Quality: - return AetheriaRuntimeStatRecipeConditions.Quality; - case StatConditionMask.Durability: - return AetheriaRuntimeStatRecipeConditions.Durability; - case StatConditionMask.Heat: - return AetheriaRuntimeStatRecipeConditions.Heat; - case StatConditionMask.Charge: - return AetheriaRuntimeStatRecipeConditions.Charge; - case StatConditionMask.Ammo: - return AetheriaRuntimeStatRecipeConditions.Ammo; - case StatConditionMask.Range: - return AetheriaRuntimeStatRecipeConditions.Range; - case StatConditionMask.Integrity: - return AetheriaRuntimeStatRecipeConditions.Integrity; - case StatConditionMask.PilotSkill: - return AetheriaRuntimeStatRecipeConditions.PilotSkill; - case StatConditionMask.Environment: - return AetheriaRuntimeStatRecipeConditions.Environment; - default: - return ""; - } - } - - private static string OperationToken(StatModifierOperation operation) - { - switch (operation) - { - case StatModifierOperation.Multiply: - return AetheriaRuntimeStatRecipeOperations.Multiply; - case StatModifierOperation.Override: - return AetheriaRuntimeStatRecipeOperations.Override; - default: - return AetheriaRuntimeStatRecipeOperations.Add; - } - } - - private static AetheriaRuntimeBehaviorValue Number(double value) - { - return new AetheriaRuntimeBehaviorValue( - "number", - "", - value, - false, - "", - "", - Array.Empty(), - Array.Empty()); - } - - private static AetheriaRuntimeBehaviorValue Text(string value) - { - return new AetheriaRuntimeBehaviorValue( - "string", - value ?? "", - 0, - false, - "", - "", - Array.Empty(), - Array.Empty()); - } - - private static AetheriaRuntimeBehaviorValue Bool(bool value) - { - return new AetheriaRuntimeBehaviorValue( - "bool", - "", - 0, - value, - "", - "", - Array.Empty(), - Array.Empty()); - } - private static bool IsWeaponGroupAssigned(EquippedItem item, int groupIndex) { return item?.Entity?.WeaponGroups != null && @@ -935,43 +691,6 @@ private string BuildEquippedItemTitle(EquippedItem item, AetheriaRuntimeCatalogI return ActionGameManager.RuntimeCatalog?.FindItem(item?.ItemKey ?? ""); } - private static float GetCargoItemMass(ItemInstance item, AetheriaRuntimeCatalogItem typedItem) - { - if (typedItem == null) - return 0f; - - return item is SimpleCommodity simpleCommodity - ? (float)typedItem.Mass * simpleCommodity.Quantity - : (float)typedItem.Mass; - } - - private static float GetMaxDurability(AetheriaRuntimeCatalogItem typedItem, EquippableItem item) - { - if (typedItem != null && typedItem.Durability > 0) - return (float)typedItem.Durability; - - return Math.Max(item?.Durability ?? 1f, 1f); - } - - private static string FormatTemperatureRange(AetheriaRuntimeCatalogItem item) - { - if (item.MaximumTemperature > item.MinimumTemperature) - { - return - $"{ActionGameManager.RuntimePlayerSettings.FormatTemperature((float)item.MinimumTemperature)} to " + - $"{ActionGameManager.RuntimePlayerSettings.FormatTemperature((float)item.MaximumTemperature)}"; - } - - return "No typed thermal range"; - } - - private static AetheriaRuntimeBehaviorField FindTypedBehaviorField(AetheriaRuntimeBehaviorPayload behavior, int? key) - { - return key == null - ? null - : behavior.Fields.FirstOrDefault(field => field.Key == key.Value); - } - private static int2 RotateTypedShapeCell( AetheriaRuntimeShapeCell cell, AetheriaRuntimeCatalogItem item, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs index 9644f07eb..d498a7d73 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs @@ -156,6 +156,29 @@ namespace GameCult.Aetheria.State.Verse public bool HasWeaponControls => WeaponGroupControls.Count > 0; } + public sealed class AetheriaRuntimeEquippedItemObservation + { + public AetheriaRuntimeEquippedItemObservation( + string itemKey, + double quality, + double durability, + double temperature, + bool overrideShutdown) + { + ItemKey = itemKey ?? ""; + Quality = quality; + Durability = durability; + Temperature = temperature; + OverrideShutdown = overrideShutdown; + } + + public string ItemKey { get; } + public double Quality { get; } + public double Durability { get; } + public double Temperature { get; } + public bool OverrideShutdown { get; } + } + public static class AetheriaRuntimeEquippedItemDetailsSurfaceBuilder { public const string SurfaceId = "aetheria.inventory.equipped_item_details"; @@ -166,6 +189,61 @@ namespace GameCult.Aetheria.State.Verse public const string BindWeaponGroup = "aetheria.inventory.equipped_item_details.weapon_group.bind"; public const string ClearActionBarBinding = "aetheria.inventory.equipped_item_details.action_bar.clear"; + public static AetheriaRuntimeEquippedItemDetailsSurfaceState Project( + AetheriaRuntimeCatalogItem typedItem, + AetheriaRuntimeEquippedItemObservation item, + string title, + string manufacturer, + Func formatValue, + Func formatTemperature, + IReadOnlyList temperatureControls, + IReadOnlyList weaponGroupControls, + IReadOnlyList actionBarSlots, + DateTime updatedAtUtc = default(DateTime)) + { + if (updatedAtUtc == default(DateTime)) + updatedAtUtc = DateTime.UtcNow; + + if (typedItem == null) + { + return new AetheriaRuntimeEquippedItemDetailsSurfaceState( + title ?? "", + "", + manufacturer ?? "", + "", + "", + "", + "", + "", + "", + temperatureControls ?? Array.Empty(), + Array.Empty(), + weaponGroupControls ?? Array.Empty(), + actionBarSlots ?? Array.Empty(), + updatedAtUtc.ToString("O", CultureInfo.InvariantCulture)); + } + + var durability = item != null && item.Durability < .01 + ? "Item Destroyed!" + : $"{(int)((item?.Durability ?? 1) / MaxDurability(typedItem, item) * 100)}%"; + + return new AetheriaRuntimeEquippedItemDetailsSurfaceState( + title ?? typedItem.Name, + typedItem.Description ?? "", + manufacturer ?? "", + FormatValue(typedItem.Mass, formatValue), + durability, + FormatTemperature(item?.Temperature ?? 0, formatTemperature), + FormatTemperatureRange(typedItem, formatTemperature), + item != null && item.OverrideShutdown ? "Enabled" : "Disabled", + item != null && item.OverrideShutdown ? "Disable Override" : "Enable Override", + temperatureControls ?? Array.Empty(), + ProjectBehaviorSections(typedItem, item, formatValue, formatTemperature).ToArray(), + weaponGroupControls ?? Array.Empty(), + actionBarSlots ?? Array.Empty(), + updatedAtUtc.ToString("O", CultureInfo.InvariantCulture)); + } + public static AetheriaRuntimeSurfaceDocument Build( AetheriaRuntimeEquippedItemDetailsSurfaceState state, long version = 1) @@ -329,6 +407,419 @@ namespace GameCult.Aetheria.State.Verse .ToArray()).Trim('-'); } + private static IEnumerable ProjectBehaviorSections( + AetheriaRuntimeCatalogItem typedItem, + AetheriaRuntimeEquippedItemObservation item, + Func formatValue, + Func formatTemperature) + { + foreach (var behavior in typedItem.BehaviorPayloads ?? Array.Empty()) + { + if (string.Equals(behavior.Kind, AetheriaRuntimeBehaviorKinds.StatModifier, StringComparison.Ordinal)) + { + var statReference = ReadStatReference(FindTypedBehaviorField(behavior, 1)?.Value); + var modifier = ReadPerformanceStat(FindTypedBehaviorField(behavior, 2)?.Value); + var modifierType = ReadEnum( + FindTypedBehaviorField(behavior, 3)?.Value, + AetheriaRuntimeEquippedItemStatModifierType.Constant); + yield return new AetheriaRuntimeEquippedItemSection( + $"{SurfaceId}.behavior.{behavior.Kind}.stat_modifier", + "Stat Modifier", + new[] + { + new AetheriaRuntimeEquippedItemMetric( + $"{SurfaceId}.behavior.{behavior.Kind}.target", + $"{SplitCamelCase(statReference.Target)}:{SplitCamelCase(statReference.Stat)}", + $"{(modifierType == AetheriaRuntimeEquippedItemStatModifierType.Constant ? "+" : "x")}{FormatCurrentItemStat(modifier, item, formatValue)}", + AetheriaRuntimeDaemonItemStatQueries.ItemStatRef( + item?.ItemKey ?? "", + behavior.Kind, + behavior.Group, + 2)) + }); + continue; + } + + var metadata = AetheriaRuntimeBehaviorMetadataCatalog.Get(behavior.Kind); + if (metadata == null) + continue; + + var fields = metadata.DisplayFields + .Select(field => ProjectBehaviorMetric(behavior, field, item, formatValue, formatTemperature)) + .Where(metric => metric != null) + .ToArray(); + + if (fields.Length == 0) + continue; + + yield return new AetheriaRuntimeEquippedItemSection( + $"{SurfaceId}.behavior.{behavior.Kind}", + FormatTypeName(behavior.Kind), + fields); + } + } + + private static AetheriaRuntimeEquippedItemMetric ProjectBehaviorMetric( + AetheriaRuntimeBehaviorPayload behavior, + AetheriaRuntimeBehaviorFieldMetadata field, + AetheriaRuntimeEquippedItemObservation item, + Func formatValue, + Func formatTemperature) + { + var payloadField = FindTypedBehaviorField(behavior, field.Key); + if (payloadField == null) + return null; + + string value; + switch (field.ValueKind) + { + case AetheriaRuntimeBehaviorFieldValueKind.Number: + value = FormatValue(payloadField.Value.NumberValue, formatValue); + break; + case AetheriaRuntimeBehaviorFieldValueKind.Temperature: + value = FormatTemperature(payloadField.Value.NumberValue, formatTemperature); + break; + case AetheriaRuntimeBehaviorFieldValueKind.Integer: + value = ((int)payloadField.Value.NumberValue).ToString(CultureInfo.InvariantCulture); + break; + case AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat: + value = FormatCurrentItemStat(payloadField.Value, item, formatValue); + break; + default: + return null; + } + + return new AetheriaRuntimeEquippedItemMetric( + $"{SurfaceId}.behavior.{behavior.Kind}.{field.Key}", + SplitCamelCase(field.Name), + value, + field.ValueKind == AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat + ? AetheriaRuntimeDaemonItemStatQueries.ItemStatRef( + item?.ItemKey ?? "", + behavior.Kind, + behavior.Group, + field.Key) + : ""); + } + + private static string FormatCurrentItemStat( + AetheriaRuntimePerformanceStat stat, + AetheriaRuntimeEquippedItemObservation item, + Func formatValue) + { + return FormatValue( + AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat( + ToBehaviorValue(stat), + ToLoadoutItem(item), + item?.Temperature ?? 0), + formatValue); + } + + private static string FormatCurrentItemStat( + AetheriaRuntimeBehaviorValue stat, + AetheriaRuntimeEquippedItemObservation item, + Func formatValue) + { + return FormatValue( + AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat( + stat, + ToLoadoutItem(item), + item?.Temperature ?? 0), + formatValue); + } + + private static AetheriaRuntimeLoadoutItemCommit ToLoadoutItem(AetheriaRuntimeEquippedItemObservation item) + { + return AetheriaRuntimeDaemonItemStatQueries.ItemCommit( + item?.ItemKey ?? "", + item?.Quality ?? 1, + item?.Durability ?? 1, + enabled: true, + overrideShutdown: item != null && item.OverrideShutdown); + } + + private static double MaxDurability( + AetheriaRuntimeCatalogItem typedItem, + AetheriaRuntimeEquippedItemObservation item) + { + if (typedItem != null && typedItem.Durability > 0) + return typedItem.Durability; + + return Math.Max(item?.Durability ?? 1, 1); + } + + private static string FormatTemperatureRange( + AetheriaRuntimeCatalogItem item, + Func formatTemperature) + { + if (item.MaximumTemperature > item.MinimumTemperature) + { + return + $"{FormatTemperature(item.MinimumTemperature, formatTemperature)} to " + + $"{FormatTemperature(item.MaximumTemperature, formatTemperature)}"; + } + + return "No typed thermal range"; + } + + private static AetheriaRuntimeBehaviorField FindTypedBehaviorField( + AetheriaRuntimeBehaviorPayload behavior, + int? key) + { + return key == null + ? null + : behavior?.Fields?.FirstOrDefault(field => field.Key == key.Value); + } + + private static AetheriaRuntimePerformanceStat ReadPerformanceStat(AetheriaRuntimeBehaviorValue value) + { + return new AetheriaRuntimePerformanceStat( + ChildNumber(value, 0), + ChildNumber(value, 1), + ChildNumber(value, 2), + ChildNumber(value, 3), + ChildNumber(value, 4), + ReadStatRecipe(ChildValue(value, 5))); + } + + private static AetheriaRuntimeStatRecipe ReadStatRecipe(AetheriaRuntimeBehaviorValue value) + { + if (value == null || value.Children.Count == 0) + return null; + + var modifiers = ChildValue(value, 1)?.Children + .Select(ReadStatRecipeModifier) + .Where(modifier => modifier != null) + .ToArray() ?? Array.Empty(); + + return new AetheriaRuntimeStatRecipe(ChildNumber(value, 0), modifiers); + } + + private static AetheriaRuntimeStatRecipeModifier ReadStatRecipeModifier(AetheriaRuntimeBehaviorValue value) + { + if (value == null) + return null; + + return new AetheriaRuntimeStatRecipeModifier( + ChildString(value, 0), + ChildString(value, 1), + ChildNumber(value, 2), + ReadCurveKeys(ChildValue(value, 3)), + value.Children.Count <= 4 || ChildValue(value, 4)?.BoolValue == true); + } + + private static IReadOnlyList ReadCurveKeys(AetheriaRuntimeBehaviorValue value) + { + if (value?.Children == null || value.Children.Count == 0) + return Array.Empty(); + + return value.Children + .Where(key => key.Children.Count >= 4) + .Select(key => new AetheriaRuntimeCurveKey( + ChildNumber(key, 0), + ChildNumber(key, 1), + ChildNumber(key, 2), + ChildNumber(key, 3))) + .ToArray(); + } + + private static AetheriaRuntimeEquippedItemStatReference ReadStatReference(AetheriaRuntimeBehaviorValue value) + { + return new AetheriaRuntimeEquippedItemStatReference( + ChildString(value, 1), + ChildString(value, 2)); + } + + private static T ReadEnum(AetheriaRuntimeBehaviorValue value, T fallback) where T : struct + { + if (!string.IsNullOrWhiteSpace(value?.StringValue) && Enum.TryParse(value.StringValue, true, out T parsed)) + return parsed; + + return value != null && Enum.IsDefined(typeof(T), (int)value.NumberValue) + ? (T)Enum.ToObject(typeof(T), (int)value.NumberValue) + : fallback; + } + + private static AetheriaRuntimeBehaviorValue ToBehaviorValue(AetheriaRuntimePerformanceStat stat) + { + return new AetheriaRuntimeBehaviorValue( + "performance-stat", + "", + 0, + false, + "", + "", + new[] + { + Number(stat?.Min ?? 0), + Number(stat?.Max ?? 0), + Number(stat?.HeatExponentMultiplier ?? 0), + Number(stat?.DurabilityExponentMultiplier ?? 0), + Number(stat?.QualityExponent ?? 0), + StatRecipeValue(stat?.Recipe) + }, + EmptyMapEntries()); + } + + private static AetheriaRuntimeBehaviorValue StatRecipeValue(AetheriaRuntimeStatRecipe recipe) + { + if (recipe == null) + return EmptyValue("stat-recipe"); + + return new AetheriaRuntimeBehaviorValue( + "stat-recipe", + "", + 0, + false, + "", + "", + new[] + { + Number(recipe.BaseValue), + new AetheriaRuntimeBehaviorValue( + "stat-recipe-modifiers", + "", + 0, + false, + "", + "", + (recipe.Modifiers ?? Array.Empty()) + .Select(StatRecipeModifierValue) + .ToArray(), + EmptyMapEntries()) + }, + EmptyMapEntries()); + } + + private static AetheriaRuntimeBehaviorValue StatRecipeModifierValue(AetheriaRuntimeStatRecipeModifier modifier) + { + if (modifier == null) + return EmptyValue("stat-recipe-modifier"); + + return new AetheriaRuntimeBehaviorValue( + "stat-recipe-modifier", + "", + 0, + false, + "", + "", + new[] + { + TextValue(modifier.Condition), + TextValue(modifier.Operation), + Number(modifier.Amount), + CurveValue(modifier.CurveKeys), + BoolValue(modifier.Enabled) + }, + EmptyMapEntries()); + } + + private static AetheriaRuntimeBehaviorValue CurveValue(IReadOnlyList keys) + { + return new AetheriaRuntimeBehaviorValue( + "curve", + "", + 0, + false, + "", + "", + (keys ?? Array.Empty()) + .Select(key => new AetheriaRuntimeBehaviorValue( + "curve-key", + "", + 0, + false, + "", + "", + new[] + { + Number(key.Time), + Number(key.Value), + Number(key.InTangent), + Number(key.OutTangent) + }, + EmptyMapEntries())) + .ToArray(), + EmptyMapEntries()); + } + + private static AetheriaRuntimeBehaviorValue Number(double value) + { + return new AetheriaRuntimeBehaviorValue("", "", value, false, "", "", Array.Empty(), EmptyMapEntries()); + } + + private static AetheriaRuntimeBehaviorValue TextValue(string value) + { + return new AetheriaRuntimeBehaviorValue("", value ?? "", 0, false, "", "", Array.Empty(), EmptyMapEntries()); + } + + private static AetheriaRuntimeBehaviorValue BoolValue(bool value) + { + return new AetheriaRuntimeBehaviorValue("", "", 0, value, "", "", Array.Empty(), EmptyMapEntries()); + } + + private static AetheriaRuntimeBehaviorValue EmptyValue(string kind) + { + return new AetheriaRuntimeBehaviorValue(kind ?? "", "", 0, false, "", "", Array.Empty(), EmptyMapEntries()); + } + + private static IReadOnlyList EmptyMapEntries() + { + return Array.Empty(); + } + + private static double ChildNumber(AetheriaRuntimeBehaviorValue value, int index) + { + return value != null && value.Children.Count > index ? value.Children[index].NumberValue : 0; + } + + private static string ChildString(AetheriaRuntimeBehaviorValue value, int index) + { + return value != null && value.Children.Count > index ? value.Children[index].StringValue ?? "" : ""; + } + + private static AetheriaRuntimeBehaviorValue ChildValue(AetheriaRuntimeBehaviorValue value, int index) + { + return value != null && value.Children.Count > index ? value.Children[index] : null; + } + + private static string FormatValue( + double value, + Func formatValue) + { + return formatValue == null + ? value.ToString("0.###", CultureInfo.InvariantCulture) + : formatValue((float)value); + } + + private static string FormatTemperature( + double value, + Func formatTemperature) + { + return formatTemperature == null + ? value.ToString("0.###", CultureInfo.InvariantCulture) + : formatTemperature((float)value); + } + + private static string FormatTypeName(string typeName) + { + if (string.IsNullOrEmpty(typeName)) + return ""; + + return SplitCamelCase(typeName.StartsWith("I", StringComparison.Ordinal) && typeName.Length > 1 + ? typeName.Substring(1) + : typeName); + } + + private static string SplitCamelCase(string value) + { + if (string.IsNullOrEmpty(value)) + return ""; + + return string.Concat(value.Select((character, index) => + index > 0 && char.IsUpper(character) ? " " + character : character.ToString())); + } + private static AetheriaRuntimeSurfaceComponent Card( string id, string title, @@ -441,6 +932,24 @@ namespace GameCult.Aetheria.State.Verse ClearActionBarBinding = 6 } + internal enum AetheriaRuntimeEquippedItemStatModifierType + { + Constant = 0, + Multiplier = 1 + } + + internal readonly struct AetheriaRuntimeEquippedItemStatReference + { + public AetheriaRuntimeEquippedItemStatReference(string target, string stat) + { + Target = target ?? ""; + Stat = stat ?? ""; + } + + public string Target { get; } + public string Stat { get; } + } + public readonly struct AetheriaRuntimeEquippedItemDetailsCommand { public AetheriaRuntimeEquippedItemDetailsCommand(