diff --git a/Aetheria.State.Unity/Aetheria.State.Unity.csproj b/Aetheria.State.Unity/Aetheria.State.Unity.csproj index b16eb26e8..0b12ca5e6 100644 --- a/Aetheria.State.Unity/Aetheria.State.Unity.csproj +++ b/Aetheria.State.Unity/Aetheria.State.Unity.csproj @@ -11,6 +11,7 @@ + diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 5e10c8a55..63447bf65 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -54,6 +54,7 @@ RequireMainMenuContinueRunState(root); RequireDeadPropertiesPanelShellDeleted(root); RequireTypedBehaviorMetadataCoverage(root); RequireNameToolsUsesUiToolkit(root); +RequireRuntimeStateReaderOwnsUnityStateAcquisition(root); RequireNoDeadRuntimeProjectionCaches(root); RequireNoDeadInspectorMetadata(root); RequireRuntimeSimulationTuningCommits(root); @@ -532,6 +533,7 @@ Console.WriteLine("Main-menu Continue authority: Continue selects typed run stat Console.WriteLine("Generic popup inspector shell: PropertiesPanel, PropertiesList, and DropdownMenu are deleted from source and serialized assets"); Console.WriteLine("Typed behavior metadata authority: live heat/mining/thermotoggle payload kinds stay owned by package metadata"); Console.WriteLine("NameTools editor shell: the remaining name helper window lowers through UI Toolkit instead of IMGUI"); +Console.WriteLine("Runtime state reader authority: Unity gameplay/UI read typed state through a shared runtime reader instead of direct store spelunking"); Console.WriteLine("Verse host authority: daemon-owned typed verse host settings now drive provider advertisement and operations telemetry"); Console.WriteLine("Runtime simulation tuning authority: UI writes flow through gameplay checkpoint commits"); Console.WriteLine("Hull conductivity authority: inventory UI toggles flow through gameplay checkpoint commits"); @@ -3829,6 +3831,15 @@ static void RequireMainMenuContinueRunState(string root) var packageSnapshot = File.Exists(packageSnapshotPath) ? File.ReadAllText(packageSnapshotPath) : throw new InvalidOperationException("Cannot verify Continue entity identity; package runtime snapshots are missing."); + 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 Continue entity readback; shared runtime state reader is missing."); var packageStorePath = Path.Combine( root, "Packages", @@ -3837,12 +3848,12 @@ static void RequireMainMenuContinueRunState(string root) "AetheriaRuntimeCatalogStore.cs"); var packageStore = File.Exists(packageStorePath) ? File.ReadAllText(packageStorePath) - : throw new InvalidOperationException("Cannot verify Continue entity readback; package runtime store is missing."); + : throw new InvalidOperationException("Cannot verify Continue entity payload readback; package runtime store is missing."); var requiredMenuSymbols = new[] { "LatestContinueRun", - "AetheriaRuntimeCatalogStore", + "AetheriaRuntimeStateReader", "ReadRunStates(ActionGameManager.RuntimeStateFilePath)", "ContinueGame(continueRun)", "ActionGameManager.ContinueRunState = run", @@ -3860,6 +3871,12 @@ static void RequireMainMenuContinueRunState(string root) string.Join(", ", missingMenuSymbols)); } + if (!runtimeStateReader.Contains("ReadRunStates", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "Shared runtime state reader no longer exposes typed run-state lookup for Continue."); + } + if (mainMenu.Contains("AddButton(\"Continue\", null)", StringComparison.Ordinal)) { throw new InvalidOperationException("MainMenu Continue regressed to a null button."); @@ -4182,6 +4199,141 @@ static void RequireNameToolsUsesUiToolkit(string root) } } +static void RequireRuntimeStateReaderOwnsUnityStateAcquisition(string root) +{ + var runtimeStateReaderPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeStateReader.cs"); + var unityFacadeProjectPath = Path.Combine(root, "Aetheria.State.Unity", "Aetheria.State.Unity.csproj"); + var actionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs"); + var mainMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "MainMenu.cs"); + var eveSurfacePresenterPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.eve-runtime", "Runtime", "AetheriaEveSurfacePresenter.cs"); + + var requiredPaths = new[] + { + runtimeStateReaderPath, + unityFacadeProjectPath, + actionGameManagerPath, + mainMenuPath, + eveSurfacePresenterPath + }; + + var missingPaths = requiredPaths + .Where(path => !File.Exists(path)) + .Select(path => Path.GetRelativePath(root, path)) + .ToArray(); + + if (missingPaths.Length > 0) + { + throw new InvalidOperationException( + "Runtime state reader authority cannot be verified because required files are missing: " + + string.Join(", ", missingPaths)); + } + + var runtimeStateReader = File.ReadAllText(runtimeStateReaderPath); + var unityFacadeProject = File.ReadAllText(unityFacadeProjectPath); + var actionGameManager = File.ReadAllText(actionGameManagerPath); + var mainMenu = File.ReadAllText(mainMenuPath); + var eveSurfacePresenter = File.ReadAllText(eveSurfacePresenterPath); + + var requiredReaderSymbols = new[] + { + "public static class AetheriaRuntimeStateReader", + "OpenRuntimeCatalog", + "ReadPlayerSettings", + "ReadLoadoutTemplates", + "ReadRunStates", + "ReadZoneStates", + "ReadEntitySnapshots", + "ReadEveSurface" + }; + + var missingReaderSymbols = requiredReaderSymbols + .Where(symbol => !runtimeStateReader.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingReaderSymbols.Length > 0) + { + throw new InvalidOperationException( + "Shared runtime state reader is incomplete: " + + string.Join(", ", missingReaderSymbols)); + } + + if (!unityFacadeProject.Contains("AetheriaRuntimeStateReader.cs", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "Aetheria.State.Unity.csproj does not include the shared runtime state reader."); + } + + var requiredActionGameManagerSymbols = new[] + { + "AetheriaRuntimeStateReader.ReadPlayerSettings", + "AetheriaRuntimeStateReader.ReadLoadoutTemplates", + "AetheriaRuntimeStateReader.OpenRuntimeCatalog", + "AetheriaRuntimeStateReader.ReadZoneStates", + "AetheriaRuntimeStateReader.ReadEntitySnapshots" + }; + + var missingActionGameManagerSymbols = requiredActionGameManagerSymbols + .Where(symbol => !actionGameManager.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingActionGameManagerSymbols.Length > 0) + { + throw new InvalidOperationException( + "ActionGameManager does not route typed state reads through the shared runtime reader: " + + string.Join(", ", missingActionGameManagerSymbols)); + } + + if (!mainMenu.Contains("AetheriaRuntimeStateReader", StringComparison.Ordinal) || + !mainMenu.Contains("ReadRunStates(ActionGameManager.RuntimeStateFilePath)", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "MainMenu no longer routes Continue-run lookup through the shared runtime state reader."); + } + + if (!eveSurfacePresenter.Contains("AetheriaRuntimeStateReader.ReadEveSurface", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "Aetheria Eve surface presenter no longer routes provider surface lookup through the shared runtime state reader."); + } + + var forbiddenDirectStoreSymbols = new Dictionary + { + [actionGameManagerPath] = new[] + { + "AetheriaRuntimeCatalogStore.ReadPlayerSettings", + "AetheriaRuntimeCatalogStore.ReadLoadoutTemplates", + "AetheriaRuntimeCatalogStore.OpenReadOnly", + "AetheriaRuntimeCatalogStore.ReadZoneStates", + "AetheriaRuntimeCatalogStore.ReadEntitySnapshots" + }, + [mainMenuPath] = new[] + { + "AetheriaRuntimeCatalogStore.ReadRunStates" + }, + [eveSurfacePresenterPath] = new[] + { + "AetheriaRuntimeCatalogStore.ReadEveSurfaces" + } + }; + + var directStoreHits = forbiddenDirectStoreSymbols + .SelectMany(entry => + { + var source = File.ReadAllText(entry.Key); + return entry.Value + .Where(symbol => source.Contains(symbol, StringComparison.Ordinal)) + .Select(symbol => $"{Path.GetRelativePath(root, entry.Key)} -> {symbol}"); + }) + .ToArray(); + + if (directStoreHits.Length > 0) + { + throw new InvalidOperationException( + "Unity gameplay/UI still reads typed state directly from the raw store instead of the shared runtime state reader: " + + string.Join("; ", directStoreHits)); + } +} + static void RequireRuntimeSimulationTuningCommits(string root) { var requiredActionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs"); @@ -4679,7 +4831,7 @@ static void RequireDroppedPickupCheckpointState(string root) "ProjectDroppedPickups", "DroppedPickups = ProjectDroppedPickups(zone)", "RestoreDroppedPickupsFromTypedZoneState", - "AetheriaRuntimeCatalogStore.ReadZoneStates(RuntimeStateFilePath)", + "AetheriaRuntimeStateReader.ReadZoneStates(RuntimeStateFilePath)", "ZoneRenderer.DropItem(" }, [Path.Combine(root, "Assets", "Scripts", "Zone Display", "ZoneRenderer.cs")] = new[] diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 08e5cacbf..ed42ac347 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -182,7 +182,7 @@ public class ActionGameManager : MonoBehaviour var settings = CreateDefaultRuntimePlayerSettings(); try { - var stored = AetheriaRuntimeCatalogStore.ReadPlayerSettings(RuntimeStateFilePath); + var stored = AetheriaRuntimeStateReader.ReadPlayerSettings(RuntimeStateFilePath); if (stored == null) return settings; @@ -245,7 +245,7 @@ public class ActionGameManager : MonoBehaviour { try { - var loadouts = AetheriaRuntimeCatalogStore.ReadLoadoutTemplates(stateFilePath); + var loadouts = AetheriaRuntimeStateReader.ReadLoadoutTemplates(stateFilePath); LoadoutTemplates.Clear(); LoadoutTemplates.AddRange(loadouts); @@ -2308,7 +2308,7 @@ public class ActionGameManager : MonoBehaviour } else { - RuntimeCatalog = AetheriaRuntimeCatalogStore.OpenReadOnly(stateBoot.StateFilePath); + RuntimeCatalog = AetheriaRuntimeStateReader.OpenRuntimeCatalog(stateBoot.StateFilePath); Debug.Log($"Aetheria typed runtime catalog: {RuntimeCatalog.Items.Count} items, {RuntimeCatalog.Corporations.Count} corporations, {RuntimeCatalog.NameFiles.Count} name files"); } @@ -2629,7 +2629,7 @@ public class ActionGameManager : MonoBehaviour AetheriaRuntimeZoneStateSnapshot zoneState; try { - zoneState = AetheriaRuntimeCatalogStore.ReadZoneStates(RuntimeStateFilePath) + zoneState = AetheriaRuntimeStateReader.ReadZoneStates(RuntimeStateFilePath) .FirstOrDefault(zone => string.Equals(zone.RecordKey, zoneKey, StringComparison.Ordinal)); } catch (Exception ex) @@ -2781,7 +2781,7 @@ public class ActionGameManager : MonoBehaviour AetheriaRuntimeEntitySnapshot[] entitySnapshots; try { - entitySnapshots = AetheriaRuntimeCatalogStore.ReadEntitySnapshots(RuntimeStateFilePath) + entitySnapshots = AetheriaRuntimeStateReader.ReadEntitySnapshots(RuntimeStateFilePath) .Where(entity => entity.RecordKey.StartsWith(zoneEntityKeyPrefix, StringComparison.Ordinal)) .OrderBy(entity => EntityIndexFromRecordKey(entity.RecordKey)) .ToArray(); diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index df85ebafd..fdf4b6156 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -160,7 +160,7 @@ public class MainMenu : MonoBehaviour { try { - return AetheriaRuntimeCatalogStore + return AetheriaRuntimeStateReader .ReadRunStates(ActionGameManager.RuntimeStateFilePath) .Where(run => !string.IsNullOrWhiteSpace(run.RunId)) .OrderByDescending(run => run.UpdatedAtUtc, StringComparer.Ordinal) diff --git a/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveSurfacePresenter.cs b/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveSurfacePresenter.cs index 20789c9d4..2fe172327 100644 --- a/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveSurfacePresenter.cs +++ b/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveSurfacePresenter.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using GameCult.Aetheria.State.Unity; using GameCult.Eve.Surface; using GameCult.Eve.UnityUIToolkit; @@ -51,8 +50,7 @@ namespace GameCult.Aetheria.EveRuntime return; } - var surface = AetheriaRuntimeCatalogStore.ReadEveSurfaces(statePath) - .FirstOrDefault(candidate => string.Equals(candidate.Surface.Id, surfaceId, StringComparison.Ordinal)); + var surface = AetheriaRuntimeStateReader.ReadEveSurface(statePath, surfaceId); if (surface == null) { root.Add(BuildError($"Eve surface not found: {surfaceId}")); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateReader.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateReader.cs new file mode 100644 index 000000000..8dd693027 --- /dev/null +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateReader.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GameCult.Eve.Surface; + +#nullable enable + +namespace GameCult.Aetheria.State.Unity +{ + // Unity runtime shells acquire typed state through this port so the transport can move to the daemon without spelunking every consumer. + public static class AetheriaRuntimeStateReader + { + public static AetheriaRuntimeCatalogSnapshot OpenRuntimeCatalog(string stateFilePath) + { + return AetheriaRuntimeCatalogStore.OpenReadOnly(stateFilePath); + } + + public static AetheriaRuntimePlayerSettingsSnapshot? ReadPlayerSettings(string stateFilePath) + { + return AetheriaRuntimeCatalogStore.ReadPlayerSettings(stateFilePath); + } + + public static IReadOnlyList ReadLoadoutTemplates(string stateFilePath) + { + return AetheriaRuntimeCatalogStore.ReadLoadoutTemplates(stateFilePath); + } + + public static IReadOnlyList ReadRunStates(string stateFilePath) + { + return AetheriaRuntimeCatalogStore.ReadRunStates(stateFilePath); + } + + public static IReadOnlyList ReadZoneStates(string stateFilePath) + { + return AetheriaRuntimeCatalogStore.ReadZoneStates(stateFilePath); + } + + public static IReadOnlyList ReadEntitySnapshots(string stateFilePath) + { + return AetheriaRuntimeCatalogStore.ReadEntitySnapshots(stateFilePath); + } + + public static EveSurfaceDocument? ReadEveSurface(string stateFilePath, string surfaceId) + { + return AetheriaRuntimeCatalogStore.ReadEveSurfaces(stateFilePath) + .FirstOrDefault(candidate => string.Equals(candidate.Surface.Id, surfaceId, StringComparison.Ordinal)); + } + } +}