Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/wiki.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <wiki/load-balancing-table>`
- :doc:`Market pricing and goods trading <wiki/market-trading>`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you replace something here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No existing entry was replaced: the diff adds only the market-trading link below the load-balancing page. The rest of the index is unchanged.

@gynt gynt Sep 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is unclear that this article will be about AI. Please add a AI subsection and add this entry to it with AI in the title. And make sure the .md file refers to AI market behavior explicitly.

- Game Mechanics
- AI Behavior
- Graphics and Sound Systems
Expand Down
29 changes: 29 additions & 0 deletions docs/wiki/market-trading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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.

| 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. |

## Prices

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.

## Buying and selling

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.

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.
29 changes: 29 additions & 0 deletions src/OpenSHC/AI/AICState/buyGoods.cpp
Original file line number Diff line number Diff line change
@@ -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::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;
}
return FALSE;
}

}
}
28 changes: 28 additions & 0 deletions src/OpenSHC/AI/AICState/sellGoods.cpp
Original file line number Diff line number Diff line change
@@ -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::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)(
playerID, 1, -amount, resourceType);
}

}
}
13 changes: 13 additions & 0 deletions src/OpenSHC/Game/GameStateStructures/getBuyPrice.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include "../GameStateStructures.func.hpp"

namespace OpenSHC {
namespace Game {

// FUNCTION: STRONGHOLDCRUSADER 0x004588A0
int GameStateStructures::getBuyPrice(undefined4 playerID, int resourceType, int amount)
{
return (this->mapAndTime.buyAndSalesPriceArray[resourceType].buyPrice / 5) * amount;
}

}
}
13 changes: 13 additions & 0 deletions src/OpenSHC/Game/GameStateStructures/getSellPrice.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include "../GameStateStructures.func.hpp"

namespace OpenSHC {
namespace Game {

// FUNCTION: STRONGHOLDCRUSADER 0x00458910
int GameStateStructures::getSellPrice(int playerID, int resourceType, int amount)
{
return (this->mapAndTime.buyAndSalesPriceArray[resourceType].salesPrice / 5) * amount;
}

}
}
8 changes: 4 additions & 4 deletions status/addresses-SHC-3BB0A8C1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down