guides: RE how-to for 2000s MSVC/DX9 games (98 sources); queue replay-checksum, class recovery, apitrace/TTD
This commit is contained in:
parent
254a077d6c
commit
5d877d373b
4 changed files with 196 additions and 0 deletions
|
|
@ -50,3 +50,4 @@ Approach & north star: `findings/00-strategy.md`. Binary facts: `findings/01-fin
|
|||
## Sibling repo
|
||||
`alex/sots-engine` — the from-scratch engine source (clean-room, public-capable). This repo keeps the
|
||||
evidence + planning for both; binary facts cross over only via `ghidra/addresses.json` → `tools/gen_addresses.py`.
|
||||
- `guides/re-windows-2000s-howto.md` — annotated bibliography + how-to for RE of mid-2000s MSVC/DX9 Windows games, with our-experience call-outs.
|
||||
|
|
|
|||
|
|
@ -33,3 +33,8 @@ most other work. Groomed by **re-quartermaster**.
|
|||
|
||||
## Parked (not on the critical path — resume only with a clear reason)
|
||||
- **Battle-load bug** — one cheap Ghidra question when convenient: does the combat loader use the job API (`0x8fc160`/`0x8fa5b0`) whose completion poll is `TryEnterCriticalSection → Sleep(1000)` @`0x0071ea60`? Dynamic resume: add HDA audio (`qm set 140 -audio0 device=ich9-intel-hda,driver=none`), build Tankers + Extended-Range DEs, move to Markab/Junction, then `verify/harness/profiling/battle-run.sh`.
|
||||
|
||||
## From the RE how-to (guides/re-windows-2000s-howto.md) — adopt
|
||||
- **Replay + per-turn state-checksum regression harness** (OpenRCT2 model): record multi-turn input scripts, checksum sim state each turn, run in CI; decide the float-parity policy now (x87 vs SSE: i686 shim vs future x64 standalone) — `-mfpmath`/strict or tolerant compare. Extends our single-step End-Turn hash oracle. (verify lane)
|
||||
- **Automated class recovery**: run Ghidra `RecoverClassesFromRTTIScript` (with Decompiler Parameter ID) and/or CMU OOAnalyzer on the 1,924 RTTI classes to build class structs + method-to-class assignment; import VS2010 FID databases for CRT/STL triage. (Ghidra lane, after tech-effects)
|
||||
- **apitrace whole-run D3D9 capture** of the original as the golden trace for the eventual `Mars::DrawDevice` reimpl (RenderDoc can't capture D3D9); **WinDbg TTD** (x86 supported) for data-flow questions like 'who writes ServerPlayer+0x164'. (VM lane)
|
||||
|
|
|
|||
|
|
@ -38,3 +38,4 @@ Each links to the finding that raised it. Promoted to backlog or closed by **re-
|
|||
- **SAVE_FORMAT tag corrections (fix Python reader + spec)** — real on-disk tags: `otnF` (not `ontF`) in Odes/Owep/Otch, `nextid` (not `nextId`) in NdGr2, Design = `FAIDes/DHide/DWep/DName`; `ords`/`wpts` are real tags. Python's positional R() matching hid these. RNG: float mapping `(float)(y*2^-32)`, `next_int` mask, and lazy-vs-eager twist at `left==0` still need binary confirmation. (from [[mars-stream]])
|
||||
- **RESOLVED: tech `allows` default** — unlisted species = 1.0 (confirmed in `FUN_005822d0` tree-creation roll, strategic-turn-internals §2); `game/data` uses 100. Still open from game/data: what the engine's converter does with the 34 malformed tokens (`force_right o`, `crew false`, `1.0f`, `0-5`, ``90\``); repeated scalars in a block are last-wins per the sequential if/else consumers (loader-prototypes §M3) — confirm on a fixture in compare mode. (from [[game-data]])
|
||||
- **RESOLVED: SAVE_FORMAT tag names** — all corrections confirmed by bytes and applied to the Python reader + spec (§10). Residual doc debt: `findings/objects/save-editor-structs.md` and `verify/design-rules/SHIP_DESIGN_RULES.md` still quote R1's `ontF/faiDes/nextId` spellings (R1-provenance; annotate rather than rewrite). (from [[SAVE_FORMAT]])
|
||||
- **Notes disagree on MSVC-2010 `std::string` layout** — `struct-recovery.md` §0 vs `turn-spine.md` §1.1 give different offsets; pin both from the `_Myres >= 16` SSO branch in `Stream::WriteString` and correct the loser. (from [[re-windows-2000s-howto]])
|
||||
|
|
|
|||
189
guides/re-windows-2000s-howto.md
Normal file
189
guides/re-windows-2000s-howto.md
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
# Reverse-engineering mid-2000s Windows C++ games — a how-to grounded in the literature and in our SOTS1 campaign
|
||||
|
||||
Scope: 32-bit PE, MSVC 2005–2010, C++ with RTTI, DirectX 9, no source, no symbols. The target is a
|
||||
**functional reimplementation** (behaviour-equivalent, asset-external, OpenRCT2-style shim first), not a
|
||||
byte-matching decomp. Written 2026-09-07 against our Sword of the Stars (2006) work; "our experience"
|
||||
call-outs cite `findings/` and `~/sots-engine/docs/`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Annotated bibliography
|
||||
|
||||
### 1.1 Prior art and communities
|
||||
|
||||
| Source | Why it matters |
|
||||
|---|---|
|
||||
| OpenRCT2 — https://github.com/OpenRCT2/OpenRCT2 (early README preserved in forks, e.g. https://github.com/jvlomax/OpenRCT2) | The canonical incremental model: `openrct2.dll` exported a `WinMain` that a patched `rct2.exe` called at startup; each procedure rewritten in C individually while the DLL read/wrote the original's memory and called its procedures. IDA 5.0 was the disassembler. |
|
||||
| OpenRCT2 independence milestone — https://rct.fandom.com/wiki/OpenRCT2 | The project called into `RCT2.EXE` for unimplemented functions until 15 Oct 2015, when the last original code was displaced (assets still required). The whole arc took ~18 months. |
|
||||
| OpenRCT2 Replay System — https://github.com/OpenRCT2/OpenRCT2/wiki/Replay-System | Records every game command, replays in CI, fails on *sprite checksum* divergence; desync logs for triage. Note: "replays on x64 and x86 platforms will generate different sprite checksums." |
|
||||
| OpenLoco — https://github.com/OpenLoco/OpenLoco , https://openloco.io/about/ | Same method on a 2004 MSVC/asm engine; unimplemented code is `Interop::call(addr)`; grep count of those calls *was* the progress metric. Reimplementation declared complete Sept 2025. |
|
||||
| OpenJKDF2 HOOKS.md — https://github.com/shinyquagsire23/OpenJKDF2/blob/master/HOOKS.md | Cleanest write-up of a hook-and-replace DLL against a 1997 MSVC exe: IPS patch replaces `Window_Main` with shellcode that `LoadLibraryA`s the DLL and calls `hook_init_win`; `hook_function()` per decompiled function; unimplemented functions fall back to `JK.EXE`. |
|
||||
| Devilution — https://github.com/galaxyhaxz/devilution ; https://www.pcgamer.com/a-coder-spent-1200-hours-reverse-engineering-diablos-source-code/ | 1,200 hours; first version was a raw IDA dump with thousands of errors fixed until it compiled. Asserts/debug strings in a debug build recovered file names and even line numbers. |
|
||||
| DevilutionX — https://github.com/diasurgical/devilutionX | The asset-external port that grew from it; requires `DIABDAT.MPQ` or the shareware `spawn.mpq`. |
|
||||
| D2MOO — https://github.com/ThePhrozenKeep/D2MOO | Diablo II reimplementation as DLL patches via a `D2.Detours` submodule; original binaries and assets required; explicitly "no bug fixes yet" to stay equivalent. Closest MSVC-C++-era analogue to our shim. |
|
||||
| Spore ModAPI "Detouring" — https://emd4600.github.io/Spore-ModAPI/_detouring.html | MSVC-2008 C++ game with RTTI, modded via `static_detour` / `member_detour` / `virtual_detour` classes — the taxonomy we need for `__thiscall` vs vtable hooks. |
|
||||
| SpaceCadetPinball — https://github.com/k4zmu2a/SpaceCadetPinball | Full decomp of a 1995 MSVC C++ game using the *public PDB* of XP's `pinball.exe`; "all subroutines were decompiled and C pseudo code was converted to compilable C++". Shows what symbols buy you. |
|
||||
| isledecomp / reccmp — https://github.com/isledecomp/reccmp | Byte-matching toolchain for MSVC 4.2 (LEGO Island): `// FUNCTION: LEGO1 0x100b12c0` annotations drive automatic verification of recompiled functions, vtables and data offsets. The matching-decomp counterpart to our address-manifest idea. |
|
||||
| TRX (TR1X/TR2X) — https://github.com/LostArtefacts/TRX | Tomb Raider I–III reimplementation from late-90s MSVC exes; mature example of decomp → enhancements. |
|
||||
| dethrace — https://github.com/dethrace-labs/dethrace , http://1amstudios.com/articles/dumping-debug-symbols-for-carma1/ | Watcom `DETHRSC.SYM` symbols from a *different build* were still worth painstaking manual re-matching. Lesson: partial/mismatched symbols still pay. |
|
||||
| re3/reVC saga — https://www.gamedeveloper.com/business/rockstar-parent-take-two-sues-modders-behind-i-gta-i-reverse-engineering-project-re3- ; https://torrentfreak.com/take-two-dismisses-claims-against-lead-defendants-in-gta-mods-lawsuit-230405/ | DMCA'd 2021, sued, settled 2023, never returned. The cautionary case for MSVC-era PC decomps of *still-sold* IP. |
|
||||
| Ship of Harkinian FAQ — https://www.shipofharkinian.com/faq | User-supplied ROM → local asset extraction (`oot.otr`); the project ships no IP. Model for our `$SOTS_DATA_DIR` rule. |
|
||||
| Sonic Mania / RSDKv5 decomp — https://github.com/RSDKModding/Sonic-Mania-Decompilation | Requires the user's `Data.rsdk`; same posture. |
|
||||
| decomp.me — https://www.decomp.me/faq , https://github.com/decompme/compilers | Collaborative per-function matching "scratches" with a live diff score; MSVC toolchains exist in the compilers repo. Useful even for a non-matching project on the handful of functions whose exact semantics matter (RNG, checksum). |
|
||||
| Gal Zaban, *Behind Enemy Lines: RE C++ in Modern Ages* (CppCon 2019) — https://www.youtube.com/watch?v=ZJpvdl_VpSM ; slides https://corecppil.github.io/CoreCpp2019/Presentations/Gal_Behind_Enemy_Lines_Reverse_Engineering_Cpp_in_Modern_Ages.pdf | Hierarchy/vcall reconstruction from the RE side; introduces Virtuailor (dynamic vtable resolution in IDA). |
|
||||
| PCGamingWiki SOTS — https://www.pcgamingwiki.com/wiki/Sword_of_the_Stars ; RE StackExchange — https://reverseengineering.stackexchange.com | Known fixes/quirks; Q&A archive for MSVC idioms. |
|
||||
| RetroReversing on PDBs — https://www.retroreversing.com/PDBFileReversing | Why you always check for shipped/leaked PDBs first; they "take away the bulk of the work". |
|
||||
|
||||
### 1.2 MSVC-vintage specifics
|
||||
|
||||
| Source | Why it matters |
|
||||
|---|---|
|
||||
| Skochinsky, *Reversing MSVC Part I: Exception Handling* — https://www.openrce.org/articles/full_view/21 | `FuncInfo`, unwind maps, try-blocks, `__CxxFrameHandler`; how EH data doubles as scope/destructor evidence. |
|
||||
| Skochinsky, *Reversing MSVC Part II: Classes, Methods and RTTI* — https://www.openrce.org/articles/full_view/23 | The reference for `TypeDescriptor` / `CompleteObjectLocator` / `ClassHierarchyDescriptor` / `BaseClassArray`, ctor/dtor patterns, `ms_rtti4.idc`. |
|
||||
| Skochinsky, Recon 2012 *Compiler Internals: Exceptions and RTTI* — http://www.hexblog.com/wp-content/uploads/2012/06/Recon-2012-Skochinsky-Compiler-Internals.pdf | Updated slides incl. x64 and GCC; the single best crib sheet. |
|
||||
| Ghidra RTTI analyzer (MicrosoftCodeAnalyzer) — https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/MicrosoftCodeAnalyzer/src/main/java/ghidra/app/plugin/prototype/MicrosoftCodeAnalyzerPlugin/RttiAnalyzer.java | Creates RTTI data + `vftable` symbols only for VS-compiled PEs. It labels; it does not build classes. |
|
||||
| Ghidra `RecoverClassesFromRTTIScript` — https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Decompiler/ghidra_scripts/RecoverClassesFromRTTIScript.java ; issues https://github.com/NationalSecurityAgency/ghidra/issues/8896 | The script that actually creates class namespaces, `ClassDataTypes` structs, typed vftables, ctor/dtor names and `this` types. Needs the RTTI analyzer *and* Decompiler Parameter ID to have run; has NPE bugs on some PEs. |
|
||||
| IDA ClassInformer — https://github.com/kweatherman/IDA_ClassInformer_PlugIn ; PyClassInformer — https://plugins.hex-rays.com/herosi/pyclassinformer | IDA's equivalent vftable finder/lister for MSVC RTTI. |
|
||||
| OOAnalyzer (CMU SEI Pharos) — https://github.com/cmu-sei/pharos/blob/master/tools/ooanalyzer/ooanalyzer.pod ; paper https://edmcman.github.io/papers/ccs18.pdf ; Ghidra import https://www.sei.cmu.edu/blog/using-ooanalyzer-to-reverse-engineer-object-oriented-code-with-ghidra/ | Prolog-based recovery of class layouts, method-to-class assignment and inheritance from MSVC binaries; >78 % of methods assigned correctly on their corpus. Targets exactly our compiler family. |
|
||||
| Raymond Chen, *Adjustor thunks* — https://devblogs.microsoft.com/oldnewthing/20040206-00/?p=40723 ; MS *vtordisp* — https://learn.microsoft.com/en-gb/cpp/build/reference/vd-disable-construction-displacements?view=msvc-170 | Why a vtable slot may point at `sub ecx, N; jmp` and why `this` inside a method is not the object base. |
|
||||
| Raymond Chen, *Inside STL: The string* — https://devblogs.microsoft.com/oldnewthing/20230803-00/?p=108532 ; Yurichev RE4B ch. on STL — https://github.com/x7dbg/reverse-engineering-for-beginners/blob/master/Chapter-33/Chapter-33.md | Dinkumware `_Bx` SSO union / `_Mysize` / `_Myres`; list/map node shapes. Offsets are version-specific — verify. |
|
||||
| Rich header — https://www.ntcore.com/files/richsign.htm ; tool https://github.com/dishather/richprint | `@comp.id` records give exact compiler/linker build numbers and object counts; the fingerprint step. |
|
||||
| Ghidra Function ID docs — https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/FunctionID/src/main/doc/fid.xml ; extra fidbs https://github.com/threatrack/ghidra-fidb-repo ; FLIRT-in-Ghidra https://github.com/NWMonster/ApplySig | Ghidra ships per-VS-version x86 FID databases; use them to knock CRT/STL/D3DX static code out of the function count. |
|
||||
| MSVC EH funclets — https://llvm.org/docs/ExceptionHandling.html (Windows section) ; Ghidra SEH issue https://github.com/NationalSecurityAgency/ghidra/issues/2477 | Catch bodies and cleanups are *funclets* called by `__CxxFrameHandler3` — decompilers show them as orphan code; Ghidra does not model SEH scope tables. |
|
||||
| Ghidra calling-convention pitfalls — https://github.com/NationalSecurityAgency/ghidra/issues/3404 , https://github.com/NationalSecurityAgency/ghidra/issues/5484 | `__thiscall`/`__cdecl` mis-detection and vtable pointer parameter shifting; the class of bug that crashed our M0 hook. |
|
||||
| CRT initialization — https://learn.microsoft.com/en-us/cpp/c-runtime-library/crt-initialization?view=msvc-170 | `.CRT$XCU` initializer table walked by `_initterm(__xc_a, __xc_z)`; every global-constructor stub lives there. |
|
||||
| Visual C++ name mangling — https://en.wikiversity.org/wiki/Visual_C++_name_mangling ; https://github.com/cmu-sei/pharos-demangle | Grammar for `.?AV…` type descriptors and `?func@Class@@QAE…` symbols; `undname.exe` is the oracle. |
|
||||
| `GetPrivateProfileString` — https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getprivateprofilestringa | The INI idiom of the era; an import of it is a free map from config keys to code. |
|
||||
|
||||
### 1.3 Dynamic-analysis toolchain
|
||||
|
||||
| Source | Why it matters |
|
||||
|---|---|
|
||||
| x64dbg conditional tracing — https://help.x64dbg.com/en/latest/introduction/ConditionalTracing.html ; plugins https://github.com/x64dbg/x64dbg/wiki/Plugins | Trace-into-until-condition with `{p:cip} {i:cip}` log formats; ScyllaHide, xAnalyzer, ret-sync (Ghidra↔debugger sync). |
|
||||
| Frida Stalker — https://frida.re/docs/stalker/ | Whole-thread instruction tracing; documented "severe issues in 32-bit" — use x64dbg tracing or TTD for x86. |
|
||||
| Microsoft Detours — https://github.com/microsoft/Detours ; MinHook https://github.com/TsudaKageyu/minhook ; hybrid https://github.com/m417z/minhook-detours | Detours is MIT since 4.0.1 (the old "Express is x86-only/$10k" lore is obsolete); MinHook is the minimal BSD alternative we vendored. |
|
||||
| Ultimate ASI Loader — https://github.com/ThirteenAG/Ultimate-ASI-Loader | The de-facto list of proxyable system DLLs: `d3d8/9/10/11, dxgi, ddraw, dinput(8), dsound, msacm32, msvfw32, version, wininet, winmm, winhttp, xlive, vorbisFile, binkw32, bink2w32, xinput*`. |
|
||||
| DLL best practices — https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices | Loader-lock rules for doing work in `DllMain` (we hook there; it worked because the process is still single-threaded). |
|
||||
| API Monitor — http://www.rohitab.com/apimonitor ; Process Monitor — https://learn.microsoft.com/en-us/sysinternals/downloads/procmon | Import-level call logging and file/registry I/O without writing code. |
|
||||
| apitrace — https://apitrace.github.io/ | Traces/replays D3D7–11 on Windows; the D3D9 frame-debugger of record. RenderDoc **does not** capture D3D9 (https://github.com/openblack/openblack/wiki/Getting-frame-captures-of-Vanilla-Black-&-White). |
|
||||
| DXVK — https://github.com/doitsujin/dxvk ; dgVoodoo2 — http://dege.freeweb.hu/dgVoodoo2/ ; dxwrapper — https://github.com/elishacloud/dxwrapper ; WineD3D for Windows — https://fdossena.com/?p=wined3d/index.frag ; lavapipe — https://docs.mesa3d.org/drivers/llvmpipe.html ; overview https://emulation.gametechwiki.com/index.php/Wrappers | Ways to run a D3D9 app today: DXVK (d3d9→Vulkan, works on CPU Vulkan), dgVoodoo2 (→D3D11), dxwrapper (D3D8→9, DDraw fixes), WineD3D (→OpenGL). |
|
||||
| WinDbg Time Travel Debugging — https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/time-travel-debugging-overview ; record https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/time-travel-debugging-record | Records x86 processes; replay backwards to "who wrote this field"; slow and multi-GB; known x86 quirk misreads adjacent 32-bit args as one 64-bit value. |
|
||||
| ReClass.NET — https://github.com/ReClassNET/ReClass.NET ; Cheat Engine dissect — https://forum.cheatengine.org/viewtopic.php?t=530548 | Live-memory struct layout viewers; complement static serializer-derived layouts. |
|
||||
|
||||
### 1.4 Legal / clean-room posture
|
||||
|
||||
| Source | Why it matters |
|
||||
|---|---|
|
||||
| OpenMW FAQ — https://openmw.org/faq/ | "Engine recreation … only replaces the program"; no assets shipped; "clean reverse-engineering"; must own Morrowind. |
|
||||
| ScummVM FAQ — https://www.scummvm.org/faq/ | Explicitly mixes engines from released source and from RE; ships none of the assets. |
|
||||
| DevilutionX installing docs — https://github.com/diasurgical/devilutionX/blob/master/docs/installing.md | Sustainable Use License, shareware-data path, "provide your own legitimate copy". |
|
||||
| OpenTTD / OpenGFX — https://wiki.openttd.org/en/Basesets/OpenGFX | Two years to replace every sprite with free ones; the only way an engine recreation ever stops needing the original. |
|
||||
| EFF Reverse Engineering FAQ — https://www.eff.org/issues/coders/reverse-engineering-faq ; *Sega v. Accolade* — https://en.wikipedia.org/wiki/Sega_v._Accolade ; EU Software Directive Art. 6 / *Top System* — https://vidstromlabs.com/blog/the-legal-boundaries-of-reverse-engineering-in-the-eu/ | Intermediate copying for interoperability as fair use (US); decompilation for interoperability and, per CJEU 2021, for error correction (EU). Non-legal summary only. |
|
||||
|
||||
### 1.5 AI-assisted RE (2026)
|
||||
|
||||
| Source | Why it matters |
|
||||
|---|---|
|
||||
| ReVa — https://github.com/cyberkaida/reverse-engineering-assistant | Ghidra 12+ MCP server; small tool-per-task design; headless mode for pipelines. What we use. |
|
||||
| GhidraMCP — https://github.com/LaurieWired/GhidraMCP ; ida-pro-mcp — https://github.com/mrexodia/ida-pro-mcp ; re-mcp — https://github.com/jtsylve/re-mcp | The alternative bridges; ida-pro-mcp's README warns LLMs are "prone to hallucinations" and that int/byte conversions are "especially problematic". |
|
||||
| aiDAPal — https://github.com/atredispartners/aidapal ; GhidrAssist — https://github.com/symgraph/GhidrAssist ; Sidekick — https://sidekick.binary.ninja/ | Local fine-tuned (aiDAPal) and OpenAI-compatible (GhidrAssist) in-tool assistants. |
|
||||
| ReSym (CCS 2024) — https://github.com/lt-asset/resym | Variable/struct symbol recovery; uses a Prolog aggregation over multiple LLM queries to "suppress uncertainty and hallucinations" — the cross-checking pattern to copy. |
|
||||
| LLM4Decompile — https://github.com/albertan017/LLM4Decompile ; Decompile-Bench — https://arxiv.org/abs/2505.12668 ; REFORGE function-naming benchmark — https://arxiv.org/pdf/2607.07738 | State of neural decompilation/naming: re-executability ~45 % on toy benchmarks, far lower on real optimized code. |
|
||||
| Kévin Drapel, *Settlers IV with Ghidra + ReVa + AI* — https://medium.com/@kevin.drapel/reverse-engineering-an-old-game-with-ghidra-reva-mcp-and-ai-846911bf740a | Field report on a 2001 Win32/C++ RTS: AI great at subsystem discovery and plausible names, but left renames half-done and "does not replace … cross-checking with runtime behaviour". |
|
||||
| Talos, *Using LLMs as a RE sidekick* — https://blog.talosintelligence.com/using-llm-as-a-reverse-engineering-sidekick/ | Practitioner framing: assistant, not oracle. |
|
||||
|
||||
---
|
||||
|
||||
## 2. How-to: fingerprint → static map → dynamic → verification → reimplementation
|
||||
|
||||
### Step 0 — Fingerprint the binary (half a day)
|
||||
|
||||
1. `objdump -p` / `pefile` for machine, linker version, imports, `LARGE_ADDRESS_AWARE`, relocs. `richprint` for the Rich header: `@comp.id` build numbers pin the compiler and count object files (ntcore; richprint).
|
||||
2. Look for `*.pdb` next to the exe and for a PDB path string inside it (`RSDS` debug directory). Even a mismatched build's symbols are worth manual re-matching (dethrace; SpaceCadetPinball).
|
||||
3. Grep for `.?AV` — non-zero count means RTTI survived; count `TypeDescriptor`s.
|
||||
4. Note the import mix: `MSVCR100/MSVCP100` = VS2010 and dynamic CRT (fewer static-lib functions to strip); `d3d9` + `d3dx9_NN` gives the renderer anchor and the exact D3DX month; `binkw32`, `dsound`, `wsock32` each become a proxy-DLL candidate.
|
||||
|
||||
> **Our experience** (`findings/01-fingerprint.md`): MajorLinkerVersion 10 + `MSVCR100` settled a VS2003/2005/2008 ambiguity in minutes; LAA was *already set*, which killed a planned "fix" before it started. 1,924 type descriptors and 41,411 functions in 7.5 MB.
|
||||
|
||||
### Step 1 — Static map (weeks, breadth-first)
|
||||
|
||||
**1a. RTTI harvest.** Run Ghidra auto-analysis with the *Windows x86 PE RTTI Analyzer* and *Decompiler Parameter ID* on; then run `RecoverClassesFromRTTIScript` (or IDA ClassInformer). The analyzer only labels `vftable`s and RTTI data; the *script* is what creates class namespaces, `ClassDataTypes` structs and `this` types. It NPEs on some PEs (issues #7927, #8896) — if so, read the type descriptors and COLs yourself.
|
||||
|
||||
> **Our experience** (`findings/objects/00-inventory.md`): the headless RTTI analyzer produced 0 `SymbolType.CLASS` namespaces — consistent with the analyzer/script split above, not with broken RTTI. Reading `.?AV` strings directly gave the full `Mars`/`Game` inventory the same day.
|
||||
|
||||
**1b. Vtables and this-adjust.** For every class, record each vftable's COL `offset` (sub-object offset). Methods reached through a secondary vftable get `this = object + offset`, and slots may be adjustor thunks (`sub ecx, N; jmp`) (Chen; Skochinsky II). Absolute member offset = decompiled `this+off` + COL offset.
|
||||
|
||||
> **Our experience** (`findings/objects/struct-recovery.md` §0): SOTS's `IStreamable` sub-vftable sits at +8 (most classes) or **+0x3a0** (`ServerPlayer`); slots point straight at the functions with no thunks, so every serializer's `this` is pre-adjusted. We created shifted "serializer view" structs (`ServerSystem_ser8`, `ServerPlayer_ser928`) so the decompiler shows field names inside `Read/Write` (`turn-spine.md` §1.1).
|
||||
|
||||
**1c. Library triage.** Apply Ghidra's VS2010 x86 FID databases (plus threatrack's) or FLIRT via ApplySig to fence off CRT/STL/D3DX code; anything left is game code. STL template instantiations compiled from headers won't match a library signature — recognise them by shape instead: MSVC-2010 `std::vector` = `{begin, end, cap}`, `std::list` = `{head*, size}`, red-black node = `{left, parent, right, key…, color/isnil tail}`, `std::string` = 0x1c bytes on x86 with the SSO union and `_Mysize`/`_Myres`.
|
||||
|
||||
> **Our experience:** our own notes disagree on whether `_Bx` starts at +0 or +4 (allocator stub) in this build (`struct-recovery.md` §0 vs `turn-spine.md` §1.1). The reliable lever is the `_Myres >= 16 ? _Ptr : _Buf` branch in any string consumer (`Stream::WriteString` @ 0x008b9d70), which pins both offsets. Verify per binary; do not trust a table.
|
||||
|
||||
**1d. Strings are the map.** Config keys, file names, log format strings and — decisively — **serialization tag names**. If the save format is tagged, each `Read/Write` references dozens of a struct's tag strings in order; rank functions by distinct tag xrefs, decompile, and read `this+offset → tag → type` straight off. This is the single highest-yield lever for the object model, and it is under-documented in the literature (see §4).
|
||||
|
||||
> **Our experience** (`struct-recovery.md`): `SerFind.java` (string→xref→function ranking) + `SerDump.java` (decompile with `DAT_` literal substitution) recovered 87-field `ServerSystem`, 110-field `ServerPlayer`, `StarFleet`, `StarShip` and 25 nested types in one round; community save-editor structs (Bardez, SOTSedit) were the Rosetta cross-check and caught two int64-vs-int32 errors in them.
|
||||
|
||||
**1e. Static initializers.** Walk `_initterm(__xc_a, __xc_z)` (MS CRT docs). Each `.CRT$XCU` stub is a global constructor; in engine code they register config vars, factories, message types.
|
||||
|
||||
> **Our experience** (`loader-prototypes.md` M1): ~600 stubs at 0x009a4720…0x009c1400 each register one `GlobalConst{storage,key,parser,file}` — the whole flat-KV config schema (`StrategyVars.txt`, `globals.txt`, `Species.txt`) fell out of one script. Our first pass missed the fourth (file-name) argument; re-check arity from the `RET n`/stack purge, not from the first decompile.
|
||||
|
||||
**1f. Exceptions.** Expect `__CxxFrameHandler3` prologues (`push -1; push handler; mov eax, fs:[0]`) and orphan funclets for catch bodies (LLVM EH doc; Ghidra #2477). Treat unwind/`FuncInfo` tables as evidence of which locals are objects with destructors (Skochinsky I). Don't chase "unreachable" funclet code as logic.
|
||||
|
||||
**1g. Config idioms.** Check imports for `GetPrivateProfile*` — if present, INI keys map 1:1 to code. If absent, the game has its own parser; find the *writer* (`fprintf("%s %d")`) as well as the reader.
|
||||
|
||||
> **Our experience** (`running-the-game.md`): SOTS uses no INI API; the video config is `display.cfg`, parsed with `_stricmp`; we lost time on `sots.cfg` because the string existed for something else. Xrefs on the writer (0x8f0e40) settled it.
|
||||
|
||||
**1h. Write it back.** Names in class namespaces, prototypes with calling convention, plate comments citing the finding doc, and an exported address manifest (`ghidra/addresses.json`) with per-entry `[verified]`/`[unverified]` provenance. Ghidra resolves a `__thiscall` in namespace `Game::X` to struct `/Game/X`, so merging recovered fields into *that* struct makes every method decompile with names (`turn-spine.md` §1.1).
|
||||
|
||||
### Step 2 — Dynamic: run it, hook it, trace it
|
||||
|
||||
**2a. Run it on a modern box (or a GPU-less VM).** Install the matching VC redist and the *full* DirectX End-User Runtime (legacy `d3dx9_NN` + shader-compiler DLLs for `vs_1_1`/`ps_1_1` effects). Choose a D3D9 path: DXVK's `d3d9.dll` (works on Mesa lavapipe = CPU Vulkan), dgVoodoo2, dxwrapper, WineD3D-for-Windows, or Microsoft's WARP.
|
||||
|
||||
> **Our experience** (`running-the-game.md`): app-local `d3dx9_42.dll` alone made every `.fx` effect fail (D3DX 42 falls back to `d3dx9_31` for SM1 profiles); the full June-2010 redist fixed it. DXVK 3.1 + lavapipe rendered the game at 100–220 % CPU; plain `d3d9.dll`+WARP also works. Exclusive fullscreen minimises on any focus change — force windowed via the game's own config first. Drive UI from a scheduled task (`schtasks /IT`), not SSH.
|
||||
|
||||
**2b. Proxy DLL + inline hooks.** Pick a proxy the exe imports by name with a small export table (`binkw32`, `dinput8`, `version`, `winmm`; Ultimate ASI Loader's list). Forward every export to `<name>_real.dll`, relocate RVAs against the ASLR'd base, hook with MinHook or Detours (both free; Detours is MIT). Hooking in `DllMain` is fine while the process is single-threaded (DLL best practices).
|
||||
|
||||
> **Our experience** (`~/sots-engine/docs/M0.md`): 66-export `binkw32` proxy, MinGW-i686 cross-built, MinHook; `--kill-at`/`--enable-stdcall-fixup` must **not** be passed or the `@N` names the exe imports vanish. **The `__thiscall` lesson:** a C++ `void __thiscall hook(void* self){ log; orig(self); }` crashed `Application::Initialize` because the real function takes one stack arg and `RET 4`s. Until a prototype is verified from the prologue and stack purge, trace hooks are an asm stub (`pushad; call logger; popad; jmp trampoline`); only replace-hooks may be plain C++ (`loader-prototypes.md` "Why the M0 detour crashed").
|
||||
|
||||
**2c. Trace and record.** x64dbg *trace-into-until-condition* with a log format, `ret-sync` to keep Ghidra in step, ScyllaHide if the game checks for debuggers. For "who wrote this byte" questions use WinDbg TTD (x86 supported; slow, multi-GB, args display quirk) rather than Frida Stalker (32-bit is a known weak spot). API Monitor / ProcMon for import- and file-level views. ReClass.NET on a live object to confirm the statically recovered layout.
|
||||
|
||||
**2d. Renderer.** apitrace (`apitrace trace -a d3d9 game.exe`, `qapitrace`) captures the whole D3D9 call stream and replays it anywhere; RenderDoc cannot capture D3D9. Use the trace as the renderer's oracle later.
|
||||
|
||||
### Step 3 — Verification: oracles and differential testing
|
||||
|
||||
1. **Find a deterministic oracle in the original.** Saves, replays, logs, network packets — anything the game writes as a pure function of state. Check for timestamps/salts by hashing repeated runs across *separate processes*.
|
||||
|
||||
> **Our experience** (`determinism-oracle.md`): load save → End Turn → autosave is **byte-identical** across five runs and two processes (gzip container included: MTIME 0). The only non-idempotent field is a per-player `Status` (4→0 on load) that moves an additive `Checksum`; both are canonicalised, nothing else. That single fact makes the whole turn pipeline testable without instrumenting the original.
|
||||
|
||||
2. **Instrument for per-call differential compare.** Per hook: `off | trace | compare | replace`. In `compare`, run original and reimplementation on the same snapshot and diff typed outputs; record JSON Lines with C types so equality rules (exact ints, tolerant floats, hashed blobs, ignored pointers) are explicit (`verify/harness/compare/TRACE_FORMAT.md`).
|
||||
3. **Replay regression in CI.** OpenRCT2 records command streams and fails the build on state-checksum divergence; desync logs localise the first divergent tick. Note their finding that x86 and x64 builds checksum differently — x87 vs SSE float paths. Any 64-bit or differently-optimised reimplementation must budget for float-parity work (or pin `-mfpmath`/`/fp:strict` and compare with tolerance).
|
||||
4. **Matching where it counts.** For RNG, hashing, checksum and fixed-point routines, a decomp.me-style exact match is cheaper than fuzzing semantics.
|
||||
|
||||
### Step 4 — Reimplementation: shim → standalone
|
||||
|
||||
- Follow OpenRCT2/OpenLoco/OpenJKDF2: original exe stays the host; the DLL owns a growing set of functions; unimplemented ones fall through to the original. Make "count of remaining `Interop::call`/trampoline sites" the public progress metric (OpenLoco).
|
||||
- Order by self-containment and oracle strength: parsers → VFS → serializers → RNG → turn pipeline → UI → renderer (our M0…M6 plan in `00-strategy.md`).
|
||||
- Keep two repos: evidence (binary-derived, private) and engine (clean, public-capable). The only crossing is a generated address header. Ship engine code only; users bring assets (`$SOTS_DATA_DIR`), as OpenMW/ScummVM/DevilutionX/SoH do.
|
||||
- Expect the independence milestone to take a year-plus (OpenRCT2: Apr 2014 → Oct 2015); the standalone frontend comes only when enough engine exists to boot without the host.
|
||||
|
||||
---
|
||||
|
||||
## 3. What is different about SOTS1
|
||||
|
||||
- **RTTI is complete and namespaced** (`Mars::` engine, `Game::` logic, 1,924 types), and the *serialization framework is reflective* (`IStreamable` + tagged `Stream::Write(name, value)`). Most literature assumes stripped C++ where class recovery is the hard part (OOAnalyzer's whole reason to exist); here the class list is free and the struct layouts fall out of the serializers. Effort shifts to control flow and semantics.
|
||||
- **Dynamic CRT (MSVCR100/MSVCP100)** — FID/FLIRT triage matters less than in statically linked targets; inlined Dinkumware STL is the noise instead.
|
||||
- **No thunks on the sub-vftables we care about** but a large secondary-base offset (+0x3a0) — the this-adjust discipline is needed even without adjustor thunks.
|
||||
- **Byte-deterministic saves** with a documented canonicalisation — a stronger free oracle than most projects had (OpenRCT2 had to build replays; Devilution had none).
|
||||
- **~600 static-init config registrations** make the data schema recoverable by script before any game-logic RE.
|
||||
- **Runs on a GPU-less VM** via DXVK+lavapipe, so the whole dynamic side is automatable in CI-like conditions.
|
||||
- Not a still-sold flagship IP (contrast re3), and we already hold the clean-room split.
|
||||
|
||||
## 4. Gaps in the literature
|
||||
|
||||
- **Serializer-driven struct recovery** (tag string → xref → `Read/Write` → layout) is not written up anywhere we found; it is our most productive technique and deserves a stand-alone note.
|
||||
- **The `__thiscall` trace-hook trap** (calling the original from a C++ detour whose stack purge is unverified) is scattered across Ghidra issues and forum posts; no guide states "asm stub until the prototype is verified".
|
||||
- **Differential/oracle-based verification for functional (non-matching) reimplementations** is thin: OpenRCT2's replay wiki is the only concrete design; nothing covers per-call compare modes, typed trace formats, or float-parity budgeting across x87/SSE.
|
||||
- **Ghidra's analyzer-vs-script split for RTTI classes** confuses many users (issues #3213, #7927, #8896); the official help does not say the analyzer builds no classes.
|
||||
- **D3D9-on-CPU test rigs** (DXVK + lavapipe on a GPU-less VM, legacy D3DX shader-compiler dependencies) are undocumented outside scattered issues.
|
||||
- **AI-assisted RE evaluation on optimized MSVC C++** is missing: benchmarks (Decompile-Bench, REFORGE) are GCC/Clang-heavy; field reports (Settlers IV) are anecdotal. Nobody has measured how often an LLM mislabels an adjustor thunk, a funclet, or an inlined STL routine — exactly our error surface.
|
||||
- **Secondary-vftable COL offsets at large distances** (hundreds of bytes) are not discussed in the MSVC RTTI write-ups, which show small multiple-inheritance examples.
|
||||
Loading…
Add table
Reference in a new issue