From be2755db656021e184a677557f359881fb60241b Mon Sep 17 00:00:00 2001 From: Krarilotus <51748815+Krarilotus@users.noreply.github.com> Date: Sat, 5 Sep 2026 10:46:40 +0200 Subject: [PATCH 1/3] Reimplement native market pricing and goods trading --- docs/wiki.rst | 1 + docs/wiki/market-trading.md | 44 +++++++++++++++++++ src/OpenSHC/AI/AICState/buyGoods.cpp | 29 ++++++++++++ src/OpenSHC/AI/AICState/sellGoods.cpp | 28 ++++++++++++ .../Game/GameStateStructures/getBuyPrice.cpp | 14 ++++++ .../Game/GameStateStructures/getSellPrice.cpp | 14 ++++++ 6 files changed, 130 insertions(+) create mode 100644 docs/wiki/market-trading.md create mode 100644 src/OpenSHC/AI/AICState/buyGoods.cpp create mode 100644 src/OpenSHC/AI/AICState/sellGoods.cpp create mode 100644 src/OpenSHC/Game/GameStateStructures/getBuyPrice.cpp create mode 100644 src/OpenSHC/Game/GameStateStructures/getSellPrice.cpp diff --git a/docs/wiki.rst b/docs/wiki.rst index ce0e938e..4f7b7024 100644 --- a/docs/wiki.rst +++ b/docs/wiki.rst @@ -29,6 +29,7 @@ For the recommended method of using SARIF files in Ghidra, see: The Game itself ------------------ - :doc:`Load balancing of the core game engine ` +- :doc:`Market pricing and goods trading ` - Game Mechanics - AI Behavior - Graphics and Sound Systems diff --git a/docs/wiki/market-trading.md b/docs/wiki/market-trading.md new file mode 100644 index 00000000..7ea7ce05 --- /dev/null +++ b/docs/wiki/market-trading.md @@ -0,0 +1,44 @@ +# Market pricing and goods trading + +These functions describe the native trading behavior used by automated market +callers. They preserve the original game rules; callers remain responsible for +deciding whether a trade should be attempted. + +| Function | Crusader 1.41 address | Behavior | +| --- | --- | --- | +| `GameStateStructures::getBuyPrice` | `0x004588A0` | Returns `(buyPrice / 5) * amount`. | +| `GameStateStructures::getSellPrice` | `0x00458910` | Returns `(salesPrice / 5) * amount`. | +| `AICState::buyGoods` | `0x004CC000` | Attempts resource storage, then deducts gold and displays the trade on success. | +| `AICState::sellGoods` | `0x004CBFA0` | Credits gold and final-result statistics, removes resources, then displays the trade. | + +## Details relevant to callers + +Both price functions read the price array through their `this` pointer (ECX in +the original assembly). The player argument is unused. Division happens before +multiplication, so a price of 14 and an amount of 5 produce 10, not 14. A +decompilation that substitutes a fixed global for `this` loses this behavior. + +`buyGoods` returns false when `processResourceGain` fails. In that case it does +not deduct gold or display a trade. It contains no affordability check of its +own. On success it subtracts the cost from both the player's gold resource and +`marketGold`. + +`sellGoods` adds the reward to those same two fields and to +`finalResults.finalGold[playerID]` before calling `processResourceLoss`. Its +trade visual uses the sell flag and a negative amount; the buy visual uses the +buy flag and a positive amount. Neither native function applies an Automarket +plugin fee. + +## Verification + +The implementations were compared against the Crusader executable with SHA256 +`3bb0a8c1e72331b3a30a5aa93ed94beca0081b476b04c1960e26d5b45387ac5a`, +using the project's MSVC 2005 SP1 x86 toolchain and RelWithDebInfo configuration. +Both price functions match at 100% in reccmp. The trade functions score 81.82% +(`sellGoods`) and 85.71% (`buyGoods`) with inactive resolvers. A separate +instruction check confirmed that every non-call instruction is byte-identical +and all six resolver call thunks target the corresponding original callees. + +This is a static binary comparison, not an in-game multiplayer test. Default +source selection and resolver activation remain separate from these +implementations. diff --git a/src/OpenSHC/AI/AICState/buyGoods.cpp b/src/OpenSHC/AI/AICState/buyGoods.cpp new file mode 100644 index 00000000..ee4c3b08 --- /dev/null +++ b/src/OpenSHC/AI/AICState/buyGoods.cpp @@ -0,0 +1,29 @@ +#include "../AICState.func.hpp" + +#include "OpenSHC/Game/GameStateStructures.func.hpp" +#include "OpenSHC/Map/Buildings/BuildingsState.func.hpp" + +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004CC000 + BOOLEnum AICState::buyGoods(int playerID, ResourceType resourceType, int amount) + { + int const cost = MACRO_CALL_MEMBER(Game::GameStateStructures_Func::getBuyPrice, DAT_GameState::ptr)( + playerID, resourceType, amount); + if (MACRO_CALL_MEMBER(Map::Buildings::BuildingsState_Func::processResourceGain, DAT_BuildingsState::ptr)( + playerID, resourceType, amount)) { + DAT_GameState::ptr->playerDataArray[playerID].currentResources[Game::Resources::RT_GOLD] -= cost; + DAT_GameState::ptr->playerDataArray[playerID].marketGold -= cost; + MACRO_CALL_MEMBER(Game::GameStateStructures_Func::displayPlayerTradeVisualEffect, DAT_GameState::ptr)( + playerID, 0, amount, resourceType); + return TRUE; + } + return FALSE; + } + +} +} diff --git a/src/OpenSHC/AI/AICState/sellGoods.cpp b/src/OpenSHC/AI/AICState/sellGoods.cpp new file mode 100644 index 00000000..81884e96 --- /dev/null +++ b/src/OpenSHC/AI/AICState/sellGoods.cpp @@ -0,0 +1,28 @@ +#include "../AICState.func.hpp" + +#include "OpenSHC/Game/GameStateStructures.func.hpp" +#include "OpenSHC/Map/Buildings/BuildingsState.func.hpp" + +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_GameState.hpp" +#include "OpenSHC/Globals/DAT_GameSynchronyState.hpp" + +namespace OpenSHC { +namespace AI { + + // FUNCTION: STRONGHOLDCRUSADER 0x004CBFA0 + void AICState::sellGoods(int playerID, ResourceType resourceType, int amount) + { + int const reward = MACRO_CALL_MEMBER(Game::GameStateStructures_Func::getSellPrice, DAT_GameState::ptr)( + playerID, resourceType, amount); + DAT_GameSynchronyState::ptr->finalResults.finalGold[playerID] += reward; + DAT_GameState::ptr->playerDataArray[playerID].currentResources[Game::Resources::RT_GOLD] += reward; + DAT_GameState::ptr->playerDataArray[playerID].marketGold += reward; + MACRO_CALL_MEMBER(Map::Buildings::BuildingsState_Func::processResourceLoss, DAT_BuildingsState::ptr)( + playerID, resourceType, amount, 0); + MACRO_CALL_MEMBER(Game::GameStateStructures_Func::displayPlayerTradeVisualEffect, DAT_GameState::ptr)( + playerID, 1, -amount, resourceType); + } + +} +} diff --git a/src/OpenSHC/Game/GameStateStructures/getBuyPrice.cpp b/src/OpenSHC/Game/GameStateStructures/getBuyPrice.cpp new file mode 100644 index 00000000..d78ddc50 --- /dev/null +++ b/src/OpenSHC/Game/GameStateStructures/getBuyPrice.cpp @@ -0,0 +1,14 @@ +#include "../GameStateStructures.func.hpp" + +namespace OpenSHC { +namespace Game { + + // FUNCTION: STRONGHOLDCRUSADER 0x004588A0 + int GameStateStructures::getBuyPrice(undefined4 playerID, int resourceType, int amount) + { + // playerID is unused; truncate the unit price before multiplying. + return (this->mapAndTime.buyAndSalesPriceArray[resourceType].buyPrice / 5) * amount; + } + +} +} diff --git a/src/OpenSHC/Game/GameStateStructures/getSellPrice.cpp b/src/OpenSHC/Game/GameStateStructures/getSellPrice.cpp new file mode 100644 index 00000000..7ddc9b50 --- /dev/null +++ b/src/OpenSHC/Game/GameStateStructures/getSellPrice.cpp @@ -0,0 +1,14 @@ +#include "../GameStateStructures.func.hpp" + +namespace OpenSHC { +namespace Game { + + // FUNCTION: STRONGHOLDCRUSADER 0x00458910 + int GameStateStructures::getSellPrice(int playerID, int resourceType, int amount) + { + // Preserve division before multiplication, including non-multiples of five. + return (this->mapAndTime.buyAndSalesPriceArray[resourceType].salesPrice / 5) * amount; + } + +} +} From 9edc0848a7a81f47d1814837a0ac0cabb4083c5f Mon Sep 17 00:00:00 2001 From: Krarilotus <51748815+Krarilotus@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:39:39 +0200 Subject: [PATCH 2/3] Address market reimplementation review and record matching status --- docs/wiki/market-trading.md | 45 +++++++------------ src/OpenSHC/AI/AICState/buyGoods.cpp | 4 +- src/OpenSHC/AI/AICState/sellGoods.cpp | 6 +-- .../Game/GameStateStructures/getBuyPrice.cpp | 1 - .../Game/GameStateStructures/getSellPrice.cpp | 1 - status/addresses-SHC-3BB0A8C1.txt | 8 ++-- 6 files changed, 24 insertions(+), 41 deletions(-) diff --git a/docs/wiki/market-trading.md b/docs/wiki/market-trading.md index 7ea7ce05..29c5b2b4 100644 --- a/docs/wiki/market-trading.md +++ b/docs/wiki/market-trading.md @@ -1,8 +1,7 @@ # Market pricing and goods trading -These functions describe the native trading behavior used by automated market -callers. They preserve the original game rules; callers remain responsible for -deciding whether a trade should be attempted. +The AI buys and sells resources using the game's price table. The trade routines +handle storage and gold accounting; the decision to trade is made by their callers. | Function | Crusader 1.41 address | Behavior | | --- | --- | --- | @@ -11,34 +10,20 @@ deciding whether a trade should be attempted. | `AICState::buyGoods` | `0x004CC000` | Attempts resource storage, then deducts gold and displays the trade on success. | | `AICState::sellGoods` | `0x004CBFA0` | Credits gold and final-result statistics, removes resources, then displays the trade. | -## Details relevant to callers +## Prices -Both price functions read the price array through their `this` pointer (ECX in -the original assembly). The player argument is unused. Division happens before -multiplication, so a price of 14 and an amount of 5 produce 10, not 14. A -decompilation that substitutes a fixed global for `this` loses this behavior. +Prices are stored for lots of five goods. The game divides that price by five +using integer division, then multiplies by the amount traded. For example, a +table price of 14 and a trade of five goods produce a price of 10. The player +argument does not affect the price calculation. -`buyGoods` returns false when `processResourceGain` fails. In that case it does -not deduct gold or display a trade. It contains no affordability check of its -own. On success it subtracts the cost from both the player's gold resource and -`marketGold`. +## Buying and selling -`sellGoods` adds the reward to those same two fields and to -`finalResults.finalGold[playerID]` before calling `processResourceLoss`. Its -trade visual uses the sell flag and a negative amount; the buy visual uses the -buy flag and a positive amount. Neither native function applies an Automarket -plugin fee. +A purchase first attempts to store the goods. If storage fails, it returns +without deducting gold or displaying a trade. The routine itself does not check +whether the player can afford the purchase. Successful purchases reduce both +the player's gold and their net market-gold total. -## Verification - -The implementations were compared against the Crusader executable with SHA256 -`3bb0a8c1e72331b3a30a5aa93ed94beca0081b476b04c1960e26d5b45387ac5a`, -using the project's MSVC 2005 SP1 x86 toolchain and RelWithDebInfo configuration. -Both price functions match at 100% in reccmp. The trade functions score 81.82% -(`sellGoods`) and 85.71% (`buyGoods`) with inactive resolvers. A separate -instruction check confirmed that every non-call instruction is byte-identical -and all six resolver call thunks target the corresponding original callees. - -This is a static binary comparison, not an in-game multiplayer test. Default -source selection and resolver activation remain separate from these -implementations. +A sale increases those two totals and the final-results gold statistic, then +removes the sold goods. Both successful purchases and sales display the trade +amount to the player. diff --git a/src/OpenSHC/AI/AICState/buyGoods.cpp b/src/OpenSHC/AI/AICState/buyGoods.cpp index ee4c3b08..7075afd5 100644 --- a/src/OpenSHC/AI/AICState/buyGoods.cpp +++ b/src/OpenSHC/AI/AICState/buyGoods.cpp @@ -16,8 +16,8 @@ namespace AI { playerID, resourceType, amount); if (MACRO_CALL_MEMBER(Map::Buildings::BuildingsState_Func::processResourceGain, DAT_BuildingsState::ptr)( playerID, resourceType, amount)) { - DAT_GameState::ptr->playerDataArray[playerID].currentResources[Game::Resources::RT_GOLD] -= cost; - DAT_GameState::ptr->playerDataArray[playerID].marketGold -= cost; + DAT_GameState::instance.playerDataArray[playerID].currentResources[Game::Resources::RT_GOLD] -= cost; + DAT_GameState::instance.playerDataArray[playerID].marketGold -= cost; MACRO_CALL_MEMBER(Game::GameStateStructures_Func::displayPlayerTradeVisualEffect, DAT_GameState::ptr)( playerID, 0, amount, resourceType); return TRUE; diff --git a/src/OpenSHC/AI/AICState/sellGoods.cpp b/src/OpenSHC/AI/AICState/sellGoods.cpp index 81884e96..5ebd21d6 100644 --- a/src/OpenSHC/AI/AICState/sellGoods.cpp +++ b/src/OpenSHC/AI/AICState/sellGoods.cpp @@ -15,9 +15,9 @@ namespace AI { { int const reward = MACRO_CALL_MEMBER(Game::GameStateStructures_Func::getSellPrice, DAT_GameState::ptr)( playerID, resourceType, amount); - DAT_GameSynchronyState::ptr->finalResults.finalGold[playerID] += reward; - DAT_GameState::ptr->playerDataArray[playerID].currentResources[Game::Resources::RT_GOLD] += reward; - DAT_GameState::ptr->playerDataArray[playerID].marketGold += reward; + DAT_GameSynchronyState::instance.finalResults.finalGold[playerID] += reward; + DAT_GameState::instance.playerDataArray[playerID].currentResources[Game::Resources::RT_GOLD] += reward; + DAT_GameState::instance.playerDataArray[playerID].marketGold += reward; MACRO_CALL_MEMBER(Map::Buildings::BuildingsState_Func::processResourceLoss, DAT_BuildingsState::ptr)( playerID, resourceType, amount, 0); MACRO_CALL_MEMBER(Game::GameStateStructures_Func::displayPlayerTradeVisualEffect, DAT_GameState::ptr)( diff --git a/src/OpenSHC/Game/GameStateStructures/getBuyPrice.cpp b/src/OpenSHC/Game/GameStateStructures/getBuyPrice.cpp index d78ddc50..4fc13374 100644 --- a/src/OpenSHC/Game/GameStateStructures/getBuyPrice.cpp +++ b/src/OpenSHC/Game/GameStateStructures/getBuyPrice.cpp @@ -6,7 +6,6 @@ namespace Game { // FUNCTION: STRONGHOLDCRUSADER 0x004588A0 int GameStateStructures::getBuyPrice(undefined4 playerID, int resourceType, int amount) { - // playerID is unused; truncate the unit price before multiplying. return (this->mapAndTime.buyAndSalesPriceArray[resourceType].buyPrice / 5) * amount; } diff --git a/src/OpenSHC/Game/GameStateStructures/getSellPrice.cpp b/src/OpenSHC/Game/GameStateStructures/getSellPrice.cpp index 7ddc9b50..c823f1ed 100644 --- a/src/OpenSHC/Game/GameStateStructures/getSellPrice.cpp +++ b/src/OpenSHC/Game/GameStateStructures/getSellPrice.cpp @@ -6,7 +6,6 @@ namespace Game { // FUNCTION: STRONGHOLDCRUSADER 0x00458910 int GameStateStructures::getSellPrice(int playerID, int resourceType, int amount) { - // Preserve division before multiplication, including non-multiples of five. return (this->mapAndTime.buyAndSalesPriceArray[resourceType].salesPrice / 5) * amount; } diff --git a/status/addresses-SHC-3BB0A8C1.txt b/status/addresses-SHC-3BB0A8C1.txt index 10a0c2b0..78c1a605 100644 --- a/status/addresses-SHC-3BB0A8C1.txt +++ b/status/addresses-SHC-3BB0A8C1.txt @@ -17715,13 +17715,13 @@ SHC_3BB0A8C1_0x00458840 | 0.0% | Pending SHC_3BB0A8C1_0x00458890 | 0.0% | Pending -SHC_3BB0A8C1_0x004588A0 | 0.0% | Pending +SHC_3BB0A8C1_0x004588A0 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004588D0 | 0.0% | Pending SHC_3BB0A8C1_0x004588F0 | 0.0% | Pending -SHC_3BB0A8C1_0x00458910 | 0.0% | Pending +SHC_3BB0A8C1_0x00458910 | 100.0% | Reimplemented SHC_3BB0A8C1_0x00458940 | 0.0% | Pending @@ -34857,9 +34857,9 @@ SHC_3BB0A8C1_0x004CBA50 | 0.0% | Pending SHC_3BB0A8C1_0x004CBE30 | 0.0% | Pending -SHC_3BB0A8C1_0x004CBFA0 | 0.0% | Pending +SHC_3BB0A8C1_0x004CBFA0 | 100.0% | Reimplemented -SHC_3BB0A8C1_0x004CC000 | 0.0% | Pending +SHC_3BB0A8C1_0x004CC000 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004CC070 | 0.0% | Pending From 5941a84d59f2ed53e884ef3c2e0cce5011c62153 Mon Sep 17 00:00:00 2001 From: Krarilotus <51748815+Krarilotus@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:04:39 +0200 Subject: [PATCH 3/3] Clarify AI market behavior in the wiki --- docs/wiki.rst | 6 ++++-- docs/wiki/market-trading.md | 13 +++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/wiki.rst b/docs/wiki.rst index 4f7b7024..131e770a 100644 --- a/docs/wiki.rst +++ b/docs/wiki.rst @@ -29,13 +29,15 @@ For the recommended method of using SARIF files in Ghidra, see: The Game itself ------------------ - :doc:`Load balancing of the core game engine ` -- :doc:`Market pricing and goods trading ` - Game Mechanics -- AI Behavior - Graphics and Sound Systems - Modding Support - Multiplayer Architecture (Coming Soon!) +AI Behavior +~~~~~~~~~~~ +- :doc:`AI market pricing and goods trading ` + .. include:: wiki/approach.rst diff --git a/docs/wiki/market-trading.md b/docs/wiki/market-trading.md index 29c5b2b4..e3e5ecb4 100644 --- a/docs/wiki/market-trading.md +++ b/docs/wiki/market-trading.md @@ -1,7 +1,8 @@ -# Market pricing and goods trading +# AI market pricing and goods trading -The AI buys and sells resources using the game's price table. The trade routines -handle storage and gold accounting; the decision to trade is made by their callers. +This article describes the AI's market behavior. The AI buys and sells resources +using the game's shared price table. Its trade routines handle storage and gold +accounting; the decision to trade is made by their AI callers. | Function | Crusader 1.41 address | Behavior | | --- | --- | --- | @@ -17,13 +18,13 @@ using integer division, then multiplies by the amount traded. For example, a table price of 14 and a trade of five goods produce a price of 10. The player argument does not affect the price calculation. -## Buying and selling +## AI buying and selling -A purchase first attempts to store the goods. If storage fails, it returns +An AI purchase first attempts to store the goods. If storage fails, it returns without deducting gold or displaying a trade. The routine itself does not check whether the player can afford the purchase. Successful purchases reduce both the player's gold and their net market-gold total. -A sale increases those two totals and the final-results gold statistic, then +An AI sale increases those two totals and the final-results gold statistic, then removes the sold goods. Both successful purchases and sales display the trade amount to the player.