From adfd078ec72962c960f99de653aedb6fd32b4861 Mon Sep 17 00:00:00 2001 From: Krarilotus <51748815+Krarilotus@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:46:20 +0200 Subject: [PATCH 1/5] Reimplement stockpile footprint cleanup with native verification --- docs/wiki/stockpile-footprint-cleanup.md | 69 ++++++++++++++++ .../clearStockpileFootprintTiles.cpp | 31 +++++++ .../verification/verify_stockpile_cleanup.py | 81 +++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 docs/wiki/stockpile-footprint-cleanup.md create mode 100644 src/OpenSHC/Map/TileMapState/clearStockpileFootprintTiles.cpp create mode 100644 tools/verification/verify_stockpile_cleanup.py diff --git a/docs/wiki/stockpile-footprint-cleanup.md b/docs/wiki/stockpile-footprint-cleanup.md new file mode 100644 index 00000000..01714702 --- /dev/null +++ b/docs/wiki/stockpile-footprint-cleanup.md @@ -0,0 +1,69 @@ +# Stockpile footprint cleanup + +`TileMapState::clearStockpileFootprintTiles` at `0x004FAF70` clears the nine +walkable tiles belonging to a stockpile. The four building parts are handled +elsewhere. This implementation preserves original behavior; it does not remove +starting stockpiles or change human/AI placement policy. + +For each entry in `TerrainDefinedData::StockpilePathableOffsets`, it: + +1. Clears only logic bits `0x102`. +2. Restores the tile's default height. +3. Reads `noRubble` from the building indexed by `AlphaGFXLayer`, before clearing + that index. Zero clears `BuildingWasLayer`; nonzero sets `MiscDisplayLayer` + bit `0x4000` and preserves `BuildingWasLayer`. +4. Clears `AlphaGFXLayer`. + +The map layers use the member-function receiver (`ECX`), not a fixed global +`TileMapState`. Building metadata, terrain offsets and row translation use the +existing global resolvers. No generated headers or resolver flags are changed. + +## Evidence and verification + +Reference: Gynt's named OpenSHC Ghidra database, cross-checked against native +Stronghold Crusader 1.41 assembly. Executable SHA-256: +`3bb0a8c1e72331b3a30a5aa93ed94beca0081b476b04c1960e26d5b45387ac5a`. + +Compiled with MSVC 14.00.50727.762 (VS2005 SP1), x86, `/O2 /Ob1 /MT /EHsc`, +`OPEN_SHC_DLL`, `REIMPLEMENTED_CRT=0`, and the project's forced `precomp/pch.h`. +The resulting function is **325 bytes, byte-identical to the original after +resolving its eight global-symbol relocations**. The compiler reproduces the +original three-way loop unrolling from the nine-iteration C++ loop. + +`tools/verification/verify_stockpile_cleanup.py` accepts the compiled COFF object +and a user-supplied original executable: + +```text +python -m pip install pefile capstone unicorn +python tools/verification/verify_stockpile_cleanup.py cleanup.obj "Stronghold Crusader.exe" +``` + +It verifies the original executable hash, resolves the object relocations, +checks exact instruction bytes, and emulates both implementations across 32 +deterministic randomized whole-map states. Cases cover zero/nonzero building +indices, both rubble branches, varied coordinates, and both original and +relocated receiver addresses. It compares all `0x554A88` receiver bytes and +checks stack cleanup and callee-saved registers. All checks pass. + +This is function-level compilation and emulation, not a full DLL link or live +gameplay test. Local development can include the new source through +`cmake/openshc-sources.txt.local` using the existing repository workflow. + +## Related keep-placement finding + +The investigation began with Ascension PR #30, which replaces two bytes at +Extreme file offset `0x115136` with `EB 0F`. In the checked Extreme executable +(SHA-256 `55648e6b05d67d37a5773fe699bbb17a2d6ad4de1bb9dbded9a21caef82bd7fb`), +this jumps from VA `0x00515136` to `0x00515147`, skipping the seven argument +pushes and the `placeStockpile` call at `0x00515142` to `0x005088C0`. +The equivalent standard Crusader block is `0x00514DB6` through `0x00514DC7`, +calling `placeStockpile` at `0x00508540` from `placeKeep` (`0x005146D0`). +There is no human/AI filter in that block. These offsets are executable-specific. + +The stockpile branch in `checkBuildingCanBePlacedHere` (`0x005037B0`) uses +the supplied owner ID to read the player's stockpile entry. An absent entry +does not require adjacency; an existing entry requires capacity and an +adjacent owned stockpile. Earlier terrain and placement checks still apply. +`AIVState::aiPlaceAIVBuilding` (`0x004ED410`) uses the shared building-placement +path. These observations do not establish that removing AI starting +stockpiles is safe. diff --git a/src/OpenSHC/Map/TileMapState/clearStockpileFootprintTiles.cpp b/src/OpenSHC/Map/TileMapState/clearStockpileFootprintTiles.cpp new file mode 100644 index 00000000..81d1377b --- /dev/null +++ b/src/OpenSHC/Map/TileMapState/clearStockpileFootprintTiles.cpp @@ -0,0 +1,31 @@ +#include "../TileMapState.func.hpp" + +#include "OpenSHC/Globals/DAT_BuildingsState.hpp" +#include "OpenSHC/Globals/DAT_TerrainDefinedData.hpp" +#include "OpenSHC/Globals/DAT_ViewportRenderState.hpp" + +namespace OpenSHC { +namespace Map { + + // FUNCTION: STRONGHOLDCRUSADER 0x004FAF70 + void TileMapState::clearStockpileFootprintTiles(int x, int y) + { + for (int i = 0; i < 9; ++i) { + int tile = DAT_ViewportRenderState::instance + .translationMatrix[y + DAT_TerrainDefinedData::instance.StockpilePathableOffsets[i].y] + .addXgetTile + + x + DAT_TerrainDefinedData::instance.StockpilePathableOffsets[i].x; + + this->LogicLayer[tile] &= ~0x102; + this->HeightLayer[tile] = this->DefaultHeightLayer[tile]; + if (DAT_BuildingsState::instance.buildings[this->AlphaGFXLayer[tile]].noRubble == 0) { + this->BuildingWasLayer[tile] = 0; + } else { + this->MiscDisplayLayer[tile] |= 0x4000; + } + this->AlphaGFXLayer[tile] = 0; + } + } + +} +} diff --git a/tools/verification/verify_stockpile_cleanup.py b/tools/verification/verify_stockpile_cleanup.py new file mode 100644 index 00000000..63a20259 --- /dev/null +++ b/tools/verification/verify_stockpile_cleanup.py @@ -0,0 +1,81 @@ +"""Compare a VS2005 /O2 DLL-mode object with original SHC 1.41 under x86 emulation. + +Requires pefile, capstone and unicorn. No game process or copyrighted binary is included. +""" +from pathlib import Path +import argparse, struct, random, hashlib +import pefile +from capstone import Cs, CS_ARCH_X86, CS_MODE_32 +from unicorn import Uc, UC_ARCH_X86, UC_MODE_32 +from unicorn.x86_const import * + +parser=argparse.ArgumentParser(description=__doc__) +parser.add_argument('object',type=Path) +parser.add_argument('original',type=Path) +args=parser.parse_args() +obj=args.object.read_bytes() +_,nsec,_,symptr,nsyms,optsize,_=struct.unpack_from(' Date: Sun, 6 Sep 2026 22:06:30 +0200 Subject: [PATCH 2/5] Record full DLL match and focus stockpile wiki on vanilla behavior --- docs/wiki/stockpile-footprint-cleanup.md | 79 ++++++------------------ status/addresses-SHC-3BB0A8C1.txt | 2 +- 2 files changed, 20 insertions(+), 61 deletions(-) diff --git a/docs/wiki/stockpile-footprint-cleanup.md b/docs/wiki/stockpile-footprint-cleanup.md index 01714702..366e77ad 100644 --- a/docs/wiki/stockpile-footprint-cleanup.md +++ b/docs/wiki/stockpile-footprint-cleanup.md @@ -1,69 +1,28 @@ # Stockpile footprint cleanup -`TileMapState::clearStockpileFootprintTiles` at `0x004FAF70` clears the nine -walkable tiles belonging to a stockpile. The four building parts are handled -elsewhere. This implementation preserves original behavior; it does not remove -starting stockpiles or change human/AI placement policy. +`TileMapState::clearStockpileFootprintTiles` (`0x004FAF70` in Crusader 1.41) +clears the nine walkable tiles belonging to a stockpile. The four building +parts are handled elsewhere. For each entry in `TerrainDefinedData::StockpilePathableOffsets`, it: -1. Clears only logic bits `0x102`. -2. Restores the tile's default height. -3. Reads `noRubble` from the building indexed by `AlphaGFXLayer`, before clearing - that index. Zero clears `BuildingWasLayer`; nonzero sets `MiscDisplayLayer` - bit `0x4000` and preserves `BuildingWasLayer`. -4. Clears `AlphaGFXLayer`. +1. Clears the tile's `0x102` logic bits and restores its default height. +2. Reads the building index from `AlphaGFXLayer` and checks that building's + `noRubble` field. Zero clears `BuildingWasLayer`; nonzero sets bit `0x4000` + in `MiscDisplayLayer` and preserves `BuildingWasLayer`. +3. Clears `AlphaGFXLayer` after reading the building reference. -The map layers use the member-function receiver (`ECX`), not a fixed global -`TileMapState`. Building metadata, terrain offsets and row translation use the -existing global resolvers. No generated headers or resolver flags are changed. +The map layers belong to the supplied `TileMapState` instance. Building +metadata, terrain offsets and row translation come from the corresponding +global game structures. -## Evidence and verification +## Placement -Reference: Gynt's named OpenSHC Ghidra database, cross-checked against native -Stronghold Crusader 1.41 assembly. Executable SHA-256: -`3bb0a8c1e72331b3a30a5aa93ed94beca0081b476b04c1960e26d5b45387ac5a`. +`placeKeep` (`0x005146D0`) creates a starting stockpile through `placeStockpile` +(`0x00508540`). That call has no human/AI filter. -Compiled with MSVC 14.00.50727.762 (VS2005 SP1), x86, `/O2 /Ob1 /MT /EHsc`, -`OPEN_SHC_DLL`, `REIMPLEMENTED_CRT=0`, and the project's forced `precomp/pch.h`. -The resulting function is **325 bytes, byte-identical to the original after -resolving its eight global-symbol relocations**. The compiler reproduces the -original three-way loop unrolling from the nine-iteration C++ loop. - -`tools/verification/verify_stockpile_cleanup.py` accepts the compiled COFF object -and a user-supplied original executable: - -```text -python -m pip install pefile capstone unicorn -python tools/verification/verify_stockpile_cleanup.py cleanup.obj "Stronghold Crusader.exe" -``` - -It verifies the original executable hash, resolves the object relocations, -checks exact instruction bytes, and emulates both implementations across 32 -deterministic randomized whole-map states. Cases cover zero/nonzero building -indices, both rubble branches, varied coordinates, and both original and -relocated receiver addresses. It compares all `0x554A88` receiver bytes and -checks stack cleanup and callee-saved registers. All checks pass. - -This is function-level compilation and emulation, not a full DLL link or live -gameplay test. Local development can include the new source through -`cmake/openshc-sources.txt.local` using the existing repository workflow. - -## Related keep-placement finding - -The investigation began with Ascension PR #30, which replaces two bytes at -Extreme file offset `0x115136` with `EB 0F`. In the checked Extreme executable -(SHA-256 `55648e6b05d67d37a5773fe699bbb17a2d6ad4de1bb9dbded9a21caef82bd7fb`), -this jumps from VA `0x00515136` to `0x00515147`, skipping the seven argument -pushes and the `placeStockpile` call at `0x00515142` to `0x005088C0`. -The equivalent standard Crusader block is `0x00514DB6` through `0x00514DC7`, -calling `placeStockpile` at `0x00508540` from `placeKeep` (`0x005146D0`). -There is no human/AI filter in that block. These offsets are executable-specific. - -The stockpile branch in `checkBuildingCanBePlacedHere` (`0x005037B0`) uses -the supplied owner ID to read the player's stockpile entry. An absent entry -does not require adjacency; an existing entry requires capacity and an -adjacent owned stockpile. Earlier terrain and placement checks still apply. -`AIVState::aiPlaceAIVBuilding` (`0x004ED410`) uses the shared building-placement -path. These observations do not establish that removing AI starting -stockpiles is safe. +The stockpile branch of `checkBuildingCanBePlacedHere` (`0x005037B0`) looks up +the supplied owner's stockpile. The first stockpile needs no adjacent existing +stockpile. Further placement requires capacity and adjacency to an owned +stockpile, in addition to the earlier terrain and placement checks. +`AIVState::aiPlaceAIVBuilding` (`0x004ED410`) uses the shared placement path. diff --git a/status/addresses-SHC-3BB0A8C1.txt b/status/addresses-SHC-3BB0A8C1.txt index 10a0c2b0..006579f7 100644 --- a/status/addresses-SHC-3BB0A8C1.txt +++ b/status/addresses-SHC-3BB0A8C1.txt @@ -40367,7 +40367,7 @@ SHC_3BB0A8C1_0x004FAE50 | 0.0% | Pending SHC_3BB0A8C1_0x004FAEE0 | 0.0% | Pending -SHC_3BB0A8C1_0x004FAF70 | 0.0% | Pending +SHC_3BB0A8C1_0x004FAF70 | 100.0% | Reimplemented SHC_3BB0A8C1_0x004FB0C0 | 0.0% | Pending From ba14b9ba0b56231a692550677871c80df23e1422 Mon Sep 17 00:00:00 2001 From: Krarilotus <51748815+Krarilotus@users.noreply.github.com> Date: Wed, 9 Sep 2026 02:42:35 +0200 Subject: [PATCH 3/5] Use existing stockpile logic flags and address review cleanup --- docs/wiki/stockpile-footprint-cleanup.md | 9 ++- .../clearStockpileFootprintTiles.cpp | 3 +- .../verification/verify_stockpile_cleanup.py | 81 ------------------- 3 files changed, 10 insertions(+), 83 deletions(-) delete mode 100644 tools/verification/verify_stockpile_cleanup.py diff --git a/docs/wiki/stockpile-footprint-cleanup.md b/docs/wiki/stockpile-footprint-cleanup.md index 366e77ad..a89bd7b6 100644 --- a/docs/wiki/stockpile-footprint-cleanup.md +++ b/docs/wiki/stockpile-footprint-cleanup.md @@ -6,7 +6,8 @@ parts are handled elsewhere. For each entry in `TerrainDefinedData::StockpilePathableOffsets`, it: -1. Clears the tile's `0x102` logic bits and restores its default height. +1. Clears `Logic1::L_STOCKPILEUnk` (`0x2`) and + `Logic1::L_WALL_OR_GATEHOUSE` (`0x100`), then restores the default height. 2. Reads the building index from `AlphaGFXLayer` and checks that building's `noRubble` field. Zero clears `BuildingWasLayer`; nonzero sets bit `0x4000` in `MiscDisplayLayer` and preserves `BuildingWasLayer`. @@ -16,6 +17,12 @@ The map layers belong to the supplied `TileMapState` instance. Building metadata, terrain offsets and row translation come from the corresponding global game structures. +These logic names come from the existing `Logic1` enum; their combination here +does not establish additional stockpile semantics. `MiscDisplayLayer` is a +separate `ushort` layer with no corresponding flag enum in the current headers. +Its `0x4000` bit is kept literal pending identification of its consumers; a +same-valued flag from another layer would not establish its meaning. + ## Placement `placeKeep` (`0x005146D0`) creates a starting stockpile through `placeStockpile` diff --git a/src/OpenSHC/Map/TileMapState/clearStockpileFootprintTiles.cpp b/src/OpenSHC/Map/TileMapState/clearStockpileFootprintTiles.cpp index 81d1377b..25adbabf 100644 --- a/src/OpenSHC/Map/TileMapState/clearStockpileFootprintTiles.cpp +++ b/src/OpenSHC/Map/TileMapState/clearStockpileFootprintTiles.cpp @@ -3,6 +3,7 @@ #include "OpenSHC/Globals/DAT_BuildingsState.hpp" #include "OpenSHC/Globals/DAT_TerrainDefinedData.hpp" #include "OpenSHC/Globals/DAT_ViewportRenderState.hpp" +#include "OpenSHC/Map/LogicHelpers/Logic1.hpp" namespace OpenSHC { namespace Map { @@ -16,7 +17,7 @@ namespace Map { .addXgetTile + x + DAT_TerrainDefinedData::instance.StockpilePathableOffsets[i].x; - this->LogicLayer[tile] &= ~0x102; + this->LogicLayer[tile] &= ~(LogicHelpers::L_STOCKPILEUnk | LogicHelpers::L_WALL_OR_GATEHOUSE); this->HeightLayer[tile] = this->DefaultHeightLayer[tile]; if (DAT_BuildingsState::instance.buildings[this->AlphaGFXLayer[tile]].noRubble == 0) { this->BuildingWasLayer[tile] = 0; diff --git a/tools/verification/verify_stockpile_cleanup.py b/tools/verification/verify_stockpile_cleanup.py deleted file mode 100644 index 63a20259..00000000 --- a/tools/verification/verify_stockpile_cleanup.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Compare a VS2005 /O2 DLL-mode object with original SHC 1.41 under x86 emulation. - -Requires pefile, capstone and unicorn. No game process or copyrighted binary is included. -""" -from pathlib import Path -import argparse, struct, random, hashlib -import pefile -from capstone import Cs, CS_ARCH_X86, CS_MODE_32 -from unicorn import Uc, UC_ARCH_X86, UC_MODE_32 -from unicorn.x86_const import * - -parser=argparse.ArgumentParser(description=__doc__) -parser.add_argument('object',type=Path) -parser.add_argument('original',type=Path) -args=parser.parse_args() -obj=args.object.read_bytes() -_,nsec,_,symptr,nsyms,optsize,_=struct.unpack_from(' Date: Thu, 10 Sep 2026 00:26:31 +0200 Subject: [PATCH 4/5] Describe stockpile cleanup in terms of game behavior --- docs/wiki/stockpile-footprint-cleanup.md | 40 ++++++++---------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/docs/wiki/stockpile-footprint-cleanup.md b/docs/wiki/stockpile-footprint-cleanup.md index a89bd7b6..63567b88 100644 --- a/docs/wiki/stockpile-footprint-cleanup.md +++ b/docs/wiki/stockpile-footprint-cleanup.md @@ -1,35 +1,21 @@ # Stockpile footprint cleanup -`TileMapState::clearStockpileFootprintTiles` (`0x004FAF70` in Crusader 1.41) -clears the nine walkable tiles belonging to a stockpile. The four building -parts are handled elsewhere. +Removing a stockpile clears its nine walkable tiles. Its four building parts +are removed separately. -For each entry in `TerrainDefinedData::StockpilePathableOffsets`, it: +For each walkable tile, cleanup: -1. Clears `Logic1::L_STOCKPILEUnk` (`0x2`) and - `Logic1::L_WALL_OR_GATEHOUSE` (`0x100`), then restores the default height. -2. Reads the building index from `AlphaGFXLayer` and checks that building's - `noRubble` field. Zero clears `BuildingWasLayer`; nonzero sets bit `0x4000` - in `MiscDisplayLayer` and preserves `BuildingWasLayer`. -3. Clears `AlphaGFXLayer` after reading the building reference. - -The map layers belong to the supplied `TileMapState` instance. Building -metadata, terrain offsets and row translation come from the corresponding -global game structures. - -These logic names come from the existing `Logic1` enum; their combination here -does not establish additional stockpile semantics. `MiscDisplayLayer` is a -separate `ushort` layer with no corresponding flag enum in the current headers. -Its `0x4000` bit is kept literal pending identification of its consumers; a -same-valued flag from another layer would not establish its meaning. +1. Clears the stockpile and wall/gatehouse map-logic flags and restores the + default terrain height. +2. Updates the former-building and display information according to the + building's rubble setting. The exact visual effect of one display flag + remains unidentified. +3. Removes the tile's building reference. ## Placement -`placeKeep` (`0x005146D0`) creates a starting stockpile through `placeStockpile` -(`0x00508540`). That call has no human/AI filter. +Placing a keep creates a starting stockpile for both human and AI players. -The stockpile branch of `checkBuildingCanBePlacedHere` (`0x005037B0`) looks up -the supplied owner's stockpile. The first stockpile needs no adjacent existing -stockpile. Further placement requires capacity and adjacency to an owned -stockpile, in addition to the earlier terrain and placement checks. -`AIVState::aiPlaceAIVBuilding` (`0x004ED410`) uses the shared placement path. +The first stockpile needs no adjacent existing stockpile. Further stockpiles +require available capacity and adjacency to the owner's stockpile, as well as +suitable terrain. AI castle placement uses the same placement rules. From 22fb73dc127885e6b571007c424327b9d90cc322 Mon Sep 17 00:00:00 2001 From: Krarilotus <51748815+Krarilotus@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:32:06 +0200 Subject: [PATCH 5/5] docs: group and link stockpile gameplay article --- docs/wiki.rst | 2 +- .../stockpile.md} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/wiki/{stockpile-footprint-cleanup.md => game-mechanics/stockpile.md} (96%) diff --git a/docs/wiki.rst b/docs/wiki.rst index ce0e938e..a52a04a6 100644 --- a/docs/wiki.rst +++ b/docs/wiki.rst @@ -29,7 +29,7 @@ For the recommended method of using SARIF files in Ghidra, see: The Game itself ------------------ - :doc:`Load balancing of the core game engine ` -- Game Mechanics +- :doc:`Stockpile ` - AI Behavior - Graphics and Sound Systems - Modding Support diff --git a/docs/wiki/stockpile-footprint-cleanup.md b/docs/wiki/game-mechanics/stockpile.md similarity index 96% rename from docs/wiki/stockpile-footprint-cleanup.md rename to docs/wiki/game-mechanics/stockpile.md index 63567b88..77adfb5b 100644 --- a/docs/wiki/stockpile-footprint-cleanup.md +++ b/docs/wiki/game-mechanics/stockpile.md @@ -1,4 +1,4 @@ -# Stockpile footprint cleanup +# Stockpile Removing a stockpile clears its nine walkable tiles. Its four building parts are removed separately.