diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index fe58beeda..fc4964d6f 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 3bee98f0e..9f837560f 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -19,6 +19,7 @@ RequireHullConductivityCommitAuthority(root); RequireInventoryEntityRenameCommitAuthority(root); RequireWeaponGroupCommitAuthority(root); RequireInventoryDoubleClickTransferCommitAuthority(root); +RequireLootPickupCommitAuthority(root); RequireTradePurchaseCommitAuthority(root); RequireInventoryLoadoutRestoreCommitAuthority(root); RequireDockedCurrentShipCommitAuthority(root); @@ -1106,6 +1107,61 @@ static void RequireTradePurchaseCommitAuthority(string root) } } +static void RequireLootPickupCommitAuthority(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 loot pickup authority; ActionGameManager.cs is missing."); + + var requiredSymbols = new[] + { + "CommitLootPickup", + "QueueRunCheckpoint(\"loot-pickup\")", + ".TryStore(item)" + }; + + var missingSymbols = requiredSymbols + .Where(symbol => !actionGameManager.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingSymbols.Length > 0) + { + throw new InvalidOperationException( + "Loot pickup no longer has a gameplay-owned checkpoint commit primitive: " + + string.Join(", ", missingSymbols)); + } + + var shieldManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ShieldManager.cs"); + var shieldManager = File.Exists(shieldManagerPath) + ? File.ReadAllText(shieldManagerPath) + : throw new InvalidOperationException("Cannot verify loot pickup authority; ShieldManager.cs is missing."); + + var forbiddenSymbols = new[] + { + ".TryStore(", + ".CargoBays.Any(" + }; + + var hits = File.ReadLines(shieldManagerPath) + .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, shieldManagerPath)}:{line.LineNumber}: {line.Line.Trim()}") + .ToArray(); + + if (hits.Length > 0) + { + throw new InvalidOperationException( + "ShieldManager still owns direct loot cargo mutation: " + + string.Join("; ", hits)); + } + + if (!shieldManager.Contains("ActionGameManager.Instance.CommitLootPickup", StringComparison.Ordinal)) + { + throw new InvalidOperationException("ShieldManager no longer routes loot pickup through ActionGameManager."); + } +} + static void RequireInventoryLoadoutRestoreCommitAuthority(string root) { var actionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs"); diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index c065e065f..7acac87c9 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -1570,6 +1570,23 @@ public class ActionGameManager : MonoBehaviour return true; } + public bool CommitLootPickup(Entity entity, ItemInstance item) + { + if (entity == null || item == null) + return false; + + foreach (var cargoBay in entity.CargoBays) + { + if (!cargoBay.TryStore(item)) + continue; + + QueueRunCheckpoint("loot-pickup"); + return true; + } + + return false; + } + public bool CommitCargoItemTransfer( EquippedCargoBay origin, EquippedCargoBay destination, diff --git a/Assets/Scripts/Gameplay/ShieldManager.cs b/Assets/Scripts/Gameplay/ShieldManager.cs index fc24c8b12..447b4b5b5 100644 --- a/Assets/Scripts/Gameplay/ShieldManager.cs +++ b/Assets/Scripts/Gameplay/ShieldManager.cs @@ -25,7 +25,8 @@ public class ShieldManager : MonoBehaviour var mine = other.collider.GetComponent(); if (itemPickup) { - if (Entity.CargoBays.Any(c => c.TryStore(itemPickup.Item))) + if (ActionGameManager.Instance != null && + ActionGameManager.Instance.CommitLootPickup(Entity, itemPickup.Item)) { // TODO: Pickup notification! Destroy(itemPickup.gameObject); diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 551c58a34..3be2d7a57 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -1409,6 +1409,11 @@ First Aetheria surfaces to publish: `ActionGameManager.CommitDockedCurrentShip`. `InventoryPanel` may request a selected docked player ship and update the button color, but gameplay owns `CurrentEntity`, `DockingBay.DockedShip`, and the typed checkpoint. + - Done: route loot pickup through `ActionGameManager.CommitLootPickup`. + `ShieldManager` may detect the collision and destroy the world pickup + after a successful commit, but it no longer stores items into cargo bays + directly; gameplay owns the cargo mutation and queues the typed run + checkpoint. - Replace the next concrete uGUI screen with an Eve-owned surface. - Move the staged packages into the Eve repo once its worktree is clean, then import them back into Aetheria from Eve instead of carrying a local copy. @@ -1742,7 +1747,9 @@ 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. Docked current-ship selection follows the same rule: UI requests selection, gameplay -owns `CurrentEntity`, `DockingBay.DockedShip`, and checkpoint. Any +owns `CurrentEntity`, `DockingBay.DockedShip`, and checkpoint. Loot pickup +follows the same rule: collision code reports the pickup opportunity, gameplay +owns cargo storage 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.