From c2bbdff76eaffbe3a8f31a86fc6fd00239291d03 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 21 Jun 2026 22:20:21 +0100 Subject: [PATCH] Move cargo item projection into runtime --- Aetheria.State.Verify/Program.cs | 25 +- Assets/Scripts/UI/Menu/InventoryMenu.cs | 133 +---- GameCult.Aetheria.State.Unity.csproj | 1 + ...iaRuntimeCargoItemDetailsSurfaceBuilder.cs | 525 ++++++++++++++++++ 4 files changed, 562 insertions(+), 122 deletions(-) diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 3336c6006..1769f1888 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -4225,6 +4225,7 @@ static void RequireInventoryCargoItemDetailsUseEveSurface(string root) "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs"); + var unityProjectPath = Path.Combine(root, "GameCult.Aetheria.State.Unity.csproj"); var requiredFiles = new[] { inventoryMenuPath, @@ -4234,7 +4235,8 @@ static void RequireInventoryCargoItemDetailsUseEveSurface(string root) eveUnitySurfaceHostPath, runtimeEveSurfaceAdapterPath, cargoItemSurfaceBuilderPath, - equippedItemSurfaceBuilderPath + equippedItemSurfaceBuilderPath, + unityProjectPath }; var missingFiles = requiredFiles .Where(path => !File.Exists(path)) @@ -4251,6 +4253,7 @@ static void RequireInventoryCargoItemDetailsUseEveSurface(string root) var actionGameManager = File.ReadAllText(actionGameManagerPath); var surfaceDocument = File.ReadAllText(surfaceDocumentPath); var daemonItemStatQueries = File.ReadAllText(daemonItemStatQueriesPath); + var unityProject = File.ReadAllText(unityProjectPath); var eveUnitySurfaceHost = File.ReadAllText(eveUnitySurfaceHostPath); var runtimeEveSurfaceAdapter = File.ReadAllText(runtimeEveSurfaceAdapterPath); var cargoItemSurfaceBuilder = File.ReadAllText(cargoItemSurfaceBuilderPath); @@ -4261,12 +4264,11 @@ static void RequireInventoryCargoItemDetailsUseEveSurface(string root) "HandleCargoItemDetailsSurfaceCommand(", "AetheriaEveUnitySurfaceHost.RenderRuntime(", "AetheriaEveUnitySurfaceHost.Hide(_cargoItemDetailsSurfaceDocument)", - "AetheriaRuntimeCargoItemDetailsSurfaceBuilder.Build(ProjectCargoItemDetailsSurfaceState(", + "AetheriaRuntimeCargoItemDetailsSurfaceBuilder.Build(ProjectCargoItemDetailsSurface(", + "AetheriaRuntimeCargoItemDetailsSurfaceBuilder.Project(", + "ProjectCargoItemObservation(", "AetheriaRuntimeCargoItemDetailsSurfaceCommands.TryRead(request, out var command)", - "AetheriaRuntimeCargoItemDetailsCommandKind.Close", - "ProjectCargoItemDetailsSurfaceState(", - "ProjectCargoItemBehaviorSections(", - "ProjectCargoItemBehaviorMetric(" + "AetheriaRuntimeCargoItemDetailsCommandKind.Close" }; var missingSymbols = requiredSymbols @@ -4307,6 +4309,7 @@ static void RequireInventoryCargoItemDetailsUseEveSurface(string root) !daemonItemStatQueries.Contains("public static string ItemStatRef(", StringComparison.Ordinal) || !daemonItemStatQueries.Contains("public static bool TryReadItemStatRef(", StringComparison.Ordinal) || !daemonItemStatQueries.Contains("public string ValueRef =>", StringComparison.Ordinal) || + !unityProject.Contains("AetheriaRuntimeDaemonItemStatQueries.cs", StringComparison.Ordinal) || !eveUnitySurfaceHost.Contains("Func stateRefResolver", StringComparison.Ordinal) || !eveUnitySurfaceHost.Contains("ContainsStateRefs(surface)", StringComparison.Ordinal) || !eveUnitySurfaceHost.Contains("CreateDefaultStateRefResolver()", StringComparison.Ordinal) || @@ -4333,7 +4336,11 @@ static void RequireInventoryCargoItemDetailsUseEveSurface(string root) "ResolveCargoItemDetailsSurfaceDocument(", "new EveUiToolkitSurfaceLowerer()", "host.AddComponent", - "string.Equals(request.Command, AetheriaRuntimeCargoItemDetailsSurfaceBuilder.Close" + "string.Equals(request.Command, AetheriaRuntimeCargoItemDetailsSurfaceBuilder.Close", + "new AetheriaRuntimeCargoItemDetailsSurfaceState(", + "ProjectCargoItemDetailsSurfaceState(", + "ProjectCargoItemBehaviorSections(", + "ProjectCargoItemBehaviorMetric(" }; var hits = forbiddenSymbols .Where(symbol => source.Contains(symbol, StringComparison.Ordinal)) @@ -4355,8 +4362,12 @@ static void RequireInventoryCargoItemDetailsUseEveSurface(string root) "public static class AetheriaRuntimeCargoItemDetailsSurfaceCommands", "public static bool TryRead(", "AetheriaRuntimeCargoItemDetailsSurfaceState", + "AetheriaRuntimeCargoItemObservation", "AetheriaRuntimeCargoItemSection", "AetheriaRuntimeCargoItemMetric", + "public static AetheriaRuntimeCargoItemDetailsSurfaceState Project(", + "ProjectBehaviorSections(", + "ProjectBehaviorMetric(", "public static AetheriaRuntimeSurfaceDocument Build(" }; var missingBuilderSymbols = requiredBuilderSymbols diff --git a/Assets/Scripts/UI/Menu/InventoryMenu.cs b/Assets/Scripts/UI/Menu/InventoryMenu.cs index 1588ac899..d7e4d6b7a 100644 --- a/Assets/Scripts/UI/Menu/InventoryMenu.cs +++ b/Assets/Scripts/UI/Menu/InventoryMenu.cs @@ -258,7 +258,7 @@ public class InventoryMenu : MonoBehaviour transform, _cargoItemDetailsSurfaceDocument, "Aetheria Inventory Cargo Item Details Surface", - AetheriaRuntimeCargoItemDetailsSurfaceBuilder.Build(ProjectCargoItemDetailsSurfaceState(item, typedItem)), + AetheriaRuntimeCargoItemDetailsSurfaceBuilder.Build(ProjectCargoItemDetailsSurface(item, typedItem)), HandleCargoItemDetailsSurfaceCommand, _cargoItemDetailsSurfaceChrome, ResolveInventorySurfaceStateRef, @@ -494,127 +494,30 @@ public class InventoryMenu : MonoBehaviour } } - private AetheriaRuntimeCargoItemDetailsSurfaceState ProjectCargoItemDetailsSurfaceState( + private AetheriaRuntimeCargoItemDetailsSurfaceState ProjectCargoItemDetailsSurface( ItemInstance item, AetheriaRuntimeCatalogItem typedItem) { - var quantity = item is SimpleCommodity simpleCommodity ? simpleCommodity.Quantity : 0; - var tier = ""; - var durability = ""; - var thermalRange = ""; - var behaviorSections = Array.Empty(); - - if (item is EquippableItem equippableItem) - { - tier = FormatItemTier(typedItem, equippableItem); - durability = $"{(int)(equippableItem.Durability / GetMaxDurability(typedItem, equippableItem) * 100)}%"; - thermalRange = FormatTemperatureRange(typedItem); - behaviorSections = ProjectCargoItemBehaviorSections(typedItem, equippableItem).ToArray(); - } - - return new AetheriaRuntimeCargoItemDetailsSurfaceState( - typedItem.Name, - typedItem.Description ?? "", + return AetheriaRuntimeCargoItemDetailsSurfaceBuilder.Project( + typedItem, + ProjectCargoItemObservation(item), ActionGameManager.RuntimeCatalog?.GetManufacturer(typedItem)?.Name ?? "GameCult", - ActionGameManager.RuntimePlayerSettings.Format(GetCargoItemMass(item, typedItem)), - typedItem.Price, - quantity, - tier, - durability, - thermalRange, - behaviorSections, - DateTime.UtcNow.ToString("O")); + item is EquippableItem equippableItem ? FormatItemTier(typedItem, equippableItem) : "", + ActionGameManager.RuntimePlayerSettings.Format, + ActionGameManager.RuntimePlayerSettings.FormatTemperature); } - private IEnumerable ProjectCargoItemBehaviorSections( - AetheriaRuntimeCatalogItem typedItem, - EquippableItem equippableItem) + private static AetheriaRuntimeCargoItemObservation ProjectCargoItemObservation(ItemInstance item) { - 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 AetheriaRuntimeCargoItemSection( - $"{AetheriaRuntimeCargoItemDetailsSurfaceBuilder.SurfaceId}.behavior.{behavior.Kind}.stat_modifier", - "Stat Modifier", - new[] - { - new AetheriaRuntimeCargoItemMetric( - $"{AetheriaRuntimeCargoItemDetailsSurfaceBuilder.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 => ProjectCargoItemBehaviorMetric(behavior, field, equippableItem)) - .Where(metric => metric != null) - .ToArray(); - - if (fields.Length == 0) - continue; - - yield return new AetheriaRuntimeCargoItemSection( - $"{AetheriaRuntimeCargoItemDetailsSurfaceBuilder.SurfaceId}.behavior.{behavior.Kind}", - behavior.Kind.FormatTypeName(), - fields); - } - } - - private AetheriaRuntimeCargoItemMetric ProjectCargoItemBehaviorMetric( - 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 AetheriaRuntimeCargoItemMetric( - $"{AetheriaRuntimeCargoItemDetailsSurfaceBuilder.SurfaceId}.behavior.{behavior.Kind}.{field.Key}", - field.Name.SplitCamelCase(), - value, - field.ValueKind == AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat - ? AetheriaRuntimeDaemonItemStatQueries.ItemStatRef( - equippableItem.ItemKey, - behavior.Kind, - behavior.Group, - field.Key) - : ""); + var equippableItem = item as EquippableItem; + return new AetheriaRuntimeCargoItemObservation( + item?.ItemKey ?? "", + item is SimpleCommodity simpleCommodity ? simpleCommodity.Quantity : 0, + equippableItem != null, + equippableItem?.Quality ?? 1, + equippableItem?.Durability ?? 1, + equippableItem?.Temperature ?? 0, + equippableItem != null && equippableItem.OverrideShutdown); } private IEnumerable ProjectEquippedItemBehaviorSections( diff --git a/GameCult.Aetheria.State.Unity.csproj b/GameCult.Aetheria.State.Unity.csproj index ba772765d..03bea47e5 100644 --- a/GameCult.Aetheria.State.Unity.csproj +++ b/GameCult.Aetheria.State.Unity.csproj @@ -82,6 +82,7 @@ + diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs index 97eb6ed4a..7b18562b0 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using GameCult.Eve.Surface; @@ -83,11 +84,94 @@ namespace GameCult.Aetheria.State.Verse !string.IsNullOrWhiteSpace(ThermalRange); } + public sealed class AetheriaRuntimeCargoItemObservation + { + public AetheriaRuntimeCargoItemObservation( + string itemKey, + int quantity, + bool isEquippable, + double quality, + double durability, + double temperature, + bool overrideShutdown) + { + ItemKey = itemKey ?? ""; + Quantity = quantity; + IsEquippable = isEquippable; + Quality = quality; + Durability = durability; + Temperature = temperature; + OverrideShutdown = overrideShutdown; + } + + public string ItemKey { get; } + public int Quantity { get; } + public bool IsEquippable { get; } + public double Quality { get; } + public double Durability { get; } + public double Temperature { get; } + public bool OverrideShutdown { get; } + } + public static class AetheriaRuntimeCargoItemDetailsSurfaceBuilder { public const string SurfaceId = "aetheria.inventory.cargo_item_details"; public const string Close = "aetheria.inventory.cargo_item_details.close"; + public static AetheriaRuntimeCargoItemDetailsSurfaceState Project( + AetheriaRuntimeCatalogItem typedItem, + AetheriaRuntimeCargoItemObservation item, + string manufacturer, + string tier, + Func formatValue, + Func formatTemperature, + DateTime updatedAtUtc = default(DateTime)) + { + if (updatedAtUtc == default(DateTime)) + updatedAtUtc = DateTime.UtcNow; + + if (typedItem == null) + { + return new AetheriaRuntimeCargoItemDetailsSurfaceState( + "", + "", + manufacturer ?? "", + "", + 0, + item?.Quantity ?? 0, + "", + "", + "", + Array.Empty(), + updatedAtUtc.ToString("O", CultureInfo.InvariantCulture)); + } + + var quantity = item?.Quantity ?? 0; + var durability = ""; + var thermalRange = ""; + var behaviorSections = Array.Empty(); + + if (item != null && item.IsEquippable) + { + durability = $"{(int)(item.Durability / MaxDurability(typedItem, item) * 100)}%"; + thermalRange = FormatTemperatureRange(typedItem, formatTemperature); + behaviorSections = ProjectBehaviorSections(typedItem, item, formatValue, formatTemperature).ToArray(); + } + + return new AetheriaRuntimeCargoItemDetailsSurfaceState( + typedItem.Name, + typedItem.Description ?? "", + manufacturer ?? "", + FormatValue(Mass(typedItem, quantity), formatValue), + typedItem.Price, + quantity, + tier ?? "", + durability, + thermalRange, + behaviorSections, + updatedAtUtc.ToString("O", CultureInfo.InvariantCulture)); + } + public static AetheriaRuntimeSurfaceDocument Build( AetheriaRuntimeCargoItemDetailsSurfaceState state, long version = 1) @@ -187,6 +271,429 @@ namespace GameCult.Aetheria.State.Verse }); } + private static IEnumerable ProjectBehaviorSections( + AetheriaRuntimeCatalogItem typedItem, + AetheriaRuntimeCargoItemObservation 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, + AetheriaRuntimeCargoItemStatModifierType.Constant); + yield return new AetheriaRuntimeCargoItemSection( + $"{SurfaceId}.behavior.{behavior.Kind}.stat_modifier", + "Stat Modifier", + new[] + { + new AetheriaRuntimeCargoItemMetric( + $"{SurfaceId}.behavior.{behavior.Kind}.target", + $"{SplitCamelCase(statReference.Target)}:{SplitCamelCase(statReference.Stat)}", + $"{(modifierType == AetheriaRuntimeCargoItemStatModifierType.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 AetheriaRuntimeCargoItemSection( + $"{SurfaceId}.behavior.{behavior.Kind}", + FormatTypeName(behavior.Kind), + fields); + } + } + + private static AetheriaRuntimeCargoItemMetric ProjectBehaviorMetric( + AetheriaRuntimeBehaviorPayload behavior, + AetheriaRuntimeBehaviorFieldMetadata field, + AetheriaRuntimeCargoItemObservation 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 AetheriaRuntimeCargoItemMetric( + $"{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, + AetheriaRuntimeCargoItemObservation item, + Func formatValue) + { + return FormatValue( + AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat( + ToBehaviorValue(stat), + ToLoadoutItem(item), + item?.Temperature ?? 0), + formatValue); + } + + private static string FormatCurrentItemStat( + AetheriaRuntimeBehaviorValue stat, + AetheriaRuntimeCargoItemObservation item, + Func formatValue) + { + return FormatValue( + AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat( + stat, + ToLoadoutItem(item), + item?.Temperature ?? 0), + formatValue); + } + + private static AetheriaRuntimeLoadoutItemCommit ToLoadoutItem(AetheriaRuntimeCargoItemObservation item) + { + return AetheriaRuntimeDaemonItemStatQueries.ItemCommit( + item?.ItemKey ?? "", + item?.Quality ?? 1, + item?.Durability ?? 1, + enabled: true, + overrideShutdown: item != null && item.OverrideShutdown); + } + + private static double Mass(AetheriaRuntimeCatalogItem typedItem, int quantity) + { + if (typedItem == null) + return 0; + + return quantity > 0 + ? typedItem.Mass * quantity + : typedItem.Mass; + } + + private static double MaxDurability( + AetheriaRuntimeCatalogItem typedItem, + AetheriaRuntimeCargoItemObservation 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 AetheriaRuntimeCargoItemStatReference ReadStatReference(AetheriaRuntimeBehaviorValue value) + { + return new AetheriaRuntimeCargoItemStatReference( + 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 string SafeId(string value) { if (string.IsNullOrWhiteSpace(value)) @@ -256,6 +763,24 @@ namespace GameCult.Aetheria.State.Verse Close = 1 } + internal enum AetheriaRuntimeCargoItemStatModifierType + { + Constant = 0, + Multiplier = 1 + } + + internal readonly struct AetheriaRuntimeCargoItemStatReference + { + public AetheriaRuntimeCargoItemStatReference(string target, string stat) + { + Target = target ?? ""; + Stat = stat ?? ""; + } + + public string Target { get; } + public string Stat { get; } + } + public readonly struct AetheriaRuntimeCargoItemDetailsCommand { public AetheriaRuntimeCargoItemDetailsCommand(AetheriaRuntimeCargoItemDetailsCommandKind kind)