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/2] 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/2] 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