loader prototypes M1-M4 + app spine signatures; address contract 80 entries; first-wins parity bug logged
This commit is contained in:
parent
f1c7f55a89
commit
078dbbbcc7
8 changed files with 1771 additions and 57 deletions
|
|
@ -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
|
||||
<!-- dashboard-metrics {"verified": 13, "mapped_plus": 29, "targets": 43, "loc": 3919, "tests": 38, "checks": 889, "addr_verified": 10, "addr_total": 25, "layouts": 21, "open_q": 20} -->
|
||||
<!-- dashboard-metrics {"verified": 14, "mapped_plus": 30, "targets": 43, "loc": 3919, "tests": 38, "checks": 889, "addr_verified": 90, "addr_total": 102, "layouts": 21, "open_q": 22} -->
|
||||
|
|
|
|||
|
|
@ -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<N> (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 |
|
||||
|
|
|
|||
|
|
@ -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]])
|
||||
|
|
|
|||
431
findings/subsystems/loader-prototypes.md
Normal file
431
findings/subsystems/loader-prototypes.md
Normal file
|
|
@ -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/<addr>.c`; post-rename verification
|
||||
decompiles in `/srv/re-lab/handoff/loader-decompiles/verify/<addr>.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<std::string> 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<key, GlobalConst*>` 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<const char*, GlobalConst*, stricmp_less>` (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 <file string>` 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/<Race>` |
|
||||
| 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<WeaponDef*> 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/<file>"
|
||||
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<SectionDef*> 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<T>`). 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, <int @0x00af6a24>, 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<IFileSystem*> 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<std::string>* 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<path,entry> (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<std::string>* out)` | union over all file systems (used for `TechTree/*.tech`, effect lists…) |
|
||||
| 0x008d7010 | `Mars::gobio::Init` | cdecl | `void (std::vector<std::string>* mountPaths)` | one-shot (guard 0x00b2e288) constructs the singleton, sets `g_gobio` |
|
||||
| 0x008d6d60 | `Mars::gobio::FileSystemSet::FileSystemSet` | thiscall | `FileSystemSet* (this, std::vector<std::string>* 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<N>=<path>` 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<N>` 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).
|
||||
|
|
@ -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<std::string> 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<N> -> 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<const char*,GlobalConst*,stricmp> 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<const char*,GlobalConst*,stricmp_less> /* +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/<Race>/_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<std::string>* mountPaths)",
|
||||
"status": "verified",
|
||||
"source": "handoff/loader-prototypes.md#m4"
|
||||
},
|
||||
{
|
||||
"name": "gobio_FileSystemSet_ctor",
|
||||
"addr": "0x008d6d60",
|
||||
"convention": "thiscall",
|
||||
"prototype": "FileSystemSet* (FileSystemSet* this, std::vector<std::string>* 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<std::string>* 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<std::string>* 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<N>; 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 <cstdint>
|
||||
|
|
@ -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<std::string> 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<N> -> 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<const char*,GlobalConst*,stricmp> 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<const char*,GlobalConst*,stricmp_less> /* +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/<Race>/_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<std::string>* mountPaths) [verified]
|
||||
constexpr uint32_t gobio_Init = 0x004d7010;
|
||||
// thiscall FileSystemSet* (FileSystemSet* this, std::vector<std::string>* 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<std::string>* 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<std::string>* 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<N>; 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
|
||||
|
|
|
|||
77
ghidra/scripts/LoaderRecon.java
Normal file
77
ghidra/scripts/LoaderRecon.java
Normal file
|
|
@ -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 <hex> decompile function at addr (+ callers/callees list)
|
||||
// C <hex> decompile all callers of addr
|
||||
// K <hex> decompile all callees of addr
|
||||
// S <string> exact NUL-delimited string: list refs, decompile referencing functions
|
||||
// P <substr> substring search in memory: list refs to containing string, decompile referencing functions
|
||||
// D <hex> <hex> disassembly range dump
|
||||
// X <hex> 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<Address> findExact(String s) throws Exception { byte[] pat = ("\0" + s + "\0").getBytes("ISO-8859-1"); List<Address> 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<Address> findSub(String s) throws Exception { byte[] pat = s.getBytes("ISO-8859-1"); List<Address> 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<Address> findLE(long v) throws Exception { byte[] pat = new byte[]{(byte)v,(byte)(v>>8),(byte)(v>>16),(byte)(v>>24)}; List<Address> 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<Long> 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");
|
||||
}
|
||||
}
|
||||
258
ghidra/scripts/LoaderWriteBack.java
Normal file
258
ghidra/scripts/LoaderWriteBack.java
Normal file
|
|
@ -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<String, DataType> types = new HashMap<String, DataType>();
|
||||
List<Long> verify = new ArrayList<Long>();
|
||||
|
||||
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<ParameterDefinition> ps = new ArrayList<ParameterDefinition>();
|
||||
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<N> in N order (default table: sots.gob, sots_local_en.gob)");
|
||||
fields(fs, new Object[][]{ {0,"ptr","fs_begin","std::vector<IFileSystem*> 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<string,entry> 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<const char*,GlobalConst*> 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<key,GlobalConst*> 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<const char*,GlobalConst*,stricmp_less> 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 <SpeciesDir>/_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/<Race> 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<string> 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<N> = 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<std::string> 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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue