Sword of the Stars (2006) reverse-engineering worklog. Infra docs live in trikilli services/re-lab.md.
Find a file
alex e9dec36d77 findings: lane AI3 -- the AI stepping order is save player order, and pass 0 writes nothing
Closes five of lane AI2's open items and corrects two published claims.

The stepping order: StrategyServer::ResumePlaying 0x007ddc90 walks the player
array in INDEX ORDER and raises SEResumePlaying at each live player; the app
callback StrategyApp::OnClientEvent 0x00838e10 delivers it inline for humans
and appends it, deduplicated, to the pending queue for AI players. So the AI
players are stepped in save player order, each at most once -- computable from
a save with no live measurement. AI2's search missed it because 0x00b29f98 is
not a pointer to the StrategyApp, it IS the StrategyApp: the enqueue writes the
absolute member address 0x00b29fb4 and never materialises the object base. The
enqueue also has zero direct callers and no vtable slot -- its address is stored
into StrategyServer+0x170 by CreateGame. Lane B6's third blind spot, twice over.

The two passes: `pass` is a tier index, not a plan/act switch. 0x006abb2a picks
between two per-candidate quota fields -- tier 0 takes +0x10, tier 1 takes +0x14
-- and the hub loops `for (i = 0; i <= pass; ++i)`. Every order-emitting exit is
gated pass == 1 (0x006bbd50, 0x006c16c0, 0x006cea50), so pass 0 claims each
task's minimum force in priority order and WRITES NOTHING. That halves the
ModCount arithmetic.

Corrections:
- AI2 §4.2: the nine "no order method" tasks are not planners. All nine emit
  orders at depth 4-9; the depth-4 cut hid it. AITColonize reaches list 7 and
  AITBuildPoliceShips reaches list 3, which is what their names promise.
- AI2 §3: the two priority overrides' flag polarity is inverted. The tunable
  applies when bit 0 of +0x4 is SET, and "committed" is not a supported name.
- AI2 §10.6: the .data invade tunables have no loader. 650 and 750, image
  constants, exactly one reader each and no writer anywhere.

Also: slot 12 named (a preemption permission, 0x006a8d20), slot 13's consumer
found (0x00696620, a range budget -- and the Zuul are exempt, a FOURTH
independent cross-check on AI2's species reading), slot 11's dispatch located.
ServerPlayer+0xf9/+0xfa -- the two bytes that decide whether a player is
AI-controlled -- sit in a hole in the serialised layout and are NOT in the save;
their writer is unfound and is the biggest remaining hole.

Static only; nothing here has run under an instrument. 160 indirect call sites
inside the AI closure are unresolved, so reachability is still a lower bound.
Four predictions with falsifiers in §7.

ghidra/addresses.d/lane-ai3.json: 15 entries, 1,105 -> 1,120, no duplicates,
validated to a scratch path.
2026-09-08 15:29:53 -04:00
campaign standalone report from merged main: 209 -> 131 leaves, closed 78 regressed 0 2026-09-08 15:10:17 -04:00
findings findings: lane AI3 -- the AI stepping order is save player order, and pass 0 writes nothing 2026-09-08 15:29:53 -04:00
ghidra findings: lane AI3 -- the AI stepping order is save player order, and pass 0 writes nothing 2026-09-08 15:29:53 -04:00
guides rules 19-20: the instrument can perturb the measurement; a count cannot separate did-not-fire from fired-and-found-nothing 2026-09-08 13:56:06 -04:00
notes init commit 2026-09-07 12:31:56 -04:00
objects regenerate both headers (669 addresses); track objects/generated so the shared clone stops sitting dirty 2026-09-08 07:36:16 -04:00
scripts lane V2: vtable inversion — resolve indirect call edges image-wide 2026-09-08 12:05:05 -04:00
tools lane H: five live probes, the oracle restored, and a harness perturbation bisected to one hook 2026-09-08 13:53:36 -04:00
verify lane W2: multiplayer Tier 0 played end to end with no server; ModCount/Frame/Status watchpoints; rcex explained 2026-09-08 15:23:52 -04:00
.gitignore lane D: automated struct recovery from the IStreamable serializers 2026-09-08 05:51:37 -04:00
README.md guides: RE how-to for 2000s MSVC/DX9 games (98 sources); queue replay-checksum, class recovery, apitrace/TTD 2026-09-07 18:05:58 -04:00

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 (labels status/* are the kanban columns; type/*, conf/*). campaign/board.md is the editable mirror — publish with scripts/forgejo_campaign.py bootstrap (needs FORGEJO_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.