diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 8e554ee77..b33e66e53 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -4287,10 +4287,23 @@ static void RequireInventoryCargoItemDetailsUseEveSurface(string root) throw new InvalidOperationException("InventoryMenu cargo click path no longer routes item inspection through the Eve surface."); } - if (!source.Contains("ResolveInventorySurfaceStateRef", StringComparison.Ordinal) || - !source.Contains("AetheriaRuntimeDaemonItemStatQueries.TryReadItemStatRef(", StringComparison.Ordinal) || - !source.Contains("AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat(", StringComparison.Ordinal) || + var runtimeStateReaderPath = Path.Combine( + root, + "Packages", + "org.gamecult.aetheria.state", + "Runtime", + "AetheriaRuntimeStateReader.cs"); + var runtimeStateReader = File.Exists(runtimeStateReaderPath) + ? File.ReadAllText(runtimeStateReaderPath) + : throw new InvalidOperationException("Cannot verify item stat state-ref authority; AetheriaRuntimeStateReader.cs is missing."); + + if (!runtimeStateReader.Contains("TryResolveDaemonItemStatRef(", StringComparison.Ordinal) || + !runtimeStateReader.Contains("AetheriaRuntimeDaemonItemStatQueries.TryReadItemStatRef(", StringComparison.Ordinal) || + !runtimeStateReader.Contains("AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat(", StringComparison.Ordinal) || + !runtimeStateReader.Contains("FindDaemonItem(", StringComparison.Ordinal) || !cargoItemSurfaceBuilder.Contains("AetheriaRuntimeDaemonItemStatQueries.ItemStatRef(", StringComparison.Ordinal) || + source.Contains("ResolveInventorySurfaceStateRef", StringComparison.Ordinal) || + source.Contains("TryResolveInventorySurfaceStateRef", StringComparison.Ordinal) || source.Contains("private static bool TryReadItemStatRef(", StringComparison.Ordinal) || source.Contains("DecodeRefToken", StringComparison.Ordinal) || source.Contains("GameManager.ItemManager.GetTier", StringComparison.Ordinal) || @@ -9708,7 +9721,9 @@ static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) "ResolveEveSurfaceStateRef", "TryResolveEveSurfaceStateRef", "TryResolveDaemonStateRef", + "TryResolveDaemonItemStatRef", "AetheriaRuntimeDaemonStateRefs.Prefix", + "AetheriaRuntimeDaemonItemStatQueries.StateRefPrefix", "AetheriaRuntimeDaemonStateRefs.CurrentEntityName", "AetheriaRuntimeDaemonPublicationStore.TryReadGameSurface", "AetheriaRuntimeDaemonPublicationStore.TryReadGameTuiSurface", @@ -9722,6 +9737,7 @@ static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) "ResolveSurfaceStateRefs(stateFilePath, surface)", "public static Func CreateEveSurfaceStateRefResolver(string stateFilePath)", "CreateStateRefResolver(string stateFilePath)", + "FindDaemonItem(", "AetheriaRuntimeEveSurfaceAdapter.ResolveStateRefs(", "AetheriaRuntimeEveSurfaceAdapter.EmptySurface(surfaceId)" }; diff --git a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs index 6c10e98a7..07ab79ba4 100644 --- a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs +++ b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs @@ -406,6 +406,82 @@ public class DaemonRuntimeDocumentTests AetheriaRuntimeDaemonStateRefs.CurrentTargetName)); } + [Test] + public void RuntimeStateReaderResolvesItemStatRefsFromDaemonFrame() + { + var frame = new AetheriaRuntimeDaemonFrameDocument + { + Run = new AetheriaRuntimeRunCheckpointCommit + { + Zones = new[] + { + new AetheriaRuntimeZoneSnapshotCommit + { + Entities = new[] + { + new AetheriaRuntimeEntitySnapshotCommit + { + Equipment = new[] + { + new AetheriaRuntimeLoadoutItemSlotCommit + { + Item = new AetheriaRuntimeLoadoutItemCommit + { + ItemKey = "test-laser", + Quality = 0.5, + Durability = 1.0, + Enabled = true + } + } + } + } + } + } + } + } + }; + var statValue = new AetheriaRuntimeBehaviorValue( + "performance-stat", + "", + 0, + false, + "", + "", + new[] + { + NumberValue(10), + NumberValue(20), + NumberValue(0), + NumberValue(0), + NumberValue(1), + EmptyBehaviorValue("stat-recipe") + }, + Array.Empty()); + var catalog = new AetheriaRuntimeCatalogSnapshot( + new[] + { + CatalogItem( + "test-laser", + new[] + { + new AetheriaRuntimeBehaviorPayload( + 0, + "Weapon", + 2, + new[] { new AetheriaRuntimeBehaviorField(7, statValue) }) + }) + }, + Array.Empty(), + Array.Empty()); + + Assert.IsTrue(AetheriaRuntimeStateReader.TryResolveDaemonItemStatRef( + frame, + catalog, + AetheriaRuntimeDaemonItemStatQueries.ItemStatRef("test-laser", "Weapon", 2, 7), + out var value)); + Assert.AreEqual("15", value); + } + [Test] public void TickRunnerSkipsPreviouslyAccountedObservedCommands() { @@ -3501,6 +3577,87 @@ public class DaemonRuntimeDocumentTests "state.cc.daemon.soa.cc"); } + private static AetheriaRuntimeCatalogItem CatalogItem( + string itemKey, + IReadOnlyList behaviorPayloads) + { + return new AetheriaRuntimeCatalogItem( + itemKey, + itemKey, + "", + "", + "", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + Array.Empty(), + 0, + 0, + 0, + Array.Empty(), + Array.Empty(), + behaviorPayloads, + "", + "", + behaviorPayloads.Select(payload => payload.Kind).ToArray(), + 1, + false, + 0, + 1, + "", + "", + "", + "", + "", + 0, + 0, + Array.Empty(), + "", + 1, + 0, + 0, + 0, + false, + 0, + 0, + "", + Array.Empty(), + Array.Empty(), + "", + ""); + } + + private static AetheriaRuntimeBehaviorValue NumberValue(double value) + { + return new AetheriaRuntimeBehaviorValue( + "", + "", + value, + false, + "", + "", + Array.Empty(), + Array.Empty()); + } + + private static AetheriaRuntimeBehaviorValue EmptyBehaviorValue(string kind) + { + return new AetheriaRuntimeBehaviorValue( + kind, + "", + 0, + false, + "", + "", + Array.Empty(), + Array.Empty()); + } + private static bool ContainsSurfaceMetric( AetheriaRuntimeSurfaceComponent component, string label, diff --git a/Assets/Scripts/UI/Menu/InventoryMenu.cs b/Assets/Scripts/UI/Menu/InventoryMenu.cs index b8236b67e..7e7539bc6 100644 --- a/Assets/Scripts/UI/Menu/InventoryMenu.cs +++ b/Assets/Scripts/UI/Menu/InventoryMenu.cs @@ -261,7 +261,6 @@ public class InventoryMenu : MonoBehaviour AetheriaRuntimeCargoItemDetailsSurfaceBuilder.Build(ProjectCargoItemDetailsSurface(item, typedItem)), HandleCargoItemDetailsSurfaceCommand, _cargoItemDetailsSurfaceChrome, - ResolveInventorySurfaceStateRef, sortingOrder: 1001); } @@ -304,7 +303,6 @@ public class InventoryMenu : MonoBehaviour AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.Build(ProjectEquippedItemDetailsSurface(item, typedItem)), HandleEquippedItemDetailsSurfaceCommand, _equippedItemDetailsSurfaceChrome, - ResolveInventorySurfaceStateRef, sortingOrder: 1002); } @@ -539,15 +537,6 @@ public class InventoryMenu : MonoBehaviour : ""; } - private static string FormatCurrentItemStat(AetheriaRuntimeBehaviorValue stat, EquippableItem item) - { - return ActionGameManager.RuntimePlayerSettings.Format( - AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat( - stat, - ToRuntimeLoadoutItem(item), - item?.Temperature ?? 0)); - } - private static AetheriaRuntimeLoadoutItemCommit ToRuntimeLoadoutItem(EquippableItem item) { return AetheriaRuntimeDaemonItemStatQueries.ItemCommit( @@ -558,51 +547,6 @@ public class InventoryMenu : MonoBehaviour overrideShutdown: item != null && item.OverrideShutdown); } - private string ResolveInventorySurfaceStateRef(string stateRef) - { - return TryResolveInventorySurfaceStateRef(stateRef, out var value) ? value : ""; - } - - private bool TryResolveInventorySurfaceStateRef(string stateRef, out string value) - { - value = ""; - if (!AetheriaRuntimeDaemonItemStatQueries.TryReadItemStatRef( - stateRef, - out var itemKey, - out var behaviorKind, - out var behaviorGroup, - out var fieldKey)) - return false; - - var item = ResolveSelectedEquippableItem(itemKey); - var typedItem = FindTypedInventoryItem(item); - var behavior = typedItem?.BehaviorPayloads?.FirstOrDefault(candidate => - string.Equals(candidate.Kind, behaviorKind, StringComparison.Ordinal) && - candidate.Group == behaviorGroup); - var field = behavior?.Fields?.FirstOrDefault(candidate => candidate.Key == fieldKey); - if (item == null || field == null) - return false; - - value = FormatCurrentItemStat(field.Value, item); - return true; - } - - private EquippableItem ResolveSelectedEquippableItem(string itemKey) - { - var equipped = _selectedEquippedItem?.EquippableItem; - if (ItemKeyMatches(equipped, itemKey)) - return equipped; - - var cargo = _selectedItem as EquippableItem; - return ItemKeyMatches(cargo, itemKey) ? cargo : null; - } - - private static bool ItemKeyMatches(EquippableItem item, string itemKey) - { - return item != null && - string.Equals(item.ItemKey ?? "", itemKey ?? "", StringComparison.Ordinal); - } - private static bool IsWeaponGroupAssigned(EquippedItem item, int groupIndex) { return item?.Entity?.WeaponGroups != null && diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateReader.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateReader.cs index b78b3ff33..2ab9c1614 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateReader.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateReader.cs @@ -121,13 +121,24 @@ namespace GameCult.Aetheria.State.Verse if (string.IsNullOrWhiteSpace(stateRef)) return false; - if (!stateRef.StartsWith(AetheriaRuntimeDaemonStateRefs.Prefix + "/", StringComparison.Ordinal)) - return false; - TryReadDaemonFrame(stateFilePath, out var frame); - AetheriaRuntimeDaemonPublicationStore.TryReadHealth(stateFilePath, out var health); - AetheriaRuntimeDaemonPublicationStore.TryReadCommandBoundary(stateFilePath, out var commandBoundary); - return TryResolveDaemonStateRef(frame, health, commandBoundary, stateRef, out value); + if (stateRef.StartsWith(AetheriaRuntimeDaemonStateRefs.Prefix + "/", StringComparison.Ordinal)) + { + AetheriaRuntimeDaemonPublicationStore.TryReadHealth(stateFilePath, out var health); + AetheriaRuntimeDaemonPublicationStore.TryReadCommandBoundary(stateFilePath, out var commandBoundary); + return TryResolveDaemonStateRef(frame, health, commandBoundary, stateRef, out value); + } + + if (stateRef.StartsWith(AetheriaRuntimeDaemonItemStatQueries.StateRefPrefix + "/", StringComparison.Ordinal)) + { + return TryResolveDaemonItemStatRef( + frame, + OpenRuntimeCatalog(stateFilePath), + stateRef, + out value); + } + + return false; } public static bool TryResolveDaemonStateRef( @@ -211,6 +222,42 @@ namespace GameCult.Aetheria.State.Verse } } + public static bool TryResolveDaemonItemStatRef( + AetheriaRuntimeDaemonFrameDocument? frame, + AetheriaRuntimeCatalogSnapshot? catalog, + string stateRef, + out string value) + { + value = ""; + if (!AetheriaRuntimeDaemonItemStatQueries.TryReadItemStatRef( + stateRef, + out var itemKey, + out var behaviorKind, + out var behaviorGroup, + out var fieldKey)) + { + return false; + } + + var item = FindDaemonItem(frame?.Run, itemKey); + var typedItem = catalog?.FindItem(itemKey); + var field = typedItem?.BehaviorPayloads? + .FirstOrDefault(candidate => + string.Equals(candidate.Kind, behaviorKind, StringComparison.Ordinal) && + candidate.Group == behaviorGroup) + ?.Fields? + .FirstOrDefault(candidate => candidate.Key == fieldKey); + if (item == null || field == null) + return false; + + value = AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat( + field.Value, + item, + heat: 0) + .ToString("0.###", CultureInfo.InvariantCulture); + return true; + } + public static bool TryReadDaemonGameSurface( string stateFilePath, out EveSurfaceDocument document) @@ -303,9 +350,71 @@ namespace GameCult.Aetheria.State.Verse AetheriaRuntimeDaemonPublicationStore.TryReadHealth(stateFilePath, out var health); AetheriaRuntimeDaemonPublicationStore.TryReadCommandBoundary(stateFilePath, out var commandBoundary); - return stateRef => TryResolveDaemonStateRef(frame, health, commandBoundary, stateRef, out var value) - ? value - : ""; + return stateRef => + { + if (TryResolveDaemonStateRef(frame, health, commandBoundary, stateRef, out var value)) + return value; + + return stateRef.StartsWith(AetheriaRuntimeDaemonItemStatQueries.StateRefPrefix + "/", StringComparison.Ordinal) && + TryResolveDaemonItemStatRef(frame, OpenRuntimeCatalog(stateFilePath), stateRef, out value) + ? value + : ""; + }; + } + + private static AetheriaRuntimeLoadoutItemCommit? FindDaemonItem( + AetheriaRuntimeRunCheckpointCommit? run, + string itemKey) + { + if (run == null || string.IsNullOrWhiteSpace(itemKey)) + return null; + + foreach (var zone in run.Zones ?? Array.Empty()) + foreach (var entity in zone.Entities ?? Array.Empty()) + { + var item = FindDaemonItem(entity.Equipment, itemKey) + ?? FindDaemonItem(entity.CargoBays, itemKey) + ?? FindDaemonItem(entity.DockingBays, itemKey) + ?? FindDaemonItem(entity.CargoContents, itemKey) + ?? FindDaemonItem(entity.DockingBayContents, itemKey); + if (item != null) + return item; + } + + return null; + } + + private static AetheriaRuntimeLoadoutItemCommit? FindDaemonItem( + IReadOnlyList? slots, + string itemKey) + { + foreach (var slot in slots ?? Array.Empty()) + { + if (IsItemMatch(slot?.Item, itemKey)) + return slot?.Item; + } + + return null; + } + + private static AetheriaRuntimeLoadoutItemCommit? FindDaemonItem( + IReadOnlyList? cargoBays, + string itemKey) + { + foreach (var cargoBay in cargoBays ?? Array.Empty()) + { + var item = FindDaemonItem(cargoBay?.Items, itemKey); + if (item != null) + return item; + } + + return null; + } + + private static bool IsItemMatch(AetheriaRuntimeLoadoutItemCommit? item, string itemKey) + { + return item != null && + string.Equals(item.ItemKey ?? "", itemKey ?? "", StringComparison.Ordinal); } private static AetheriaRuntimeZoneSnapshotCommit FindCurrentZone(AetheriaRuntimeRunCheckpointCommit run)