Emulating a whole system routine: stubs, whole records, and stale registers
What to know
2026-10-24, Hardwar's economy_truth.py, crates/economy/tests/emulated.rs.
The economy's routines call message posting, searches over the world, sound and the updates of systems not yet ported. Four practices made them testable bit for bit.
Stub internal functions at their entry. pe32.py stubs only imports. For a function in the game's own code, hook its entry address with UC_HOOK_CODE and, in the hook, record the registers (and whatever the pointers in them point at), pop the return address into EIP and set EAX. Watcom's register convention keeps every register but EAX across a call, so that is a faithful return. A hook can also skip a routine for one run only (keep a set of addresses to skip): bg_set_function runs the first factory or shop update at its end, and the configuration test wants the state without it. Hook only the addresses you stub or skip; a skip set the hook never sees does nothing (the first run of the script walked straight into the Moth roll-out that way).
Compare whole records, not chosen fields. Each case writes the 900-byte building record and its function memory before and after, and the port reads the record into typed fields while keeping every other byte (Building::raw), then writes it back. A field the port forgot to write, or wrote at the wrong offset, shows as a byte difference without anyone having to think of it: the five 0x4000 halfwords at +0x32c (not four) were found that way.
Mind state that carries between cases. Globals the routine steps keep their value from case to case unless the script resets them: bg_create's light-group counter 0x582140 made every configuration after the first differ until the test followed it.
Following constant arguments through a disassembly needs care. To read which kind and subtype bg_table_build gives each building, a scan tracked mov reg, imm before each call. EBX survives calls in Watcom code, so a value set for one call (bg_create's EBX = 7) looked like the next call's subtype; the real subtype came from a global tested a few instructions earlier. Forget a register on any other write to it, and check odd results by hand.
A small trap on the way: a helper script named dis.py shadows the standard library's dis, which Capstone imports through inspect, and Capstone then fails with a circular import.