diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index c13e86833..1a9bf94e8 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 7cf2fe78e..3dc04a364 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -13,6 +13,7 @@ RequireEveRuntimeBootstrap(root); RequireNoRendererLocalConsole(root); RequireNoRendererLocalDebugPanels(root); RequireMainMenuSettingsCommit(root); +RequireMainMenuContinueRunState(root); RequirePropertiesPanelReadOnlyInspector(root); RequireRuntimeSimulationTuningCommits(root); RequireHullConductivityCommitAuthority(root); @@ -467,6 +468,7 @@ Console.WriteLine("Eve runtime bootstrap: operations surface mounts through UI T Console.WriteLine("Renderer-local console authority: deleted; UI commands flow through Eve command documents"); Console.WriteLine("Renderer-local debug panels: obsolete uGUI field tester authority is deleted"); Console.WriteLine("Main-menu settings authority: gameplay and graphics settings return through typed player-settings commits"); +Console.WriteLine("Main-menu Continue authority: Continue selects typed run state instead of a null button"); Console.WriteLine("PropertiesPanel inspector authority: reflection inspection is read-only display"); 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"); @@ -778,6 +780,65 @@ static void RequireMainMenuSettingsCommit(string root) } } +static void RequireMainMenuContinueRunState(string root) +{ + var mainMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "MainMenu.cs"); + var mainMenu = File.Exists(mainMenuPath) + ? File.ReadAllText(mainMenuPath) + : throw new InvalidOperationException("Cannot verify main-menu Continue path; MainMenu.cs is missing."); + + var actionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs"); + var actionGameManager = File.Exists(actionGameManagerPath) + ? File.ReadAllText(actionGameManagerPath) + : throw new InvalidOperationException("Cannot verify Continue run authority; ActionGameManager.cs is missing."); + + var requiredMenuSymbols = new[] + { + "LatestContinueRun", + "AetheriaRuntimeCatalogStore", + "ReadRunStates(ActionGameManager.RuntimeStateFilePath)", + "ContinueGame(continueRun)", + "ActionGameManager.ContinueRunState = run", + "SceneManager.LoadScene(\"ARPG\")" + }; + + var missingMenuSymbols = requiredMenuSymbols + .Where(symbol => !mainMenu.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingMenuSymbols.Length > 0) + { + throw new InvalidOperationException( + "MainMenu Continue no longer selects typed run state: " + + string.Join(", ", missingMenuSymbols)); + } + + if (mainMenu.Contains("AddButton(\"Continue\", null)", StringComparison.Ordinal)) + { + throw new InvalidOperationException("MainMenu Continue regressed to a null button."); + } + + var requiredGameplaySymbols = new[] + { + "AetheriaRuntimeRunStateSnapshot ContinueRunState", + "ResolveStartZone(continuingRun)", + "if (continuingRun != null)", + "ContinueRunState = null", + "RestoreDroppedPickupsFromTypedZoneState" + }; + + var missingGameplaySymbols = requiredGameplaySymbols + .Where(symbol => !actionGameManager.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingGameplaySymbols.Length > 0) + { + throw new InvalidOperationException( + "ActionGameManager no longer has the typed Continue boot path: " + + string.Join(", ", missingGameplaySymbols)); + } +} + static void RequirePropertiesPanelReadOnlyInspector(string root) { var propertiesPanelPath = Path.Combine(root, "Assets", "Scripts", "UI", "Properties Panel", "PropertiesPanel.cs"); diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 67b8ee0de..641c3fb8b 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -46,7 +46,7 @@ public class ActionGameManager : MonoBehaviour } private static string _runtimeStateFilePath; - private static string RuntimeStateFilePath => + public static string RuntimeStateFilePath => _runtimeStateFilePath ??= AetheriaRuntimeStateBoundary.GetStateFilePath(GameDataDirectory); private static RuntimePlayerSettings _runtimePlayerSettings; @@ -315,6 +315,7 @@ public class ActionGameManager : MonoBehaviour public static Galaxy CurrentGalaxy; public static bool IsTutorial; + public static AetheriaRuntimeRunStateSnapshot ContinueRunState { get; set; } public static AetheriaRuntimeCatalogSnapshot RuntimeCatalog { get; private set; } public GameSettings Settings; @@ -2420,8 +2421,17 @@ public class ActionGameManager : MonoBehaviour { if (CurrentGalaxy != null) { - SectorMap.QueueZoneReveal(CurrentGalaxy.Entrance.AdjacentZones.Prepend(CurrentGalaxy.Entrance)); - PopulateLevel(CurrentGalaxy.Entrance); + var continuingRun = ContinueRunState; + var startZone = ResolveStartZone(continuingRun); + SectorMap.QueueZoneReveal(startZone.AdjacentZones.Prepend(startZone)); + PopulateLevel(startZone); + + if (continuingRun != null) + { + ContinueRunState = null; + return; + } + var loadoutGenerator = new LoadoutGenerator(ref ItemManager.Random, ItemManager, RuntimeCatalog, CurrentGalaxy, Zone.GalaxyZone, IsTutorial ? CurrentGalaxy.ResolveFaction(Settings.TutorialGenerationSettings.ProtagonistFaction) : null, 2); var ship = EntityConstructionBlueprintProjector.InstantiateFromBlueprint( ItemManager, @@ -2437,6 +2447,21 @@ public class ActionGameManager : MonoBehaviour } } + private static GalaxyZone ResolveStartZone(AetheriaRuntimeRunStateSnapshot run) + { + if (CurrentGalaxy?.Zones == null || CurrentGalaxy.Zones.Length == 0) + return CurrentGalaxy?.Entrance; + + if (run != null && + run.CurrentZoneIndex >= 0 && + run.CurrentZoneIndex < CurrentGalaxy.Zones.Length) + { + return CurrentGalaxy.Zones[run.CurrentZoneIndex]; + } + + return CurrentGalaxy.Entrance; + } + private IEnumerator IntroCutscene(Ship ship) { ZoneRenderer.PerspectiveEntity = ship; diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index 0e4bf36f2..5fd3c337e 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using GameCult.Aetheria.State.Unity; using TMPro; using UniRx; using UnityEngine; @@ -88,7 +89,8 @@ public class MainMenu : MonoBehaviour _nextMenu.panel.Title.text = TitleSubtitle("aetheria", "terminus"); if (!InGame) { - _nextMenu.panel.AddButton("Continue", null); + var continueRun = LatestContinueRun(); + _nextMenu.panel.AddButton("Continue", continueRun == null ? null : () => ContinueGame(continueRun)); } _nextMenu.panel.AddButton("New Game", () => @@ -171,6 +173,75 @@ public class MainMenu : MonoBehaviour return seed == 0 ? 1u : seed; } + private static AetheriaRuntimeRunStateSnapshot LatestContinueRun() + { + try + { + return AetheriaRuntimeCatalogStore + .ReadRunStates(ActionGameManager.RuntimeStateFilePath) + .Where(run => !string.IsNullOrWhiteSpace(run.RunId)) + .OrderByDescending(run => run.UpdatedAtUtc, StringComparer.Ordinal) + .ThenByDescending(run => run.RunId, StringComparer.Ordinal) + .FirstOrDefault(); + } + catch (Exception ex) + { + Debug.LogError($"Failed to read typed Aetheria run state for Continue: {ex}"); + return null; + } + } + + private void ContinueGame(AetheriaRuntimeRunStateSnapshot run) + { + if (run == null) + return; + + var generatorState = "Loading typed run"; + Action setState = s => generatorState = s; + + Fade(true); + _nextMenu.panel.gameObject.SetActive(false); + Dialog.Clear(); + Dialog.Title.text = "Continuing Run"; + Dialog.AddProperty(() => generatorState); + Dialog.Show(); + + ActionGameManager.IsTutorial = run.IsTutorial; + ActionGameManager.ContinueRunState = run; + var generationSeed = run.GenerationSeed == 0 ? NextGenerationSeed() : run.GenerationSeed; + var sectorSettings = run.IsTutorial ? Settings.TutorialGenerationSettings : Settings.SectorGenerationSettings; + var backgroundSettings = run.IsTutorial ? Settings.TutorialBackgroundSettings : Settings.SectorBackgroundSettings; + backgroundSettings.NoisePosition = generationSeed; + + Task.Run(() => + { + var sector = run.IsTutorial + ? new Galaxy( + Settings.TutorialGenerationSettings, + Settings.TutorialBackgroundSettings, + Settings.NameGeneratorSettings, + ActionGameManager.RuntimeCatalog, + ActionGameManager.RuntimePlayerSettings, + ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"), + Debug.Log, + setState, + generationSeed) + : new Galaxy( + sectorSettings, + backgroundSettings, + Settings.NameGeneratorSettings, + ActionGameManager.RuntimeCatalog, + Debug.Log, + setState, + generationSeed); + Observable.NextFrame().Subscribe(_ => + { + ActionGameManager.CurrentGalaxy = sector; + SceneManager.LoadScene("ARPG"); + }); + }).WrapErrors(); + } + private void ShowSettings() { _nextMenu.panel.Clear(); diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index adf13584a..c055f0a5f 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -962,11 +962,18 @@ First Aetheria surfaces to publish: zone load. `AetheriaRuntimeZoneStateSnapshot.RecordKey` preserves the exact typed zone identity, and `ActionGameManager` restores only the matching `RunId + ZoneIndex` pickup rows through `ZoneRenderer.DropItem`. + - Done: replace the main-menu null `Continue` button with a typed run-state + entry point. `MainMenu` reads available `AetheriaRunState` records through + `AetheriaRuntimeCatalogStore`, selects the newest run, regenerates the + galaxy from the saved generation seed, and passes the selected run to + `ActionGameManager` so boot enters the saved current zone and lowers typed + pickup rows. Entity/player reconstruction from `AetheriaEntitySnapshot` + remains future Continue work, not a fake restored ship. - Remaining: add typed documents/mappers for runtime object graphs, - complete Continue/run restore from typed state, typed behavior factory - construction, remaining behavior-private state not covered by progress, - weapon, sensor, radiator, reactor, or capacitor rows, and any catalog - fields not covered by the stable scalar/fingerprint/payload pass. + complete Continue/run entity reconstruction from typed state, typed + behavior factory construction, remaining behavior-private state not covered + by progress, weapon, sensor, radiator, reactor, or capacitor rows, and any + catalog fields not covered by the stable scalar/fingerprint/payload pass. 4. Runtime cutover - Done: add a Unity-facing typed catalog read facade and smoke proving it can