Sword of the Stars (2006) reverse-engineering worklog. Infra docs live in trikilli services/re-lab.md.
Find a file
alex 888f318d8c lane I: the complete inlined-draw inventory, and the seven RNG entry points
Re-ran lane J's image-wide tempering-immediate scan at real instruction
boundaries and audited it site by site.  Sixteen functions carry the two masks
inside a decoded instruction, 67 occurrences, and a brute byte scan finds zero
orphans -- recall is complete.  Of the sixteen:

  * ONE IS A FALSE POSITIVE.  0x008cca30 has no temper chain at all; the four
    bytes read as the second mask are the rel32 displacement of a call.
  * FOUR ARE RNG ENTRY POINTS, not game code -- the tempering there is the
    primitive's own.  Two were known; 0x004f7670 and 0x008e6e30 were not.

So the figure is ELEVEN game functions with inlined draws over 28 sites, not
fourteen.  Exactly ONE of the eleven is reachable from StrategyServer::ProcessTurn
(0x007aa240, depth 4) and one more from OnAllCombatDone_Tail (lane J's R2).  The
other nine are map setup, the lobby, the network layer and two scripted
encounters, all reached through vtable slots with no direct caller -- so their
absence from the turn closure is proved for direct edges only, and that caveat
is stated as loudly as the result.

Both functions lane J handed over are read completely from the instruction
stream:

  0x004f7670 (84 B) is Mars::RNG::NextUInt -- ONE WORD, UNCONDITIONAL, no loop,
  no branch but the lazy twist.  ECX is the generator OBJECT, where NextFloat
  and NextInt take the object PLUS FOUR; ProbabilisticJump uses both conventions
  0x6b bytes apart.  Ghidra's size is right here, which is worth saying.

  0x007aa240 (Ghidra says 944; the body is 953 and ends past the reported range)
  is the encounter-detection roll.  ONE INLINED NextFloat PER (contact, detector)
  TRIAL, drawn BEFORE the accept test, so a detector holding neither of two
  specific techs still burns a word and can never succeed.  The accept test is
  `thresh >= r` -- equality accepts -- derived from the fcompp/test ah,5/jp
  encoding rather than the mnemonic.  The outer repeat-until-no-progress loop
  cannot redraw a pair: the "tried" bitset is filled above the back-edge target
  and never cleared, so the whole call is bounded by |contacts| x |detectors|.

Three more draw entry points nobody had listed: a float range (one word, and it
NARROWS TWICE), a triangular integer range (at least two words), and a
truncated-normal range whose two draws are BOTH inlined and which costs two
words per attempt with an unbounded attempt count -- and which scales by 2^-32
where NextFloat scales by 1/(2^32-1).  Two divisors, one image.

RECONCILIATION, stated honestly.  The complete draw-site inventory of the
ProcessTurn closure is 22 sites: 21 entry-point calls plus the one inlined site.
This lane adds two previously-uncounted sources to that list and accounts for
NONE of lane Z's 18-20 words per turn with certainty, because both new sources
are gated and neither has been measured.  Lane J's prediction that these two
functions would explain the gap is NOT confirmed.  What is now provable is the
negative: there is no twenty-third mechanism, so the 18-20 words are distributed
among exactly these 22 sites.  The search space closes; the count does not.

Also corrects, in place: my own Ghidra comment claiming the masks are a Mars
variant of MT19937.  They are the textbook masks applied before the shift
instead of after -- (y & 0xff3a58ad) << 7 == (y << 7) & 0x9d2c5680, verified
over 200k words -- so mars::rng was never wrong, but a scan for the textbook
constants finds nothing in this image.

Fragment validated by generating to a scratch path (797 entries, no duplicate
name); the tracked header is untouched while lane Z is in flight.  Eight
prototypes, nine plate comments and eight pre-comments written back to Ghidra.
2026-09-08 10:03:33 -04:00
campaign board+rules: lane J combat resolver; inlined draws invisible to call-graph sweeps; Ghidra sizes untrustworthy 2026-09-08 09:38:55 -04:00
findings lane I: the complete inlined-draw inventory, and the seven RNG entry points 2026-09-08 10:03:33 -04:00
ghidra lane I: the complete inlined-draw inventory, and the seven RNG entry points 2026-09-08 10:03:33 -04:00
guides board+rules: lane J combat resolver; inlined draws invisible to call-graph sweeps; Ghidra sizes untrustworthy 2026-09-08 09:38:55 -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 campaign: forgejo issues sync script; issues are the published board 2026-09-07 15:04:57 -04:00
tools lane Z: the RNG ledger for one strategic turn, measured end to end 2026-09-08 09:45:06 -04:00
verify lane Z: the RNG ledger for one strategic turn, measured end to end 2026-09-08 09:45:06 -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.