diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index c152c0d3a..bf2f2765a 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 5f097e4d9..4ae7f02d4 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -466,7 +466,7 @@ Console.WriteLine("Runtime simulation tuning authority: UI writes flow through g 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"); -Console.WriteLine("Inventory double-click transfer authority: UI transfer requests flow through gameplay checkpoint commits"); +Console.WriteLine("Inventory transfer authority: UI transfer and drag/drop requests flow through gameplay checkpoint commits"); static void RequireCount(AetheriaMigrationLedger ledger, string documentType, int actual) { @@ -931,9 +931,11 @@ static void RequireInventoryDoubleClickTransferCommitAuthority(string root) "CommitCargoItemTransfer", "CommitCargoItemEquip", "CommitEquippedItemStore", + "CommitEquippedItemEquip", "QueueRunCheckpoint(\"cargo-item-transfer\")", "QueueRunCheckpoint(\"cargo-item-equip\")", - "QueueRunCheckpoint(\"equipped-item-store\")" + "QueueRunCheckpoint(\"equipped-item-store\")", + "QueueRunCheckpoint(\"equipped-item-equip\")" }; var missingCommits = requiredCommits @@ -947,6 +949,11 @@ static void RequireInventoryDoubleClickTransferCommitAuthority(string root) string.Join(", ", missingCommits)); } + var inventoryPanelPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "InventoryPanel.cs"); + var inventoryPanel = File.Exists(inventoryPanelPath) + ? File.ReadAllText(inventoryPanelPath) + : throw new InvalidOperationException("Cannot verify inventory transfer authority; InventoryPanel.cs is missing."); + var inventoryMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "InventoryMenu.cs"); var inventoryMenu = File.Exists(inventoryMenuPath) ? File.ReadAllText(inventoryMenuPath) @@ -956,19 +963,23 @@ static void RequireInventoryDoubleClickTransferCommitAuthority(string root) { ".DropItem(", ".CargoBay.Remove(", - ".TryUnequip(" + ".TryUnequip(", + ".TryEquip(", + ".TryStore(", + ".OriginInventory.Remove(" }; - var hits = File.ReadLines(inventoryMenuPath) - .Select((line, index) => new { LineNumber = index + 1, Line = line }) + var hits = new[] { inventoryMenuPath, inventoryPanelPath } + .SelectMany(path => File.ReadLines(path) + .Select((line, index) => new { Path = path, LineNumber = index + 1, Line = line })) .Where(line => forbiddenSymbols.Any(symbol => line.Line.Contains(symbol, StringComparison.Ordinal))) - .Select(line => $"{Path.GetRelativePath(root, inventoryMenuPath)}:{line.LineNumber}: {line.Line.Trim()}") + .Select(line => $"{Path.GetRelativePath(root, line.Path)}:{line.LineNumber}: {line.Line.Trim()}") .ToArray(); if (hits.Length > 0) { throw new InvalidOperationException( - "InventoryMenu double-click transfer still owns direct inventory mutation: " + + "Inventory UI still owns direct inventory mutation: " + string.Join("; ", hits)); } @@ -976,17 +987,20 @@ static void RequireInventoryDoubleClickTransferCommitAuthority(string root) { "GameManager.CommitCargoItemTransfer", "GameManager.CommitCargoItemEquip", - "GameManager.CommitEquippedItemStore" + "GameManager.CommitEquippedItemStore", + "GameManager.CommitEquippedItemEquip" }; var missingUiCalls = requiredUiCalls - .Where(symbol => !inventoryMenu.Contains(symbol, StringComparison.Ordinal)) + .Where(symbol => + !inventoryMenu.Contains(symbol, StringComparison.Ordinal) && + !inventoryPanel.Contains(symbol, StringComparison.Ordinal)) .ToArray(); if (missingUiCalls.Length > 0) { throw new InvalidOperationException( - "InventoryMenu no longer routes double-click transfers through ActionGameManager: " + + "Inventory UI no longer routes transfer requests through ActionGameManager: " + string.Join(", ", missingUiCalls)); } } diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 99c33b95a..c62f8ed57 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -1493,6 +1493,61 @@ public class ActionGameManager : MonoBehaviour return true; } + public bool CommitCargoItemTransfer( + EquippedCargoBay origin, + EquippedCargoBay destination, + ItemInstance item, + int2 destinationPosition) + { + if (origin == null || + destination == null || + item == null || + !origin.Cargo.ContainsKey(item) || + !destination.ItemFits(item, destinationPosition)) + { + return false; + } + + if (origin == destination) + { + var originalPosition = origin.Cargo[item]; + origin.Remove(item); + if (!destination.TryStore(item, destinationPosition)) + { + origin.TryStore(item, originalPosition); + return false; + } + + QueueRunCheckpoint("cargo-item-transfer"); + return true; + } + + if (item is SimpleCommodity commodity) + { + var quantity = commodity.Quantity; + var transferItem = new SimpleCommodity + { + Reference = commodity.Reference, + Quantity = quantity, + Rotation = commodity.Rotation + }; + + if (!destination.TryStore(transferItem, destinationPosition)) + return false; + + origin.Remove(commodity, quantity); + QueueRunCheckpoint("cargo-item-transfer"); + return true; + } + + if (!destination.TryStore(item, destinationPosition)) + return false; + + origin.Remove(item); + QueueRunCheckpoint("cargo-item-transfer"); + return true; + } + public bool CommitCargoItemEquip(EquippedCargoBay origin, Entity destination, EquippableItem item) { if (origin == null || @@ -1512,6 +1567,29 @@ public class ActionGameManager : MonoBehaviour return true; } + public bool CommitCargoItemEquip( + EquippedCargoBay origin, + Entity destination, + EquippableItem item, + int2 destinationPosition) + { + if (origin == null || + destination == null || + item == null || + !origin.Cargo.ContainsKey(item) || + !destination.ItemFits(item, destinationPosition)) + { + return false; + } + + if (!destination.TryEquip(item, destinationPosition)) + return false; + + origin.Remove(item); + QueueRunCheckpoint("cargo-item-equip"); + return true; + } + public bool CommitEquippedItemStore(Entity origin, EquippedItem equippedItem, EquippedCargoBay destination) { if (origin == null || @@ -1537,6 +1615,66 @@ public class ActionGameManager : MonoBehaviour return true; } + public bool CommitEquippedItemStore( + Entity origin, + EquippedItem equippedItem, + EquippedCargoBay destination, + int2 destinationPosition) + { + if (origin == null || + equippedItem?.EquippableItem == null || + destination == null || + !origin.Equipment.Contains(equippedItem) || + !destination.ItemFits(equippedItem.EquippableItem, destinationPosition)) + { + return false; + } + + var originalPosition = equippedItem.Position; + var item = origin.TryUnequip(equippedItem); + if (item == null) + return false; + + if (!destination.TryStore(item, destinationPosition)) + { + origin.TryEquip(item, originalPosition); + return false; + } + + QueueRunCheckpoint("equipped-item-store"); + return true; + } + + public bool CommitEquippedItemEquip( + Entity origin, + EquippedItem equippedItem, + Entity destination, + int2 destinationPosition) + { + if (origin == null || + equippedItem?.EquippableItem == null || + destination == null || + !origin.Equipment.Contains(equippedItem) || + !destination.ItemFits(equippedItem.EquippableItem, destinationPosition)) + { + return false; + } + + var originalPosition = equippedItem.Position; + var item = origin.TryUnequip(equippedItem); + if (item == null) + return false; + + if (!destination.TryEquip(item, destinationPosition)) + { + origin.TryEquip(item, originalPosition); + return false; + } + + QueueRunCheckpoint("equipped-item-equip"); + return true; + } + public void CommitEquippedItemOverrideShutdown(EquippedItem item, bool enabled) { if (item?.EquippableItem == null) diff --git a/Assets/Scripts/UI/Menu/InventoryPanel.cs b/Assets/Scripts/UI/Menu/InventoryPanel.cs index 417e1a8d4..327fbc70e 100644 --- a/Assets/Scripts/UI/Menu/InventoryPanel.cs +++ b/Assets/Scripts/UI/Menu/InventoryPanel.cs @@ -438,27 +438,10 @@ public class InventoryPanel : MonoBehaviour, IPointerClickHandler GameManager.RegisterDragTarget(drag => { //Debug.Log("Entity Drag Callback"); - if (GameManager.DragObject is EquippedItemDragObject equippedItemDragObject) - { - if(equippedItemDragObject.OriginEntity.TryUnequip(equippedItemDragObject.EquippedItem) == null) - { - // AkSoundEngine.PostEvent("UI_Fail", gameObject); - // TODO: SFX: Fail - Dialog.Clear(); - Dialog.Title.text = "Unable to move item!"; - Dialog.AddProperty("Verify that cargo bays are empty before un-equipping them."); - Dialog.Show(); - Dialog.MoveToCursor(); - } - // else - // AkSoundEngine.PostEvent("Unequip", gameObject); - // TODO: SFX: Unequip - } - else if (GameManager.DragObject is ItemInstanceDragObject itemInstanceDragObject) - itemInstanceDragObject.OriginInventory.Remove(itemInstanceDragObject.Item); - FakeOccupancy = null; - var success = entity.TryEquip(equippableItem, placementPosition); + var success = CommitDraggedItemToEntity(drag, entity, placementPosition); + if (!success) + ShowUnableToMoveItemDialog(); RefreshCells(); // TODO: SFX: Equip return success; @@ -658,26 +641,12 @@ public class InventoryPanel : MonoBehaviour, IPointerClickHandler GameManager.RegisterDragTarget(drag => { //Debug.Log("Inventory Drag Callback"); - if (GameManager.DragObject is EquippedItemDragObject equippedItemDragObject) - { - if(equippedItemDragObject.OriginEntity.TryUnequip(equippedItemDragObject.EquippedItem) == null) - { - // TODO: SFX: Fail - Dialog.Clear(); - Dialog.Title.text = "Unable to move item!"; - Dialog.AddProperty("Verify that cargo bays are empty before un-equipping them."); - Dialog.Show(); - Dialog.MoveToCursor(); - } - //else - // TODO: SFX: Unequip - } - else if (GameManager.DragObject is ItemInstanceDragObject itemInstanceDragObject) - itemInstanceDragObject.OriginInventory.Remove(itemInstanceDragObject.Item); - cargo.TryStore(item, placementPosition); FakeOccupancy = null; + var success = CommitDraggedItemToCargo(drag, cargo, placementPosition); + if (!success) + ShowUnableToMoveItemDialog(); // TODO: SFX: Drop - return true; + return success; }); } }); @@ -937,34 +906,55 @@ public class InventoryPanel : MonoBehaviour, IPointerClickHandler return ActionGameManager.RuntimeCatalog?.FindItem(item?.ItemKey ?? ""); } - public bool CanDropItem(ItemInstance item) + private bool CommitDraggedItemToEntity(DragObject drag, Entity destination, int2 destinationPosition) { - if (_displayedEntity != null && item is EquippableItem equippableItem) + switch (drag) { - return _displayedEntity.TryFindSpace(equippableItem, out _); + case EquippedItemDragObject equippedItemDragObject: + return GameManager.CommitEquippedItemEquip( + equippedItemDragObject.OriginEntity, + equippedItemDragObject.EquippedItem, + destination, + destinationPosition); + case ItemInstanceDragObject itemInstanceDragObject when itemInstanceDragObject.Item is EquippableItem equippableItem: + return GameManager.CommitCargoItemEquip( + itemInstanceDragObject.OriginInventory, + destination, + equippableItem, + destinationPosition); + default: + return false; } - - if (_displayedCargo != null) - { - return _displayedCargo.TryFindSpace(item); - } - - return false; } - public bool DropItem(ItemInstance item) + private bool CommitDraggedItemToCargo(DragObject drag, EquippedCargoBay destination, int2 destinationPosition) { - if (_displayedEntity != null && item is EquippableItem equippableItem) + switch (drag) { - return _displayedEntity.TryEquip(equippableItem); + case EquippedItemDragObject equippedItemDragObject: + return GameManager.CommitEquippedItemStore( + equippedItemDragObject.OriginEntity, + equippedItemDragObject.EquippedItem, + destination, + destinationPosition); + case ItemInstanceDragObject itemInstanceDragObject: + return GameManager.CommitCargoItemTransfer( + itemInstanceDragObject.OriginInventory, + destination, + itemInstanceDragObject.Item, + destinationPosition); + default: + return false; } + } - if(_displayedCargo != null) - { - return _displayedCargo.TryStore(item); - } - - return false; + private void ShowUnableToMoveItemDialog() + { + Dialog.Clear(); + Dialog.Title.text = "Unable to move item!"; + Dialog.AddProperty("Verify that cargo bays are empty before un-equipping them."); + Dialog.Show(); + Dialog.MoveToCursor(); } public Subject OnBackgroundClick = new Subject(); diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 6593efd84..5c65f3744 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -1389,13 +1389,13 @@ First Aetheria surfaces to publish: 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. - - Done: route inventory double-click transfer through gameplay-owned - checkpoint commits. `InventoryMenu` no longer drops items, removes cargo, - or unequips gear directly for the double-click path; it asks - `ActionGameManager` to commit cargo-to-cargo, cargo-to-equipment, or - equipment-to-cargo movement and then refreshes display. The deeper - `InventoryPanel` drag/drop path still has direct mutation and is the next - inventory cut. + - Done: route inventory double-click transfer and drag/drop placement through + gameplay-owned checkpoint commits. `InventoryMenu` and `InventoryPanel` no + longer drop items, remove cargo, unequip gear, equip gear, or store cargo + directly for those UI paths; they ask `ActionGameManager` to commit + cargo-to-cargo, cargo-to-equipment, equipment-to-cargo, or + equipment-to-equipment movement and then refresh display. uGUI still owns + the presentation shell for now, not the inventory mutation. - 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. @@ -1718,9 +1718,8 @@ the grid mutation and checkpoint. Entity renames follow the same rule: UI 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. Inventory double-click transfer follows the same -rule: UI requests cargo/equipment movement, gameplay owns the move and queues -the checkpoint. Drag/drop inventory placement is not pure yet and should be cut -through the same commit primitive family next. Any +rule, and drag/drop placement now shares that commit family: UI requests +cargo/equipment movement, gameplay owns the move and queues the 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.