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
28 changes: 28 additions & 0 deletions docs/wiki/stockpile-footprint-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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.

For each entry in `TerrainDefinedData::StockpilePathableOffsets`, it:

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 belong to the supplied `TileMapState` instance. Building
metadata, terrain offsets and row translation come from the corresponding
global game structures.

## Placement

`placeKeep` (`0x005146D0`) creates a starting stockpile through `placeStockpile`
(`0x00508540`). That call has no human/AI filter.

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.
31 changes: 31 additions & 0 deletions src/OpenSHC/Map/TileMapState/clearStockpileFootprintTiles.cpp
Original file line number Diff line number Diff line change
@@ -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;
}
}

}
}
2 changes: 1 addition & 1 deletion status/addresses-SHC-3BB0A8C1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
81 changes: 81 additions & 0 deletions tools/verification/verify_stockpile_cleanup.py
Original file line number Diff line number Diff line change
@@ -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('<HHIIIHH',obj)
strings=obj[symptr+nsyms*18:]
symbols={}
i=0
while i<nsyms:
pos=symptr+i*18
raw,value,section,typ,storage,naux=struct.unpack_from('<8sIhHBB',obj,pos)
name=(strings[struct.unpack_from('<I',raw,4)[0]:].split(b'\0')[0] if raw[:4]==b'\0'*4 else raw.split(b'\0')[0]).decode()
symbols[i]=(name,value,section)
i+=1+naux
name,start,section=next(v for v in symbols.values() if v[0].startswith('?clearStockpileFootprintTiles@'))
sec=20+optsize+(section-1)*40
size,rawptr,relptr=struct.unpack_from('<III',obj,sec+16)
nrel=struct.unpack_from('<H',obj,sec+32)[0]
code=bytearray(obj[rawptr:rawptr+size])
for i in range(nrel):
offset,sym,kind=struct.unpack_from('<IIH',obj,relptr+i*10)
symbol=symbols[sym][0]
assert kind==6, (symbol,kind)
addr=next(a for n,a in [('TerrainDefinedData',0xb48f54),('ViewportRenderState',0x21aebd8),('BuildingsState',0xf98520)] if n in symbol)
struct.pack_into('<I',code,offset,struct.unpack_from('<I',code,offset)[0]+addr)
assert hashlib.sha256(args.original.read_bytes()).hexdigest()=='3bb0a8c1e72331b3a30a5aa93ed94beca0081b476b04c1960e26d5b45387ac5a', 'Unsupported original executable'
original=pefile.PE(str(args.original))
native=original.get_data(0x4faf70-0x400000,0x150)
for insn in Cs(CS_ARCH_X86,CS_MODE_32).disasm(native,0x4faf70):
if insn.mnemonic=='ret':
native=native[:insn.address+insn.size-0x4faf70]
break
else:
raise AssertionError('Original return not found')
print('Recompiled code bytes:',len(code),'relocations:',nrel)
print('Original/recompiled byte equality:',bytes(code)==native)
assert bytes(code)==native, 'Machine code differs after resolving global relocations'
REGS=[UC_X86_REG_EBX,UC_X86_REG_EBP,UC_X86_REG_ESI,UC_X86_REG_EDI]
rng=random.Random(30)
def execute(payload, data, x, y, this):
uc=Uc(UC_ARCH_X86,UC_MODE_32)
uc.mem_map(0x400000,0x3000000)
uc.mem_write(0x600000,bytes(payload))
uc.mem_write(this,bytes(data))
uc.mem_write(0xb49130,original.get_data(0xb49130-0x400000,72))
for row in range(400): uc.mem_write(0x2337300+row*12,struct.pack('<I',row*200))
for bid in range(32): uc.mem_write(0xf985f8+bid*0x32c,struct.pack('<H',bid%3))
stack=0x3300000
uc.mem_write(stack,struct.pack('<III',0x610000,x,y))
uc.reg_write(UC_X86_REG_ESP,stack)
uc.reg_write(UC_X86_REG_ECX,this)
for i,reg in enumerate(REGS):uc.reg_write(reg,0x12345000+i)
uc.emu_start(0x600000,0x610000,count=10000)
assert uc.reg_read(UC_X86_REG_EIP)==0x610000
assert uc.reg_read(UC_X86_REG_ESP)==stack+12
assert [uc.reg_read(r) for r in REGS]==[0x12345000+i for i in range(4)]
return bytes(uc.mem_read(this,len(data)))
offsets=struct.unpack('<18i',original.get_data(0xb49130-0x400000,72))
for case in range(32):
this=0x1a93208 if case%2==0 else 0x2600000
x,y=rng.randrange(10,180),rng.randrange(10,380)
data=bytearray(rng.randbytes(0x554a88))
for i in range(9):
tile=(y+offsets[i*2+1])*200+x+offsets[i*2]
struct.pack_into('<H',data,0x798a0+tile*2,(case+i)%32)
a=execute(native,data,x,y,this)
b=execute(code,data,x,y,this)
assert a==b,case
print('PASS: 32 randomized full TileMapState differential comparisons, both noRubble branches, zero and nonzero building IDs, original and relocated this; stack and callee-saved registers preserved.')