From f58e738cf8d7d9471664ca282998cdfa8719cf63 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 16 Jun 2026 02:57:45 +0100 Subject: [PATCH] Route trade purchases through checkpoints --- .voidbot/state/aetheria.cc | Bin 143398 -> 143633 bytes Aetheria.State.Verify/Program.cs | 60 ++++++++++++ Assets/Scripts/Gameplay/ActionGameManager.cs | 70 ++++++++++++++ Assets/Scripts/UI/Menu/TradeMenu.cs | 94 ++++++------------- docs/aetheria-perfect-machine-map.md | 9 +- 5 files changed, 169 insertions(+), 64 deletions(-) diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index bf2f2765aeaa342013ce60cc96be00e143565821..0366bb887bc05ea4ed1f2c6516a8b51bd459ca87 100644 GIT binary patch delta 993 zcmZvbO-NKx6vul{^9v;fHOtB)IZ2@7x1&Z-Nr($eA~&|W^WL4Ahi~p3?}s`osa>?H zw|M(B7eOFM7_^8M(ZWqZ5Q0htfvZASeL!^H9j6RhzH|Tge}3ou-(C4oasN}r>WW=C zFfh_R7*7oLhIKK!F?eO)J17nH^+cspfi6+y1=Ud4(`GjX6{RgUyjwe8`gA+o_+nM9 z-Tl5j8-D#|H3YjK7urW}3gS}8Y2jpuq>$B$D33E>d>)e2=PWHj@c@3DUEJI?;t9$$ z9z%3S1xTG#f;dWoBdF^s*#Y>?WQq#;$JYgih&1m2F2LbTDxsqr#kMZUjF*$(dV)I2 z&tCvffuOURN>p8TBBHTYlBE-b5o{VC<=L#K49?^MdIyjPTMaFIo*{7Ne_R0%06f<3(@tC_JIYvwi z@S|)QdQJ^NR|lTajj5HET!cqo+${+zstXAe$x)5P9p-pu#&-pAYgk>2{2e!XR$C`?oe zQ@LVsdTGA9F7iGN})H zAG@;G2tyBSLELd$@BpNuo(Fy#fwE@+2(uw^U{isX@)~0xIneHisnqjS5QGAufogF^ zOY2%dqy#HeGIk=VA9&i;p*hxto{fkVm~KV>=Y}LOhHDCNE#MOEXkgtK&g{Lr!kMBh zL|ATs;GsUd(dMEt2uUA0P79M*s`}ZjCic=|OITHr6HO%Ua(FPd`N9*QZ5oT4nxPgw`KQi>-nLuanY@$>NklC$E zcZ}hImXF50ql5WMp;Vd3=gQNCrRR#c8gi!f_pWi(*#7+e&uZ(sXFPq|-&u&lWOFBV z`grHo{GA$#CeN844tAlO> !actionGameManager.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingCommits.Length > 0) + { + throw new InvalidOperationException( + "Trade purchase no longer has a gameplay-owned checkpoint commit primitive: " + + string.Join(", ", missingCommits)); + } + + var tradeMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "TradeMenu.cs"); + var tradeMenu = File.Exists(tradeMenuPath) + ? File.ReadAllText(tradeMenuPath) + : throw new InvalidOperationException("Cannot verify trade purchase authority; TradeMenu.cs is missing."); + + var forbiddenSymbols = new[] + { + "GameManager.Credits -=", + "Inventory.TryTransferItem(", + "new Ship(", + ".SetParent(GameManager.DockedEntity)" + }; + + var hits = File.ReadLines(tradeMenuPath) + .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, tradeMenuPath)}:{line.LineNumber}: {line.Line.Trim()}") + .ToArray(); + + if (hits.Length > 0) + { + throw new InvalidOperationException( + "TradeMenu still owns direct purchase mutation: " + + string.Join("; ", hits)); + } + + if (!tradeMenu.Contains("GameManager.CommitTradePurchase", StringComparison.Ordinal)) + { + throw new InvalidOperationException("TradeMenu no longer routes purchases 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 c62f8ed57..a44dde53c 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -1675,6 +1675,76 @@ public class ActionGameManager : MonoBehaviour return true; } + public bool CommitTradePurchase( + EquippedCargoBay stationInventory, + EquippedCargoBay targetCargo, + CraftedItemInstance item, + int price, + bool createsDockedShip) + { + if (item == null || + price < 0 || + price > Credits) + { + return false; + } + + if (createsDockedShip) + { + if (!(item is EquippableItem hull) || + Zone == null || + DockedEntity == null) + { + return false; + } + + var ship = new Ship(ItemManager, Zone, hull, NewEntitySettings) { IsPlayerShip = true }; + ship.SetParent(DockedEntity); + Credits -= price; + QueueRunCheckpoint("trade-purchase"); + return true; + } + + if (stationInventory == null || + targetCargo == null || + !stationInventory.TryTransferItem(targetCargo, item)) + { + return false; + } + + Credits -= price; + QueueRunCheckpoint("trade-purchase"); + return true; + } + + public bool CommitTradePurchase( + EquippedCargoBay stationInventory, + EquippedCargoBay targetCargo, + SimpleCommodity item, + int quantity, + int unitPrice) + { + if (stationInventory == null || + targetCargo == null || + item == null || + quantity <= 0 || + unitPrice < 0) + { + return false; + } + + var totalPrice = (long)quantity * unitPrice; + if (totalPrice > Credits || totalPrice > int.MaxValue) + return false; + + if (!stationInventory.TryTransferItem(targetCargo, item, quantity)) + return false; + + Credits -= (int)totalPrice; + QueueRunCheckpoint("trade-purchase"); + return true; + } + public void CommitEquippedItemOverrideShutdown(EquippedItem item, bool enabled) { if (item?.EquippableItem == null) diff --git a/Assets/Scripts/UI/Menu/TradeMenu.cs b/Assets/Scripts/UI/Menu/TradeMenu.cs index f87a29287..4da0c7fb3 100644 --- a/Assets/Scripts/UI/Menu/TradeMenu.cs +++ b/Assets/Scripts/UI/Menu/TradeMenu.cs @@ -528,41 +528,27 @@ public class TradeMenu : MonoBehaviour } var price = GetTypedTradePrice(item, typedItem); - if (price < GameManager.Credits) + var isShipHull = !string.IsNullOrWhiteSpace(typedItem?.HullType); + if (isShipHull && + !string.Equals(typedItem.HullType, nameof(HullType.Ship), StringComparison.Ordinal)) { - if (!string.IsNullOrWhiteSpace(typedItem?.HullType)) - { - if (!string.Equals(typedItem.HullType, nameof(HullType.Ship), StringComparison.Ordinal)) - throw new ArgumentException("Attempted to buy non-ship hull from station, WTF are you doing?!"); - - var ship = new Ship(GameManager.ItemManager, GameManager.Zone, item as EquippableItem, GameManager.NewEntitySettings) { IsPlayerShip = true }; - ship.SetParent(GameManager.DockedEntity); - - GameManager.Credits -= price; - UpdateCreditsLabel(); - } - else if (Inventory.TryTransferItem(_targetCargo, item)) - { - GameManager.Credits -= price; - UpdateCreditsLabel(); - } - else - { - Dialog.Clear(); - Dialog.Title.text = "Unable to buy: Insufficient Cargo Space!"; - Dialog.Show(); - Dialog.MoveToCursor(); - return; - } - } - else - { - Dialog.Clear(); - Dialog.Title.text = "Unable to buy: Insufficient Credits!"; - Dialog.Show(); - Dialog.MoveToCursor(); + ShowUnableToBuy("Unsupported hull purchase!"); return; } + + if (price > GameManager.Credits) + { + ShowUnableToBuy("Insufficient Credits!"); + return; + } + + if (!GameManager.CommitTradePurchase(Inventory, _targetCargo, item, price, isShipHull)) + { + ShowUnableToBuy(isShipHull ? "Unable to create ship!" : "Insufficient Cargo Space!"); + return; + } + + UpdateCreditsLabel(); } private void Buy(SimpleCommodity simpleCommodity, int quantity) @@ -574,40 +560,22 @@ public class TradeMenu : MonoBehaviour return; } - var maxStack = typedItem.MaxStack > 0 ? typedItem.MaxStack : 1; var price = typedItem.Price; - // Up-rounded integer division from https://stackoverflow.com/a/503201 - int lots = (quantity - 1) / maxStack + 1; - int remaining = quantity; - for (int i = 0; i < lots; i++) + var clampedQuantity = min(quantity, simpleCommodity.Quantity); + var totalPrice = (long)clampedQuantity * price; + if (totalPrice > GameManager.Credits) { - int q = min(remaining, maxStack); - if (q * price < GameManager.Credits) - { - if (Inventory.TryTransferItem(_targetCargo, simpleCommodity, quantity)) - { - GameManager.Credits -= q * price; - UpdateCreditsLabel(); - remaining -= q; - } - else - { - Dialog.Clear(); - Dialog.Title.text = "Unable to buy: Insufficient Cargo Space!"; - Dialog.Show(); - Dialog.MoveToCursor(); - return; - } - } - else - { - Dialog.Clear(); - Dialog.Title.text = "Unable to buy: Insufficient Credits!"; - Dialog.Show(); - Dialog.MoveToCursor(); - return; - } + ShowUnableToBuy("Insufficient Credits!"); + return; } + + if (!GameManager.CommitTradePurchase(Inventory, _targetCargo, simpleCommodity, clampedQuantity, price)) + { + ShowUnableToBuy("Insufficient Cargo Space!"); + return; + } + + UpdateCreditsLabel(); } private int GetTypedTradePrice(CraftedItemInstance item, AetheriaRuntimeCatalogItem typedItem) diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 5c65f3744..716ded19c 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -1004,6 +1004,11 @@ First Aetheria surfaces to publish: - Done: move trade menu buy price, ship-hull classification, and simple commodity stack-size decisions onto typed catalog rows; inventory transfer and ship construction remain the runtime mutation owners. + - Done: route trade purchase mutation through gameplay-owned checkpoint + commits. `TradeMenu` still presents typed trade rows and calculates display + prices, but no longer subtracts credits, transfers purchased cargo, or + constructs purchased ships directly. `ActionGameManager.CommitTradePurchase` + owns the purchase mutation and queues a typed run checkpoint. - Done: move TradeMenu dynamic behavior columns onto typed behavior payload fields; the menu no longer hydrates `ItemData` or `BehaviorData` for row display, filtering, sorting, or buy decisions. @@ -1719,7 +1724,9 @@ 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, and drag/drop placement now shares that commit family: UI requests -cargo/equipment movement, gameplay owns the move and queues the checkpoint. Any +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 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.