From 8d802ce1e0675935a072b6a907d46a1d4c435055 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 16 Jun 2026 03:18:55 +0100 Subject: [PATCH] Route loadout restore through checkpoints --- .voidbot/state/aetheria.cc | Bin 143633 -> 143833 bytes Aetheria.State.Verify/Program.cs | 59 +++++++++++++++++++ Assets/Scripts/Gameplay/ActionGameManager.cs | 30 ++++++++++ Assets/Scripts/UI/Menu/InventoryPanel.cs | 12 +--- docs/aetheria-perfect-machine-map.md | 9 ++- 5 files changed, 99 insertions(+), 11 deletions(-) diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 0366bb887bc05ea4ed1f2c6516a8b51bd459ca87..9029284c77741b46292b56d075bba93665a74d36 100644 GIT binary patch delta 1034 zcmZuwF;5gh6m}O>M6E=jD~eE;vL10Y%XtHdiV8l*4?iy66n2 z!+fAMzO+L$6eLRm9IFf;Phtqm<}RQ@#`EV%*q?k-dpaSh#e=a5Xy9d)BseXii=rQJLmhe2L!J|0NP9)rgwI~p2qa2E%6_`dY4&zG{R3I0K zMf;x-ZlUBHT~J$lZm<8^4o3IieIn_?=8bsHHpY7Xdn(p2G5e_!fwX7^eKA3IMsq)# z<2f*n!Okg~^pi#W3%?n!SjM@-f1R~>iWUIqERb@2?$;bqrzrmviUG;DI&2@(b(9%ow+;n+?n?#?_)PZime43 z*t1QB_qu$isGY0LWHmXo_OG^-*G7hqm*L&Unc?Rjd$)hCPcDl#3L{kt zzGA_&uVey3c0fsMP=g8S5QM$aUnjd01@;;f814E2A}In?pfHwg`p;u3kQJ~3S1nI8 z1jtkcc3q?bO~N~1@VP?IMCp0pW=B4wnKE*U4%ZxEQOKxbvOZvS#{tmcnDBG8oqB^f##Xt0;);$*eqQ3SF>wM1c?V~4tzq@dCYG+7HldY(;~@aW!kQ-?O!mPIKm$EyHG*KgP4*QJxR~~lIu*Gsg|{g? zXRsq}u*dKeoxBmUm z=9_xya%FU;UYo7g7OV5Mxy=(n>BruUx1aBhZ%)Le2QLm3`#jfwI9VPq_I7Ep=61RN z`(*j$f5oN2g752pnk>h~-%ICu--eF%#i??40A8$~oy|6X^YH2NtNlfFW^Vr0U-@W@ AI{*Lx diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index ccdb41b70..2eef7a521 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -20,6 +20,7 @@ RequireInventoryEntityRenameCommitAuthority(root); RequireWeaponGroupCommitAuthority(root); RequireInventoryDoubleClickTransferCommitAuthority(root); RequireTradePurchaseCommitAuthority(root); +RequireInventoryLoadoutRestoreCommitAuthority(root); await using var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-state-verify"); @@ -469,6 +470,7 @@ Console.WriteLine("Inventory entity rename authority: UI rename flows through ga Console.WriteLine("Weapon group authority: UI assignment flows through gameplay checkpoint commits"); Console.WriteLine("Inventory transfer authority: UI transfer and drag/drop requests flow through gameplay checkpoint commits"); Console.WriteLine("Trade purchase authority: UI buy requests flow through gameplay checkpoint commits"); +Console.WriteLine("Inventory loadout restore authority: UI restore requests flow through gameplay checkpoint commits"); static void RequireCount(AetheriaMigrationLedger ledger, string documentType, int actual) { @@ -1065,6 +1067,63 @@ static void RequireTradePurchaseCommitAuthority(string root) } } +static void RequireInventoryLoadoutRestoreCommitAuthority(string root) +{ + var actionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs"); + var actionGameManager = File.Exists(actionGameManagerPath) + ? File.ReadAllText(actionGameManagerPath) + : throw new InvalidOperationException("Cannot verify loadout restore authority; ActionGameManager.cs is missing."); + + var requiredSymbols = new[] + { + "CommitRuntimeLoadoutRestore", + "EntityConstructionBlueprintProjector.InstantiateFromBlueprint", + "QueueRunCheckpoint(\"loadout-restore\")", + "Credits -=" + }; + + var missingSymbols = requiredSymbols + .Where(symbol => !actionGameManager.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingSymbols.Length > 0) + { + throw new InvalidOperationException( + "Loadout restore no longer has a gameplay-owned checkpoint commit primitive: " + + string.Join(", ", missingSymbols)); + } + + var inventoryPanelPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "InventoryPanel.cs"); + var inventoryPanel = File.Exists(inventoryPanelPath) + ? File.ReadAllText(inventoryPanelPath) + : throw new InvalidOperationException("Cannot verify loadout restore authority; InventoryPanel.cs is missing."); + + var forbiddenSymbols = new[] + { + "EntityConstructionBlueprintProjector.InstantiateFromBlueprint", + "GameManager.Credits -=", + ".SetParent(GameManager.DockedEntity)" + }; + + var hits = File.ReadLines(inventoryPanelPath) + .Select((line, index) => new { LineNumber = index + 1, Line = line }) + .Where(line => forbiddenSymbols.Any(symbol => line.Line.Contains(symbol, StringComparison.Ordinal))) + .Select(line => $"{Path.GetRelativePath(root, inventoryPanelPath)}:{line.LineNumber}: {line.Line.Trim()}") + .ToArray(); + + if (hits.Length > 0) + { + throw new InvalidOperationException( + "InventoryPanel still owns direct loadout restore mutation: " + + string.Join("; ", hits)); + } + + if (!inventoryPanel.Contains("GameManager.CommitRuntimeLoadoutRestore", StringComparison.Ordinal)) + { + throw new InvalidOperationException("InventoryPanel no longer routes loadout restore through ActionGameManager."); + } +} + static string ExtractMethodBody(string source, string methodName) { var signature = "private void " + methodName + "()"; diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index a44dde53c..d5449e5b8 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -1306,6 +1306,36 @@ public class ActionGameManager : MonoBehaviour } } + public bool CommitRuntimeLoadoutRestore(AetheriaRuntimeLoadoutTemplateSnapshot template, out Entity entity) + { + entity = null; + var blueprint = CreateEntityConstructionBlueprint(template); + if (blueprint == null || + Zone == null || + DockedEntity == null) + { + return false; + } + + var price = blueprint.Price(ItemManager); + if (price > Credits) + return false; + + entity = EntityConstructionBlueprintProjector.InstantiateFromBlueprint(ItemManager, Zone, blueprint, true); + entity.SetParent(DockedEntity); + Credits -= price; + CurrentEntity = entity; + if (entity is Ship ship) + { + ship.IsPlayerShip = true; + if (DockingBay != null) + DockingBay.DockedShip = ship; + } + + QueueRunCheckpoint("loadout-restore"); + return true; + } + private static AetheriaRuntimeLoadoutTemplateSnapshot CreateRuntimeLoadoutTemplateSnapshot( AetheriaRuntimeLoadoutTemplateCommit template) { diff --git a/Assets/Scripts/UI/Menu/InventoryPanel.cs b/Assets/Scripts/UI/Menu/InventoryPanel.cs index 327fbc70e..02b331af4 100644 --- a/Assets/Scripts/UI/Menu/InventoryPanel.cs +++ b/Assets/Scripts/UI/Menu/InventoryPanel.cs @@ -198,16 +198,8 @@ public class InventoryPanel : MonoBehaviour, IPointerClickHandler var price = blueprint.Price(GameManager.ItemManager); return ($"{template.Name} - {price:n0}", () => { - var entity = EntityConstructionBlueprintProjector.InstantiateFromBlueprint(GameManager.ItemManager, GameManager.Zone, blueprint, true); - entity.SetParent(GameManager.DockedEntity); - GameManager.Credits -= price; - GameManager.CurrentEntity = entity; - if(entity is Ship ship) - { - ship.IsPlayerShip = true; - GameManager.DockingBay.DockedShip = ship; - } - Display(entity); + if (GameManager.CommitRuntimeLoadoutRestore(template, out var entity)) + Display(entity); }, price < GameManager.Credits); } diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 716ded19c..b4b311a33 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -869,6 +869,11 @@ First Aetheria surfaces to publish: - Done: add the Unity runtime read projection for typed loadout templates, so the restore menu can rehydrate saved loadouts from CultCache instead of relying on the in-memory session list after restart. + - Done: route runtime loadout restore through + `ActionGameManager.CommitRuntimeLoadoutRestore`. `InventoryPanel` may list + typed loadout templates and display the restored entity, but gameplay owns + blueprint instantiation, credit spend, docked-ship assignment, current + entity update, and typed run checkpoint. - Done: add typed node ports and smoke coverage for run -> zone -> entity snapshots so `.zone` no longer lacks a durable Verse replacement graph. - Done: extend run checkpoint commits and smoke coverage to carry typed @@ -1726,7 +1731,9 @@ group mutation and checkpoint. Inventory double-click transfer follows the same rule, and drag/drop placement now shares that commit family: UI requests cargo/equipment movement, gameplay owns the move and queues the checkpoint. Trade purchases follow the same rule: UI requests the purchase, gameplay owns -credit changes, cargo transfer, ship creation, and checkpoint. Any +credit changes, cargo transfer, ship creation, and checkpoint. Runtime loadout +restore follows the same rule: UI requests restoration, gameplay owns +instantiation, credits, dock assignment, current entity, and checkpoint. Any predicate that still needs legacy DTO objects must earn that dependency by using behavior objects or simulation-only methods that typed facets do not yet expose.