diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 5d18650ca..7aa175586 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 b1007f4c4..772ba2cfc 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -17,6 +17,7 @@ RequirePropertiesPanelReadOnlyInspector(root); RequireRuntimeSimulationTuningCommits(root); RequireHullConductivityCommitAuthority(root); RequireInventoryEntityRenameCommitAuthority(root); +RequireWeaponGroupCommitAuthority(root); await using var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-state-verify"); @@ -463,6 +464,7 @@ Console.WriteLine("PropertiesPanel inspector authority: reflection inspection is 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"); Console.WriteLine("Inventory entity rename authority: UI rename flows through gameplay checkpoint commits"); +Console.WriteLine("Weapon group authority: UI assignment flows through gameplay checkpoint commits"); static void RequireCount(AetheriaMigrationLedger ledger, string documentType, int actual) { @@ -871,6 +873,50 @@ static void RequireInventoryEntityRenameCommitAuthority(string root) } } +static void RequireWeaponGroupCommitAuthority(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 weapon group authority; ActionGameManager.cs is missing."); + + if (!actionGameManager.Contains("CommitWeaponGroupMembership", StringComparison.Ordinal) || + !actionGameManager.Contains("QueueRunCheckpoint(\"weapon-group-membership\")", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "Weapon group membership no longer has a gameplay-owned checkpoint commit primitive."); + } + + var assignmentPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "WeaponGroupAssignment.cs"); + var assignment = File.Exists(assignmentPath) + ? File.ReadAllText(assignmentPath) + : throw new InvalidOperationException("Cannot verify weapon group authority; WeaponGroupAssignment.cs is missing."); + + var forbiddenSymbols = new[] + { + ".WeaponGroups[i1].items.Add", + ".WeaponGroups[i1].items.Remove", + ".WeaponGroups[i1].weapons.Add", + ".WeaponGroups[i1].weapons.Remove" + }; + + var hits = forbiddenSymbols + .Where(symbol => assignment.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (hits.Length > 0) + { + throw new InvalidOperationException( + "WeaponGroupAssignment still mutates weapon groups directly: " + + string.Join(", ", hits)); + } + + if (!assignment.Contains("CommitWeaponGroupMembership", StringComparison.Ordinal)) + { + throw new InvalidOperationException("WeaponGroupAssignment no longer routes membership changes 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 34585bae8..8c51d2153 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -1424,6 +1424,37 @@ public class ActionGameManager : MonoBehaviour QueueRunCheckpoint("entity-name"); } + public bool CommitWeaponGroupMembership(EquippedItem item, int groupIndex, bool assigned) + { + if (item?.Entity?.WeaponGroups == null || + groupIndex < 0 || + groupIndex >= item.Entity.WeaponGroups.Length) + { + return false; + } + + var weapon = item.GetBehavior(); + if (weapon == null) + return false; + + var group = item.Entity.WeaponGroups[groupIndex]; + if (assigned) + { + if (!group.items.Contains(item)) + group.items.Add(item); + if (!group.weapons.Contains(weapon)) + group.weapons.Add(weapon); + } + else + { + group.items.Remove(item); + group.weapons.Remove(weapon); + } + + QueueRunCheckpoint("weapon-group-membership"); + return true; + } + public void CommitEquippedItemOverrideShutdown(EquippedItem item, bool enabled) { if (item?.EquippableItem == null) diff --git a/Assets/Scripts/UI/Menu/WeaponGroupAssignment.cs b/Assets/Scripts/UI/Menu/WeaponGroupAssignment.cs index ddd453d88..874a96368 100644 --- a/Assets/Scripts/UI/Menu/WeaponGroupAssignment.cs +++ b/Assets/Scripts/UI/Menu/WeaponGroupAssignment.cs @@ -33,18 +33,9 @@ public class WeaponGroupAssignment : MonoBehaviour group.Label.color = groupContainsItem() ? ActiveColor : InactiveColor; group.Button.onClick.AddListener(() => { - if (groupContainsItem()) - { - item.Entity.WeaponGroups[i1].items.Remove(item); - item.Entity.WeaponGroups[i1].weapons.Remove(weapon); - group.Label.color = InactiveColor; - } - else - { - item.Entity.WeaponGroups[i1].items.Add(item); - item.Entity.WeaponGroups[i1].weapons.Add(weapon); - group.Label.color = ActiveColor; - } + var assigned = !groupContainsItem(); + if (ActionGameManager.Instance.CommitWeaponGroupMembership(item, i1, assigned)) + group.Label.color = assigned ? ActiveColor : InactiveColor; }); group.BeginDragTrigger.OnBeginDragAsObservable() diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index aca08c753..949277752 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -1384,6 +1384,11 @@ First Aetheria surfaces to publish: `ActionGameManager.CommitEntityName`. `InventoryPanel` may collect the name in a dialog, but gameplay owns the entity `Name` mutation and queues the typed entity snapshot checkpoint. + - Done: route weapon group membership changes through + `ActionGameManager.CommitWeaponGroupMembership`. `WeaponGroupAssignment` + remains a uGUI control for now, but it no longer adds/removes items or + weapons from `Entity.WeaponGroups` directly; gameplay owns the membership + mutation and typed 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. @@ -1703,7 +1708,9 @@ gameplay-owned commit methods that queue typed checkpoint state; direct UI mutation of entity/item/behavior settings is obsolete authority. Hull conductivity changes follow the same rule: UI requests the toggle, gameplay owns the grid mutation and checkpoint. Entity renames follow the same rule: UI -collects text, gameplay owns the name mutation and checkpoint. Any +collects text, gameplay owns the name mutation and checkpoint. Weapon group +assignment follows the same rule: UI requests membership, gameplay owns the +group mutation 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.