diff --git a/campaign/DASHBOARD.md b/campaign/DASHBOARD.md index 41888dd..847c9fd 100644 --- a/campaign/DASHBOARD.md +++ b/campaign/DASHBOARD.md @@ -1,18 +1,18 @@ # SotS RE campaign — coverage dashboard -Generated 2026-09-07 21:41 UTC · `sots-re` @ ea866da,2026-09-07 · `sots-engine` @ 352577a,2026-09-07 (16 commits) · regenerate with `tools/dashboard.py` +Generated 2026-09-07 21:45 UTC · `sots-re` @ f1c7f55,2026-09-07 · `sots-engine` @ 352577a,2026-09-07 (16 commits) · regenerate with `tools/dashboard.py` > **North star:** A functional reimplementation of the engine — behavior-equivalent, NOT byte-for-byte ## 1. Map coverage (campaign/board.md) -43 targets · mapped-or-better **29/43** `[███████░░░] 67%` · verified **13/43** `[███░░░░░░░] 30%` +43 targets · mapped-or-better **30/43** `[███████░░░] 70%` · verified **14/43** `[███░░░░░░░] 33%` | Status | Count | % | |---|---:|---:| -| verified | 13 | 30% | +| verified | 14 | 33% | | mapped | 16 | 37% | -| in-progress | 6 | 14% | +| in-progress | 5 | 12% | | backlog | 7 | 16% | | blocked | 1 | 2% | @@ -24,13 +24,13 @@ Generated 2026-09-07 21:41 UTC · `sots-re` @ ea866da,2026-09-07 · `sots-engine | engine | 3 | 1 | 3 | 0 | 0 | 7 | | verify | 2 | 2 | 1 | 0 | 0 | 5 | | phase2 | 1 | 0 | 0 | 4 | 0 | 5 | -| meta | 1 | 4 | 1 | 0 | 0 | 6 | +| meta | 2 | 4 | 0 | 0 | 0 | 6 | ## 2. Binary understanding - RTTI type descriptors: **1,924** (`Game::` 1,404, `Mars::` 194; serializable types 179) - Classes with recovered member layouts: **21** / 179 serializable types `[█░░░░░░░░░] 12%` — heuristic: distinct `Game::X`/`Mars::X` in `##`–`####` headings of `struct-recovery.md` + `schema-gaps-resolved.md` -- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **25**, verified **10** `[████░░░░░░] 40%` +- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **102**, verified **90** `[█████████░] 88%` ## 3. Data layer @@ -58,10 +58,10 @@ Board `engine:` rows: verified **3**, mapped 1, in flight 3 (of 7) — verified | Milestone | Status | Coverage | Notes | |---|---|---:|---| | P2-M0 shim bootstrap | ✅ verified | 100% | sots-engine 5f5bc41: proxy binkw32.dll (66 exports identical), MinHook, asm-stub trace hoo | -| P2-M1 flat-KV config loader | ⬜ backlog | 0% | CfgVar_RegisterKey 0x008b76a0; trace->compare->replace; save byte-identical oracle | -| P2-M2 manifest/id registry | ⬜ backlog | 0% | _weapons.txt/_shipsections.txt stable ids | -| P2-M3 Mars brace-block parser | ⬜ backlog | 0% | + .effect TXT reader; compare over full catalog load | -| P2-M4 gobio VFS read | ⬜ backlog | 0% | ZIP entries + NativeFileSystem override order | +| P2-M1 flat-KV config loader | ⬜ backlog | 0% | HOOK: GlobalConsts::LoadFile(const char* file, map*) 0x008b73c0 cdecl (driven by LoadAll 0 | +| P2-M2 manifest/id registry | ⬜ backlog | 0% | no registry object: WeaponDictionary::Init 0x0059a4c0 / SectionDictionary ctor 0x00576f40 | +| P2-M3 Mars brace-block parser | ⬜ backlog | 0% | Mars::Script pull tokenizer (Open 0x008cd7d0, ReadToken 0x008cd2f0, Next 0x008cd3e0, SkipB | +| P2-M4 gobio VFS read | ⬜ backlog | 0% | choke point: bool __cdecl gobio::ReadFile(const char*, IBuffer**) 0x008d5140; FileSystemSe | ## 6. Verification ledger @@ -74,22 +74,22 @@ Board `engine:` rows: verified **3**, mapped 1, in flight 3 (of 7) — verified ## 7. Open questions -Open **20** · resolved/parked 4 · backlog items: Now 4, Next 3, Later 2, Breadth queue 7, Parked 1 +Open **22** · resolved/parked 5 · backlog items: Now 4, Next 3, Later 2, Breadth queue 7, Parked 1 Most recent open: +- Not traced end-to-end — `Species/_NPC/weapons/*.weapon` loading and the `.effect` dictionary entry… +- PARITY BUG (fix in mars/text + Python flat_kv): duplicate config keys are FIRST-occurrence-wins in t… - Formula gaps surfaced by the game/sim port (feed back to Ghidra) — which bankruptcy limit carries … - `Mars::Application::Initialize` signature (P2-M0 finding) — a C++ `__thiscall` detour that called … - Corrections from turn internals — `ServerPlayer+0xf9` is the AI flag (not bTurnDone); `FUN_007d7f7… -- Ship-design code rules (Ghidra, from SHIP_DESIGN_RULES.md §8) — hidden default rider designs are b… -- Strict-parse gaps vs real save (verifier output, round 3) — (1) `ServerSystem`: 7/28 systems carry… ## 8. Delta since previous dashboard -- verified targets: 12 → 13 (+1) · mapped-or-better: 28 → 29 (+1) -- engine LOC: 3,131 → 3,919 (+788) · test files: 29 → 38 (+9) · checks: 749 → 889 (+140) -- addresses verified: 10 → 10 (+0) · recovered layouts: 21 → 21 (+0) · open questions: 20 → 20 (+0) +- verified targets: 13 → 14 (+1) · mapped-or-better: 29 → 30 (+1) +- engine LOC: 3,919 → 3,919 (+0) · test files: 38 → 38 (+0) · checks: 889 → 889 (+0) +- addresses verified: 10 → 90 (+80) · recovered layouts: 21 → 21 (+0) · open questions: 20 → 22 (+2) --- warnings: mars-vfs.md: no oracle total row parsed - + diff --git a/campaign/board.md b/campaign/board.md index d3305b8..1a93e16 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -34,11 +34,11 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | Ghidra type write-back | meta | verified | high | 100% | 2026-09-07 | structs saved in project (ServerSystem 87f, ServerPlayer 110f, StarFleet, StarShip, StrategyServer partial, 22 nested); 52 serializers + primitives + ~60 spine fns renamed; decompile shows field names | | strategic turn internals (economy/research/colony/movement/diplomacy) | subsystem | mapped | high | 90% | 2026-09-07 | findings/subsystems/strategic-turn-internals.md: budget/RP/trade/bankruptcy formulas, research rolls (unlisted race=1.0, Zuul x2), colony growth, movement, RNG=MT19937; ~600 g_KEY labels in Ghidra | | P2-M0 shim bootstrap | phase2 | verified | high | 100% | 2026-09-07 | sots-engine 5f5bc41: proxy binkw32.dll (66 exports identical), MinHook, asm-stub trace hook on Application::Initialize; menu reached; evidence verify/results/shim/. Initialize signature unverified -> asm stubs only | -| P2-M1 flat-KV config loader | phase2 | backlog | — | 0% | 2026-09-07 | CfgVar_RegisterKey 0x008b76a0; trace->compare->replace; save byte-identical oracle | -| P2-M2 manifest/id registry | phase2 | backlog | — | 0% | 2026-09-07 | _weapons.txt/_shipsections.txt stable ids | -| P2-M3 Mars brace-block parser | phase2 | backlog | — | 0% | 2026-09-07 | + .effect TXT reader; compare over full catalog load | -| P2-M4 gobio VFS read | phase2 | backlog | — | 0% | 2026-09-07 | ZIP entries + NativeFileSystem override order | -| loader prototypes (Ghidra) | meta | in-progress | — | 0% | 2026-09-07 | R6 running: cfgvar parser, manifest loader, brace-block entry+node model, gobio read; + Initialize/Run/OnTick signature checks (M0 crash lesson) | +| P2-M1 flat-KV config loader | phase2 | backlog | — | 0% | 2026-09-07 | HOOK: GlobalConsts::LoadFile(const char* file, map*) 0x008b73c0 cdecl (driven by LoadAll 0x008b76d0 from Initialize). Semantics: _stricmp keys, FIRST occurrence wins (dupes log 'multiply defined'), unknown keys ignored, type = parser pushed by stub (int/float/float*k/colour->float[4]). Needs shim-trace emitter | +| P2-M2 manifest/id registry | phase2 | backlog | — | 0% | 2026-09-07 | no registry object: WeaponDictionary::Init 0x0059a4c0 / SectionDictionary ctor 0x00576f40 read 'id file' pairs via Script::ReadToken; id stored on def (+4), sorted vector at dict+8; DELETED lines are comments; call site DemoApp::LoadGameData 0x00899280 (deferred to first OnTick) | +| P2-M3 Mars brace-block parser | phase2 | backlog | — | 0% | 2026-09-07 | Mars::Script pull tokenizer (Open 0x008cd7d0, ReadToken 0x008cd2f0, Next 0x008cd3e0, SkipBlock 0x008cd4b0); whitespace-only delimiting, quotes " ' ` no escapes, EOF ends, stray } ignored, LAST PAIR W/O TRAILING NEWLINE DROPPED; consumers = _stricmp if/else chains (WeaponDef::ParseScript 0x00599070, SectionDef::ParseScript 0x005744e0, MasterTechTree::ParseTech 0x0058b050). .effect = Mars::TextFileStream (Open 0x008cfb90) | +| P2-M4 gobio VFS read | phase2 | backlog | — | 0% | 2026-09-07 | choke point: bool __cdecl gobio::ReadFile(const char*, IBuffer**) 0x008d5140; FileSystemSet ctor 0x008d6d60 pushes NativeFileSystem(".") FIRST then sots.ini [Modules] Mount (fallback sots.gob, sots_local_en.gob); zip lookup case+slash-insensitive 0x008d4a50 -> matches mars/vfs NativeFirst | +| loader prototypes (Ghidra) | meta | verified | high | 100% | 2026-09-07 | findings/subsystems/loader-prototypes.md: 80 entries (79 verified) in ghidra/addresses.json; Initialize = bool(this, AppStartup*) RET 4 (M0 crash explained); 93 fns renamed + 9 structs in Ghidra | | compare harness | verify | mapped | high | 100% | 2026-09-07 | verify/harness/compare/: TRACE_FORMAT.md, tracecmp.py, replay, oracle bridge | | engine: mars/parse | engine | verified | high | 100% | 2026-09-07 | sots-engine: brace-block + .effect readers; oracle 1531/1531; cross-compiles i686; wired into host+shim builds | | engine: mars/text | engine | verified | high | 100% | 2026-09-07 | flat-kv, id-manifest, csv; oracle 64/64 (Strings.csv 5722 rows); ctest green | diff --git a/campaign/open-questions.md b/campaign/open-questions.md index 72eb5d0..09306a2 100644 --- a/campaign/open-questions.md +++ b/campaign/open-questions.md @@ -31,3 +31,6 @@ Each links to the finding that raised it. Promoted to backlog or closed by **re- - **Corrections from turn internals** — `ServerPlayer+0xf9` is the AI flag (not bTurnDone); `FUN_007d7f70` is encounter detection; ProcessTurn phase 2 is trade, not diplomacy; the spine's `FUN_00840fe0` 'income' is special projects — income is `FUN_00863030` ComputeBudget. `TECHBEN_*` data blocks are inert (effects hard-coded via the 116-entry tech-name table @0x00a19718). Open: TRA/TRP producers, `FPsp2` derivation, `PERGATETRAFFIC_*` readers, exact BnkEl/BnkPr expression. (from [[strategic-turn-internals]]) - **`Mars::Application::Initialize` signature (P2-M0 finding)** — a C++ `__thiscall` detour that called the original crashed inside Initialize; the trace hook now uses a convention-agnostic asm stub. Verify the real signature (stack args? EDX use? return value?) before any `compare`/`replace` hook on it. Same caution for every `[unverified]` entry in `ghidra/addresses.json`. (from [[m0]]) - **Formula gaps surfaced by the game/sim port (feed back to Ghidra)** — which bankruptcy limit carries the 3.3 factor and when the bankruptcy start turn is stamped; the suitability→carrying-capacity hazard curve shape; the trade-points→money system-income tail; the `POPBONUS_INC` population increment; the expense-slider request term; which running total the tech-income bonus / savings aid read; node-line speed clamp at the influence radius; whether `DecayAllResearch` also hits the current target. (from [[game-sim]]) +- **RESOLVED: Initialize signature** — `bool __thiscall Application::Initialize(this, AppStartup*)`, RET 4; Run = `void(this)`; OnTick = `bool(this)`. Plain thiscall wrappers are safe for Run/OnTick; Initialize needs the extra arg. (from [[loader-prototypes]]) +- **PARITY BUG (fix in mars/text + Python flat_kv): duplicate config keys are FIRST-occurrence-wins in the engine** (entry erased on use; duplicates log 'multiply defined'); our readers implement last-wins. Also the brace parser DROPS a final key/value pair that lacks a trailing newline, and uses whitespace-only delimiting (braces not delimiters) with `" ' \`` quotes and no escapes — mars/parse must match these for compare mode; verify on the 12 broken sections + a no-trailing-newline fixture. (from [[loader-prototypes]]) +- **Not traced end-to-end** — `Species/_NPC/weapons/*.weapon` loading and the `.effect` dictionary entry `EffectDictionary_Load` (0x008b42b0, unverified). (from [[loader-prototypes]]) diff --git a/findings/subsystems/loader-prototypes.md b/findings/subsystems/loader-prototypes.md new file mode 100644 index 0000000..c83f568 --- /dev/null +++ b/findings/subsystems/loader-prototypes.md @@ -0,0 +1,431 @@ +# Data-loader prototypes — Sword of the Stars.exe (GOG 1.8.1) + +Ghidra round 2026-09-07, program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, MSVC-2010 32-bit. +All addresses are image VAs (runtime = `GetModuleHandle(NULL) + (VA - 0x00400000)`). +Machine-readable copy of every entry: `/srv/re-lab/handoff/addresses-loaders.json` (same schema as `ghidra/addresses.json`). + +Everything below was **written back into the Ghidra project** (function names in the `Mars::`/`Game::` namespaces, +prototypes with calling convention, plate comments, `/SOTS/*` structs, `g_*` data labels). Scripts: +`/srv/re-lab/handoff/scripts/LoaderRecon.java` (request-driven decompile dump) and +`/srv/re-lab/handoff/scripts/LoaderWriteBack.java` (idempotent write-back + verification decompiles). +Raw decompiles (before rename) are in `/srv/re-lab/handoff/loader-decompiles/recon/.c`; post-rename verification +decompiles in `/srv/re-lab/handoff/loader-decompiles/verify/.c`. + +Conventions: `thiscall` = `this` in ECX, args pushed right-to-left, callee pops (`RET n`); `cdecl` = all on stack, caller +pops. `std::string` = MSVC-2010 layout (16-byte SSO buffer / pointer, size @+0x10, capacity @+0x14, 0x18 bytes). +"Case-insensitive" always means MSVCR100 `_stricmp` (ASCII fold). + +--- + +## Application spine (coordinator request) {#application} + +| VA | name | convention | prototype | status | +|---|---|---|---|---| +| 0x008a0e50 | `Mars::Application::Initialize` (was `AppStartup_ReadConfig`) | **thiscall, 1 stack arg, `RET 4`** | `bool Initialize(Application* this, AppStartup* startup)` | verified | +| 0x0089f5b0 | `Mars::Application::Run` | thiscall, 0 stack args | `void Run(Application* this)` | verified | +| 0x0089a640 | `Game::DemoApp::OnTick` | thiscall, 0 stack args | `bool OnTick(DemoApp* this)` | verified | + +**Why the M0 detour crashed:** `Initialize` takes one stack parameter and cleans it itself. Prologue +`MOV EBX,[EBP+0x8]; MOV EDI,ECX` (0x008a0e7e/0x008a0e81); Ghidra stack purge = 4. WinMain (0x0089dd30) calls it as +`if (Initialize(&startup)) Run();` where `startup` is a stack struct built in WinMain: + +``` +struct AppStartup { // built at WinMain local_44 + HINSTANCE hInst; // +0 (param_1 of WinMain) + std::vector args; // +4 command line tokens (0x00460fa0 tokeniser) + int unk_10; // +0x10 + int nShow; // +0x14 (WinMain param_4) + const char* wndClass; // +0x18 "Kerberos_SwordOfTheStars_WndCls" + int iconId; // +0x1c 0x66 +}; +``` + +A `__thiscall` wrapper declared as `void (Application*)` returns with a plain `RET`, leaving 4 bytes on the stack ⇒ the +caller's frame is off by 4 and the very next thing (the SEH frame / cookie restore in WinMain) faults. Declare the hook as +`bool __thiscall Initialize_hook(Application* self, AppStartup* startup)` and call the original with the same argument. +The return value is a `bool` in AL (`cVar1 = Initialize(&local_44); if (cVar1 != 0) Run();`). EDX is not read. +Inside, the order that matters for the loaders is: `MountModules()` (0x008a0a20, gobio init) → display/sound config → +… → "Initializing GlobalConsts" `GlobalConsts::LoadAll()` (0x008b76d0) → textures/effects/sprites/keymap/font → +`IApplication::OnStartup()` (vft+0x10). + +`Run` (0x0089f5b0): `PUSH EBX/ESI/EDI; MOV ESI,ECX` — no `[ESP+…]` argument reads, plain `RET`; EDX is overwritten +before use. Return value is meaningless (EAX = last `PumpMessages()`), WinMain ignores it. `OnTick` (0x0089a640): +`PUSH EBX/ESI; MOV ESI,ECX`, plain `RET`; returns `0` when `this+0x1b2` (quit flag) is set, else `1` (tail +`return 1`). Safe to wrap with `bool __thiscall (DemoApp*)`. + +--- + +## M1 — flat `KEY value` config loader (GlobalConsts) {#m1} + +### The static-init stub (~600 of them, 0x009a4720 … 0x009c1400) +``` +009ba5b0 PUSH 0xa238f8 ; "Data/Strategy/StrategyVars.txt" (file) +009ba5b5 PUSH 0x8b7000 ; parser fn (int) +009ba5ba PUSH 0xa26a80 ; "KEY" +009ba5bf PUSH 0xb23e24 ; &g_KEY (storage word the game reads) +009ba5c4 MOV ECX,0xb2484c ; &cfgvar (16-byte GlobalConst object) +009ba5c9 CALL 0x008b76a0 ; GlobalConst::GlobalConst +009ba5ce RET +``` +The earlier note (`strategic-turn-internals.md §0`) missed the **fourth argument, the file name**; the parser is the +third. `Data/globals.txt` keys use the lower-case string `"data/globals.txt"` @0x009e56f4 in some stubs and +`"Data/globals.txt"` @0x009e8f40 in others — both are the same file for the case-insensitive VFS. + +### Object and functions +``` +struct GlobalConst { // /SOTS/GlobalConst, 0x10 bytes, one static per key + void* storage; // +0 int* / float* / float[4]; the code reads *storage (this is the "PTR_g_KEY" slot) + const char* key; // +4 matched case-insensitively + GlobalConstParseFn parser; // +8 void __cdecl (*)(void* storage, const char* valueToken) + const char* file; // +c "Data/Strategy/StrategyVars.txt" | "Data/globals.txt" | "Data/Species.txt" | "Data/encounters.txt" | … +}; +``` +| VA | name | conv | prototype | notes | +|---|---|---|---|---| +| 0x008b76a0 | `Mars::GlobalConst::GlobalConst` | thiscall, `RET 0x10` | `GlobalConst* (GlobalConst* this, void* storage, const char* key, GlobalConstParseFn parser, const char* file)` | stores the 4 words, calls `Register(this)` | +| 0x008b7610 | `Mars::GlobalConst::Register` | cdecl | `void (GlobalConst* g)` | insert into `g_GlobalConstRegistry`; logs `GlobalConsts: %s already registered. Overriding previous definition.` / `%s being registered after loading const table.` | +| 0x008b7000 | `Mars::GlobalConst::ParseInt` | cdecl | `void (int* storage, const char* text)` | `sscanf(text,"%d",storage)` | +| 0x008b7020 | `Mars::GlobalConst::ParseFloat` | cdecl | `void (float* storage, const char* text)` | `sscanf(text,"%f",storage)` | +| 0x008b70e0 | `Mars::GlobalConst::ParseFloatScaled` | cdecl | `void (float* storage, const char* text)` | `%f` then `*storage *= *(float*)0x00af5210` (angle → radians style constant) | +| 0x008b7110 | `Mars::GlobalConst::ParseColour` | cdecl | `void (float rgba[4], const char* text)` | `sscanf(text,"%d %d %d %d")` with all four defaulted to **255**; each `/255.0` and clamped to 0..1 → **storage is `float[4]`**; `"r g b"` gives alpha 1.0 | +| 0x008b76d0 | `Mars::GlobalConsts::LoadAll` | cdecl | `void (void)` | called **once from `Application::Initialize`** (only caller). Builds the set of distinct `file` names over the registry, then for each file builds `std::map` of that file's keys and calls `LoadFile(file, &map)`; finally `g_GlobalConstsLoaded = 1` | +| 0x008b73c0 | `Mars::GlobalConsts::LoadFile` | cdecl | `void (const char* file, GlobalConstMap* consts)` | **the file-level read to hook** (see below) | +| 0x00b2d740 | `g_GlobalConstsLoaded` | data | `bool` | | +| 0x00b2d744 | `g_GlobalConstRegistry` | data | `std::map` (header node ptr at +4 = 0x00b2d748, size at +8) | node: `{left,parent,right, const char* key @+0xc, GlobalConst* @+0x10, color @+0x14, isnil @+0x15}` | + +**Type decision:** the *stub* decides the type by which parser it pushes — there is no runtime sniffing of the value +text. int (`%d`), float (`%f`), scaled float, or colour (`float[4]`). So a reimplementation needs the +key → parser map, which is exactly `turn-decompiles/turn2/constants.txt` (parser column) — 0x008b7000/0x008b7020/ +0x008b70e0/0x008b7110. + +### `LoadFile` behaviour (verify decompile `verify/008b73c0.c`) +```c +Script::Script(&s); if (!Script::Open(&s, file)) { log("GlobalConsts: Could not open %s.\n", file); return; } +tok.type = 0; +for (;;) { + if (Script::Next(&s, &tok) != 0) break; // EOF (or last pair without trailing newline) + if (tok.type == 1) { // KEY value + node = consts->lower_bound(tok.key); // FUN_008c8ff0: _stricmp ordering + if (node == end || _stricmp(tok.key, node->key) < 0) log(2, "[%s] %s not recognized or is multiply defined.\n", file, tok.key); + else { g = node->value; g->parser(g->storage, tok.value); consts->erase(node); // FUN_008b8d20 + log(4, "[%s] %s = %s\n", file, tok.key, tok.value); } + } else if (tok.type == 2) { if (Script::SkipBlock(&s, 1) != 0) break; } // a "name {" block is skipped +} +Script::Close(&s); +for (node in consts) log("[%s] %s expected but not found.\n", file, node->value->key); +``` +Leniency facts the shim must reproduce: +* keys are matched **case-insensitively**; unknown keys are logged and ignored; +* a key is consumed on first sight ⇒ **first occurrence wins**, a duplicate logs "multiply defined" (the Python + `flat_kv.parse_kv(on_dup='last')` default is the wrong way round for this loader); +* the value is **one token**: `TRADE_SECTOR_SIZE 10.0` fine, colours must be quoted `"48 29 2"` (the colour parser + gets the whole quoted token); an unquoted `0 0 0` would parse `0` and then choke on the stray `0 0` as unknown keys; +* `//` comments, `{}` blocks and quoted tokens follow the `Mars::Script` tokenizer rules (M3); +* keys that are registered but absent from the file keep the image default (logged "expected but not found"). + +Files seen in the stubs: `Data/Strategy/StrategyVars.txt` (0x00a238f8), `Data/globals.txt` / `data/globals.txt`, +`Data/Species.txt` (0x009f93e8), `Data/encounters.txt` (0x009f0254). `Data/Combat/*.txt` use the same mechanism +(same parsers, different file strings); grep the stub region for `PUSH ` to enumerate. + +--- + +## M2 — manifest / id registry (`_weapons.txt`, `_shipsections.txt`) {#m2} + +There is **no separate manifest parser or id→name map object**. The manifests are read with `Script::ReadToken` pairs +straight inside the dictionary constructors, and the id is stored on the definition object itself. + +| VA | name | conv | prototype | +|---|---|---|---| +| 0x0059a4c0 | `Game::WeaponDictionary::Init` | thiscall | `void (WeaponDictionary* this)` | +| 0x0059a230 | `Game::WeaponDictionary::LoadWeapon` | thiscall (`RET 0xc`) | `void (WeaponDictionary* this, const char* path, int id, WeaponDef** out)` | +| 0x00598a20 | `Game::WeaponDef::WeaponDef` | thiscall | `WeaponDef* (WeaponDef* this, int index, int id, const char* path)` (sizeof 0x278) | +| 0x00590a10 | `Game::WeaponDictionary::FindByName` | thiscall | `WeaponDef* (this, const char* name)` — linear `_stricmp` on `def->name` (+0x40) | +| 0x00591e30 | `Game::WeaponDictionary::FindByNameSorted` | thiscall | `WeaponDef* (this, const char* name)` — binary search; logs `Weapon not found: "%s" - Was it added to the index file?` | +| 0x00576f40 | `Game::SectionDictionary::SectionDictionary` | thiscall | `SectionDictionary* (this, TechTree* tree)` | +| 0x00576cd0 | `Game::SectionDictionary::LoadSection` | thiscall (`RET 0xc`) | `SectionDef* (this, const char* path, int species, int id)` | +| 0x00574020 | `Game::SectionDef::SectionDef` | thiscall | `SectionDef* (this, int index, int species, int id)` (sizeof 0x3d8) | +| 0x00545ec0 | `Game::Species::GetDirName` | cdecl | `const char* (uint species)` — species 0..6 → `Species/` | +| 0x00899280 | `Game::DemoApp::LoadGameData` | thiscall | `void (DemoApp* this)` — **call site**: `new MasterTechTree` → `this+0x110`; `new WeaponDictionary` (ctor 0x005917e0) → `+0x118`, then `Init()`; `new SectionDictionary(tree)` → `+0x114` | +| 0x00899870 | `Game::DemoApp::EnsureGameData` | thiscall | `void (DemoApp* this)` — runs `LoadGameData` once (flag `+0x10d`); called from `DemoApp::OnTick` when `+0x10c` is set, i.e. **deferred to the first tick after startup**, not inside `Initialize` | + +Structures: +``` +struct WeaponDictionary { // 0x1c bytes + int unk0; // +0 + TechTree* tree; // +4 passed to WeaponDef::ParseScript + std::vector defs; // +8 begin, +c end, +10 cap — sorted after load (0x00597850, comparator 0x00590b00), then def->index rewritten to position + WeaponDef* emt_light; // +18 FindByName("emt_light") +}; +struct WeaponDef (partial, /SOTS/WeaponDefPartial) { + int index; // +0 position in dictionary vector + int id; // +4 stable id from Weapons/_weapons.txt <-- the "registry" + std::string path; // +8 "Weapons/" + std::string name; // +40 'name' key (used for lookups, _stricmp) + void* payload; // +5c behaviour sub-block object (bolt/beam/…) + int damagetype; // +70 default 7 + int weaponclass; // +84 enum; name table 0x00a02760 (stride 0xc, name @+8), 0x27 entries + int weaponfamily; // +88 + int turretclass; // +b8 0x18 = not specified (warned) + int turretsize; // +bc 4 = not specified (warned) + int munitionsize; // +c0 + int cpoints; // +c4 + int cost; // +c8 + float hpbonus; // +cc + int payload_kind; // +264 index in the 17-entry behaviour-name table (0x0058f230) +}; +struct SectionDictionary { // 0x18 bytes + int cur; // +0 (-1) + TechTree* tree; // +4 + std::vector defs; // +8/+c/+10 +}; +SectionDef: +4 = species?, +0x28 = flag, +0x17c std::string localisation token ('@SECTIONNAME_…'), id/index set by ctor. +``` + +`WeaponDictionary::Init` (verify `0059a4c0.c`): +```c +Script::Script(&s); +if (!Script::Open(&s, "Weapons/_weapons.txt")) { log("WeaponDictionary: Couldn't open %s\n", …); } +else for (;;) { + if (Script::ReadToken(&s, tok, 0x100) == 0) id = atoi(tok); // note: id keeps its previous value if this read fails + if (Script::ReadToken(&s, tok, 0x100) != 0) break; // file name token; break at EOF + path = std::string("Weapons/") + tok; + LoadWeapon(this, path.c_str(), id, NULL); +} +sort(defs); for i: defs[i]->index = i; emt_light = FindByName("emt_light"); +``` +`// DELETED - n` lines are ordinary `//` comments to the tokenizer, so deleted ids simply never appear. Because +`ReadToken` returns 2 (end) when the token it just read ends exactly at EOF, **a manifest whose last line has no +trailing newline loses its last entry** — the shipped files end with CRLF. Non-numeric first token → `atoi` = 0 +(no error). Missing files (`SectionDictionary: Couldn't load [%s]`) are logged and skipped, matching the 10 dangling +`_shipsections.txt` ids. + +`SectionDictionary::SectionDictionary` loops `species = 0..6`, path = `Species::GetDirName(species) + "/" + +"_shipsections.txt"`, same token-pair loop, `LoadSection(dir + "/" + file, species, id)`, `push_back` if non-NULL. +After all seven it checks every def's `@` localisation token against the string table (`Section name %s not +localized.` / `Section description %s not found.`). + +--- + +## M3 — Mars brace-block parser (`Mars::Script`) {#m3} + +Not a tree builder: it is a **pull tokenizer**; every consumer (`WeaponDef::ParseScript`, `SectionDef::ParseScript`, +`MasterTechTree::ParseTech`, GUI `.script` callbacks, `GlobalConsts::LoadFile`) drives it with `Next()` and an +`_stricmp` if/else chain, and skips what it does not understand with `SkipBlock(1)`. There is no generic node model to +fill — the shim reimplements per-consumer fills against the same token stream. + +``` +struct Script { // /SOTS/Script, 0x28 bytes, vtable Mars::Script::vftable + void* vftable; // +0 + std::string path; // +4 .. +0x1b + int pad; // +1c + IBuffer* buf; // +20 whole file from gobio::ReadFile (NULL when closed) + uint cursor; // +24 offset into buf->data +}; +struct ScriptToken { // /SOTS/ScriptToken, 0x804 bytes, lives on the caller's stack + int type; // +0 0 none, 1 "key value", 2 "key {", 3 "}" + char key[1024]; // +4 + char value[1024]; // +404 value, or "{" +}; +``` +| VA | name | conv | prototype | notes | +|---|---|---|---|---| +| 0x008cd700 | `Mars::Script::Script` | thiscall | `Script* (Script* this)` | | +| 0x008cd720 | `Mars::Script::~Script` | thiscall | `void (Script* this)` | releases `buf` | +| 0x008cd770 | `Mars::Script::ScalarDeletingDtor` | thiscall | `Script* (Script* this, byte flags)` | | +| 0x008cd7d0 | `Mars::Script::Open` | thiscall | `bool (Script* this, const char* path)` | `path` → std::string; releases old buffer; `gobio::ReadFile(path, &this->buf)`; **cursor is NOT reset here** (Close/ctor do it) | +| 0x008cd6c0 | `Mars::Script::Close` | thiscall | `void (Script* this)` | release buffer, clear path, cursor = 0 | +| 0x008cd2f0 | `Mars::Script::ReadToken` | thiscall | `int (Script* this, char* out, uint outMax)` | 0 = token read, 1 = no buffer / cursor past end, 2 = cursor reached end of buffer (also right after the *last* token) | +| 0x008cd3e0 | `Mars::Script::Next` | thiscall | `int (Script* this, ScriptToken* tok)` | see below | +| 0x008cd4b0 | `Mars::Script::SkipBlock` | thiscall | `int (Script* this, int depth)` | consumes tokens counting `{`/`}` until depth 0; returns 0, or nonzero at EOF | +| 0x008cd1f0 | `Mars::Script::ScanToken` | custom regs | EAX cursor, ECX "edFlag, EDX out; stack end, outEnd, &len | internal | +| 0x008cd180 | `Mars::Script::ScanBareword` | custom regs | | copies until whitespace | +| 0x008cd2b0 | `Mars::Script::SkipLineComment` | custom regs | | to after next `\r`/`\n` run | +| 0x008cd820 | `Mars::ConfigParser::ParseFile` | cdecl | `void (IConfigCallback* cb, const char* path, const char* blockName, bool* found)` | GUI `.script` / startup config entry: `blockName` empty → `cb->vft[1](&script)` once; else for each top-level block `_stricmp(tok.key, blockName)==0 → cb->vft[1](&script)` else `SkipBlock(1)` | +| 0x008a7230 | `Mars::RefCounted_Release` | fastcall (ECX) | `int (IBuffer* obj)` | `--refcount`, `vft[0](1)` at zero | + +### Tokenizer rules (from `ReadToken`/`ScanToken`/`ScanBareword`, `recon/008cd2f0.c`, `008cd1f0.c`, `008cd180.c`) +* whitespace = `' '`, `\t`, `\r`, `\n` only; +* a token that starts with `//` (after whitespace) is a comment: skip to the end of the line and read again. The check is + done on the extracted token, so `//` glued to a preceding word is *not* a comment, and (edge case) a *quoted* token + whose content starts with `//` is also treated as a comment; +* quotes: an opening `"`, `'` or `` ` `` starts a quoted token that ends at the **same** character; no escape processing, + the quotes are stripped, whitespace inside is kept; an unterminated quote runs to EOF; +* barewords end **only at whitespace** — braces are not delimiters, so `weapon{` is one token (`weapon {` is required — + which is what every shipped file does); `key{value}` style never occurs; +* output is truncated silently at `outMax-1` (1023 for `Next`, 255 in the manifest loops); +* no `/* */`, no `=`, no BOM handling (a UTF-8 BOM would become part of the first token). + +### `Next()` (verify `008cd3e0.c`, disasm `recon/disasm_008cd3e0.txt`) +```c +tok->type = 0; tok->key[0] = tok->value[0] = 0; +rc = ReadToken(this, tok->key, 0x400); if (rc != 0) return rc; // EOF: type stays 0 +if (strcmp(tok->key, "}") == 0) { tok->type = 3; return 0; } +rc = ReadToken(this, tok->value, 0x400); // value may be "" at EOF +tok->type = (strcmp(tok->value, "{") == 0) ? 2 : 1; +return rc; // <- status of the SECOND read +``` +Consequences (all observed in the shipped data and matching `data-parsers.md`): +* callers loop `while (Next(&s,&tok) == 0)`, so **EOF anywhere simply ends parsing** — an unclosed outer `{` is fine + (the 11 broken shipsections); +* a stray top-level `}` yields type 3, which top-level loops ignore (`CrPropaganda.shipsection`); +* a final `key value` pair whose value token touches EOF (no trailing newline) returns 2 and is **dropped**; +* `"bare quoted item"` on its own (systemnames.txt) is read as key = the quoted text and value = the next token — + consumers that expect that format handle it themselves; +* the brace/`}` tests are exact `strcmp`, everything else in the consumers is `_stricmp`. + +### How the catalog loaders consume it +`Game::WeaponDef::ParseScript` 0x00599070 — thiscall `bool (WeaponDef* this, Script* script, int* rc, TechTree* tree)`, +called by `LoadWeapon` right after `Next()` returned the `weapon {` token: +```c +this->turretclass = 0x18; this->turretsize = 4; // "unset" sentinels +while (*rc == 0) { + *rc = Script::Next(script, &tok); if (*rc != 0) break; + if (tok.type == 1) { // 66 keys, all _stricmp, unknown keys silently ignored + if (!_stricmp(tok.key,"hidden")) flags ^= (ParseBool(tok.value)<<1 ^ flags) & 2; // 0x008ccfc0 bool parser + else if (!_stricmp(tok.key,"name")) this->name = ResolveStringToken(tok.value); // 0x008c97a0: '@TOKEN' -> Strings.csv + else if (!_stricmp(tok.key,"weaponclass")) { if (LookupClass(tok.value,&p) < 1) { log("Weapon::ParseScript: Unrecognized weapon class \"%s\"\n"); class = 0; } else class = *p; } + else if (!_stricmp(tok.key,"weaponfamily")) … // 0x0058fad0, 0 if unknown + else if (!_stricmp(tok.key,"weapondamagetype")) … // 0x0058fba0, 7 if unknown + else if (!_stricmp(tok.key,"icon_file")) … "icon_rect" (0x008ccf60 rect parser) "turretclass" (0x0058f7a0) "turretsize"/"munitionsize" (0x0058f7e0) + else if (!_stricmp(tok.key,"cpoints")) this->cpoints = atoi(tok.value); + else if (!_stricmp(tok.key,"cost")) { this->cost = atoi(v); if (cost < 0) log("…savings cost < 0"); } + else if (!_stricmp(tok.key,"hpbonus")) this->hpbonus = (float)atof(v); + … (range, range_planet, recharge_time, burst_volleys, volley_period, requires -> tree lookup, …) + } else if (tok.type == 2) { // sub-block + k = index of tok.key in the 17-name behaviour table (0x0058f230: bolt, beam, torpedo, rider, missile, chainlightning, col, mine, disintegrator, grapple, projectedshield, mirv, nodecannon, siege, mesonprojector, spyship, wraith; _stricmp) + if (k == 17) *rc = Script::SkipBlock(script, 1); // unknown block (e.g. anim) skipped + else { delete old payload ("deleting old payload"); this->payload = CreatePayload(tok.key) /*0x00597ae0*/; this->payload_kind = k; + *rc = this->payload->vft[1](script); } // payload parses its own block (bolt{rangetable{…}} etc.) + } else if (tok.type == 3) break; // closing '}' of weapon{} +} +if (turretclass == 0x18) log("[%s] Required 'turretclass' not specified.\n"); if (turretsize == 4) log(… 'turretsize' …); +return true; +``` +Numbers are `atoi`/`atof` (so `.5`, `-.8`, `7e+8` are fine, `"8"` quoted also parses — quoting never changes typing, +unlike the Python reader). Booleans go through 0x008ccfc0 (`true/false/1/0`, case-insensitive). `@TOKEN` strings are +resolved through the string table at parse time (0x008c97a0). + +`Game::SectionDef::ParseScript` 0x005744e0 — same signature and shape (87 keys; sub-blocks `bank`→0x00572bf0, +`mount`, `thruster`→0x00570f80, `option`/`optiondef`, `anim`, `netforcelimits`→0x00573d70). Scalar and block forms of +`option` coexist because type 1 and type 2 are separate branches. `Game::MasterTechTree::LoadTechFile` 0x0058b770 +(thiscall `void (MasterTechTree* this, const char* path, int flag)`) loops top-level blocks, `tech` → `ParseTech` +0x0058b050 (thiscall `void (this, Script*, int flag)`, details in `strategic-turn-internals.md §2.5`), anything else +`SkipBlock(1)`. `MasterTechTree::MasterTechTree` 0x0058b870 builds the file list starting from +`TechTree/MasterTechList.tech`. + +### `.effect` `TXT` / `BEGIN`–`END` reader — a different class: `Mars::TextFileStream` {#effect} +This is the text back-end of the `Mars::Stream` serializer (the same interface the save game uses through +`StreamableHelper`). Vtable 0x00a428c0 (RTTI `.?AVTextFileStream@Mars@@`), object 0x420 bytes: +``` +struct TextFileStream { // /SOTS/TextFileStream + void* vftable; // +0 + bool label_missing_reported; // +4 + int mode; // +8 1 read (gobio buffer), 2 write (FILE*) + IBuffer* buf; // +c + uint cursor; // +10 + FILE* file; // +14 write mode + int indent; // +18 write mode tab depth + char line[1024]; // +1c current line + bool line_pushed_back; // +41c the last line did not match; re-use it for the next label +}; +``` +| VA | name | conv | prototype | +|---|---|---|---| +| 0x008cfcb0 | `Mars::Stream::OpenFile` | cdecl | `Stream* (const char* path)` — sniffs the `TXT` magic through gobio (0x008cf940/0x008cf8c0) and returns a `new TextFileStream` opened for read, `NULL` on failure | +| 0x0091e460 | `Mars::TextFileStream::TextFileStream` | thiscall | `TextFileStream* (this)` | +| 0x008cfb90 | `Mars::TextFileStream::Open` | thiscall | `bool (this, const char* path, int mode /*must be 1*/)` — `gobio::ReadFile`; buffer must start with `"TXT"` (0x00af6718), then `\r`/`\n` skipped | +| 0x008cfa30 | `Mars::FileStream::ReadLine` | thiscall | `uint (this, char* out, int max)` — copies through `\n`, NUL over the `\n` | +| 0x0091e760 | `Mars::TextFileStream::MatchLabel` | thiscall | `const char* (this, const char* line, const char* label)` — first word `END` → NULL; `label==NULL` → accept; else skip blanks, **exact case-sensitive** label followed by blank/EOL → pointer to the value text; mismatch → logs `TextFileStream: missing label: %s` once, returns NULL | +| 0x0091ec90 | `ReadString` vft[1] | thiscall | `bool (this, const char* label, char* out, int outMax)` — value = text between the first `"` pair, backslash escapes `\n \t \r \b \f \v`, other `\x` → `x` | +| 0x0091eba0 | `ReadBool` vft[2] | thiscall | `bool (this, const char* label, bool* out)` — `0/F/f` false, `1/T/t` true, else untouched | +| 0x0091eb30 | `ReadFloat` vft[3] | thiscall | `bool (this, const char* label, float* out)` — `sscanf %f` | +| 0x0091eab0 | `ReadInt` vft[4] | thiscall | `bool (this, const char* label, int* out, int dflt)` — `sscanf %d`, `*out = 0` if unparsable | +| 0x0091e8e0 | `ReadNested` vft[5] | thiscall | `void (this, const char* label, IStreamable* obj)` — label line, then `BEGIN` … `END`; `obj == NULL` skips the block counting nested BEGIN/END; else `obj->vft[1](this)` | +| 0x0091ee80/0x0091e6c0/0x0091e630/0x0091e5a0/0x0091e4c0 | `WriteString/WriteBool/WriteFloat/WriteInt/WriteNested` vft[6..10] | thiscall | writers: `LABEL "str"`, `LABEL TRUE|FALSE`, `LABEL %f`, `LABEL %d`, `LABEL` + `BEGIN`/`END` with tab indent | +| 0x008b9d90/0x008b9d20/0x008b9c00/0x008b9bc0 | `Mars::Stream::ReadString/ReadInt/ReadBool/ReadFloat` | cdecl | `void (Stream* s, const char* label, T* out, int flag)` — thin wrappers used by the `*::Read` functions | +| 0x008db650 | `Mars::ParticleSystem::Read` | thiscall | `void (ParticleSystem* this, Stream* s)` — the `.effect` body | +| 0x008dad60 | `Mars::ParticleSystem::Write` | thiscall | `void (this, Stream* s)` — gives the canonical label order | +| 0x008b42b0 | `Mars::EffectDictionary::Load` (probable) | thiscall | `void* (this, const char* name)` — `"effects/" + name`, `Stream::OpenFile`, 0x1c0-byte effect object | + +Semantics for the reimplementation: every read is `label → typed value`; **labels are matched in order and are +case-sensitive**; a missing label leaves the default and the same line is retried for the next label (so labels may be +omitted but not reordered); `END` terminates a nested block; `BEGIN`/`END` are always balanced (the writer emits them); +nested objects are `PARTICLEDATATYPE n` followed by `CREATION`/`VARIATION`/`OVERLIFE` blocks (`OVERLIFE` only when +`PARTICLEDATATYPE != 0`). The label list (from `Write`): `NAME, ALIGNTOMOTION, FORCESINGLE, USEWORLDSPACE, MOMXFER, +MESHFILENAME, TEXTUREFILENAME, SHADERFILENAME, , RANDOMIZESEED, BACKTOFRONTDRAW, PENNANTTRAIL, +CREATIONRATE{…}, COLOR_R, COLOR_G, COLOR_B, COLOR_A, NUMINITIALPARTICLES, NUM_TRACKS, per track (PARTICLEDATATYPE, +CREATION{…}, VARIATION{…}, [OVERLIFE{…}]), NUM_ATTRACTORS, ATTRACTORINDEX*`. + +--- + +## M4 — `gobio` virtual file system {#m4} + +``` +struct FileSystemSet { // /SOTS/FileSystemSet; singleton g_gobioInstance @0x00b2e274, g_gobio @0x00b2e270 -> it + std::vector fs; // +0 begin, +4 end, +8 cap + int pad; // +c + int current; // +10 index of the file system that has the currently open file, -1 +}; +IFileSystem vtable (RTTI .?AVIFileSystem@gobio@Mars@@, abstract 0x00a39870): + [0] dtor(flags) [1] bool Open(const char* path) [2] void Close() [3] uint Size() [4] uint Read(void* dst, uint n) + [5] void ListFiles(const char* pattern, std::set* out) [6] bool Exists(const char* path) +NativeFileSystem (vtable 0x00a398ac, 0x28 bytes): +4 valid, +8 std::string root ("" = cwd, else "dir/"), +24 FILE* + [0] 0x008d5220 [1] 0x008d4ef0 fopen(root+path,"rb") [2] 0x008d4ba0 [3] 0x008d4bc0 [4] 0x008d4c30 fread [5] 0x008d6730 _findfirst [6] 0x008d4f60 +ZipFileSystem (vtable 0x00a39934, 0x34 bytes): +4 std::string archive, +20 unz handle, +28 std::map (PathCompareCI) + [0] 0x008d6d30 [1] 0x008d56a0 Locate+unzOpenCurrentFile [2] 0x008d4af0 [3] 0x008d4b00 [4] 0x008d4b40 unzRead loop [5] 0x008d64d0 PathMatchSpecA [6] 0x008d56e0 +struct IBuffer / gobio::Buffer (vtable 0x00a39898, 0x10 bytes): +0 vft, +4 refcount, +8 data, +c size + [0] dtor [1]=[2] 0x00682d50 Data() [3] 0x008d4c80 Size() +``` +| VA | name | conv | prototype | notes | +|---|---|---|---|---| +| 0x008d5140 | `Mars::gobio::ReadFile` | cdecl | **`bool (const char* path, IBuffer** out)`** | **"read whole file by relative path"**: `g_gobio->Open(path)`; `n = fs->Size()`; `Buffer::Create(n)`; `fs->Read(buf->data, n) == n ? *out = buf, true : release, false`; `fs->Close()`. Used by `Script::Open`, `TextFileStream::Open`, textures, sounds, shaders (14 callers). Release with `RefCounted_Release(buf)` (0x008a7230, ECX). | +| 0x008d5060 | `Mars::gobio::FileSystemSet::Open` | thiscall | `bool (FileSystemSet* this, const char* path)` | closes any open file, then the **first** file system in vector order whose `Open` succeeds wins | +| 0x008d50d0 | `Mars::gobio::Open` | cdecl | `bool (const char* path)` | `g_gobio->Open` | +| 0x008d4d20 / 0x008d4d50 / 0x008d4d80 | `gobio::Close / Size / Read` | cdecl | `void()`, `uint()`, `uint(void* dst, uint n)` | streaming access to the currently open file | +| 0x008d50f0 | `Mars::gobio::Exists` | cdecl | `bool (const char* path)` | any file system | +| 0x008d71d0 / 0x008d7090 | `gobio::ListFiles` / `FileSystemSet::ListFiles` | cdecl / thiscall | `void (const char* pattern, std::vector* out)` | union over all file systems (used for `TechTree/*.tech`, effect lists…) | +| 0x008d7010 | `Mars::gobio::Init` | cdecl | `void (std::vector* mountPaths)` | one-shot (guard 0x00b2e288) constructs the singleton, sets `g_gobio` | +| 0x008d6d60 | `Mars::gobio::FileSystemSet::FileSystemSet` | thiscall | `FileSystemSet* (this, std::vector* mounts)` | mount order, see below | +| 0x008d57a0 | `Mars::gobio::NativeFileSystem::NativeFileSystem` | thiscall | `(this, const char* root)` | logs `Mounting current directory...` / `Mounting subdirectory %s...`; `PathIsDirectoryA` check; appends `/` | +| 0x008d6c40 | `Mars::gobio::ZipFileSystem::ZipFileSystem` | thiscall | `(this, const char* zipPath)` | opens the `.gob` (zip) and indexes entries | +| 0x008d4a50 | `Mars::gobio::PathCompareCI` | custom regs | | `\` == `/`, ASCII case-fold ⇒ **zip entry lookup is case- and slash-insensitive** (native side relies on NTFS being case-insensitive) | +| 0x008a0a20 | `Mars::Application::MountModules` | cdecl | `void (void)` | called from `Application::Initialize` first thing after the CPU-affinity check | +| 0x008991c0 | `Game::GetDefaultMounts` | cdecl | `void (int* count, const char*** table)` | `count = 3` (0x00b2d514), table `g_DefaultMountTable` 0x00a35e98 (`"sots_local_en.gob"` @0x00a35ea4, `"sots.gob"` @0x00a35eb8, …) | + +### How the `.gob` list is registered and the override order +`MountModules` reads `sots.ini` (`GetPrivateProfileSectionA("Modules", …)` relative to the current directory, +0x008e7a50) and collects every `Mount=` key sorted by `N`; with no entries it falls back to +`GetDefaultMounts()`. The list goes to `gobio::Init(paths)` → `FileSystemSet::FileSystemSet(paths)`: +1. if no path is `.`, `./` or `.\`, push `NativeFileSystem("")` (**current directory**) **first**; +2. then, in list order: `.`-style → `NativeFileSystem("")`; `PathIsDirectoryA(path)` → `NativeFileSystem(path)`; + otherwise → `ZipFileSystem(path)` (`Invalid mount path: %s` for empty strings). + +`FileSystemSet::Open` walks that vector in order and stops at the first hit ⇒ **loose files in the game directory +override archived ones, and earlier `Mount` entries override later ones** (native-first, then gobs in ini order). +`ListFiles` merges all of them. + +--- + +## Startup / call-site summary (what the shim hooks and when) + +``` +WinMain 0x0089dd30 + new Game::DemoApp (0x1b8) -> g_pDemoApp + Mars::Application::Initialize(app, &startup) 0x008a0e50 [thiscall, RET 4, bool] + Application::MountModules() 0x008a0a20 sots.ini [Modules] -> gobio::Init (VFS live from here) + ... display/audio config via ConfigParser::ParseFile(cb, "sots.cfg"/…) + GlobalConsts::LoadAll() 0x008b76d0 -> GlobalConsts::LoadFile(file, map) per data file [M1 hook] + textures / effects (Stream::OpenFile -> TextFileStream) / sprites / keymap / font + IApplication::OnStartup() (vft+0x10) + Mars::Application::Run(app) 0x0089f5b0 main loop + DemoApp::OnTick() 0x0089a640 + if (+0x10c) DemoApp::EnsureGameData() 0x00899870 (once) + DemoApp::LoadGameData() 0x00899280 + new MasterTechTree 0x0058b870 TechTree/MasterTechList.tech -> LoadTechFile -> ParseTech + WeaponDictionary::Init 0x0059a4c0 Weapons/_weapons.txt -> LoadWeapon -> WeaponDef::ParseScript [M2/M3 hooks] + new SectionDictionary(tree) 0x00576f40 Species/*/_shipsections.txt -> LoadSection -> SectionDef::ParseScript + GUI scripts via ConfigParser::ParseFile +``` +Every file read on these paths bottoms out in `gobio::ReadFile(path, &buf)` 0x008d5140 — hooking that one function +(cdecl, two stack args) intercepts every data file the game opens, with the relative path exactly as the game spells +it (mixed case, `/` separators). diff --git a/ghidra/addresses.json b/ghidra/addresses.json index 53b4104..2a1ec1a 100644 --- a/ghidra/addresses.json +++ b/ghidra/addresses.json @@ -1,31 +1,822 @@ { - "_schema": "facts about Sword of the Stars.exe (GOG 1.8.1, build 19833). rva = file address - 0x00400000. Runtime address = GetModuleHandle(NULL) + rva (exe is ASLR-relocated). Prototypes marked 'unverified' are best current reading; a Ghidra round confirms them.", - "image_base": "0x00400000", - "entries": [ - {"name":"Mars_Application_Initialize","addr":"0x008a0e50","convention":"thiscall","prototype":"void (Application* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"}, - {"name":"Mars_Application_Run","addr":"0x0089f5b0","convention":"thiscall","prototype":"int (Application* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"}, - {"name":"Process_PinAffinity","addr":"0x0089ee70","convention":"cdecl","prototype":"void (int coreIndex)","status":"unverified","source":"findings/objects/ghidra-recon.md"}, - {"name":"CfgVar_RegisterKey","addr":"0x008b76a0","convention":"thiscall","prototype":"void (CfgVar* this, void* parser, const char* key, void* storage)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md#config-plumbing"}, - {"name":"DemoApp_OnTick","addr":"0x0089a640","convention":"thiscall","prototype":"void (DemoApp* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"}, - {"name":"StrategyClient_EndTurn","addr":"0x00783be0","convention":"thiscall","prototype":"void (StrategyClient* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"}, - {"name":"StrategyServer_BeginProcessTurn","addr":"0x007d98e0","convention":"thiscall","prototype":"void (StrategyServer* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"}, - {"name":"StrategyServer_ProcessTurn","addr":"0x007dc6c0","convention":"thiscall","prototype":"void (StrategyServer* this)","status":"unverified","source":"findings/control-flow/turn-spine.md"}, - {"name":"ServerPlayer_ComputeBudget","addr":"0x00863030","convention":"thiscall","prototype":"void (ServerPlayer* this, int* budget25)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md"}, - {"name":"TechTree_ProcessResearch","addr":"0x005876c0","convention":"thiscall","prototype":"void (TechTree* this, ...)","status":"unverified","source":"findings/control-flow/turn-spine.md"}, - {"name":"ServerSystem_Read","addr":"0x0075d4b0","convention":"thiscall","prototype":"void (ServerSystem* this /*+8 IStreamable*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"}, - {"name":"ServerSystem_Write","addr":"0x00749630","convention":"thiscall","prototype":"void (ServerSystem* this /*+8*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"}, - {"name":"ServerPlayer_Read","addr":"0x008804d0","convention":"thiscall","prototype":"void (ServerPlayer* this /*+0x3a0*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"}, - {"name":"ServerPlayer_Write","addr":"0x008563e0","convention":"thiscall","prototype":"void (ServerPlayer* this /*+0x3a0*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"}, - {"name":"StarFleet_Read","addr":"0x00702470","convention":"thiscall","prototype":"void (StarFleet* this /*+8*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"}, - {"name":"StarFleet_Write","addr":"0x00701070","convention":"thiscall","prototype":"void (StarFleet* this /*+8*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"}, - {"name":"StarShip_Read","addr":"0x00853fa0","convention":"thiscall","prototype":"void (StarShip* this /*+8*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"}, - {"name":"StarShip_Write","addr":"0x008291f0","convention":"thiscall","prototype":"void (StarShip* this /*+8*/, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"}, - {"name":"StrategyServer_Read","addr":"0x007d27a0","convention":"thiscall","prototype":"void (StrategyServer* this, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"}, - {"name":"StrategyServer_Write","addr":"0x0079fa70","convention":"thiscall","prototype":"void (StrategyServer* this, Stream* s)","status":"verified-by-save","source":"findings/objects/struct-recovery.md"}, - {"name":"Stream_WriteNetworkObjectId","addr":"0x00816490","convention":"thiscall","prototype":"void (Stream* this, const char* tag, NetworkObject* obj)","status":"unverified","source":"findings/objects/struct-recovery.md"}, - {"name":"RNG_Twist","addr":"0x00426e00","convention":"thiscall","prototype":"void (RNG* this)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md#rng"}, - {"name":"RNG_Seed","addr":"0x0049fdf0","convention":"thiscall","prototype":"void (RNG* this, uint32_t seed)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md#rng"}, - {"name":"RNG_NextFloat","addr":"0x0047d830","convention":"thiscall","prototype":"float (RNG* this)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md#rng"}, - {"name":"RNG_NextInt","addr":"0x004271c0","convention":"thiscall","prototype":"uint32_t (RNG* this)","status":"unverified","source":"findings/subsystems/strategic-turn-internals.md#rng"} - ] -} + "_schema": "facts about Sword of the Stars.exe (GOG 1.8.1, build 19833). rva = file address - 0x00400000. Runtime address = GetModuleHandle(NULL) + rva (exe is ASLR-relocated). Prototypes marked 'unverified' are best current reading; a Ghidra round confirms them.", + "image_base": "0x00400000", + "entries": [ + { + "name": "Mars_Application_Initialize", + "addr": "0x008a0e50", + "convention": "thiscall", + "prototype": "bool (Application* this, AppStartup* startup) /* ONE stack arg, RET 4, returns AL; EDX unused. startup = {HINSTANCE hInst; std::vector args; ...; int nShow; const char* wndClass; int iconId} built in WinMain */", + "status": "verified", + "source": "handoff/loader-prototypes.md#application" + }, + { + "name": "Mars_Application_Run", + "addr": "0x0089f5b0", + "convention": "thiscall", + "prototype": "void (Application* this) /* no stack args, plain RET, EDX not an input, EAX = last PumpMessages result (ignore) */", + "status": "verified", + "source": "handoff/loader-prototypes.md#application" + }, + { + "name": "Process_PinAffinity", + "addr": "0x0089ee70", + "convention": "cdecl", + "prototype": "void (int coreIndex)", + "status": "unverified", + "source": "findings/objects/ghidra-recon.md" + }, + { + "name": "CfgVar_RegisterKey", + "addr": "0x008b76a0", + "convention": "thiscall", + "prototype": "GlobalConst* (GlobalConst* this, void* storage, const char* key, ParseFn parser, const char* file) // RET 0x10", + "status": "verified", + "source": "findings/subsystems/loader-prototypes.md" + }, + { + "name": "DemoApp_OnTick", + "addr": "0x0089a640", + "convention": "thiscall", + "prototype": "bool (DemoApp* this) /* no stack args; returns 0 when quitting (this+0x1b2), else 1 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#application" + }, + { + "name": "StrategyClient_EndTurn", + "addr": "0x00783be0", + "convention": "thiscall", + "prototype": "void (StrategyClient* this)", + "status": "unverified", + "source": "findings/control-flow/turn-spine.md" + }, + { + "name": "StrategyServer_BeginProcessTurn", + "addr": "0x007d98e0", + "convention": "thiscall", + "prototype": "void (StrategyServer* this)", + "status": "unverified", + "source": "findings/control-flow/turn-spine.md" + }, + { + "name": "StrategyServer_ProcessTurn", + "addr": "0x007dc6c0", + "convention": "thiscall", + "prototype": "void (StrategyServer* this)", + "status": "unverified", + "source": "findings/control-flow/turn-spine.md" + }, + { + "name": "ServerPlayer_ComputeBudget", + "addr": "0x00863030", + "convention": "thiscall", + "prototype": "void (ServerPlayer* this, int* budget25)", + "status": "unverified", + "source": "findings/subsystems/strategic-turn-internals.md" + }, + { + "name": "TechTree_ProcessResearch", + "addr": "0x005876c0", + "convention": "thiscall", + "prototype": "void (TechTree* this, ...)", + "status": "unverified", + "source": "findings/control-flow/turn-spine.md" + }, + { + "name": "ServerSystem_Read", + "addr": "0x0075d4b0", + "convention": "thiscall", + "prototype": "void (ServerSystem* this /*+8 IStreamable*/, Stream* s)", + "status": "verified-by-save", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "ServerSystem_Write", + "addr": "0x00749630", + "convention": "thiscall", + "prototype": "void (ServerSystem* this /*+8*/, Stream* s)", + "status": "verified-by-save", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "ServerPlayer_Read", + "addr": "0x008804d0", + "convention": "thiscall", + "prototype": "void (ServerPlayer* this /*+0x3a0*/, Stream* s)", + "status": "verified-by-save", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "ServerPlayer_Write", + "addr": "0x008563e0", + "convention": "thiscall", + "prototype": "void (ServerPlayer* this /*+0x3a0*/, Stream* s)", + "status": "verified-by-save", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "StarFleet_Read", + "addr": "0x00702470", + "convention": "thiscall", + "prototype": "void (StarFleet* this /*+8*/, Stream* s)", + "status": "verified-by-save", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "StarFleet_Write", + "addr": "0x00701070", + "convention": "thiscall", + "prototype": "void (StarFleet* this /*+8*/, Stream* s)", + "status": "verified-by-save", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "StarShip_Read", + "addr": "0x00853fa0", + "convention": "thiscall", + "prototype": "void (StarShip* this /*+8*/, Stream* s)", + "status": "verified-by-save", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "StarShip_Write", + "addr": "0x008291f0", + "convention": "thiscall", + "prototype": "void (StarShip* this /*+8*/, Stream* s)", + "status": "verified-by-save", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "StrategyServer_Read", + "addr": "0x007d27a0", + "convention": "thiscall", + "prototype": "void (StrategyServer* this, Stream* s)", + "status": "verified-by-save", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "StrategyServer_Write", + "addr": "0x0079fa70", + "convention": "thiscall", + "prototype": "void (StrategyServer* this, Stream* s)", + "status": "verified-by-save", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "Stream_WriteNetworkObjectId", + "addr": "0x00816490", + "convention": "thiscall", + "prototype": "void (Stream* this, const char* tag, NetworkObject* obj)", + "status": "unverified", + "source": "findings/objects/struct-recovery.md" + }, + { + "name": "RNG_Twist", + "addr": "0x00426e00", + "convention": "thiscall", + "prototype": "void (RNG* this)", + "status": "unverified", + "source": "findings/subsystems/strategic-turn-internals.md#rng" + }, + { + "name": "RNG_Seed", + "addr": "0x0049fdf0", + "convention": "thiscall", + "prototype": "void (RNG* this, uint32_t seed)", + "status": "unverified", + "source": "findings/subsystems/strategic-turn-internals.md#rng" + }, + { + "name": "RNG_NextFloat", + "addr": "0x0047d830", + "convention": "thiscall", + "prototype": "float (RNG* this)", + "status": "unverified", + "source": "findings/subsystems/strategic-turn-internals.md#rng" + }, + { + "name": "RNG_NextInt", + "addr": "0x004271c0", + "convention": "thiscall", + "prototype": "uint32_t (RNG* this)", + "status": "unverified", + "source": "findings/subsystems/strategic-turn-internals.md#rng" + }, + { + "name": "Mars_Application_MountModules", + "addr": "0x008a0a20", + "convention": "cdecl", + "prototype": "void (void) /* sots.ini [Modules] Mount -> gobio::Init */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "Game_GetDefaultMounts", + "addr": "0x008991c0", + "convention": "cdecl", + "prototype": "void (int* count, const char*** table) /* count 3 @0x00b2d514, table @0x00a35e98 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "GlobalConst_ctor", + "addr": "0x008b76a0", + "convention": "thiscall", + "prototype": "GlobalConst* (GlobalConst* this, void* storage, const char* key, GlobalConstParseFn parser, const char* file) /* RET 0x10; static-init stub call */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m1" + }, + { + "name": "GlobalConst_Register", + "addr": "0x008b7610", + "convention": "cdecl", + "prototype": "void (GlobalConst* g)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m1" + }, + { + "name": "GlobalConst_ParseInt", + "addr": "0x008b7000", + "convention": "cdecl", + "prototype": "void (int* storage, const char* text)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m1" + }, + { + "name": "GlobalConst_ParseFloat", + "addr": "0x008b7020", + "convention": "cdecl", + "prototype": "void (float* storage, const char* text)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m1" + }, + { + "name": "GlobalConst_ParseFloatScaled", + "addr": "0x008b70e0", + "convention": "cdecl", + "prototype": "void (float* storage, const char* text) /* *storage = sscanf(%f) * *(float*)0x00af5210 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m1" + }, + { + "name": "GlobalConst_ParseColour", + "addr": "0x008b7110", + "convention": "cdecl", + "prototype": "void (float rgba[4], const char* text) /* '%d %d %d %d', defaults 255, /255, clamp 0..1 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m1" + }, + { + "name": "GlobalConsts_LoadAll", + "addr": "0x008b76d0", + "convention": "cdecl", + "prototype": "void (void) /* called once from Application::Initialize; sets g_GlobalConstsLoaded */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m1" + }, + { + "name": "GlobalConsts_LoadFile", + "addr": "0x008b73c0", + "convention": "cdecl", + "prototype": "void (const char* file, GlobalConstMap* consts) /* consts = std::map of the keys registered for this file; hook this for the file-level read */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m1" + }, + { + "name": "g_GlobalConstsLoaded", + "addr": "0x00b2d740", + "convention": "data", + "prototype": "bool", + "status": "verified", + "source": "handoff/loader-prototypes.md#m1" + }, + { + "name": "g_GlobalConstRegistry", + "addr": "0x00b2d744", + "convention": "data", + "prototype": "std::map /* +4 (0x00b2d748) = header node pointer */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m1" + }, + { + "name": "Script_ctor", + "addr": "0x008cd700", + "convention": "thiscall", + "prototype": "Script* (Script* this) /* sizeof(Script) = 0x28 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "Script_dtor", + "addr": "0x008cd720", + "convention": "thiscall", + "prototype": "void (Script* this)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "Script_Open", + "addr": "0x008cd7d0", + "convention": "thiscall", + "prototype": "bool (Script* this, const char* path) /* gobio::ReadFile into this->buf */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "Script_Close", + "addr": "0x008cd6c0", + "convention": "thiscall", + "prototype": "void (Script* this)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "Script_ReadToken", + "addr": "0x008cd2f0", + "convention": "thiscall", + "prototype": "int (Script* this, char* out, uint outMax) /* 0 token, 1 no buffer/past end, 2 reached end */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "Script_Next", + "addr": "0x008cd3e0", + "convention": "thiscall", + "prototype": "int (Script* this, ScriptToken* tok) /* tok: {int type; char key[1024]; char value[1024]}; type 1 kv, 2 block open, 3 '}' */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "Script_SkipBlock", + "addr": "0x008cd4b0", + "convention": "thiscall", + "prototype": "int (Script* this, int depth) /* 0 ok, nonzero EOF */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "Script_ScanToken", + "addr": "0x008cd1f0", + "convention": "custom", + "prototype": "/* EAX=cursor, ECX="edFlag, EDX=out; stack: end, outEnd, &len \u2014 internal, do not hook */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "ConfigParser_ParseFile", + "addr": "0x008cd820", + "convention": "cdecl", + "prototype": "void (IConfigCallback* cb, const char* path, const char* blockName, bool* found) /* cb->vft[1](Script*) per matching block */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "RefCounted_Release", + "addr": "0x008a7230", + "convention": "fastcall", + "prototype": "int (IBuffer* obj) /* ECX=obj; --refcount, vft[0](1) at 0 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "WeaponDictionary_Init", + "addr": "0x0059a4c0", + "convention": "thiscall", + "prototype": "void (WeaponDictionary* this) /* reads Weapons/_weapons.txt manifest */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m2" + }, + { + "name": "WeaponDictionary_LoadWeapon", + "addr": "0x0059a230", + "convention": "thiscall", + "prototype": "void (WeaponDictionary* this, const char* path, int id, WeaponDef** out)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m2" + }, + { + "name": "WeaponDictionary_FindByName", + "addr": "0x00590a10", + "convention": "thiscall", + "prototype": "WeaponDef* (WeaponDictionary* this, const char* name) /* _stricmp over def->name @+0x40 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m2" + }, + { + "name": "WeaponDictionary_FindByNameSorted", + "addr": "0x00591e30", + "convention": "thiscall", + "prototype": "WeaponDef* (WeaponDictionary* this, const char* name) /* 'Weapon not found: \"%s\" - Was it added to the index file?' */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m2" + }, + { + "name": "WeaponDef_ctor", + "addr": "0x00598a20", + "convention": "thiscall", + "prototype": "WeaponDef* (WeaponDef* this, int index, int id, const char* path) /* sizeof 0x278 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "WeaponDef_ParseScript", + "addr": "0x00599070", + "convention": "thiscall", + "prototype": "bool (WeaponDef* this, Script* script, int* rc, TechTree* tree)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "SectionDictionary_ctor", + "addr": "0x00576f40", + "convention": "thiscall", + "prototype": "SectionDictionary* (SectionDictionary* this, TechTree* tree) /* reads Species//_shipsections.txt for species 0..6 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m2" + }, + { + "name": "SectionDictionary_LoadSection", + "addr": "0x00576cd0", + "convention": "thiscall", + "prototype": "SectionDef* (SectionDictionary* this, const char* path, int species, int id)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m2" + }, + { + "name": "SectionDef_ctor", + "addr": "0x00574020", + "convention": "thiscall", + "prototype": "SectionDef* (SectionDef* this, int index, int species, int id) /* sizeof 0x3d8 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "SectionDef_ParseScript", + "addr": "0x005744e0", + "convention": "thiscall", + "prototype": "bool (SectionDef* this, Script* script, int* rc, TechTree* tree)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "Species_GetDirName", + "addr": "0x00545ec0", + "convention": "cdecl", + "prototype": "const char* (uint species)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m2" + }, + { + "name": "MasterTechTree_ctor", + "addr": "0x0058b870", + "convention": "thiscall", + "prototype": "MasterTechTree* (MasterTechTree* this) /* TechTree/MasterTechList.tech */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "MasterTechTree_LoadTechFile", + "addr": "0x0058b770", + "convention": "thiscall", + "prototype": "void (MasterTechTree* this, const char* path, int flag)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "MasterTechTree_ParseTech", + "addr": "0x0058b050", + "convention": "thiscall", + "prototype": "void (MasterTechTree* this, Script* script, int flag) /* one tech{} block */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m3" + }, + { + "name": "DemoApp_LoadGameData", + "addr": "0x00899280", + "convention": "thiscall", + "prototype": "void (DemoApp* this) /* MasterTechTree -> +0x110, WeaponDictionary -> +0x118, SectionDictionary -> +0x114 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m2" + }, + { + "name": "DemoApp_EnsureGameData", + "addr": "0x00899870", + "convention": "thiscall", + "prototype": "void (DemoApp* this) /* once, from OnTick when this+0x10c is set */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m2" + }, + { + "name": "gobio_Init", + "addr": "0x008d7010", + "convention": "cdecl", + "prototype": "void (std::vector* mountPaths)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_FileSystemSet_ctor", + "addr": "0x008d6d60", + "convention": "thiscall", + "prototype": "FileSystemSet* (FileSystemSet* this, std::vector* mounts)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_FileSystemSet_Open", + "addr": "0x008d5060", + "convention": "thiscall", + "prototype": "bool (FileSystemSet* this, const char* path)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_FileSystemSet_ListFiles", + "addr": "0x008d7090", + "convention": "thiscall", + "prototype": "void (FileSystemSet* this, const char* pattern, std::vector* out)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_ReadFile", + "addr": "0x008d5140", + "convention": "cdecl", + "prototype": "bool (const char* path, IBuffer** out) /* whole file by relative path; THE hook point for M4 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_Open", + "addr": "0x008d50d0", + "convention": "cdecl", + "prototype": "bool (const char* path)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_Close", + "addr": "0x008d4d20", + "convention": "cdecl", + "prototype": "void (void)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_Size", + "addr": "0x008d4d50", + "convention": "cdecl", + "prototype": "uint (void)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_Read", + "addr": "0x008d4d80", + "convention": "cdecl", + "prototype": "uint (void* dst, uint n)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_Exists", + "addr": "0x008d50f0", + "convention": "cdecl", + "prototype": "bool (const char* path)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_ListFiles", + "addr": "0x008d71d0", + "convention": "cdecl", + "prototype": "void (const char* pattern, std::vector* out)", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_Buffer_Create", + "addr": "0x008d4c90", + "convention": "custom", + "prototype": "/* size in EBX, IBuffer** out on stack \u2014 internal, do not hook */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_Buffer_Data", + "addr": "0x00682d50", + "convention": "thiscall", + "prototype": "void* (IBuffer* this) /* vft[1] and vft[2]; = this+8 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_Buffer_Size", + "addr": "0x008d4c80", + "convention": "thiscall", + "prototype": "uint (IBuffer* this) /* vft[3]; = this+0xc */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_NativeFileSystem_ctor", + "addr": "0x008d57a0", + "convention": "thiscall", + "prototype": "NativeFileSystem* (NativeFileSystem* this, const char* root) /* sizeof 0x28; vtable 0x00a398ac */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_NativeFileSystem_Open", + "addr": "0x008d4ef0", + "convention": "thiscall", + "prototype": "bool (NativeFileSystem* this, const char* path) /* vft[1] fopen(root+path,'rb') */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_NativeFileSystem_Read", + "addr": "0x008d4c30", + "convention": "thiscall", + "prototype": "uint (NativeFileSystem* this, void* dst, uint n) /* vft[4] */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_ZipFileSystem_ctor", + "addr": "0x008d6c40", + "convention": "thiscall", + "prototype": "ZipFileSystem* (ZipFileSystem* this, const char* zipPath) /* sizeof 0x34; vtable 0x00a39934 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_ZipFileSystem_Open", + "addr": "0x008d56a0", + "convention": "thiscall", + "prototype": "bool (ZipFileSystem* this, const char* path) /* vft[1]; case- and slash-insensitive */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "gobio_ZipFileSystem_Read", + "addr": "0x008d4b40", + "convention": "thiscall", + "prototype": "uint (ZipFileSystem* this, void* dst, uint n) /* vft[4] */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "g_gobio", + "addr": "0x00b2e270", + "convention": "data", + "prototype": "FileSystemSet* /* -> 0x00b2e274 after gobio::Init */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "g_DefaultMountTable", + "addr": "0x00a35e98", + "convention": "data", + "prototype": "const char*[3] /* used when sots.ini has no [Modules] Mount; contains sots_local_en.gob, sots.gob */", + "status": "verified", + "source": "handoff/loader-prototypes.md#m4" + }, + { + "name": "Stream_OpenFile", + "addr": "0x008cfcb0", + "convention": "cdecl", + "prototype": "Stream* (const char* path) /* sniffs 'TXT' magic, returns a TextFileStream (0x420 bytes) opened for read */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "TextFileStream_ctor", + "addr": "0x0091e460", + "convention": "thiscall", + "prototype": "TextFileStream* (TextFileStream* this) /* vtable 0x00a428c0 */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "TextFileStream_Open", + "addr": "0x008cfb90", + "convention": "thiscall", + "prototype": "bool (TextFileStream* this, const char* path, int mode /*1*/)", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "FileStream_ReadLine", + "addr": "0x008cfa30", + "convention": "thiscall", + "prototype": "uint (Stream* this, char* out, int max)", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "TextFileStream_MatchLabel", + "addr": "0x0091e760", + "convention": "thiscall", + "prototype": "const char* (TextFileStream* this, const char* line, const char* label) /* NULL on END/mismatch */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "TextFileStream_ReadString", + "addr": "0x0091ec90", + "convention": "thiscall", + "prototype": "bool (TextFileStream* this, const char* label, char* out, int outMax) /* vft[1] */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "TextFileStream_ReadBool", + "addr": "0x0091eba0", + "convention": "thiscall", + "prototype": "bool (TextFileStream* this, const char* label, bool* out) /* vft[2] */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "TextFileStream_ReadFloat", + "addr": "0x0091eb30", + "convention": "thiscall", + "prototype": "bool (TextFileStream* this, const char* label, float* out) /* vft[3] */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "TextFileStream_ReadInt", + "addr": "0x0091eab0", + "convention": "thiscall", + "prototype": "bool (TextFileStream* this, const char* label, int* out, int dflt) /* vft[4] */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "TextFileStream_ReadNested", + "addr": "0x0091e8e0", + "convention": "thiscall", + "prototype": "void (TextFileStream* this, const char* label, IStreamable* obj /*NULL=skip*/) /* vft[5]; BEGIN..END */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "ParticleSystem_Read", + "addr": "0x008db650", + "convention": "thiscall", + "prototype": "void (ParticleSystem* this, Stream* s) /* .effect body */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "ParticleSystem_Write", + "addr": "0x008dad60", + "convention": "thiscall", + "prototype": "void (ParticleSystem* this, Stream* s)", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "EffectDictionary_Load", + "addr": "0x008b42b0", + "convention": "thiscall", + "prototype": "void* (EffectDictionary* this, const char* name) /* 'effects/' + name; Stream::OpenFile; 0x1c0-byte effect */", + "status": "unverified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "Stream_ReadString", + "addr": "0x008b9d90", + "convention": "cdecl", + "prototype": "void (Stream* s, const char* label, std::string* out, int flag) /* wrapper over vft[1] */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "Stream_ReadInt", + "addr": "0x008b9d20", + "convention": "cdecl", + "prototype": "void (Stream* s, const char* label, int* out, int dflt) /* wrapper over vft[4] */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "Stream_ReadBool", + "addr": "0x008b9c00", + "convention": "cdecl", + "prototype": "void (Stream* s, const char* label, bool* out) /* wrapper over vft[2] */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + }, + { + "name": "Stream_ReadFloat", + "addr": "0x008b9bc0", + "convention": "cdecl", + "prototype": "void (Stream* s, const char* label, float* out, int flag) /* wrapper over vft[3] */", + "status": "verified", + "source": "handoff/loader-prototypes.md#effect" + } + ] +} \ No newline at end of file diff --git a/ghidra/generated/sots_addresses.h b/ghidra/generated/sots_addresses.h index 0f30196..f73b87e 100644 --- a/ghidra/generated/sots_addresses.h +++ b/ghidra/generated/sots_addresses.h @@ -1,5 +1,5 @@ // GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1). -// Source: sots-re ghidra/addresses.json @ daea98f, generated 2026-09-07 by tools/gen_addresses.py +// Source: sots-re ghidra/addresses.json @ f1c7f55, generated 2026-09-07 by tools/gen_addresses.py // Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated). #pragma once #include @@ -7,15 +7,15 @@ namespace sots::addr { constexpr uint32_t IMAGE_BASE = 0x00400000; -// thiscall void (Application* this) [unverified] +// thiscall bool (Application* this, AppStartup* startup) /* ONE stack arg, RET 4, returns AL; EDX unused. startup = {HINSTANCE hInst; std::vector args; ...; int nShow; const char* wndClass; int iconId} built in WinMain */ [verified] constexpr uint32_t Mars_Application_Initialize = 0x004a0e50; -// thiscall int (Application* this) [unverified] +// thiscall void (Application* this) /* no stack args, plain RET, EDX not an input, EAX = last PumpMessages result (ignore) */ [verified] constexpr uint32_t Mars_Application_Run = 0x0049f5b0; // cdecl void (int coreIndex) [unverified] constexpr uint32_t Process_PinAffinity = 0x0049ee70; -// thiscall void (CfgVar* this, void* parser, const char* key, void* storage) [unverified] +// thiscall GlobalConst* (GlobalConst* this, void* storage, const char* key, ParseFn parser, const char* file) // RET 0x10 [verified] constexpr uint32_t CfgVar_RegisterKey = 0x004b76a0; -// thiscall void (DemoApp* this) [unverified] +// thiscall bool (DemoApp* this) /* no stack args; returns 0 when quitting (this+0x1b2), else 1 */ [verified] constexpr uint32_t DemoApp_OnTick = 0x0049a640; // thiscall void (StrategyClient* this) [unverified] constexpr uint32_t StrategyClient_EndTurn = 0x00383be0; @@ -57,5 +57,159 @@ constexpr uint32_t RNG_Seed = 0x0009fdf0; constexpr uint32_t RNG_NextFloat = 0x0007d830; // thiscall uint32_t (RNG* this) [unverified] constexpr uint32_t RNG_NextInt = 0x000271c0; +// cdecl void (void) /* sots.ini [Modules] Mount -> gobio::Init */ [verified] +constexpr uint32_t Mars_Application_MountModules = 0x004a0a20; +// cdecl void (int* count, const char*** table) /* count 3 @0x00b2d514, table @0x00a35e98 */ [verified] +constexpr uint32_t Game_GetDefaultMounts = 0x004991c0; +// thiscall GlobalConst* (GlobalConst* this, void* storage, const char* key, GlobalConstParseFn parser, const char* file) /* RET 0x10; static-init stub call */ [verified] +constexpr uint32_t GlobalConst_ctor = 0x004b76a0; +// cdecl void (GlobalConst* g) [verified] +constexpr uint32_t GlobalConst_Register = 0x004b7610; +// cdecl void (int* storage, const char* text) [verified] +constexpr uint32_t GlobalConst_ParseInt = 0x004b7000; +// cdecl void (float* storage, const char* text) [verified] +constexpr uint32_t GlobalConst_ParseFloat = 0x004b7020; +// cdecl void (float* storage, const char* text) /* *storage = sscanf(%f) * *(float*)0x00af5210 */ [verified] +constexpr uint32_t GlobalConst_ParseFloatScaled = 0x004b70e0; +// cdecl void (float rgba[4], const char* text) /* '%d %d %d %d', defaults 255, /255, clamp 0..1 */ [verified] +constexpr uint32_t GlobalConst_ParseColour = 0x004b7110; +// cdecl void (void) /* called once from Application::Initialize; sets g_GlobalConstsLoaded */ [verified] +constexpr uint32_t GlobalConsts_LoadAll = 0x004b76d0; +// cdecl void (const char* file, GlobalConstMap* consts) /* consts = std::map of the keys registered for this file; hook this for the file-level read */ [verified] +constexpr uint32_t GlobalConsts_LoadFile = 0x004b73c0; +// data bool [verified] +constexpr uint32_t g_GlobalConstsLoaded = 0x0072d740; +// data std::map /* +4 (0x00b2d748) = header node pointer */ [verified] +constexpr uint32_t g_GlobalConstRegistry = 0x0072d744; +// thiscall Script* (Script* this) /* sizeof(Script) = 0x28 */ [verified] +constexpr uint32_t Script_ctor = 0x004cd700; +// thiscall void (Script* this) [verified] +constexpr uint32_t Script_dtor = 0x004cd720; +// thiscall bool (Script* this, const char* path) /* gobio::ReadFile into this->buf */ [verified] +constexpr uint32_t Script_Open = 0x004cd7d0; +// thiscall void (Script* this) [verified] +constexpr uint32_t Script_Close = 0x004cd6c0; +// thiscall int (Script* this, char* out, uint outMax) /* 0 token, 1 no buffer/past end, 2 reached end */ [verified] +constexpr uint32_t Script_ReadToken = 0x004cd2f0; +// thiscall int (Script* this, ScriptToken* tok) /* tok: {int type; char key[1024]; char value[1024]}; type 1 kv, 2 block open, 3 '}' */ [verified] +constexpr uint32_t Script_Next = 0x004cd3e0; +// thiscall int (Script* this, int depth) /* 0 ok, nonzero EOF */ [verified] +constexpr uint32_t Script_SkipBlock = 0x004cd4b0; +// custom /* EAX=cursor, ECX="edFlag, EDX=out; stack: end, outEnd, &len — internal, do not hook */ [verified] +constexpr uint32_t Script_ScanToken = 0x004cd1f0; +// cdecl void (IConfigCallback* cb, const char* path, const char* blockName, bool* found) /* cb->vft[1](Script*) per matching block */ [verified] +constexpr uint32_t ConfigParser_ParseFile = 0x004cd820; +// fastcall int (IBuffer* obj) /* ECX=obj; --refcount, vft[0](1) at 0 */ [verified] +constexpr uint32_t RefCounted_Release = 0x004a7230; +// thiscall void (WeaponDictionary* this) /* reads Weapons/_weapons.txt manifest */ [verified] +constexpr uint32_t WeaponDictionary_Init = 0x0019a4c0; +// thiscall void (WeaponDictionary* this, const char* path, int id, WeaponDef** out) [verified] +constexpr uint32_t WeaponDictionary_LoadWeapon = 0x0019a230; +// thiscall WeaponDef* (WeaponDictionary* this, const char* name) /* _stricmp over def->name @+0x40 */ [verified] +constexpr uint32_t WeaponDictionary_FindByName = 0x00190a10; +// thiscall WeaponDef* (WeaponDictionary* this, const char* name) /* 'Weapon not found: "%s" - Was it added to the index file?' */ [verified] +constexpr uint32_t WeaponDictionary_FindByNameSorted = 0x00191e30; +// thiscall WeaponDef* (WeaponDef* this, int index, int id, const char* path) /* sizeof 0x278 */ [verified] +constexpr uint32_t WeaponDef_ctor = 0x00198a20; +// thiscall bool (WeaponDef* this, Script* script, int* rc, TechTree* tree) [verified] +constexpr uint32_t WeaponDef_ParseScript = 0x00199070; +// thiscall SectionDictionary* (SectionDictionary* this, TechTree* tree) /* reads Species//_shipsections.txt for species 0..6 */ [verified] +constexpr uint32_t SectionDictionary_ctor = 0x00176f40; +// thiscall SectionDef* (SectionDictionary* this, const char* path, int species, int id) [verified] +constexpr uint32_t SectionDictionary_LoadSection = 0x00176cd0; +// thiscall SectionDef* (SectionDef* this, int index, int species, int id) /* sizeof 0x3d8 */ [verified] +constexpr uint32_t SectionDef_ctor = 0x00174020; +// thiscall bool (SectionDef* this, Script* script, int* rc, TechTree* tree) [verified] +constexpr uint32_t SectionDef_ParseScript = 0x001744e0; +// cdecl const char* (uint species) [verified] +constexpr uint32_t Species_GetDirName = 0x00145ec0; +// thiscall MasterTechTree* (MasterTechTree* this) /* TechTree/MasterTechList.tech */ [verified] +constexpr uint32_t MasterTechTree_ctor = 0x0018b870; +// thiscall void (MasterTechTree* this, const char* path, int flag) [verified] +constexpr uint32_t MasterTechTree_LoadTechFile = 0x0018b770; +// thiscall void (MasterTechTree* this, Script* script, int flag) /* one tech{} block */ [verified] +constexpr uint32_t MasterTechTree_ParseTech = 0x0018b050; +// thiscall void (DemoApp* this) /* MasterTechTree -> +0x110, WeaponDictionary -> +0x118, SectionDictionary -> +0x114 */ [verified] +constexpr uint32_t DemoApp_LoadGameData = 0x00499280; +// thiscall void (DemoApp* this) /* once, from OnTick when this+0x10c is set */ [verified] +constexpr uint32_t DemoApp_EnsureGameData = 0x00499870; +// cdecl void (std::vector* mountPaths) [verified] +constexpr uint32_t gobio_Init = 0x004d7010; +// thiscall FileSystemSet* (FileSystemSet* this, std::vector* mounts) [verified] +constexpr uint32_t gobio_FileSystemSet_ctor = 0x004d6d60; +// thiscall bool (FileSystemSet* this, const char* path) [verified] +constexpr uint32_t gobio_FileSystemSet_Open = 0x004d5060; +// thiscall void (FileSystemSet* this, const char* pattern, std::vector* out) [verified] +constexpr uint32_t gobio_FileSystemSet_ListFiles = 0x004d7090; +// cdecl bool (const char* path, IBuffer** out) /* whole file by relative path; THE hook point for M4 */ [verified] +constexpr uint32_t gobio_ReadFile = 0x004d5140; +// cdecl bool (const char* path) [verified] +constexpr uint32_t gobio_Open = 0x004d50d0; +// cdecl void (void) [verified] +constexpr uint32_t gobio_Close = 0x004d4d20; +// cdecl uint (void) [verified] +constexpr uint32_t gobio_Size = 0x004d4d50; +// cdecl uint (void* dst, uint n) [verified] +constexpr uint32_t gobio_Read = 0x004d4d80; +// cdecl bool (const char* path) [verified] +constexpr uint32_t gobio_Exists = 0x004d50f0; +// cdecl void (const char* pattern, std::vector* out) [verified] +constexpr uint32_t gobio_ListFiles = 0x004d71d0; +// custom /* size in EBX, IBuffer** out on stack — internal, do not hook */ [verified] +constexpr uint32_t gobio_Buffer_Create = 0x004d4c90; +// thiscall void* (IBuffer* this) /* vft[1] and vft[2]; = this+8 */ [verified] +constexpr uint32_t gobio_Buffer_Data = 0x00282d50; +// thiscall uint (IBuffer* this) /* vft[3]; = this+0xc */ [verified] +constexpr uint32_t gobio_Buffer_Size = 0x004d4c80; +// thiscall NativeFileSystem* (NativeFileSystem* this, const char* root) /* sizeof 0x28; vtable 0x00a398ac */ [verified] +constexpr uint32_t gobio_NativeFileSystem_ctor = 0x004d57a0; +// thiscall bool (NativeFileSystem* this, const char* path) /* vft[1] fopen(root+path,'rb') */ [verified] +constexpr uint32_t gobio_NativeFileSystem_Open = 0x004d4ef0; +// thiscall uint (NativeFileSystem* this, void* dst, uint n) /* vft[4] */ [verified] +constexpr uint32_t gobio_NativeFileSystem_Read = 0x004d4c30; +// thiscall ZipFileSystem* (ZipFileSystem* this, const char* zipPath) /* sizeof 0x34; vtable 0x00a39934 */ [verified] +constexpr uint32_t gobio_ZipFileSystem_ctor = 0x004d6c40; +// thiscall bool (ZipFileSystem* this, const char* path) /* vft[1]; case- and slash-insensitive */ [verified] +constexpr uint32_t gobio_ZipFileSystem_Open = 0x004d56a0; +// thiscall uint (ZipFileSystem* this, void* dst, uint n) /* vft[4] */ [verified] +constexpr uint32_t gobio_ZipFileSystem_Read = 0x004d4b40; +// data FileSystemSet* /* -> 0x00b2e274 after gobio::Init */ [verified] +constexpr uint32_t g_gobio = 0x0072e270; +// data const char*[3] /* used when sots.ini has no [Modules] Mount; contains sots_local_en.gob, sots.gob */ [verified] +constexpr uint32_t g_DefaultMountTable = 0x00635e98; +// cdecl Stream* (const char* path) /* sniffs 'TXT' magic, returns a TextFileStream (0x420 bytes) opened for read */ [verified] +constexpr uint32_t Stream_OpenFile = 0x004cfcb0; +// thiscall TextFileStream* (TextFileStream* this) /* vtable 0x00a428c0 */ [verified] +constexpr uint32_t TextFileStream_ctor = 0x0051e460; +// thiscall bool (TextFileStream* this, const char* path, int mode /*1*/) [verified] +constexpr uint32_t TextFileStream_Open = 0x004cfb90; +// thiscall uint (Stream* this, char* out, int max) [verified] +constexpr uint32_t FileStream_ReadLine = 0x004cfa30; +// thiscall const char* (TextFileStream* this, const char* line, const char* label) /* NULL on END/mismatch */ [verified] +constexpr uint32_t TextFileStream_MatchLabel = 0x0051e760; +// thiscall bool (TextFileStream* this, const char* label, char* out, int outMax) /* vft[1] */ [verified] +constexpr uint32_t TextFileStream_ReadString = 0x0051ec90; +// thiscall bool (TextFileStream* this, const char* label, bool* out) /* vft[2] */ [verified] +constexpr uint32_t TextFileStream_ReadBool = 0x0051eba0; +// thiscall bool (TextFileStream* this, const char* label, float* out) /* vft[3] */ [verified] +constexpr uint32_t TextFileStream_ReadFloat = 0x0051eb30; +// thiscall bool (TextFileStream* this, const char* label, int* out, int dflt) /* vft[4] */ [verified] +constexpr uint32_t TextFileStream_ReadInt = 0x0051eab0; +// thiscall void (TextFileStream* this, const char* label, IStreamable* obj /*NULL=skip*/) /* vft[5]; BEGIN..END */ [verified] +constexpr uint32_t TextFileStream_ReadNested = 0x0051e8e0; +// thiscall void (ParticleSystem* this, Stream* s) /* .effect body */ [verified] +constexpr uint32_t ParticleSystem_Read = 0x004db650; +// thiscall void (ParticleSystem* this, Stream* s) [verified] +constexpr uint32_t ParticleSystem_Write = 0x004dad60; +// thiscall void* (EffectDictionary* this, const char* name) /* 'effects/' + name; Stream::OpenFile; 0x1c0-byte effect */ [unverified] +constexpr uint32_t EffectDictionary_Load = 0x004b42b0; +// cdecl void (Stream* s, const char* label, std::string* out, int flag) /* wrapper over vft[1] */ [verified] +constexpr uint32_t Stream_ReadString = 0x004b9d90; +// cdecl void (Stream* s, const char* label, int* out, int dflt) /* wrapper over vft[4] */ [verified] +constexpr uint32_t Stream_ReadInt = 0x004b9d20; +// cdecl void (Stream* s, const char* label, bool* out) /* wrapper over vft[2] */ [verified] +constexpr uint32_t Stream_ReadBool = 0x004b9c00; +// cdecl void (Stream* s, const char* label, float* out, int flag) /* wrapper over vft[3] */ [verified] +constexpr uint32_t Stream_ReadFloat = 0x004b9bc0; } // namespace sots::addr diff --git a/ghidra/scripts/LoaderRecon.java b/ghidra/scripts/LoaderRecon.java new file mode 100644 index 0000000..2dfdd8b --- /dev/null +++ b/ghidra/scripts/LoaderRecon.java @@ -0,0 +1,77 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.*; +import ghidra.program.model.mem.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; +import ghidra.app.decompiler.*; +import java.util.*; +import java.util.regex.*; +import java.io.*; + +// Loader recon: request-driven decompile dump. Reads /tmp/ldr/req.txt lines: +// A decompile function at addr (+ callers/callees list) +// C decompile all callers of addr +// K decompile all callees of addr +// S exact NUL-delimited string: list refs, decompile referencing functions +// P substring search in memory: list refs to containing string, decompile referencing functions +// D disassembly range dump +// X list refs (data/code) to addr, with containing function names +public class LoaderRecon extends GhidraScript { + DecompInterface decomp; Memory mem; ReferenceManager rm; + static final String OUT = "/tmp/ldr/out"; + String cstr(Address a) { try { byte[] b = new byte[100]; mem.getBytes(a, b); int i = 0; while (i < 100 && b[i] != 0 && (b[i]&0xff) >= 0x20 && (b[i]&0xff) < 0x7f) i++; if (i >= 1 && i < 100 && b[i] == 0) return new String(b, 0, i, "ISO-8859-1"); } catch (Exception e) {} return null; } + String subst(String c) { Matcher m = Pattern.compile("&?(DAT|PTR_s_|s_[A-Za-z0-9_]*|PTR_DAT|u_[A-Za-z0-9_]*)_([0-9a-f]{8})").matcher(c); StringBuffer sb = new StringBuffer(); while (m.find()) { Address a = toAddr(Long.parseLong(m.group(2), 16)); String s = cstr(a); String rep = m.group(0); if (s != null && s.length() <= 80 && !m.group(1).equals("PTR_DAT")) rep = "\"" + s + "\""; m.appendReplacement(sb, Matcher.quoteReplacement(rep)); } m.appendTail(sb); return sb.toString(); } + String decompRaw(Function f) { try { DecompileResults res = decomp.decompileFunction(f, 900, monitor); if (res == null || !res.decompileCompleted()) return "[decompile failed] " + res.getErrorMessage(); return res.getDecompiledFunction().getC(); } catch (Exception e) { return "[exception " + e.getMessage() + "]"; } } + List
findExact(String s) throws Exception { byte[] pat = ("\0" + s + "\0").getBytes("ISO-8859-1"); List
out = new ArrayList<>(); Address start = mem.getMinAddress(); while (true) { Address a = mem.findBytes(start, pat, null, true, monitor); if (a == null) break; out.add(a.add(1)); start = a.add(1); } return out; } + List
findSub(String s) throws Exception { byte[] pat = s.getBytes("ISO-8859-1"); List
out = new ArrayList<>(); Address start = mem.getMinAddress(); while (true) { Address a = mem.findBytes(start, pat, null, true, monitor); if (a == null) break; Address st = a; try { while (mem.getByte(st.subtract(1)) != 0) st = st.subtract(1); } catch (Exception e) {} if (!out.contains(st)) out.add(st); start = a.add(1); } return out; } + List
findLE(long v) throws Exception { byte[] pat = new byte[]{(byte)v,(byte)(v>>8),(byte)(v>>16),(byte)(v>>24)}; List
out = new ArrayList<>(); Address start = mem.getMinAddress(); while (out.size() < 400) { Address a = mem.findBytes(start, pat, null, true, monitor); if (a == null) break; out.add(a); start = a.add(1); } return out; } + Set done = new HashSet<>(); + PrintWriter idx; + void dumpFunc(Function f) throws Exception { + if (f == null || done.contains(f.getEntryPoint().getOffset())) return; done.add(f.getEntryPoint().getOffset()); + String t = String.format("%08x", f.getEntryPoint().getOffset()); + PrintWriter w = new PrintWriter(new FileWriter(OUT + "/" + t + ".c")); + w.println("// " + f.getName(true) + " @ " + f.getEntryPoint() + " size=" + f.getBody().getNumAddresses()); + w.println("// SIG: " + f.getSignature().getPrototypeString() + " conv=" + f.getCallingConventionName() + " stackPurge=" + f.getStackPurgeSize()); + StringBuilder calls = new StringBuilder(); for (Function cf : f.getCalledFunctions(monitor)) calls.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); w.println("// CALLS: " + calls); + StringBuilder callers = new StringBuilder(); int n = 0; for (Function cf : f.getCallingFunctions(monitor)) { if (n++ < 60) callers.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); } w.println("// CALLERS(" + n + "): " + callers); + w.println(subst(decompRaw(f))); w.close(); idx.println("decompiled " + t + " " + f.getName(true)); idx.flush(); + } + void refs(Address a, String label, boolean decomp) throws Exception { + ReferenceIterator ri = rm.getReferencesTo(a); int n = 0; + while (ri.hasNext()) { Reference r = ri.next(); Function f = getFunctionContaining(r.getFromAddress()); idx.println("REF " + label + " @" + a + " from " + r.getFromAddress() + " " + r.getReferenceType() + " in " + (f == null ? "?" : f.getName(true) + "@" + f.getEntryPoint())); if (decomp && f != null && n++ < 25) dumpFunc(f); } + idx.flush(); + } + void dumpDisasm(Address a, Address b, PrintWriter w) { InstructionIterator it = currentProgram.getListing().getInstructions(new AddressSet(a, b), true); while (it.hasNext()) { Instruction in = it.next(); String s = in.toString(); Matcher m = Pattern.compile("0x(00[9a][0-9a-f]{5})").matcher(s); StringBuffer sb = new StringBuffer(); while (m.find()) { String c = cstr(toAddr(Long.parseLong(m.group(1), 16))); m.appendReplacement(sb, Matcher.quoteReplacement(c != null ? "\"" + c + "\"" : m.group(0))); } m.appendTail(sb); w.println(in.getAddress() + " " + sb); } } + @Override public void run() throws Exception { + mem = currentProgram.getMemory(); rm = currentProgram.getReferenceManager(); + decomp = new DecompInterface(); decomp.openProgram(currentProgram); new File(OUT).mkdirs(); + idx = new PrintWriter(new FileWriter(OUT + "/index.txt")); + BufferedReader br = new BufferedReader(new FileReader("/tmp/ldr/req.txt")); String line; + while ((line = br.readLine()) != null) { + line = line.trim(); if (line.isEmpty() || line.startsWith("#")) continue; + char op = line.charAt(0); String arg = line.substring(1).trim(); + idx.println("== " + line); + try { + if (op == 'A') dumpFunc(getFunctionAt(toAddr(Long.parseLong(arg, 16)))); + else if (op == 'C') { Function f = getFunctionAt(toAddr(Long.parseLong(arg, 16))); if (f != null) for (Function cf : f.getCallingFunctions(monitor)) dumpFunc(cf); } + else if (op == 'K') { Function f = getFunctionAt(toAddr(Long.parseLong(arg, 16))); if (f != null) for (Function cf : f.getCalledFunctions(monitor)) dumpFunc(cf); } + else if (op == 'S') { for (Address a : findExact(arg)) refs(a, "\"" + arg + "\"", true); } + else if (op == 'P') { for (Address a : findSub(arg)) { String s = cstr(a); idx.println("STR @" + a + " = " + s); refs(a, "\"" + s + "\"", true); } } + else if (op == 'N') { for (Symbol sy : currentProgram.getSymbolTable().getSymbols(arg)) { idx.println("SYM " + sy.getName(true) + " @" + sy.getAddress()); refs(sy.getAddress(), sy.getName(true), true); } } + else if (op == 'F') { Pattern pt = Pattern.compile(arg); for (Function f : currentProgram.getFunctionManager().getFunctions(true)) { if (pt.matcher(f.getName(true)).find()) { idx.println("FN " + f.getName(true) + " @" + f.getEntryPoint() + " sig=" + f.getSignature().getPrototypeString()); dumpFunc(f); } } } + else if (op == 'B') { long v = Long.parseLong(arg, 16); for (Address a : findLE(v)) { Function f = getFunctionContaining(a); idx.println("IMM " + arg + " @" + a + " in " + (f == null ? "data" : f.getName(true) + "@" + f.getEntryPoint())); } } + else if (op == 'V') { for (Address s : findSub(".?AV" + arg)) { String nm = cstr(s); Address td = s.subtract(8); idx.println("TD " + nm + " @" + td); + for (Address colp : findLE(td.getOffset())) { Address col = colp.subtract(0xc); try { if (mem.getInt(col) != 0) continue; } catch (Exception e) { continue; } + int off = mem.getInt(col.add(4)); idx.println(" COL @" + col + " offset=" + off); + for (Address vp : findLE(col.getOffset())) { Address vt = vp.add(4); idx.println(" VTABLE @" + vt + " (offset " + off + ")"); + for (int i = 0; i < 40; i++) { long fp = mem.getInt(vt.add(i*4)) & 0xffffffffL; Function f = getFunctionAt(toAddr(fp)); if (f == null) { Symbol sy = getSymbolAt(toAddr(fp)); if (fp < 0x401000L || fp > 0x990000L) break; idx.println(" [" + i + "] " + toAddr(fp) + " (no function)"); continue; } idx.println(" [" + i + "] " + f.getName(true) + " @" + f.getEntryPoint()); if (i < 24) dumpFunc(f); } + } } } } + else if (op == 'X') { + refs(toAddr(Long.parseLong(arg, 16)), "addr", false); } + else if (op == 'D') { String[] p = arg.split("\\s+"); PrintWriter dw = new PrintWriter(new FileWriter(OUT + "/disasm_" + p[0] + ".txt")); dumpDisasm(toAddr(Long.parseLong(p[0], 16)), toAddr(Long.parseLong(p[1], 16)), dw); dw.close(); } + } catch (Exception e) { idx.println("ERR " + line + ": " + e); } + } + idx.close(); decomp.dispose(); println("done"); + } +} diff --git a/ghidra/scripts/LoaderWriteBack.java b/ghidra/scripts/LoaderWriteBack.java new file mode 100644 index 0000000..0926d53 --- /dev/null +++ b/ghidra/scripts/LoaderWriteBack.java @@ -0,0 +1,258 @@ +import ghidra.app.script.GhidraScript; +import ghidra.app.cmd.function.ApplyFunctionSignatureCmd; +import ghidra.app.util.parser.FunctionSignatureParser; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; +import ghidra.program.model.data.*; +import ghidra.app.decompiler.*; +import java.util.*; +import java.io.*; + +// Write-back for the data-loader round (2026-09-07): GlobalConst config plumbing, Mars::Script brace parser, +// catalog loaders (weapons/sections/tech), gobio VFS, TextFileStream (.effect), and the Application spine +// signatures (Initialize/Run/OnTick). Log: /tmp/ldr/writeback.log. Verification decompiles: /tmp/ldr/verify/. +public class LoaderWriteBack extends GhidraScript { + DataTypeManager dtm; SymbolTable st; PrintWriter log; DecompInterface decomp; + CategoryPath CAT = new CategoryPath("/SOTS"); + static final String DOC = "Source: /srv/re-lab/handoff/loader-prototypes.md (data-loader prototypes, 2026-09-07)"; + Map types = new HashMap(); + List verify = new ArrayList(); + + DataType prim(String k) { + if (k.equals("int")) return IntegerDataType.dataType; + if (k.equals("uint")) return UnsignedIntegerDataType.dataType; + if (k.equals("float")) return FloatDataType.dataType; + if (k.equals("bool")) return BooleanDataType.dataType; + if (k.equals("u8")) return ByteDataType.dataType; + if (k.equals("ptr")) return new PointerDataType(VoidDataType.dataType); + if (k.equals("cstr")) return new PointerDataType(CharDataType.dataType); + if (k.endsWith("*")) { DataType t = types.get(k.substring(0, k.length()-1)); return new PointerDataType(t == null ? VoidDataType.dataType : t); } + if (k.startsWith("char[")) { int n = Integer.parseInt(k.substring(5, k.length()-1)); return new ArrayDataType(CharDataType.dataType, n, 1); } + if (k.startsWith("float[")) { int n = Integer.parseInt(k.substring(6, k.length()-1)); return new ArrayDataType(FloatDataType.dataType, n, 4); } + DataType t = types.get(k); if (t != null) return t; + throw new RuntimeException("unknown type " + k); + } + Structure mk(String name, int size, String desc) throws Exception { + StructureDataType s = new StructureDataType(CAT, name, size, dtm); + s.setDescription(desc + "\n" + DOC); + Structure r = (Structure) dtm.addDataType(s, DataTypeConflictHandler.REPLACE_HANDLER); + types.put(name, r); log.println(" type " + r.getPathName() + " size=" + r.getLength()); return r; + } + void fields(Structure s, Object[][] rows) { + for (Object[] r : rows) { int off = ((Number) r[0]).intValue(); DataType dt = prim((String) r[1]); String name = (String) r[2]; String cmt = r.length > 3 ? (String) r[3] : null; + try { s.replaceAtOffset(off, dt, dt.getLength(), name, cmt); } catch (Exception e) { log.println(" !! " + s.getName() + "." + name + ": " + e.getMessage()); } } + } + Namespace ns(String path) throws Exception { + Namespace cur = currentProgram.getGlobalNamespace(); + for (String part : path.split("::")) { Namespace n = st.getNamespace(part, cur); if (n == null) n = st.createClass(cur, part, SourceType.USER_DEFINED); cur = n; } + return cur; + } + // rename + prototype. sig: C declaration using the function's new simple name, e.g. "bool Open(Script * this, char * path)" + void fn(long addr, String nsPath, String name, String conv, String sig, String comment) { + Address a = toAddr(addr); Function f = getFunctionAt(a); + try { + if (f == null) { f = createFunction(a, name); if (f == null) { log.println("!! no function at " + a); return; } } + try { f.getSymbol().setNameAndNamespace(name, ns(nsPath), SourceType.USER_DEFINED); } + catch (ghidra.util.exception.DuplicateNameException d) { for (Symbol s2 : st.getSymbols(a)) { if (s2.getName().equals(name) && !s2.isPrimary()) { s2.delete(); } } f.getSymbol().setNameAndNamespace(name, ns(nsPath), SourceType.USER_DEFINED); } + if (sig != null) { + FunctionDefinitionDataType fd = buildDef(name, sig); + if (conv != null) fd.setCallingConvention(conv); + ApplyFunctionSignatureCmd cmd = new ApplyFunctionSignatureCmd(a, fd, SourceType.USER_DEFINED); + if (!cmd.applyTo(currentProgram)) log.println("!! sig failed " + a + ": " + cmd.getStatusMsg()); + } + if (conv != null) f.setCallingConvention(conv); + if (comment != null) { String old = f.getComment(); f.setComment((old == null || old.isEmpty() ? "" : old + "\n") + comment + "\n" + DOC); } + log.println(" fn " + a + " " + f.getName(true) + " :: " + f.getSignature().getPrototypeString() + " [" + f.getCallingConventionName() + "]"); + } catch (Exception e) { log.println("!! fn " + a + " " + name + ": " + e); } + verify.add(addr); + } + DataType resolve(String t) { + t = t.trim(); int stars = 0; while (t.endsWith("*")) { stars++; t = t.substring(0, t.length()-1).trim(); } + DataType b; + if (t.equals("void")) b = VoidDataType.dataType; else if (t.equals("char")) b = CharDataType.dataType; else if (t.equals("byte")) b = ByteDataType.dataType; else b = prim(t); + for (int i = 0; i < stars; i++) b = new PointerDataType(b); + return b; + } + FunctionDefinitionDataType buildDef(String name, String sig) { + int lp = sig.indexOf('('), rp = sig.lastIndexOf(')'); + String head = sig.substring(0, lp).trim(); String ret = head.substring(0, head.lastIndexOf(' ')).trim(); + String args = sig.substring(lp+1, rp).trim(); + FunctionDefinitionDataType fd = new FunctionDefinitionDataType(name); + fd.setReturnType(resolve(ret)); + List ps = new ArrayList(); + if (!args.isEmpty() && !args.equals("void")) for (String p : args.split(",")) { p = p.trim(); int sp = p.lastIndexOf(' '); String pn = p.substring(sp+1).trim(); String pt = p.substring(0, sp).trim(); ps.add(new ParameterDefinitionImpl(pn, resolve(pt), null)); } + fd.setArguments(ps.toArray(new ParameterDefinition[0])); + return fd; + } + void glob(long addr, String name, String type, String comment) { + try { Address a = toAddr(addr); createLabel(a, name, true, SourceType.USER_DEFINED); + DataType dt = prim(type); try { currentProgram.getListing().clearCodeUnits(a, a.add(dt.getLength()-1), false); currentProgram.getListing().createData(a, dt); } catch (Exception e) { log.println(" (data) " + name + ": " + e.getMessage()); } + if (comment != null) setEOLComment(a, comment); log.println(" global " + a + " " + name); } + catch (Exception e) { log.println("!! global " + name + ": " + e); } + } + void verifyDump() throws Exception { + new File("/tmp/ldr/verify").mkdirs(); + for (long addr : verify) { Function f = getFunctionAt(toAddr(addr)); if (f == null) continue; + PrintWriter w = new PrintWriter(new FileWriter(String.format("/tmp/ldr/verify/%08x.c", addr))); + w.println("// " + f.getName(true) + " :: " + f.getSignature().getPrototypeString() + " [" + f.getCallingConventionName() + "]"); + try { DecompileResults r = decomp.decompileFunction(f, 600, monitor); w.println(r.decompileCompleted() ? r.getDecompiledFunction().getC() : "[failed] " + r.getErrorMessage()); } catch (Exception e) { w.println("[exc] " + e); } + w.close(); } + } + + @Override public void run() throws Exception { + dtm = currentProgram.getDataTypeManager(); st = currentProgram.getSymbolTable(); + new File("/tmp/ldr").mkdirs(); log = new PrintWriter(new FileWriter("/tmp/ldr/writeback.log")); + decomp = new DecompInterface(); decomp.openProgram(currentProgram); + + // ---------- types ---------- + log.println("== types"); + String[] opaque = {"Application","DemoApp","AppStartup","IConfigCallback","WeaponDictionary","WeaponDef","SectionDictionary","SectionDef","MasterTechTree","TechTree","Stream","IStreamable","ParticleSystem","EffectDictionary","StringVector","IFileSystem","GlobalConstMap","NativeFileSystem","ZipFileSystem"}; + for (String o : opaque) { if (dtm.getDataType(CAT, o) == null && types.get(o) == null) { StructureDataType s = new StructureDataType(CAT, o, 0, dtm); s.setDescription("opaque (referenced by pointer)\n" + DOC); types.put(o, dtm.addDataType(s, DataTypeConflictHandler.KEEP_HANDLER)); } else { types.put(o, dtm.getDataType(CAT, o)); } } + // GlobalConst parse fn pointer + FunctionDefinitionDataType pf = new FunctionDefinitionDataType(CAT, "GlobalConstParseFn", dtm); + pf.setReturnType(VoidDataType.dataType); pf.setArguments(new ParameterDefinition[]{ new ParameterDefinitionImpl("storage", new PointerDataType(VoidDataType.dataType), "int*/float*/float[4]"), new ParameterDefinitionImpl("text", new PointerDataType(CharDataType.dataType), "value token") }); + try { pf.setCallingConvention("__cdecl"); } catch (Exception e) {} + DataType pfd = dtm.addDataType(pf, DataTypeConflictHandler.REPLACE_HANDLER); types.put("GlobalConstParseFn", new PointerDataType(pfd)); + dtm.addDataType(new TypedefDataType(CAT, "GlobalConstParseFnPtr", new PointerDataType(pfd), dtm), DataTypeConflictHandler.REPLACE_HANDLER); + + Structure gc = mk("GlobalConst", 0x10, "One registered data-file constant (static-init stub: push file; push parser; push key; push &storage; mov ecx,&cfgvar; call GlobalConst::GlobalConst). Code reads *storage."); + fields(gc, new Object[][]{ {0,"ptr","storage","int* / float* / float[4] the code reads"}, {4,"cstr","key","KEY token, matched case-insensitively (_stricmp)"}, {8,"GlobalConstParseFn","parser","0x008b7000 int, 0x008b7020 float, 0x008b70e0 float*k, 0x008b7110 colour"}, {0xc,"cstr","file","data file this key lives in, e.g. Data/Strategy/StrategyVars.txt"} }); + Structure ib = mk("IBuffer", 0x10, "Mars::gobio::Buffer: refcounted whole-file buffer returned by gobio::ReadFile. vtable: [0] dtor(flags) [1] Data() [2] Data() [3] Size()"); + fields(ib, new Object[][]{ {0,"ptr","vftable"}, {4,"int","refcount","RefCounted_Release (0x008a7230) decrements; deletes at 0"}, {8,"ptr","data"}, {0xc,"uint","size"} }); + Structure sc = mk("Script", 0x28, "Mars::Script: whitespace tokenizer over a gobio buffer (brace-block files, manifests, KEY value tables)"); + fields(sc, new Object[][]{ {0,"ptr","vftable","Mars::Script::vftable"}, {4,"char[16]","path_buf","std::string path (SSO buffer / pointer)"}, {0x14,"uint","path_size"}, {0x18,"uint","path_cap"}, {0x20,"IBuffer*","buf","whole file (NULL when not open)"}, {0x24,"uint","cursor","byte offset into buf->data"} }); + Structure tk = mk("ScriptToken", 0x804, "Token record filled by Mars::Script::Next: type 0 = none/EOF, 1 = key value, 2 = key { (block open), 3 = } (block close)"); + fields(tk, new Object[][]{ {0,"int","type"}, {4,"char[1024]","key","first token (key / block name / '}')"}, {0x404,"char[1024]","value","second token (value, or '{')"} }); + Structure fs = mk("FileSystemSet", 0x14, "Mars::gobio file-system set (singleton at 0x00b2e274, pointer at 0x00b2e270). Open() tries file systems in vector order: native '.' first, then sots.ini [Modules] Mount in N order (default table: sots.gob, sots_local_en.gob)"); + fields(fs, new Object[][]{ {0,"ptr","fs_begin","std::vector begin"}, {4,"ptr","fs_end"}, {8,"ptr","fs_cap"}, {0xc,"int","pad"}, {0x10,"int","current","index of the file system holding the open file, -1 none"} }); + Structure nfs = mk("NativeFileSystemImpl", 0x28, "Mars::gobio::NativeFileSystem (loose files under root, fopen/fread)"); + fields(nfs, new Object[][]{ {0,"ptr","vftable"}, {4,"bool","valid"}, {8,"char[16]","root_buf","std::string root (empty = cwd), trailing slash appended"}, {0x18,"uint","root_size"}, {0x1c,"uint","root_cap"}, {0x24,"ptr","file","FILE* of the open file"} }); + Structure zfs = mk("ZipFileSystemImpl", 0x34, "Mars::gobio::ZipFileSystem (.gob = zip; entries matched case-insensitively with \\ and / equal)"); + fields(zfs, new Object[][]{ {0,"ptr","vftable"}, {4,"char[16]","path_buf","std::string archive path"}, {0x14,"uint","path_size"}, {0x18,"uint","path_cap"}, {0x20,"ptr","unz","unzip handle"}, {0x28,"ptr","entries","std::map header (case-insensitive path compare)"} }); + Structure tfs = mk("TextFileStream", 0x420, "Mars::TextFileStream: 'TXT' magic line + 'LABEL value' lines + LABEL/BEGIN/END nesting (.effect files). Base FileStream 0x1c bytes."); + fields(tfs, new Object[][]{ {0,"ptr","vftable","[1] ReadString(label,buf,max) [2] ReadBool [3] ReadFloat [4] ReadInt [5] ReadNested(label,IStreamable*|NULL=skip) [6..10] Write*"}, {4,"bool","label_missing_reported"}, {8,"int","mode","1 read (gobio buffer), 2 write (FILE*)"}, {0xc,"IBuffer*","buf"}, {0x10,"uint","cursor"}, {0x14,"ptr","file","FILE* (write mode)"}, {0x18,"int","indent"}, {0x1c,"char[1024]","line","current line"}, {0x41c,"bool","line_pushed_back","true when the last line did not match the requested label"} }); + Structure wd = mk("WeaponDefPartial", 0x278, "Game::WeaponDef (0x278 bytes) — only the fields confirmed from WeaponDef::WeaponDef / ParseScript / dictionary lookups"); + fields(wd, new Object[][]{ {0,"int","index","position in WeaponDictionary vector (rewritten after sort)"}, {4,"int","id","stable id from Weapons/_weapons.txt"}, {8,"char[16]","path_buf","std::string file path"}, {0x18,"uint","path_size"}, {0x1c,"uint","path_cap"}, {0x40,"char[16]","name_buf","std::string 'name' (looked up with _stricmp)"}, {0x50,"uint","name_size"}, {0x54,"uint","name_cap"}, {0x5c,"ptr","payload","behaviour sub-block object (bolt/beam/missile...), created by 0x00597ae0 from the block name"}, {0x70,"int","damagetype","7 = default"}, {0x84,"int","weaponclass","enum index into table 0x00a02760 (stride 0xc, +8 name)"}, {0x88,"int","weaponfamily"}, {0xb8,"int","turretclass","0x18 = unset"}, {0xbc,"int","turretsize","4 = unset"}, {0xc0,"int","munitionsize"}, {0xc4,"int","cpoints"}, {0xc8,"int","cost"}, {0xcc,"float","hpbonus"}, {0x264,"int","payload_kind","index of the behaviour block name in table 0x0058f230 (17 kinds)"} }); + + // ---------- M1: GlobalConst plumbing ---------- + log.println("== M1"); + fn(0x008b76a0L, "Mars::GlobalConst", "GlobalConst", "__thiscall", "GlobalConst * GlobalConst(GlobalConst * this, void * storage, char * key, GlobalConstParseFn parser, char * file)", "Static-init constructor for one data-file constant (~600 stubs at 0x009a4720..0x009c1400). Stores {storage,key,parser,file} then Register(). The file loader (GlobalConsts::LoadAll) later overwrites *storage from the data file, so the image value is only a fallback."); + fn(0x008b7610L, "Mars::GlobalConst", "Register", "__cdecl", "void Register(GlobalConst * g)", "Inserts g into g_GlobalConstRegistry (std::map with _stricmp ordering, so keys are case-insensitive). Logs 'GlobalConsts: %s already registered' on duplicates and 'being registered after loading const table' if g_GlobalConstsLoaded."); + fn(0x008b7000L, "Mars::GlobalConst", "ParseInt", "__cdecl", "void ParseInt(int * storage, char * text)", "sscanf(text, \"%d\")"); + fn(0x008b7020L, "Mars::GlobalConst", "ParseFloat", "__cdecl", "void ParseFloat(float * storage, char * text)", "sscanf(text, \"%f\")"); + fn(0x008b70e0L, "Mars::GlobalConst", "ParseFloatScaled", "__cdecl", "void ParseFloatScaled(float * storage, char * text)", "sscanf %f then *storage *= *(float*)0x00af5210 (degrees->radians style scale)"); + fn(0x008b7110L, "Mars::GlobalConst", "ParseColour", "__cdecl", "void ParseColour(float * rgba, char * text)", "sscanf(text, \"%d %d %d %d\") with defaults 255; each /255 and clamped to 0..1 into float[4] (missing alpha -> 1.0)"); + fn(0x008b76d0L, "Mars::GlobalConsts", "LoadAll", "__cdecl", "void LoadAll(void)", "Called once from Application::Initialize. Collects the distinct file names of every registered GlobalConst, builds a per-file std::map and calls LoadFile(file, map) for each; sets g_GlobalConstsLoaded."); + fn(0x008b73c0L, "Mars::GlobalConsts", "LoadFile", "__cdecl", "void LoadFile(char * file, GlobalConstMap * consts)", "Opens file with Mars::Script (gobio), loops Script::Next: type-1 pairs are looked up case-insensitively in consts; found -> parser(storage, valueToken) and the entry is erased (so a second occurrence logs '[file] KEY not recognized or is multiply defined' => first occurrence wins); blocks are skipped with SkipBlock(1); leftovers log '[file] KEY expected but not found'."); + glob(0x00b2d740L, "g_GlobalConstsLoaded", "bool", "set by GlobalConsts::LoadAll"); + glob(0x00b2d744L, "g_GlobalConstRegistry", "ptr", "std::map object (+4 = header node ptr at 0x00b2d748)"); + + // ---------- M3: Mars::Script ---------- + log.println("== M3 Script"); + fn(0x008cd700L, "Mars::Script", "Script", "__thiscall", "Script * Script(Script * this)", "ctor: vftable, empty path, buf = NULL, cursor = 0"); + fn(0x008cd720L, "Mars::Script", "~Script", "__thiscall", "void ~Script(Script * this)", null); + fn(0x008cd770L, "Mars::Script", "ScalarDeletingDtor", "__thiscall", "Script * ScalarDeletingDtor(Script * this, byte flags)", null); + fn(0x008cd7d0L, "Mars::Script", "Open", "__thiscall", "bool Open(Script * this, char * path)", "path -> std::string; releases the previous buffer; gobio::ReadFile(path, &buf). Returns false if no file system has the file."); + fn(0x008cd6c0L, "Mars::Script", "Close", "__thiscall", "void Close(Script * this)", "release buffer, clear path, cursor = 0"); + fn(0x008cd2f0L, "Mars::Script", "ReadToken", "__thiscall", "int ReadToken(Script * this, char * out, uint outMax)", "Returns 0 = token read, 1 = no buffer / cursor past end, 2 = end of buffer reached (also when the token just read was the last byte of the file). Skips whitespace (space,\\t,\\r,\\n); a token starting with // is a comment to end of line; quotes \" ' ` delimit a token (no escapes, closing quote = same char); barewords end only at whitespace (so 'name{' is one token); output truncated silently at outMax-1."); + fn(0x008cd3e0L, "Mars::Script", "Next", "__thiscall", "int Next(Script * this, ScriptToken * tok)", "Reads one or two tokens: first token \"}\" -> tok->type = 3 (returns the ReadToken status of that token); otherwise reads a second token: \"{\" -> type 2 (block open, key = block name), else type 1 (key value). Returns the second ReadToken's status: callers loop while it is 0, so a final pair with no trailing whitespace/newline is dropped, and EOF inside a block simply ends parsing (lenient close)."); + fn(0x008cd4b0L, "Mars::Script", "SkipBlock", "__thiscall", "int SkipBlock(Script * this, int depth)", "Consumes tokens counting { / } until depth reaches 0. Returns 0 normally, nonzero if EOF hit first."); + fn(0x008cd1f0L, "Mars::Script", "ScanToken", null, null, "register-convention helper (EAX=cursor ptr, ECX="edFlag, EDX=out; stack: end, outEnd, &len): skips whitespace then quoted or bareword token"); + fn(0x008cd180L, "Mars::Script", "ScanBareword", null, null, "copies bytes until space/\\t/\\r/\\n (braces are NOT delimiters)"); + fn(0x008cd2b0L, "Mars::Script", "SkipLineComment", null, null, "advance to after the next \\n/\\r run"); + fn(0x008cd820L, "Mars::ConfigParser", "ParseFile", "__cdecl", "void ParseFile(IConfigCallback * cb, char * path, char * blockName, bool * found)", "Opens path with Mars::Script; blockName NULL/empty -> cb->vft[1](script) on the whole file; else scans top-level blocks and calls cb->vft[1](script) for the block whose name matches blockName (_stricmp), SkipBlock(1) otherwise. Used for the startup config and GUI .script files."); + fn(0x008a7230L, "Mars", "RefCounted_Release", "__fastcall", "int RefCounted_Release(IBuffer * obj)", "--refcount; vft[0](1) (deleting dtor) when it reaches 0. Used for gobio buffers and other refcounted engine objects."); + + // ---------- M2 / catalogs ---------- + log.println("== M2 catalogs"); + fn(0x0059a4c0L, "Game::WeaponDictionary", "Init", "__thiscall", "void Init(WeaponDictionary * this)", "Reads Weapons/_weapons.txt with Script::ReadToken pairs: id = atoi(tok1), file = tok2 (// DELETED lines are comments); LoadWeapon(\"Weapons/\"+file, id, NULL). Then sorts the vector (this+8..+0xc) and rewrites every def->index; this+0x18 = FindByName(\"emt_light\")."); + fn(0x0059a230L, "Game::WeaponDictionary", "LoadWeapon", "__thiscall", "void LoadWeapon(WeaponDictionary * this, char * path, int id, WeaponDef * * out)", "Script::Open(path); loops Next(): the first block whose name is \"weapon\" (_stricmp) -> new WeaponDef(index, id, path); WeaponDef::ParseScript(def, script, &rc, this->techTree); success -> push_back into this+8 vector (and *out); failure -> 'WeaponDictionary::Init: failed to load: %s'. Other blocks: SkipBlock(1)."); + fn(0x00599070L, "Game::WeaponDef", "ParseScript", "__thiscall", "bool ParseScript(WeaponDef * this, Script * script, int * rc, TechTree * tree)", "Loop while *rc == 0: *rc = Script::Next(script,&tok); type 1 -> if/else chain of _stricmp(tok.key, \"hidden\"|\"name\"|\"weaponclass\"|...) (66 keys, case-insensitive; unknown keys ignored); type 2 -> block name looked up in the 17-entry behaviour table (0x0058f230) -> payload object created (0x00597ae0) and its vft[1](script) parses the sub-block, otherwise SkipBlock(1); type 3 -> break. Warns when turretclass/turretsize were not given."); + fn(0x00598a20L, "Game::WeaponDef", "WeaponDef", "__thiscall", "WeaponDef * WeaponDef(WeaponDef * this, int index, int id, char * path)", null); + fn(0x00590a10L, "Game::WeaponDictionary", "FindByName", "__thiscall", "WeaponDef * FindByName(WeaponDictionary * this, char * name)", "linear scan, _stricmp against def->name (+0x40)"); + fn(0x00591e30L, "Game::WeaponDictionary", "FindByNameSorted", "__thiscall", "WeaponDef * FindByNameSorted(WeaponDictionary * this, char * name)", "binary search in the sorted vector; logs 'Weapon not found: \"%s\" - Was it added to the index file?'"); + fn(0x00576f40L, "Game::SectionDictionary", "SectionDictionary", "__thiscall", "SectionDictionary * SectionDictionary(SectionDictionary * this, TechTree * tree)", "For species 0..6: opens /_shipsections.txt with Script::ReadToken pairs (id = atoi(tok1), file = tok2), LoadSection(dir+\"/\"+file, species, id), push_back non-NULL results into this+8 vector. Then validates '@' localisation tokens ('Section name %s not localized.')."); + fn(0x00576cd0L, "Game::SectionDictionary", "LoadSection", "__thiscall", "SectionDef * LoadSection(SectionDictionary * this, char * path, int species, int id)", "Script::Open(path); first block named \"shipsection\" -> new SectionDef(index, species, id) (0x3d8 bytes) + SectionDef::ParseScript; returns NULL on failure ('SectionDictionary: Couldn't load [%s]')."); + fn(0x005744e0L, "Game::SectionDef", "ParseScript", "__thiscall", "bool ParseScript(SectionDef * this, Script * script, int * rc, TechTree * tree)", "Same shape as WeaponDef::ParseScript (87 keys + bank/mount/thruster/option/optiondef/anim/netforcelimits sub-blocks, all _stricmp)."); + fn(0x00574020L, "Game::SectionDef", "SectionDef", "__thiscall", "SectionDef * SectionDef(SectionDef * this, int index, int species, int id)", null); + fn(0x00545ec0L, "Game::Species", "GetDirName", "__cdecl", "char * GetDirName(uint species)", "species 0..6 -> Species/ directory name (table 0x00b10a00 stride 0x184, name at +0xb8)"); + fn(0x0058b870L, "Game::MasterTechTree", "MasterTechTree", "__thiscall", "MasterTechTree * MasterTechTree(MasterTechTree * this)", "Builds the tech list from TechTree/MasterTechList.tech (vector of files, each via LoadTechFile(path, 1)), then sorts/links techs."); + fn(0x0058b770L, "Game::MasterTechTree", "LoadTechFile", "__thiscall", "void LoadTechFile(MasterTechTree * this, char * path, int flag)", "Script::Open(path); every top-level block named \"tech\" -> ParseTech(script, flag); other blocks SkipBlock(1)."); + fn(0x0058b050L, "Game::MasterTechTree", "ParseTech", "__thiscall", "void ParseTech(MasterTechTree * this, Script * script, int flag)", "Parses one tech{} block (name/family/type/threat/requires/allows/strategy/ship/weapon) into a new TechDef; see strategic-turn-internals.md 2.5"); + fn(0x00899280L, "Game::DemoApp", "LoadGameData", "__thiscall", "void LoadGameData(DemoApp * this)", "new MasterTechTree -> this+0x110; new WeaponDictionary (ctor 0x005917e0) -> +0x118 then WeaponDictionary::Init; new SectionDictionary(tree) -> +0x114; 0x00598ff0 -> +0x174; then cross-checks section bank weapons ('%s specifies weapon %s, but weapon not found.')."); + fn(0x00899870L, "Game::DemoApp", "EnsureGameData", "__thiscall", "void EnsureGameData(DemoApp * this)", "Once (flag this+0x10d): LoadGameData(); 0x004d7790 (GUI scripts via ConfigParser::ParseFile); creates StrategyAIContext. Called from DemoApp::OnTick when this+0x10c is set (deferred data load after the first frame)."); + + // ---------- M4: gobio ---------- + log.println("== M4 gobio"); + fn(0x008d7010L, "Mars::gobio", "Init", "__cdecl", "void Init(StringVector * mountPaths)", "Once: constructs the FileSystemSet singleton (0x00b2e274) from the mount list and sets g_gobio. Called from Application::MountModules."); + fn(0x008d6d60L, "Mars::gobio::FileSystemSet", "FileSystemSet", "__thiscall", "FileSystemSet * FileSystemSet(FileSystemSet * this, StringVector * mounts)", "If no mount equals \".\" / \"./\" / \".\\\", a NativeFileSystem(\"\") (current directory) is added FIRST. Then each mount in order: \".\" -> NativeFileSystem(\"\"); PathIsDirectoryA(path) -> NativeFileSystem(path); else ZipFileSystem(path) (.gob). Open() searches in this order => loose files override .gob contents."); + fn(0x008d5060L, "Mars::gobio::FileSystemSet", "Open", "__thiscall", "bool Open(FileSystemSet * this, char * path)", "Close current; first IFileSystem whose vft[1] Open(path) succeeds becomes this->current."); + fn(0x008d7090L, "Mars::gobio::FileSystemSet", "ListFiles", "__thiscall", "void ListFiles(FileSystemSet * this, char * pattern, StringVector * out)", "Union over all file systems (vft[5]) into a set, then out vector."); + fn(0x008d5140L, "Mars::gobio", "ReadFile", "__cdecl", "bool ReadFile(char * path, IBuffer * * out)", "THE 'read whole file by relative path' entry: g_gobio->Open(path); size = fs->Size(); Buffer::Create(size) ; fs->Read(data,size) == size ? *out = buffer : release; fs->Close(). Returns true on success. Used by Script::Open, TextFileStream::Open, textures, sounds, etc."); + fn(0x008d50d0L, "Mars::gobio", "Open", "__cdecl", "bool Open(char * path)", "g_gobio->Open(path)"); + fn(0x008d4d20L, "Mars::gobio", "Close", "__cdecl", "void Close(void)", null); + fn(0x008d4d50L, "Mars::gobio", "Size", "__cdecl", "uint Size(void)", "size of the currently open file"); + fn(0x008d4d80L, "Mars::gobio", "Read", "__cdecl", "uint Read(void * dst, uint n)", "read from the currently open file"); + fn(0x008d50f0L, "Mars::gobio", "Exists", "__cdecl", "bool Exists(char * path)", "any file system vft[6] Exists(path)"); + fn(0x008d71d0L, "Mars::gobio", "ListFiles", "__cdecl", "void ListFiles(char * pattern, StringVector * out)", "g_gobio->ListFiles"); + fn(0x008d4c90L, "Mars::gobio::Buffer", "Create", null, null, "allocates a gobio::Buffer of EBX bytes (register convention: size in EBX, out IBuffer** on stack)"); + fn(0x008d4c80L, "Mars::gobio::Buffer", "Size", "__thiscall", "uint Size(IBuffer * this)", null); + fn(0x00682d50L, "Mars::gobio::Buffer", "Data", "__thiscall", "void * Data(IBuffer * this)", null); + fn(0x008d57a0L, "Mars::gobio::NativeFileSystem", "NativeFileSystem", "__thiscall", "NativeFileSystem * NativeFileSystem(NativeFileSystem * this, char * root)", "'Mounting current directory...' / 'Mounting subdirectory %s...'; root must be a directory; a trailing slash is appended"); + fn(0x008d4ef0L, "Mars::gobio::NativeFileSystem", "Open", "__thiscall", "bool Open(NativeFileSystem * this, char * path)", "fopen(root+path, \"rb\") (vft[1])"); + fn(0x008d4ba0L, "Mars::gobio::NativeFileSystem", "Close", "__thiscall", "void Close(NativeFileSystem * this)", "vft[2]"); + fn(0x008d4bc0L, "Mars::gobio::NativeFileSystem", "Size", "__thiscall", "uint Size(NativeFileSystem * this)", "vft[3] ftell/fseek"); + fn(0x008d4c30L, "Mars::gobio::NativeFileSystem", "Read", "__thiscall", "uint Read(NativeFileSystem * this, void * dst, uint n)", "vft[4] fread"); + fn(0x008d6730L, "Mars::gobio::NativeFileSystem", "ListFiles", "__thiscall", "void ListFiles(NativeFileSystem * this, char * pattern, void * outSet)", "vft[5] _findfirst/_findnext"); + fn(0x008d4f60L, "Mars::gobio::NativeFileSystem", "Exists", "__thiscall", "bool Exists(NativeFileSystem * this, char * path)", "vft[6]"); + fn(0x008d4e90L, "Mars::gobio::NativeFileSystem", "MakePath", "__thiscall", "void MakePath(NativeFileSystem * this, char * rel, char * out260)", "_snprintf(\"%s%s\", root, rel)"); + fn(0x008d6c40L, "Mars::gobio::ZipFileSystem", "ZipFileSystem", "__thiscall", "ZipFileSystem * ZipFileSystem(ZipFileSystem * this, char * zipPath)", "opens the .gob (zip) and indexes its entries into a map ordered by PathCompareCI"); + fn(0x008d56a0L, "Mars::gobio::ZipFileSystem", "Open", "__thiscall", "bool Open(ZipFileSystem * this, char * path)", "vft[1]: Locate(path) then unzOpenCurrentFile"); + fn(0x008d4af0L, "Mars::gobio::ZipFileSystem", "Close", "__thiscall", "void Close(ZipFileSystem * this)", "vft[2]"); + fn(0x008d4b00L, "Mars::gobio::ZipFileSystem", "Size", "__thiscall", "uint Size(ZipFileSystem * this)", "vft[3]"); + fn(0x008d4b40L, "Mars::gobio::ZipFileSystem", "Read", "__thiscall", "uint Read(ZipFileSystem * this, void * dst, uint n)", "vft[4] unzReadCurrentFile loop"); + fn(0x008d64d0L, "Mars::gobio::ZipFileSystem", "ListFiles", "__thiscall", "void ListFiles(ZipFileSystem * this, char * pattern, void * outSet)", "vft[5] PathMatchSpecA over entries"); + fn(0x008d56e0L, "Mars::gobio::ZipFileSystem", "Exists", "__thiscall", "bool Exists(ZipFileSystem * this, char * path)", "vft[6]"); + fn(0x008d55c0L, "Mars::gobio::ZipFileSystem", "Locate", "__thiscall", "bool Locate(ZipFileSystem * this, char * path)", "map lookup with PathCompareCI, then unzGoToFilePos"); + fn(0x008d4a50L, "Mars::gobio", "PathCompareCI", null, null, "register-convention strcmp variant: '\\\\' == '/', ASCII case-folded => zip entry names are case- and slash-insensitive"); + fn(0x008a0a20L, "Mars::Application", "MountModules", "__cdecl", "void MountModules(void)", "Reads sots.ini [Modules] (GetPrivateProfileSectionA): keys Mount = path, sorted by N; if none, uses the built-in table g_DefaultMountTable (sots_local_en.gob, sots.gob; count at 0x00b2d514); then gobio::Init(paths). Called from Application::Initialize before GlobalConsts::LoadAll."); + fn(0x008991c0L, "Game", "GetDefaultMounts", "__cdecl", "void GetDefaultMounts(int * count, char * * * table)", "*count = 3 (0x00b2d514), *table = g_DefaultMountTable (0x00a35e98)"); + glob(0x00b2e270L, "g_gobio", "FileSystemSet*", "pointer to the FileSystemSet singleton (NULL before gobio::Init)"); + glob(0x00b2e274L, "g_gobioInstance", "FileSystemSet", null); + glob(0x00a35e98L, "g_DefaultMountTable", "ptr", "const char* table used when sots.ini has no [Modules] Mount entries"); + + // ---------- effect / TextFileStream ---------- + log.println("== TextFileStream"); + fn(0x008cfcb0L, "Mars::Stream", "OpenFile", "__cdecl", "Stream * OpenFile(char * path)", "Factory: sniffs the file (0x008cf940: 'TXT' magic via gobio) and returns a TextFileStream opened for reading, else another FileStream flavour; NULL on failure."); + fn(0x0091e460L, "Mars::TextFileStream", "TextFileStream", "__thiscall", "TextFileStream * TextFileStream(TextFileStream * this)", null); + fn(0x008cfaa0L, "Mars::FileStream", "FileStream", "__thiscall", "Stream * FileStream(Stream * this)", null); + fn(0x008cfb90L, "Mars::TextFileStream", "Open", "__thiscall", "bool Open(TextFileStream * this, char * path, int mode)", "mode must be 1 (read): gobio::ReadFile(path,&buf); requires the buffer to start with the magic 'TXT' (PTR 0x00af6718) then skips the line break(s). Returns true on success."); + fn(0x008cfb00L, "Mars::TextFileStream", "Create", "__thiscall", "bool Create(TextFileStream * this, char * path, int mode)", "mode 2: fopen(path,\"wb\") and writes the 'TXT' magic line"); + fn(0x008cfa30L, "Mars::FileStream", "ReadLine", "__thiscall", "uint ReadLine(Stream * this, char * out, int max)", "copies up to and including \\n from buf+cursor, NUL-terminates over the \\n, advances cursor; returns bytes consumed"); + fn(0x0091e760L, "Mars::TextFileStream", "MatchLabel", "__thiscall", "char * MatchLabel(TextFileStream * this, char * line, char * label)", "First word == \"END\" -> NULL. label NULL -> accept. Else skip leading whitespace, label must match exactly (case-sensitive) and be followed by whitespace/EOL; returns pointer to the value text; on mismatch logs 'TextFileStream: missing label: %s' once and returns NULL (caller pushes the line back)."); + fn(0x0091ec90L, "Mars::TextFileStream", "ReadString", "__thiscall", "bool ReadString(TextFileStream * this, char * label, char * out, int outMax)", "vft[1]: value is the text between the first pair of double quotes, with backslash escapes (\\n \\t \\r \\b \\f \\v, else literal next char)"); + fn(0x0091eba0L, "Mars::TextFileStream", "ReadBool", "__thiscall", "bool ReadBool(TextFileStream * this, char * label, bool * out)", "vft[2]: first char 0/F/f -> false, 1/T/t -> true, anything else leaves *out unchanged"); + fn(0x0091eb30L, "Mars::TextFileStream", "ReadFloat", "__thiscall", "bool ReadFloat(TextFileStream * this, char * label, float * out)", "vft[3]: sscanf %f"); + fn(0x0091eab0L, "Mars::TextFileStream", "ReadInt", "__thiscall", "bool ReadInt(TextFileStream * this, char * label, int * out, int dflt)", "vft[4]: sscanf %d, *out = 0 if it does not parse"); + fn(0x0091e8e0L, "Mars::TextFileStream", "ReadNested", "__thiscall", "void ReadNested(TextFileStream * this, char * label, IStreamable * obj)", "vft[5]: label line, then BEGIN ... END; obj NULL -> skip the block counting nested BEGIN/END; else obj->vft[1](stream) reads the body"); + fn(0x0091ee80L, "Mars::TextFileStream", "WriteString", "__thiscall", "void WriteString(TextFileStream * this, char * label, char * value)", "vft[6]"); + fn(0x0091e6c0L, "Mars::TextFileStream", "WriteBool", "__thiscall", "void WriteBool(TextFileStream * this, char * label, bool value)", "vft[7]: 'LABEL TRUE|FALSE'"); + fn(0x0091e630L, "Mars::TextFileStream", "WriteFloat", "__thiscall", "void WriteFloat(TextFileStream * this, char * label, float value)", "vft[8]: 'LABEL %f'"); + fn(0x0091e5a0L, "Mars::TextFileStream", "WriteInt", "__thiscall", "void WriteInt(TextFileStream * this, char * label, int value)", "vft[9]: 'LABEL %d'"); + fn(0x0091e4c0L, "Mars::TextFileStream", "WriteNested", "__thiscall", "void WriteNested(TextFileStream * this, char * label, IStreamable * obj)", "vft[10]: label line, BEGIN, indent++, obj->vft[2](stream), indent--, END (tab-indented)"); + fn(0x008db650L, "Mars::ParticleSystem", "Read", "__thiscall", "void Read(ParticleSystem * this, Stream * s)", ".effect body reader: NAME, ALIGNTOMOTION, FORCESINGLE, USEWORLDSPACE, MOMXFER, MESHFILENAME, TEXTUREFILENAME, SHADERFILENAME, ..., RANDOMIZESEED, BACKTOFRONTDRAW, PENNANTTRAIL, CREATIONRATE{BEGIN..END}, COLOR_R/G/B/A, NUMINITIALPARTICLES, NUM_TRACKS then per track PARTICLEDATATYPE + CREATION/VARIATION/OVERLIFE channels, NUM_ATTRACTORS/ATTRACTORINDEX. Order is significant; missing labels keep defaults."); + fn(0x008dad60L, "Mars::ParticleSystem", "Write", "__thiscall", "void Write(ParticleSystem * this, Stream * s)", "mirror of Read (label order is the file order)"); + fn(0x008b42b0L, "Mars::EffectDictionary", "Load", "__thiscall", "void * Load(EffectDictionary * this, char * name)", "probable: builds \"effects/\"+name(.effect), Stream::OpenFile, allocates 0x1c0-byte effect and reads it (ParticleSystem::Read via helper). Cached in this+0x108 vector."); + + // ---------- Application spine (coordinator request) ---------- + log.println("== Application"); + fn(0x008a0e50L, "Mars::Application", "Initialize", "__thiscall", "bool Initialize(Application * this, AppStartup * startup)", "VERIFIED: __thiscall, ONE stack argument (AppStartup* built in WinMain: {HINSTANCE hInst; std::vector args; ...; int nShow; const char* wndClass=\"Kerberos_SwordOfTheStars_WndCls\"; int iconId=0x66}), callee pops 4 (RET 4), returns bool in AL (WinMain: if (Initialize(&startup)) Run()). EDX is not an input. A detour must be declared with the extra parameter or it will unbalance the stack."); + fn(0x0089f5b0L, "Mars::Application", "Run", "__thiscall", "void Run(Application * this)", "VERIFIED: __thiscall, no stack arguments (RET), EDX not read before being clobbered; no meaningful return value (EAX = PumpMessages result)."); + fn(0x0089a640L, "Game::DemoApp", "OnTick", "__thiscall", "bool OnTick(DemoApp * this)", "VERIFIED: __thiscall, no stack arguments, returns bool in AL: 0 when quitting (this+0x1b2 set), else 1. IApplication vft+0x1c."); + + log.println("== verify"); + verifyDump(); + log.close(); decomp.dispose(); println("done"); + } +}