Ghidra does not index ModRM displacements, so `lea reg,[reg+disp]` -- the MSVC idiom for taking a member's address -- is invisible to find-constant-uses. That blind spot parked ServerPlayer+0x274 and covers every non-trivial member of the ~1,600 classes still to map. tools/x86disp.py: full x86-32 length decoder (prefixes, 1/2/3-byte opcodes, ModRM, SIB, sign-extended disp8, disp32, every immediate form) swept from Ghidra's 41,089 function starts so decodes begin on real instruction boundaries. 2,174,504 instructions, 612,166 displacement sites, 100.0% code coverage, 70 desyncs (0.17%), zero unknown opcodes. Excludes no-base disp32 forms (mod=0/rm=5, sib.base=5) which are absolute globals, not member offsets. Commands: build/query/cohort/func/dis/stats/brute. Works off a gitignored local cache in dumps/ rather than hammering CT111. Validated before use: re-finds lea eax,[ecx+0x29c] in ServerPlayer::GetEventStorage (0x0080db00) and both known OnTechResearched +0x29c sites, plus a new one in ProcessTurn. Positive control: the ServerPlayer serializer scores 50/50 known offsets. sizeof(Game::ObservedTech) = 0x2c (44), proven three ways: the exact magic divide 0x2e8ba2e9 sar 3 at 0x0087239f, imul reg,reg,0x2c at 0x0087243a and 0x007b735b, and the search stride add edi,0x2c at 0x007ba257. Append site: RecordObservedTech+0xdf (0x007ba27f) -- lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320 RecordObservedTech (0x007ba1a0) is a direct callee of OnTechResearched and de-duplicates by tech name before appending. The realloc through 0x007b5820 is why lane R's guard saw all three vector words move. Element carries a vptr (RTTI .?AVObservedTech@Game@@) at +0 and a 0x18-byte std::string at +0x0c; the four on-disk ints map onto +0x04/+0x06/+0x08/+0x24/+0x28 in an order this read does NOT determine, and is not guessed. Also corrects harness-audit row 11: ComputeBudget has no store to Budget+0x64 (its only +0x64 accesses are loads off a different base), and ProcessResearch's int* overbudget arg is a ProcessTurn stack local, not Budget+0x64. Agrees with lane R's guard seeing 0 changes in 4284 calls. Honest limits are recorded in the note and the board: this is a recall tool, not an oracle. Class-level precision at 0x274 is ~13% by function, i.e. a ~900x search-space cut that still needs one call-graph check. Cohort ranking must not be used as a hard filter -- it would have discarded the correct answer here. Ghidra writeback: labels + plate comments on RecordObservedTech, vector_ObservedTech_push_back, ObservedTech_ctor, vector_ObservedTech_assign, vector_44B_grow, vftable_ObservedTech. |
||
|---|---|---|
| campaign | ||
| findings | ||
| ghidra | ||
| guides | ||
| notes | ||
| scripts | ||
| tools | ||
| verify | ||
| .gitignore | ||
| README.md | ||
sots-re
Reverse-engineering worklog for Sword of the Stars (2006, SOTS1) — the 32-bit DX9 original + expansions. The nitty-gritty: static/dynamic analysis notes, Ghidra & ReVa scripts, function/struct maps, D3D9 call traces, decomp progress, findings.
Where this runs
Analysis lab on spicy (PVE, 192.168.3.201):
- CT111
sots-re— Linux workspace: Ghidra + headless ReVa server, radare2/rizin/cutter, binwalk. Hosts the Samba share and this repo's working tree at/srv/re-lab/notes. - VM140
sots-re-win10— Win10 runtime + dynamic analysis (x64dbg, Cheat Engine, RenderDoc/apitrace, DXVK→CPU-Vulkan for GPU-less rendering).
Infra (guests, storage, network, share, ReVa endpoint) is documented from the
system-maintainer POV in trikilli → services/re-lab.md. This repo is everything else.
Layout
findings/— the running findings log (append-only), one file per subsystem.ghidra/— exported scripts, data-type archives, struct definitions.traces/— D3D9 / Win32 API call captures + analysis.scripts/— helper tooling (loaders, extractors, parsers).notes/— session notes, scratch, hypotheses.
Ownership / legality
Game binaries come from the owner's own GOG/Steam copy. RE is for personal
interoperability, bug-fixing, and preservation. Binaries themselves are not committed
here (see .gitignore) — they live on the lab's Samba share /srv/re-lab/samples.
Campaign (how this repo is run)
A 4-agent crew (defined in ~/.claude/agents/re-*.md) runs the exploration:
re-quartermaster (backlog + board) → re-analyst (maps via ReVa) →
re-verifier (proves vs real data; old-vs-new differential once reimpl starts) →
re-scribe (files the note, links it, commits).
campaign/board.md— live status board (start here).- Live tracking = Forgejo issues on
alex/sots-re(labelsstatus/*are the kanban columns;type/*,conf/*).campaign/board.mdis the editable mirror — publish withscripts/forgejo_campaign.py bootstrap(needsFORGEJO_TOKEN). campaign/backlog.md— prioritized target queue.campaign/open-questions.md— unresolved threads.findings/_template.md— the record format every finding follows.findings/{objects,control-flow,subsystems}/— the growing engine map.verify/{parsers,traces,harness,results}/— validation: struct parsers now, golden-trace replay + shim compare-mode for reimplementation.ghidra/— exported scripts + datatype archives.
Approach & north star: findings/00-strategy.md. Binary facts: findings/01-fingerprint.md.
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.