lane D: automated struct recovery from the IStreamable serializers

Every serializable class carries an enumeration of its own fields -- its
Write(Stream&), walking the members in order with a 4-char tag. This decodes
that idiom mechanically for the whole binary in 0.35 s.

Validation first (tools/serializers.py validate), against answers the campaign
already had before the tool existed:
  A  305/307 field offsets+kinds exact across 17 classes, 0 WRONG, vs
     struct-recovery.md 1-4 and observedtech-append.md
  B  sizeof from the container-stride divides: ObservedTech 0x2c, MoraleEvent
     0x50, PlayerReport 0x30, DiplomacyStats 0x24 -- all matching
  C  22 of save_reader.py's shapes, tag order identical (Sys 78 tags,
     Player 104, CreateParams 25, Ship 22): 22 agree, 0 disagree
  D  Read/Write cross-check on every class: 437/437 field offsets agree

At scale: 386 classes with a Write, 1,682 member fields.
  verified 87 (542 fields) | clean 77 (328) | unnamed 176 (471)
  partial 31 (341) | empty 15
  58 classes with a sizeof corroborated by a second line of evidence
  (45 container stride, 13 enumeration meeting the embedding bound); the rest
  report a lower bound and say so.

Four things each worth 10-170 classes: the RTTI class hierarchy descriptor as
the only honest "is this an IStreamable" test (a 3-slot vftable also matches
TacAISquadRule_* and the row parsers); mod=0 memory operands, which x86disp.py
cannot index and which hide every field at offset 0; the member->id pointer
idiom behind every handle field; and sub-writers, both base-class and private
(StrategyServer's six id lists live in FUN_00794cd0).

Failure classes are enumerated in the finding -- 176 anonymous-tag classes are
a hard limit on names but not on layout, and the other 64 are bounded
mechanical fixes. Two fields lost to a value assembled across a branch were
left unrecovered rather than patched with an unverifiable heuristic.

Write-back: 288 structures + 328 labels into Ghidra (0 failures), +201
addresses.json entries, header regenerated with tools/gen_addresses.py.

Note: ghidra/addresses.json also carries lane V's already-written live
confirmation text on ObservedTech_sizeof and ServerPlayer_off_ObservedTechs --
their edit, swept in only because we share the file.
This commit is contained in:
alex 2026-09-08 05:51:37 -04:00
parent 795aa471d4
commit d7ea0a048c
11 changed files with 45572 additions and 5 deletions

2
.gitignore vendored
View file

@ -12,3 +12,5 @@ captures/
*.lock *.lock
__pycache__/ __pycache__/
*.pyc *.pyc
# local ghidra write-back progress (machine-local, resumable)
objects/.ghidra_pushed

View file

@ -0,0 +1,248 @@
# Automated struct recovery from the `Mars::IStreamable` serializers
Lane D, 2026-09-08. Static/host only — VM140 was held by lane V and the game was never run.
The campaign had ~1,600 classes mapped by name and a few dozen by layout. This round makes the
layout step mechanical: every serializable class in the exe carries an **enumeration** of its own
fields — its `Write(Stream&)`, which walks the members in order with a 4-char name tag — and a
decoder for that idiom turns "read one class in an afternoon" into "read them all in 0.35 s".
Tools: `tools/rtti_map.py` (RTTI → vftables), `tools/serializers.py` (the recovery),
`tools/serializers_golden.py` (the regression set), `tools/serializers_ghidra.py` (write-back).
Outputs: `objects/layouts.json`, `objects/layouts.md`, `objects/layouts.h`.
---
## 1. Validation first
`uv run python3 tools/serializers.py validate` runs three checks, all against answers this
campaign already had **before** the tool existed. Nothing below §1 is claimed until this passes.
### A — field offsets and kinds, vs `struct-recovery.md` §1–§4 and `observedtech-append.md`
```
[OK ] Game::DiplomacyStats 14/14 exact
[OK ] Game::FlightPlan 6/6 exact
[OK ] Game::FlightPlan::Waypoint 3/3 exact
[OK ] Game::IndependenceInfo 5/5 exact
[OK ] Game::MoraleEvent 6/6 exact
[OK ] Game::NodeRoute 3/3 exact
[OK ] Game::ObservedTech 5/5 exact
[OK ] Game::ObservedWeapon 5/5 exact
[OK ] Game::PlayerReport 11/11 exact
[OK ] Game::PopulationGroup 3/3 exact
[MISS] Game::ServerPlayer 101/103 exact 2 missing
[OK ] Game::ServerSystem 79/79 exact
[OK ] Game::ShipBuildOrder 5/5 exact
[OK ] Game::StarFleet 16/16 exact
[OK ] Game::StarShip 23/23 exact
[OK ] Game::StarSystem::OutputRates 7/7 exact
[OK ] Game::StarSystem::PlayerView 13/13 exact
fields: 305/307 exact, 2 not recovered, 0 WRONG
```
**Zero wrong.** Every offset, every width, every tag the tool emits for these 17 classes matches the
hand-recovered tables — including the ones that were traps: `ObservedTech` `+0x24` is *not* reported
as a field (it is the string's trailing `_Alval`), `Bats2`/`rcex` come out as int64 and not int,
`TShn`/`ETS`/the whole `DiplomacyStats` block come out int16, `ltis` comes out int, `pswd` comes out
string, `TRM`/`CstR`/`CstE`/`CstT`/`shrm`/`RefCap`/`RepCap`/`PlayerView.Infra` come out float.
The two misses are one failure class, described in §4: a value assembled **across a branch**
(`ResTNm` writes `tech ? tech->name : ""`; `tnc` writes `max(member, 1)`). A linear pass sees only
the last branch's expression. Neither was papered over with a heuristic.
### B — `sizeof`, from the container-stride divides
```
[OK ] Game::DiplomacyStats got 0x24 [OK ] Game::ObservedWeapon got 0x2c
[OK ] Game::MoraleEvent got 0x50 [OK ] Game::PlayerReport got 0x30
[OK ] Game::ObservedTech got 0x2c
```
`sizeof(ObservedTech) = 0x2c` — the value lane X pinned three ways — falls out mechanically, from a
completely different place: `VectorHelper<ObservedTech>::Write` divides the vector's byte span by
the element stride, and MSVC's magic-number sequence for that divide is invertible
(`M = ceil(2^(32+k)/s)`). `PopulationGroup` and `OutputRates` are `[--]` — they have no
`VectorHelper`, so this oracle is silent on them rather than guessing.
### C — on-disk tag order, vs `save_reader.py`
22 of `save_reader.py`'s shapes name the exact `::Write` that produces them. For every one, the
tool's recovered tag sequence contains the oracle's tag sequence **in the same relative order**:
```
22 shapes agree, 0 disagree
```
That includes `Sys` (78 tags), `Player` (104), `CreateParams` (25), `Ship` (22), `Slot` (18),
`DipStat` (14), `PlayerView` (14). `save_reader.py` parses the three real saves `--strict` clean
with 36 tests and lane C's `state_checksum.py` proves its coverage by re-serialising byte-for-byte,
so this is disassembly and the save oracle agreeing independently, at scale.
### D — Read/Write cross-check, run on every class
The campaign's own standard ("the Read functions agree on every member address"), automated: the
class's `Read` is extracted the same way and its member offsets compared to `Write`'s, matched by
tag name.
```
Read/Write cross-check : 437/437 fields agree (0 classes with any disagreement)
```
Two subtleties had to be handled or this check manufactures false alarms, and both are worth
carrying: a tag used **twice** in one class (`ServerPlayer` writes both an `int Team` at `0xac` and
a `PlayerAlliances Team` at `0x168`) cannot be matched by name, and an **anonymous** tag (`"."`,
the writer passed NULL) carries no identity at all, so positional matching turns one side's
under-recovery into a cascade of fake conflicts. Both are excluded, and the classes affected are
reported as not-cross-checkable rather than as agreeing.
---
## 2. How it works
Five things had to be got right; each of the last four was worth 10–170 classes on its own.
**The idiom.** Writer side, one shape under two calling conventions:
```
push 0xff ; default-value argument
<member expression> ; lea r,[this+D] | mov r,[this+D] | movzx r,word [this+D]
push r
push <tag> ; -> .rdata "otch", "pswd", ... (or 0 -> "." on disk)
push <stream> / mov ecx,<stream>
call WriteString/WriteBool/… | call [[stream]+slot]
```
The push **immediately before the tag push** is always the member. Stream vftable slots, read off
the wrappers rather than assumed: `+0x18` string, `+0x1c` bool, `+0x20` float, `+0x24` int,
`+0x28` nested, `+0x30` raw bytes (`n = 8` → int64).
**Sizes come from the kind, never from the offsets touched** — lane S's rule. `bool` 1, `int16` 2,
int/float/enum/handle 4, `int64` 8, `std::string` **0x1c**, `std::vector` **0x10**, both
*allocator-last* in this build's STL and therefore invisible to any touch-scan.
**(a) RTTI gives the owner and the this-adjustment.** `tools/rtti_map.py` walks
type-descriptor ← `COL.pTypeDescriptor` ← `vftable[-1]` for the whole image: 1,924 type
descriptors (exactly the inventory's count), 2,172 COLs, 2,172 vftables. It reproduces the known
anchors exactly — `ObservedTech` vftable `0x00a2439c` / COL `0x00a81c78`; `ServerSystem`'s
IStreamable vftable `0x00a2043c` at COL offset **+8** with Read `0x0075d4b0` / Write `0x00749630`.
`COL.offset` is what turns a decompiled offset into an absolute one, and for `ServerPlayer` that is
`+0x3a0`, so most of its members are at *negative* decompiled offsets.
**(b) The class hierarchy descriptor is the only honest membership test.** A 3-slot vftable is
*not* a serializer test: `TacAISquadRule_*`, the CSV row parsers and ~100 other classes have three
virtuals. Walking `COL → ClassHierarchyDescriptor → base class array` and requiring
`Mars::IStreamable` in the base list removes them all. This dropped a 116-class "empty" bucket to
11 — i.e. it removed 105 classes that were never serializers, instead of reporting them as failures.
**(c) `mod=0` memory operands.** `tools/x86disp.py` indexes operands that carry a *displacement*, so
`mov eax,[edi]` — a member at offset **0** — is invisible to it. For a plain struct with no vptr,
offset 0 is a real field: `StarSystem::OutputRates.SRt` is exactly that, and so is every
`handle`/element read through a bare iterator. Synthesising the mod=0 case recovered `SRt` and every
`Flt`/`GF`/`SnF`/`MnF` element in `ServerSystem`.
**(d) The pointer-member idiom.** `HomeSys`, `PlrID`, `DesID`, `FltID`, `SrnTo` are written as
`member->id`, so the pushed value is a load off a *pointer that came from* a member, three to six
instructions and a conditional branch earlier. Remembering where a register's pointer came from
(and reading the base's this-ness **before** retiring the destination — `mov edi,[edi+0x1c0]`
overwrites the this-register with the member it is reading) recovered those five plus
`ServerSystem::indi`.
**(e) Sub-writers.** Two kinds, both invisible without special handling. A **base-class Write**
(`StarMapNode::Write` inside `ServerSystem::Write`, giving `Pos`) and a **private sub-writer of the
same class** (`StrategyServer` writes its six id lists in `FUN_00794cd0`, so `PlayerIDs`,
`DesignIDs`, `SystemIDs`, `FleetIDs`, `ShipIDs`, `TradeIDs` are simply absent without it — that was
the last disagreement in check C). Both are spliced in at the call site, which preserves disk order.
A third kind, a **tag-forwarding** writer `f(stream, name, member)` that passes the caller's name
through to a stream primitive, is *discovered* (by spotting a tag push that is `[ebp+0xc]`) rather
than listed by hand, because a hand list is exactly what silently under-reports on 1,600 classes.
**Containers.** A count argument is never a member; it is `(_Mylast - _Myfirst)/stride`. Recovering
the subtraction gives the container's offset (`NumFlts` → the `vector` at `0x16c`, `NVO` → the map
`_Mysize` at `0x278`), and inverting the divide gives the element stride — which is `sizeof(T)`.
---
## 3. Results at scale
`uv run python3 tools/serializers.py all` — 0.35 s for the whole binary.
| tier | classes | member fields | meaning |
|---|---|---|---|
| **verified** | 87 | 542 | every field resolved and sized, and `Read` agrees on every comparable offset |
| **clean** | 77 | 328 | every field resolved and sized; no `Read`-side comparison available |
| **unnamed** | 176 | 471 | offsets and types resolved, but the writer passes a NULL name so the fields have no on-disk names (combat commands, network messages) |
| **partial** | 31 | 341 | see §4 |
| **empty** | 15 | 0 | the class's `Write` writes no tagged field |
| **total** | **386** | **1,682** | |
* **58 classes have a `sizeof` corroborated by a second, independent line of evidence** — 45 from a
`VectorHelper<T>` element-stride divide, 13 where the field enumeration's lower bound meets the
upper bound implied by every embedding of that type in another class. For the rest the tool
reports a **lower bound** and says so: a serializer enumerates *serialised* members, so a
non-streamed cache or back-pointer at the tail is invisible to it. That is the honest limit of
this method and it is stated everywhere the number appears.
* Coverage of the population: 368 non-template classes have `Mars::IStreamable` in their RTTI base
list; 348 of them are covered here, plus 38 POD types (`Vector3`, `OutputRates`, …) that have no
vftable of their own and are reachable only through a specialised `StreamableHelper` thunk
(`push ebp; mov ebp,esp; mov ecx,[ecx+8]; pop ebp; jmp writer`).
* This is roughly a **10× increase** in classes with a known layout, and it is reproducible from the
exe in under a second.
---
## 4. Failure classes — what defeats the tool, and whether it scales
This is the part that says whether the remaining classes are reachable.
| # | failure | classes | why it happens | fixable? |
|---|---|---|---|---|
| 1 | **anonymous tags** | 176 | the writer passes a NULL name, so the field is `"."` on disk. Offsets and types are still recovered; only the *names* are missing, and the Read cross-check cannot run | not by this method — the names do not exist in the binary. These are combat commands and network messages, where the reader is positional |
| 2 | **nested member of unknown size** | 19 | an inline sub-object whose own type has neither a `VectorHelper` stride nor a tight embedding bound (`TechTree`, `ShipRecords`, `FleetLayout`) | yes — one constructor or `operator new` size each |
| 3 | **container elements only** | 9 | everything the serializer writes is a loop body over a `std::map`/`std::list` node, so the offsets are within the node, not the object (`BuildQueue`, `SpyReport`'s four lists) | partly — needs a node-layout model for the map/list |
| 4 | **untyped sub-writer** | 3 | a discovered tag-forwarding writer whose argument type could not be classified, so the member's offset is known but its size is not | yes, per sub-writer |
| 5 | **value built across a branch** | 2 fields | `tech ? tech->name : ""`, `max(member, 1)`. The pass is linear, so it sees the last branch only | yes, with a basic-block-aware pass — deliberately **not** patched with a backward-search heuristic, because there is no oracle for the 1,600 classes on which a heuristic would silently be wrong |
| 6 | **`std::map` / `std::list` members** | — | recovered as their `_Mysize` int, not as the container. `ServerSystem::NVO` comes out as `int @0x278`; the map itself starts at `0x274` | yes, and cheaply: this build's `std::map` is 0x10 bytes with head@0/size@4, from `NVO@0x274` → `NVE@0x284` |
| 7 | **runtime-built tag** | ~1 | `Mars::ParticleSystem::Write` makes six write-side calls and not one tag is a compile-time constant | no |
| 8 | **stub `Write`** | 15 | e.g. `Game::ServerTradeManager`'s Read and Write are both the inherited no-op `0x924fb0`; `Game::ShipDesign::Write` makes no stream call at all. These classes are not streamed through `IStreamable` and something else persists them | n/a — a finding, not a failure |
**Does it generalise?** Yes, with one caveat that matters. Categories 2, 3, 4, 5 and 6 are 64
classes and every one of them is a bounded, mechanical fix — none is a research problem. Category 1
(176 classes, 46% of the population) is a hard limit, but a *soft* one: those layouts are complete
in offsets and types, and only the names are missing, so a reimplementation can use them as-is.
Category 7 is genuinely out of reach and is one class.
The caveat: **this method covers only what the class serialises.** `ServerSystem` is `>= 0x2d8` by
enumeration and the campaign's hand table agrees, but neither can see a non-streamed member past the
last serialised one. Every `sizeof` in the output is therefore labelled with how it was obtained,
and the 58 corroborated ones are the only ones that should be treated as closed.
---
## 5. Write-back
* **`objects/layouts.json`** — the full machine-readable result (fields, offsets, kinds, tags,
nested types, container strides, grade, why, Read/Write agreement).
* **`objects/layouts.md`** — the same as tables, one section per class.
* **`objects/layouts.h`** — 288 C structs, gaps made explicit as padding, `Mars_string` (0x1c) and
`Mars_vector` (0x10) declared once with the allocator word in place.
* **Ghidra** — `tools/serializers_ghidra.py structs` pushes every struct through
`parse-c-structure` (dependencies first, resumable) — **288 pushed, 0 failed** — and `… labels`
names the `Write`/`Read` entry points (**328 labels, 0 failed**). `Game_ObservedTech` comes back from Ghidra as `size: 44`.
* **`ghidra/addresses.json`** — +201 entries: `<Class>_Write` / `<Class>_Read` for all 87
verified-tier classes, and `sizeof_<Class>` for every corroborated size, each carrying its
Read/Write agreement count in the prototype comment. Header regenerated with
`tools/gen_addresses.py` (never hand-edited).
## 6. Open items for the next lane
1. **`Game::ShipDesign::Write` (`0x8747a0`) makes no stream call.** `ShipDesign` is an important
class and its IStreamable slots are inert — worth knowing how designs actually persist
(`ShipDesignDef` is already verified separately, which may be the whole answer).
2. **`std::map`/`std::list` node layouts** would close failure classes 3 and 6 — ~15 classes.
3. **The 19 unsized nested types** need one size each; a constructor or `operator new` argument
scan would do it in bulk and would also give sizes for the non-streamed tail.
4. `Game::SVSOJewelsOfTheCrown` writes the tag `JEWELLOCATIONID` **twice**, at `+0x8` and `+0x10`.
Not an error in the tool (the Write really does emit it twice) but it means the on-disk record
has a duplicated name — a save reader matching that shape by name will get the wrong field.

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1). // GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
// Source: sots-re ghidra/addresses.json @ d523d27, generated 2026-09-08 by tools/gen_addresses.py // Source: sots-re ghidra/addresses.json @ 795aa47, generated 2026-09-08 by tools/gen_addresses.py
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated). // Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
#pragma once #pragma once
#include <cstdint> #include <cstdint>
@ -369,7 +369,7 @@ constexpr uint32_t ServerPlayer_off_SetupResearchMult = 0x0000022c;
constexpr uint32_t ServerPlayer_off_Sav = 0x00000284; constexpr uint32_t ServerPlayer_off_Sav = 0x00000284;
// offset Tech* current research target (ResT); NULL = none [verified-by-save] // offset Tech* current research target (ResT); NULL = none [verified-by-save]
constexpr uint32_t ServerPlayer_off_ResearchTarget = 0x00000294; constexpr uint32_t ServerPlayer_off_ResearchTarget = 0x00000294;
// offset std::vector<ObservedTech> otch -- 3 words {_Myfirst@0x274,_Mylast@0x278,_Myend@0x27c}; save tag otch. sizeof(ObservedTech) = 0x2c (44), PINNED three ways: magic divide 0x2e8ba2e9 sar 3 (=/44) in vector_ObservedTech_assign 0x0087239f, imul reg,reg,0x2c at 0x0087243a / 0x007b735b, and the linear search stride `add edi,0x2c` at 0x007ba257. Append site = RecordObservedTech+0xdf (0x007ba27f): lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320. All three words move because push_back reallocs through vector_44B_grow 0x007b5820. [verified] // offset std::vector<ObservedTech> otch -- 3 words {_Myfirst@0x274,_Mylast@0x278,_Myend@0x27c}; save tag otch. sizeof(ObservedTech) = 0x2c (44), PINNED three ways: magic divide 0x2e8ba2e9 sar 3 (=/44) in vector_ObservedTech_assign 0x0087239f, imul reg,reg,0x2c at 0x0087243a / 0x007b735b, and the linear search stride `add edi,0x2c` at 0x007ba257. Append site = RecordObservedTech+0xdf (0x007ba27f): lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320. All three words move because push_back reallocs through vector_44B_grow 0x007b5820. LIVE CONFIRMATION (lane V, 2026-09-08, VM140, build eventlive-dd38117-20260908T0916Z): the ProcessResearch `observed_techs` Result region measured the vector's byte span growing by exactly 44 on each of the two tech-completion calls of the five-End-Turn run from ref-turn2 (call 3: 440 -> 484; call 9: 484 -> 528) -- an independent behavioural confirmation of the static pin. Both non-completing players' spans were 880 = 20 x 44 and never moved. [verified]
constexpr uint32_t ServerPlayer_off_ObservedTechs = 0x00000274; constexpr uint32_t ServerPlayer_off_ObservedTechs = 0x00000274;
// offset float IncMod [verified-by-save] // offset float IncMod [verified-by-save]
constexpr uint32_t ServerPlayer_off_IncMod = 0x0000030c; constexpr uint32_t ServerPlayer_off_IncMod = 0x0000030c;
@ -799,7 +799,7 @@ constexpr uint32_t Mars_Vec3_NormaliseEpsilon = 0x005e1ef8;
constexpr uint32_t StrategyServer_MoveFleet_straight_leg = 0x003da0f2; constexpr uint32_t StrategyServer_MoveFleet_straight_leg = 0x003da0f2;
// site site inside StrategyServer::MoveFleet /* the move != distance branch: exactly two roundings per component, tmp.c = float32(dir.c * move) then pos.c = float32(pos.c + tmp.c). `move` is reloaded from a float32 slot. The sibling branch (move == distance, an EXACT float compare) copies the destination's three words verbatim with mov, so an arrival never steps onto its destination. */ [verified] // site site inside StrategyServer::MoveFleet /* the move != distance branch: exactly two roundings per component, tmp.c = float32(dir.c * move) then pos.c = float32(pos.c + tmp.c). `move` is reloaded from a float32 slot. The sibling branch (move == distance, an EXACT float compare) copies the destination's three words verbatim with mov, so an arrival never steps onto its destination. */ [verified]
constexpr uint32_t StrategyServer_MoveFleet_position_update = 0x003da2ac; constexpr uint32_t StrategyServer_MoveFleet_position_update = 0x003da2ac;
// constant sizeof(Game::ObservedTech) = 0x2c (44 bytes). Confirmed independently by (a) the compiler's magic division by 44 (mov eax,0x2e8ba2e9; imul; sar edx,3) at 0x0087239f and 0x007b7339, (b) imul reg,reg,0x2c at 0x0087243a, 0x00872468, 0x007b735b, and (c) the iterator advance `add edi,0x2c` in RecordObservedTech's linear search at 0x007ba257. FULL MEMBER MAP, from ObservedTech_Write 0x00817cf0 / ObservedTech_Read 0x00817c40 (lane S 2026-09-08): +0x00 vptr 0x00a2439c; +0x04 uint16 otnF; +0x06 uint16 otnL; +0x08 bool odet (1 byte, +3 pad); +0x0c std::string otch (0x1c, so _Mysize at +0x1c, _Myres at +0x20, _Alval at +0x24); +0x28 int owith. 4 + 2 + 2 + 4 + 0x1c + 4 = 0x2c exactly, no padding slack and no unaccounted field. [verified] // constant sizeof(Game::ObservedTech) = 0x2c (44 bytes). Confirmed independently by (a) the compiler's magic division by 44 (mov eax,0x2e8ba2e9; imul; sar edx,3) at 0x0087239f and 0x007b7339, (b) imul reg,reg,0x2c at 0x0087243a, 0x00872468, 0x007b735b, and (c) the iterator advance `add edi,0x2c` in RecordObservedTech's linear search at 0x007ba257. FULL MEMBER MAP, from ObservedTech_Write 0x00817cf0 / ObservedTech_Read 0x00817c40 (lane S 2026-09-08): +0x00 vptr 0x00a2439c; +0x04 uint16 otnF; +0x06 uint16 otnL; +0x08 bool odet (1 byte, +3 pad); +0x0c std::string otch (0x1c, so _Mysize at +0x1c, _Myres at +0x20, _Alval at +0x24); +0x28 int owith. 4 + 2 + 2 + 4 + 0x1c + 4 = 0x2c exactly, no padding slack and no unaccounted field. LIVE CONFIRMATION (lane V, 2026-09-08, VM140, build eventlive-dd38117-20260908T0916Z): the ProcessResearch `observed_techs` Result region measured the vector's byte span growing by exactly 44 on each of the two tech-completion calls of the five-End-Turn run from ref-turn2 (call 3: 440 -> 484; call 9: 484 -> 528) -- an independent behavioural confirmation of the static pin. Both non-completing players' spans were 880 = 20 x 44 and never moved. [verified]
constexpr uint32_t ObservedTech_sizeof = 0x0000002c; constexpr uint32_t ObservedTech_sizeof = 0x0000002c;
// data Game::ObservedTech vftable. RTTI COL 0x00a81c78 -> type descriptor 0x00aeede4 = '.?AVObservedTech@Game@@'. Written to element+0x00 by ObservedTech_ctor 0x008562a0 -- so ObservedTech is polymorphic and its first word is the vptr, not a data field. [verified] // data Game::ObservedTech vftable. RTTI COL 0x00a81c78 -> type descriptor 0x00aeede4 = '.?AVObservedTech@Game@@'. Written to element+0x00 by ObservedTech_ctor 0x008562a0 -- so ObservedTech is polymorphic and its first word is the vptr, not a data field. [verified]
constexpr uint32_t ObservedTech_vftable = 0x0062439c; constexpr uint32_t ObservedTech_vftable = 0x0062439c;
@ -835,5 +835,407 @@ constexpr uint32_t ObservedWeapon_Write = 0x00417bc0;
constexpr uint32_t std_string_sizeof = 0x0000001c; constexpr uint32_t std_string_sizeof = 0x0000001c;
// thiscall vector<T>::_Reserve/grow for a 44-byte element type. Shared with FUN_0086dec0, so it may be a COMDAT-folded body -- do NOT assume it is ObservedTech-specific. [mapped] // thiscall vector<T>::_Reserve/grow for a 44-byte element type. Shared with FUN_0086dec0, so it may be a COMDAT-folded body -- do NOT assume it is ObservedTech-specific. [mapped]
constexpr uint32_t vector_44B_grow = 0x003b5820; constexpr uint32_t vector_44B_grow = 0x003b5820;
// thiscall void (Game_AIAutoPeaceRun* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a199b4, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_AIAutoPeaceRun_Write = 0x00292cf0;
// thiscall void (Game_AIAutoPeaceRun* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a199b4, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_AIAutoPeaceRun_Read = 0x00292c90;
// layout sizeof(Game::AIAutoPeaceRun) -- container stride [verified]
constexpr uint32_t sizeof_Game_AIAutoPeaceRun = 0x00000010;
// thiscall void (Game_BackEngProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a3185c, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_BackEngProject_Write = 0x00453390;
// thiscall void (Game_BackEngProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a3185c, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_BackEngProject_Read = 0x00453280;
// thiscall void (Game_CombatPlayerReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a0623c, COL offset +0x0; 11 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CombatPlayerReport_Write = 0x0019d6d0;
// thiscall void (Game_CombatPlayerReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a0623c, COL offset +0x0; 11 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CombatPlayerReport_Read = 0x0019d360;
// layout sizeof(Game::CombatPlayerReport) -- container stride [verified]
constexpr uint32_t sizeof_Game_CombatPlayerReport = 0x00000054;
// thiscall void (Game_CombatPlayerStats* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009e4b74, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CombatPlayerStats_Write = 0x00059140;
// thiscall void (Game_CombatPlayerStats* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009e4b74, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CombatPlayerStats_Read = 0x00059070;
// layout sizeof(Game::CombatPlayerStats) -- container stride [verified]
constexpr uint32_t sizeof_Game_CombatPlayerStats = 0x00000024;
// thiscall void (Game_CombatReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a0624c, COL offset +0x0; 14 member fields; 6/6 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CombatReport_Write = 0x0019db30;
// thiscall void (Game_CombatReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a0624c, COL offset +0x0; 14 member fields; 6/6 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CombatReport_Read = 0x0019d8f0;
// thiscall void (Game_CombatShipReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a06064, COL offset +0x0; 12 member fields; 7/7 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CombatShipReport_Write = 0x0019b960;
// thiscall void (Game_CombatShipReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a06064, COL offset +0x0; 12 member fields; 7/7 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CombatShipReport_Read = 0x0019c650;
// layout sizeof(Game::CombatShipReport) -- container stride [verified]
constexpr uint32_t sizeof_Game_CombatShipReport = 0x00000050;
// thiscall void (Game_CombatWeaponReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a06074, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CombatWeaponReport_Write = 0x0019bb70;
// thiscall void (Game_CombatWeaponReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a06074, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CombatWeaponReport_Read = 0x0019bad0;
// layout sizeof(Game::CombatWeaponReport) -- container stride [verified]
constexpr uint32_t sizeof_Game_CombatWeaponReport = 0x00000030;
// thiscall void (Game_CommDeclareShareSystemNotes* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a3157c, COL offset +0x0; 8 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CommDeclareShareSystemNotes_Write = 0x00410da0;
// thiscall void (Game_CommDeclareShareSystemNotes* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a3157c, COL offset +0x0; 8 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CommDeclareShareSystemNotes_Read = 0x00410d50;
// thiscall void (Game_CommonPlague* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f8b50, COL offset +0x0; 6 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CommonPlague_Write = 0x001360e0;
// thiscall void (Game_CommonPlague* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f8b50, COL offset +0x0; 6 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_CommonPlague_Read = 0x001373e0;
// thiscall void (Game_DefenceFleetAssignment* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009e3c48, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_DefenceFleetAssignment_Write = 0x004119d0;
// thiscall void (Game_DefenceFleetAssignment* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009e3c48, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_DefenceFleetAssignment_Read = 0x00432330;
// thiscall void (Game_DiplomacyStats* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21430, COL offset +0x0; 14 member fields; 13/13 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_DiplomacyStats_Write = 0x00418cb0;
// thiscall void (Game_DiplomacyStats* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21430, COL offset +0x0; 14 member fields; 13/13 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_DiplomacyStats_Read = 0x00418b80;
// layout sizeof(Game::DiplomacyStats) -- container stride [verified]
constexpr uint32_t sizeof_Game_DiplomacyStats = 0x00000024;
// thiscall void (Game_EventStorage* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a313c8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_EventStorage_Write = 0x00425cc0;
// thiscall void (Game_EventStorage* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a313c8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_EventStorage_Read = 0x004850f0;
// thiscall void (Game_EventStorage_Event* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21958, COL offset +0x0; 8 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_EventStorage_Event_Write = 0x00425ab0;
// thiscall void (Game_EventStorage_Event* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21958, COL offset +0x0; 8 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_EventStorage_Event_Read = 0x00425970;
// thiscall void (Game_EventStorage_TurnEvents* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a0f07c, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_EventStorage_TurnEvents_Write = 0x00425c40;
// thiscall void (Game_EventStorage_TurnEvents* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a0f07c, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_EventStorage_TurnEvents_Read = 0x00425bb0;
// layout sizeof(Game::EventStorage::TurnEvents) -- container stride [verified]
constexpr uint32_t sizeof_Game_EventStorage_TurnEvents = 0x00000018;
// thiscall void (Game_FleetNameGenerator* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a32760, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_FleetNameGenerator_Write = 0x00417460;
// thiscall void (Game_FleetNameGenerator* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a32760, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_FleetNameGenerator_Read = 0x00417440;
// thiscall void (Game_FlightPlan* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1d50c, COL offset +0x0; 6 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_FlightPlan_Write = 0x00300f60;
// thiscall void (Game_FlightPlan* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1d50c, COL offset +0x0; 6 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_FlightPlan_Read = 0x00304c70;
// layout sizeof(Game::FlightPlan) -- enumeration meets embedding [verified]
constexpr uint32_t sizeof_Game_FlightPlan = 0x00000038;
// thiscall void (Game_IndependenceInfo* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2005c, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_IndependenceInfo_Write = 0x00348ee0;
// thiscall void (Game_IndependenceInfo* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2005c, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_IndependenceInfo_Read = 0x00348df0;
// thiscall void (Game_JewelsProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa438, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_JewelsProject_Write = 0x00148910;
// thiscall void (Game_JewelsProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa438, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_JewelsProject_Read = 0x001488d0;
// thiscall void (Game_MonitorProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a3180c, COL offset +0x0; 7 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_MonitorProject_Write = 0x004287d0;
// thiscall void (Game_MonitorProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a3180c, COL offset +0x0; 7 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_MonitorProject_Read = 0x00428740;
// thiscall void (Game_Morale* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1f7c8, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_Morale_Write = 0x00344ea0;
// thiscall void (Game_Morale* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1f7c8, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_Morale_Read = 0x00344dd0;
// thiscall void (Game_MoraleEvent* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2003c, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_MoraleEvent_Write = 0x003491b0;
// thiscall void (Game_MoraleEvent* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2003c, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_MoraleEvent_Read = 0x003490b0;
// layout sizeof(Game::MoraleEvent) -- container stride [verified]
constexpr uint32_t sizeof_Game_MoraleEvent = 0x00000050;
// thiscall void (Game_NodePath* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1cb50, COL offset +0x0; 11 member fields; 6/6 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_NodePath_Write = 0x002e1fd0;
// thiscall void (Game_NodePath* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1cb50, COL offset +0x0; 11 member fields; 6/6 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_NodePath_Read = 0x002e1e90;
// layout sizeof(Game::NodePath) -- container stride [verified]
constexpr uint32_t sizeof_Game_NodePath = 0x00000030;
// thiscall void (Game_ObservedTech* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2439c, COL offset +0x0; 5 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ObservedTech_Write = 0x00417cf0;
// thiscall void (Game_ObservedTech* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2439c, COL offset +0x0; 5 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ObservedTech_Read = 0x00417c40;
// layout sizeof(Game::ObservedTech) -- container stride [verified]
constexpr uint32_t sizeof_Game_ObservedTech = 0x0000002c;
// thiscall void (Game_ObservedWeapon* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2438c, COL offset +0x0; 5 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ObservedWeapon_Write = 0x00417bc0;
// thiscall void (Game_ObservedWeapon* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2438c, COL offset +0x0; 5 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ObservedWeapon_Read = 0x00417b10;
// layout sizeof(Game::ObservedWeapon) -- container stride [verified]
constexpr uint32_t sizeof_Game_ObservedWeapon = 0x0000002c;
// thiscall void (Game_PlayerAid* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a286dc, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_PlayerAid_Write = 0x00418dc0;
// thiscall void (Game_PlayerAid* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a286dc, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_PlayerAid_Read = 0x004207b0;
// layout sizeof(Game::PlayerAid) -- container stride [verified]
constexpr uint32_t sizeof_Game_PlayerAid = 0x00000018;
// thiscall void (Game_PlayerNotes* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21948, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_PlayerNotes_Write = 0x004132b0;
// thiscall void (Game_PlayerNotes* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21948, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_PlayerNotes_Read = 0x00413250;
// thiscall void (Game_PlayerReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21440, COL offset +0x0; 11 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_PlayerReport_Write = 0x00417480;
// thiscall void (Game_PlayerReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21440, COL offset +0x0; 11 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_PlayerReport_Read = 0x004200a0;
// layout sizeof(Game::PlayerReport) -- container stride [verified]
constexpr uint32_t sizeof_Game_PlayerReport = 0x00000030;
// thiscall void (Game_PopulationGroup* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f8d50, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_PopulationGroup_Write = 0x00136af0;
// thiscall void (Game_PopulationGroup* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f8d50, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_PopulationGroup_Read = 0x00136a80;
// thiscall void (Game_RaidTargets* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21968, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_RaidTargets_Write = 0x00418ec0;
// thiscall void (Game_RaidTargets* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21968, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_RaidTargets_Read = 0x0043a090;
// thiscall void (Game_SVSOAntiquarians* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fab94, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOAntiquarians_Write = 0x0014bb40;
// thiscall void (Game_SVSOAntiquarians* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fab94, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOAntiquarians_Read = 0x00151e90;
// thiscall void (Game_SVSOCrowDefenders* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f5904, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOCrowDefenders_Write = 0x000f8c90;
// thiscall void (Game_SVSOCrowDefenders* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f5904, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOCrowDefenders_Read = 0x0010fe90;
// thiscall void (Game_SVSOLandGrab* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa92c, COL offset +0x0; 2 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOLandGrab_Write = 0x00148f70;
// thiscall void (Game_SVSOLandGrab* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa92c, COL offset +0x0; 2 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOLandGrab_Read = 0x0014a9e0;
// thiscall void (Game_SVSOLocust* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f56bc, COL offset +0x0; 5 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOLocust_Write = 0x00108ef0;
// thiscall void (Game_SVSOLocust* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f56bc, COL offset +0x0; 5 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOLocust_Read = 0x0010d380;
// thiscall void (Game_SVSOOrtgay* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f0e94, COL offset +0x0; 9 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOOrtgay_Write = 0x00109510;
// thiscall void (Game_SVSOOrtgay* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f0e94, COL offset +0x0; 9 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOOrtgay_Read = 0x00109390;
// thiscall void (Game_SVSOProgressionWars* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fb34c, COL offset +0x0; 10 member fields; 8/8 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOProgressionWars_Write = 0x0014cdb0;
// thiscall void (Game_SVSOProgressionWars* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fb34c, COL offset +0x0; 10 member fields; 8/8 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOProgressionWars_Read = 0x00153490;
// thiscall void (Game_SVSOPuppetMaster* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f3dcc, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOPuppetMaster_Write = 0x00109f80;
// thiscall void (Game_SVSOPuppetMaster* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f3dcc, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOPuppetMaster_Read = 0x00111b80;
// thiscall void (Game_SVSORefugees* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f575c, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSORefugees_Write = 0x00109640;
// thiscall void (Game_SVSORefugees* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f575c, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSORefugees_Read = 0x00126420;
// thiscall void (Game_SVSORefugees_PlayerStatus* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f11cc, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSORefugees_PlayerStatus_Write = 0x000fbf50;
// thiscall void (Game_SVSORefugees_PlayerStatus* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f11cc, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSORefugees_PlayerStatus_Read = 0x000fbeb0;
// thiscall void (Game_SVSOSlaversRefuel* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f4834, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOSlaversRefuel_Write = 0x000fdf80;
// thiscall void (Game_SVSOSlaversRefuel* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f4834, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOSlaversRefuel_Read = 0x00120b00;
// thiscall void (Game_SVSOSwarm_Infestation* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f1720, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOSwarm_Infestation_Write = 0x000fe0e0;
// thiscall void (Game_SVSOSwarm_Infestation* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f1720, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOSwarm_Infestation_Read = 0x00104bd0;
// thiscall void (Game_SVSOSwarmQueen_HiveInfo* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f1a68, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOSwarmQueen_HiveInfo_Write = 0x000fe730;
// thiscall void (Game_SVSOSwarmQueen_HiveInfo* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f1a68, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOSwarmQueen_HiveInfo_Read = 0x000fe6f0;
// layout sizeof(Game::SVSOSwarmQueen::HiveInfo) -- container stride [verified]
constexpr uint32_t sizeof_Game_SVSOSwarmQueen_HiveInfo = 0x00000010;
// thiscall void (Game_SVSOSystemKiller* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f3e6c, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOSystemKiller_Write = 0x0010af20;
// thiscall void (Game_SVSOSystemKiller* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f3e6c, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOSystemKiller_Read = 0x00111f90;
// thiscall void (Game_SVSOUpstartApes* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fb4cc, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_Write = 0x0014d1b0;
// thiscall void (Game_SVSOUpstartApes* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fb4cc, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_Read = 0x00163850;
// thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_SHIPS* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fb290, COL offset +0x0; 6 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_Write = 0x0014cfe0;
// thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_SHIPS* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fb290, COL offset +0x0; 6 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_Read = 0x0014cef0;
// thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_TRADE_GOODS* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa564, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_TRADE_GOODS_Write = 0x00148ce0;
// thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_TRADE_GOODS* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa564, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_TRADE_GOODS_Read = 0x00148c90;
// thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_VICTORY* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa534, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_VICTORY_Write = 0x00148ce0;
// thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_VICTORY* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa534, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_VICTORY_Read = 0x00148c90;
// thiscall void (Game_SVSOUpstartApes_EO_ENERGY_WEAPONS_DECADENT* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa504, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_ENERGY_WEAPONS_DECADENT_Write = 0x00148ce0;
// thiscall void (Game_SVSOUpstartApes_EO_ENERGY_WEAPONS_DECADENT* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa504, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_ENERGY_WEAPONS_DECADENT_Read = 0x00148c90;
// thiscall void (Game_SVSOUpstartApes_EO_FEARS_YOUR_POWER* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa4d4, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_FEARS_YOUR_POWER_Write = 0x00148ce0;
// thiscall void (Game_SVSOUpstartApes_EO_FEARS_YOUR_POWER* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa4d4, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_FEARS_YOUR_POWER_Read = 0x00148c90;
// thiscall void (Game_SVSOUpstartApes_EO_GIFTS* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa54c, COL offset +0x0; 2 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_GIFTS_Write = 0x00148c50;
// thiscall void (Game_SVSOUpstartApes_EO_GIFTS* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa54c, COL offset +0x0; 2 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_GIFTS_Read = 0x00148c10;
// thiscall void (Game_SVSOUpstartApes_EO_MUST_EXPAND* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa51c, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_MUST_EXPAND_Write = 0x00148bb0;
// thiscall void (Game_SVSOUpstartApes_EO_MUST_EXPAND* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa51c, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_MUST_EXPAND_Read = 0x00148b50;
// thiscall void (Game_SVSOUpstartApes_EO_REQUIRES_CASH* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa4ec, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_REQUIRES_CASH_Write = 0x00148ce0;
// thiscall void (Game_SVSOUpstartApes_EO_REQUIRES_CASH* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa4ec, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_REQUIRES_CASH_Read = 0x00148c90;
// thiscall void (Game_SVSOVonNeumann* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f59cc, COL offset +0x0; 24 member fields; 14/14 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOVonNeumann_Write = 0x0012b8d0;
// thiscall void (Game_SVSOVonNeumann* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f59cc, COL offset +0x0; 24 member fields; 14/14 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOVonNeumann_Read = 0x0010e130;
// thiscall void (Game_SVSOVonNeumann_DefeatRecord* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f1c9c, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOVonNeumann_DefeatRecord_Write = 0x000ff7f0;
// thiscall void (Game_SVSOVonNeumann_DefeatRecord* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f1c9c, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOVonNeumann_DefeatRecord_Read = 0x001051f0;
// layout sizeof(Game::SVSOVonNeumann::DefeatRecord) -- container stride [verified]
constexpr uint32_t sizeof_Game_SVSOVonNeumann_DefeatRecord = 0x00000018;
// thiscall void (Game_ServerNodeGraph* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1cc4c, COL offset +0x44; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ServerNodeGraph_Write = 0x002e3530;
// thiscall void (Game_ServerNodeGraph* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1cc4c, COL offset +0x44; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ServerNodeGraph_Read = 0x002e61e0;
// thiscall void (Game_ServerSpyManager* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a30728, COL offset +0x4; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ServerSpyManager_Write = 0x00428a40;
// thiscall void (Game_ServerSpyManager* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a30728, COL offset +0x4; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ServerSpyManager_Read = 0x004381b0;
// thiscall void (Game_ServerTradeManagerImpl* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a31b64, COL offset +0x4c; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ServerTradeManagerImpl_Write = 0x0042cb60;
// thiscall void (Game_ServerTradeManagerImpl* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a31b64, COL offset +0x4c; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ServerTradeManagerImpl_Read = 0x00458a10;
// thiscall void (Game_ServerTradeSector* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a31b30, COL offset +0x8; 12 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ServerTradeSector_Write = 0x0042c8c0;
// thiscall void (Game_ServerTradeSector* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a31b30, COL offset +0x8; 12 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ServerTradeSector_Read = 0x00458290;
// thiscall void (Game_ServerTradeSector_FreighterWarning* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2d858, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ServerTradeSector_FreighterWarning_Write = 0x004197d0;
// thiscall void (Game_ServerTradeSector_FreighterWarning* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2d858, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ServerTradeSector_FreighterWarning_Read = 0x00419780;
// layout sizeof(Game::ServerTradeSector::FreighterWarning) -- container stride [verified]
constexpr uint32_t sizeof_Game_ServerTradeSector_FreighterWarning = 0x0000000c;
// thiscall void (Game_ShipBuildOrder* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a0c160, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ShipBuildOrder_Write = 0x00413800;
// thiscall void (Game_ShipBuildOrder* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a0c160, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ShipBuildOrder_Read = 0x00413770;
// thiscall void (Game_ShipBuildOrderDef* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a0ad08, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ShipBuildOrderDef_Write = 0x00413800;
// thiscall void (Game_ShipBuildOrderDef* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a0ad08, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ShipBuildOrderDef_Read = 0x00413770;
// layout sizeof(Game::ShipBuildOrderDef) -- container stride [verified]
constexpr uint32_t sizeof_Game_ShipBuildOrderDef = 0x00000018;
// thiscall void (Game_ShipRecords* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a3144c, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ShipRecords_Write = 0x004176a0;
// thiscall void (Game_ShipRecords* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a3144c, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ShipRecords_Read = 0x004560d0;
// thiscall void (Game_SpecialProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21938, COL offset +0x0; 6 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpecialProject_Write = 0x00414760;
// thiscall void (Game_SpecialProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21938, COL offset +0x0; 6 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpecialProject_Read = 0x004146c0;
// layout sizeof(Game::SpecialProject) -- enumeration meets embedding [verified]
constexpr uint32_t sizeof_Game_SpecialProject = 0x00000034;
// thiscall void (Game_SpyCraft* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a30718, COL offset +0x0; 13 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpyCraft_Write = 0x00414940;
// thiscall void (Game_SpyCraft* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a30718, COL offset +0x0; 13 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpyCraft_Read = 0x00437dc0;
// thiscall void (Game_SpyReportDefences* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a313e8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpyReportDefences_Write = 0x00428ba0;
// thiscall void (Game_SpyReportDefences* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a313e8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpyReportDefences_Read = 0x00428b10;
// thiscall void (Game_SpyReportTechTree* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a318b4, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpyReportTechTree_Write = 0x00414ba0;
// thiscall void (Game_SpyReportTechTree* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a318b4, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpyReportTechTree_Read = 0x00414b40;
// thiscall void (Game_SpyReportTrade* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a313f8, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpyReportTrade_Write = 0x00428cf0;
// thiscall void (Game_SpyReportTrade* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a313f8, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpyReportTrade_Read = 0x00428c20;
// thiscall void (Game_SpyReportTradeRoute* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a3079c, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpyReportTradeRoute_Write = 0x00414aa0;
// thiscall void (Game_SpyReportTradeRoute* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a3079c, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SpyReportTradeRoute_Read = 0x00438980;
// layout sizeof(Game::SpyReportTradeRoute) -- container stride [verified]
constexpr uint32_t sizeof_Game_SpyReportTradeRoute = 0x00000020;
// thiscall void (Game_StrategyGameCreateParams* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a25574, COL offset +0x0; 25 member fields; 19/19 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StrategyGameCreateParams_Write = 0x0042ae40;
// thiscall void (Game_StrategyGameCreateParams* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a25574, COL offset +0x0; 25 member fields; 19/19 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StrategyGameCreateParams_Read = 0x00432cc0;
// thiscall void (Game_StrategyGameInfo* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a07ba0, COL offset +0x0; 13 member fields; 10/10 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StrategyGameInfo_Write = 0x00429960;
// thiscall void (Game_StrategyGameInfo* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a07ba0, COL offset +0x0; 13 member fields; 10/10 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StrategyGameInfo_Read = 0x00475cb0;
// thiscall void (Game_StrategyPlayerInfo* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a077d8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StrategyPlayerInfo_Write = 0x004298d0;
// thiscall void (Game_StrategyPlayerInfo* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a077d8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StrategyPlayerInfo_Read = 0x00429820;
// layout sizeof(Game::StrategyPlayerInfo) -- container stride [verified]
constexpr uint32_t sizeof_Game_StrategyPlayerInfo = 0x000000c4;
// thiscall void (Game_StrategyServer* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a26084, COL offset +0x0; 38 member fields; 18/18 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StrategyServer_Write = 0x0039fa70;
// thiscall void (Game_StrategyServer* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a26084, COL offset +0x0; 38 member fields; 18/18 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StrategyServer_Read = 0x003d27a0;
// thiscall void (Game_StrategyTimerParams* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a07334, COL offset +0x0; 4 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StrategyTimerParams_Write = 0x004173e0;
// thiscall void (Game_StrategyTimerParams* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a07334, COL offset +0x0; 4 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StrategyTimerParams_Read = 0x00420040;
// thiscall void (Game_TacReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009e4600, COL offset +0x0; 17 member fields; 13/13 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_TacReport_Write = 0x00058e60;
// thiscall void (Game_TacReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009e4600, COL offset +0x0; 17 member fields; 13/13 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_TacReport_Read = 0x00058c00;
// thiscall void (Game_TacReportEvents* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009e45f0, COL offset +0x0; 7 member fields; 7/7 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_TacReportEvents_Write = 0x000565d0;
// thiscall void (Game_TacReportEvents* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009e45f0, COL offset +0x0; 7 member fields; 7/7 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_TacReportEvents_Read = 0x00056540;
// layout sizeof(Game::TacReportEvents) -- enumeration meets embedding [verified]
constexpr uint32_t sizeof_Game_TacReportEvents = 0x00000020;
// thiscall void (Game_TechOfferProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a31884, COL offset +0x0; 9 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_TechOfferProject_Write = 0x004535c0;
// thiscall void (Game_TechOfferProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a31884, COL offset +0x0; 9 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_TechOfferProject_Read = 0x00453530;
// thiscall void (Game_TechProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a31834, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_TechProject_Write = 0x00453390;
// thiscall void (Game_TechProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a31834, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_TechProject_Read = 0x00453280;
// thiscall void (Game_TradeRoute* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2d848, COL offset +0x0; 8 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_TradeRoute_Write = 0x00419650;
// thiscall void (Game_TradeRoute* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2d848, COL offset +0x0; 8 member fields; 5/5 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_TradeRoute_Read = 0x004208d0;
// thiscall void (Game_ZuulInfestation* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f8cb0, COL offset +0x0; 5 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ZuulInfestation_Write = 0x001361e0;
// thiscall void (Game_ZuulInfestation* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f8cb0, COL offset +0x0; 5 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ZuulInfestation_Read = 0x00136160;
// thiscall void (Game_AICombatReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1a6f0, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_AICombatReport_Write = 0x002986e0;
// thiscall void (Game_AICombatReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1a6f0, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_AICombatReport_Read = 0x0029c6d0;
// thiscall void (Game_FieldTemplate_Point* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a05df8, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_FieldTemplate_Point_Write = 0x0019bfa0;
// thiscall void (Game_FieldTemplate_Point* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a05df8, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_FieldTemplate_Point_Read = 0x0019bf10;
// layout sizeof(Game::FieldTemplate::Point) -- container stride [verified]
constexpr uint32_t sizeof_Game_FieldTemplate_Point = 0x00000014;
// thiscall void (Game_LocustFleetLogistics* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f1f00, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_LocustFleetLogistics_Write = 0x00108e50;
// thiscall void (Game_LocustFleetLogistics* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f1f00, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_LocustFleetLogistics_Read = 0x00108d60;
// thiscall void (Game_Objective* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2e0cc, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_Objective_Write = 0x004505b0;
// thiscall void (Game_Objective* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2e0cc, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_Objective_Read = 0x00450430;
// layout sizeof(Game::Objective) -- container stride [verified]
constexpr uint32_t sizeof_Game_Objective = 0x00000044;
// thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_ShipRequest* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa614, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_ShipRequest_Write = 0x0015d120;
// thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_ShipRequest* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa614, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_ShipRequest_Read = 0x0015d040;
// layout sizeof(Game::SVSOUpstartApes_EO_DEMANDS_SHIPS::ShipRequest) -- container stride [verified]
constexpr uint32_t sizeof_Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_ShipRequest = 0x00000020;
// thiscall void (Game_ShipDesignDef_LegacyWeaponDef* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2df7c, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ShipDesignDef_LegacyWeaponDef_Write = 0x00413950;
// thiscall void (Game_ShipDesignDef_LegacyWeaponDef* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2df7c, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ShipDesignDef_LegacyWeaponDef_Read = 0x00446260;
// thiscall void (Game_ShipWeapon* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fdc8c, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ShipWeapon_Write = 0x0016f450;
// thiscall void (Game_ShipWeapon* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fdc8c, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_ShipWeapon_Read = 0x00171130;
// thiscall void (Game_SlotDef* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2db6c, COL offset +0x0; 18 member fields; 15/15 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SlotDef_Write = 0x004276d0;
// thiscall void (Game_SlotDef* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2db6c, COL offset +0x0; 18 member fields; 15/15 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_SlotDef_Read = 0x00432790;
// layout sizeof(Game::SlotDef) -- enumeration meets embedding [verified]
constexpr uint32_t sizeof_Game_SlotDef = 0x000000bc;
// thiscall void (Game_StarSystem_OutputRates* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1f884, COL offset +0x0; 7 member fields; 7/7 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StarSystem_OutputRates_Write = 0x00345190;
// thiscall void (Game_StarSystem_OutputRates* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1f884, COL offset +0x0; 7 member fields; 7/7 field offsets agree between Read and Write */ [verified]
constexpr uint32_t Game_StarSystem_OutputRates_Read = 0x003472a0;
// layout sizeof(Game::StarSystem::OutputRates) -- enumeration meets embedding [verified]
constexpr uint32_t sizeof_Game_StarSystem_OutputRates = 0x0000001c;
} // namespace sots::addr } // namespace sots::addr

2224
objects/layouts.h Normal file

File diff suppressed because it is too large Load diff

34628
objects/layouts.json Normal file

File diff suppressed because it is too large Load diff

4342
objects/layouts.md Normal file

File diff suppressed because it is too large Load diff

261
tools/rtti_map.py Normal file
View file

@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""MSVC RTTI walker for the SOTS1 exe: type descriptors -> COLs -> vftables.
Why this exists
---------------
`findings/objects/00-inventory.md` has 1,924 type-descriptor *names* but no
addresses, so a recovered function can be named "a serializer" but not "*whose*
serializer". Every serializable class in this binary reaches its `Read`/`Write`
through an `IStreamable` sub-vftable, and MSVC puts a Complete Object Locator at
`vftable[-1]`. Walking
type descriptor <- COL.pTypeDescriptor <- vftable[-1]
gives, for every vftable in the image: the owning class name, the sub-object
offset (`COL.offset` -- the this-adjustment that turns a decompiled offset into
an absolute member offset), and the slot functions.
Structures (32-bit MSVC):
TypeDescriptor { void* pVFTable; void* spare; char name[]; } name at +8
COL { u32 signature; u32 offset; u32 cdOffset;
TypeDescriptor* pTypeDescriptor;
ClassHierarchyDescriptor* pClassDescriptor; } 20 bytes
vftable[-1] = COL*
Usage:
uv run python3 tools/rtti_map.py build # -> dumps/rtti.json
uv run python3 tools/rtti_map.py show ObservedTech
uv run python3 tools/rtti_map.py vftable 0x00a2439c
uv run python3 tools/rtti_map.py stats
"""
import json
import os
import re
import struct
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(HERE)
EXE = os.path.join(REPO, "dumps", "sots.exe")
FUNCS = os.path.join(REPO, "dumps", "functions.json")
RTTI = os.path.join(REPO, "dumps", "rtti.json")
# ----------------------------------------------------------------- PE image
class Image:
"""Whole-image VA reader (all sections, not just executable ones)."""
def __init__(self, path=EXE):
data = open(path, "rb").read()
pe = struct.unpack_from("<I", data, 0x3C)[0]
assert data[pe:pe + 4] == b"PE\0\0", "not a PE"
nsec = struct.unpack_from("<H", data, pe + 6)[0]
optsz = struct.unpack_from("<H", data, pe + 20)[0]
self.base = struct.unpack_from("<I", data, pe + 24 + 28)[0]
self.secs = [] # (va, size, bytes, name, exec)
off = pe + 24 + optsz
for k in range(nsec):
s = off + k * 40
name = data[s:s + 8].rstrip(b"\0").decode("latin1")
vsize, vaddr, rsize, raddr = struct.unpack_from("<IIII", data, s + 8)
chars = struct.unpack_from("<I", data, s + 36)[0]
n = min(vsize, rsize) if vsize else rsize
self.secs.append((self.base + vaddr, n, data[raddr:raddr + n],
name, bool(chars & 0x20000000)))
self.lo = min(s[0] for s in self.secs)
self.hi = max(s[0] + s[1] for s in self.secs)
self.text = next(s for s in self.secs if s[4])
def find(self, va):
for sva, n, buf, name, ex in self.secs:
if sva <= va < sva + n:
return buf, va - sva, name
return None, 0, None
def u32(self, va):
buf, o, _ = self.find(va)
if buf is None or o + 4 > len(buf):
return None
return struct.unpack_from("<I", buf, o)[0]
def i32(self, va):
buf, o, _ = self.find(va)
if buf is None or o + 4 > len(buf):
return None
return struct.unpack_from("<i", buf, o)[0]
def cstr(self, va, maxn=512):
buf, o, _ = self.find(va)
if buf is None:
return None
e = buf.find(b"\0", o, o + maxn)
if e < 0:
return None
try:
return buf[o:e].decode("latin1")
except UnicodeDecodeError:
return None
def in_image(self, va):
return va is not None and self.lo <= va < self.hi
def in_text(self, va):
return va is not None and self.text[0] <= va < self.text[0] + self.text[1]
# ------------------------------------------------------------------ demangle
def demangle(mangled: str) -> str:
"""`.?AVObservedTech@Game@@` -> `Game::ObservedTech`.
Templates keep their raw inner text (`?$StreamableHelper@...`) because a
full MSVC template demangler is not needed here -- only a stable key.
"""
m = mangled
for p in (".?AV", ".?AU", ".?AW", ".?AT"):
if m.startswith(p):
m = m[len(p):]
break
if m.endswith("@@"):
m = m[:-2]
# A template argument list can itself contain '@'; split only the trailing
# namespace chain, which is everything after the outermost template body.
if m.startswith("?$"):
return m # template: keep raw
parts = m.split("@")
return "::".join(reversed([p for p in parts if p]))
# --------------------------------------------------------------------- build
def build():
img = Image()
funcs = json.load(open(FUNCS))
fstarts = {int(k, 16) for k in funcs}
fnames = {int(k, 16): v[0] for k, v in funcs.items()}
# 1. type descriptors: the mangled name lives at TD+8
tds = {} # td_va -> mangled
pat = re.compile(rb"\.\?A[VUWT][\x20-\x7e]{0,300}?@@\x00")
for sva, n, buf, name, ex in img.secs:
if ex:
continue
for m in pat.finditer(buf):
nva = sva + m.start()
td = nva - 8
if img.in_image(td):
tds[td] = m.group()[:-1].decode("latin1")
# 2. COLs: any aligned dword equal to a TD address, at COL+0xC
cols = {} # col_va -> dict
for sva, n, buf, name, ex in img.secs:
if ex:
continue
for o in range(0, n - 3, 4):
v = struct.unpack_from("<I", buf, o)[0]
if v not in tds:
continue
col = sva + o - 0xC
sig = img.u32(col)
if sig not in (0, 1):
continue
off = img.u32(col + 4)
cd = img.u32(col + 8)
chd = img.u32(col + 0x10)
if off is None or off > 0x10000 or cd is None or cd > 0x10000:
continue
if not img.in_image(chd):
continue
# Class Hierarchy Descriptor -> base class list. This is the only
# exact test for "is this class an IStreamable?": a 3-slot vftable
# on its own also matches TacAISquadRule_*, the row parsers and any
# other class that happens to have three virtuals.
bases = []
nb = img.u32(chd + 8)
arr = img.u32(chd + 0xC)
if nb and nb < 64 and img.in_image(arr):
for b in range(nb):
bcd = img.u32(arr + 4 * b)
if not img.in_image(bcd):
break
btd = img.u32(bcd)
if btd in tds:
bases.append(demangle(tds[btd]))
cols[col] = {"td": v, "offset": off, "cdOffset": cd,
"mangled": tds[v], "name": demangle(tds[v]),
"bases": bases}
# 3. vftables: any aligned dword equal to a COL address; table starts at +4
vfts = {}
for sva, n, buf, name, ex in img.secs:
if ex:
continue
for o in range(0, n - 3, 4):
v = struct.unpack_from("<I", buf, o)[0]
if v not in cols:
continue
vf = sva + o + 4
slots = []
a = vf
while True:
p = img.u32(a)
if p is None or p not in fstarts:
break
slots.append(p)
a += 4
if len(slots) > 400:
break
if not slots:
continue
c = cols[v]
vfts[vf] = {"col": v, "class": c["name"], "mangled": c["mangled"],
"offset": c["offset"], "slots": slots,
"bases": c["bases"],
"slotNames": [fnames.get(s, "") for s in slots]}
out = {"typeDescriptors": {hex(k): v for k, v in tds.items()},
"cols": {hex(k): v for k, v in cols.items()},
"vftables": {hex(k): v for k, v in vfts.items()}}
with open(RTTI, "w") as fh:
json.dump(out, fh)
print(f"type descriptors : {len(tds)}")
print(f"COLs : {len(cols)}")
print(f"vftables : {len(vfts)}")
ns = {}
for v in vfts.values():
ns[v["class"].split("::")[0]] = ns.get(v["class"].split("::")[0], 0) + 1
print("vftables by top-level namespace:",
", ".join(f"{k}={v}" for k, v in
sorted(ns.items(), key=lambda x: -x[1])[:6]))
def load():
with open(RTTI) as fh:
r = json.load(fh)
return ({int(k, 16): v for k, v in r["typeDescriptors"].items()},
{int(k, 16): v for k, v in r["cols"].items()},
{int(k, 16): v for k, v in r["vftables"].items()})
def main():
if len(sys.argv) < 2 or sys.argv[1] == "build":
return build()
cmd = sys.argv[1]
tds, cols, vfts = load()
if cmd == "show":
pat = sys.argv[2]
for va, v in sorted(vfts.items()):
if pat.lower() in v["class"].lower():
print(f"vftable 0x{va:08x} COL 0x{v['col']:08x} "
f"offset +0x{v['offset']:x} {v['class']}")
for i, (s, nm) in enumerate(zip(v["slots"], v["slotNames"])):
print(f" [{i}] 0x{s:08x} {nm}")
elif cmd == "vftable":
va = int(sys.argv[2], 0)
v = vfts.get(va)
print(json.dumps(v, indent=2) if v else "not a vftable")
elif cmd == "stats":
print(f"tds={len(tds)} cols={len(cols)} vftables={len(vfts)}")
return 0
if __name__ == "__main__":
sys.exit(main() or 0)

1425
tools/serializers.py Normal file

File diff suppressed because it is too large Load diff

152
tools/serializers_ghidra.py Normal file
View file

@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Push the recovered layouts into Ghidra so later lanes inherit them.
Reads `objects/layouts.json` + `objects/layouts.h` (produced by
`tools/serializers.py all`) and, through `tools/reva_call.py`:
* `parse-c-structure` for every emittable struct, dependencies first
* `create-label` for every serializer `Write` / `Read` entry point
Idempotent: re-running replaces the structures and re-applies the labels.
Progress is written to `objects/.ghidra_pushed` so an interrupted run resumes.
uv run python3 tools/serializers_ghidra.py structs
uv run python3 tools/serializers_ghidra.py labels
"""
import json
import os
import re
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(HERE)
OUT = os.path.join(REPO, "objects")
PROG = "/Sword of the Stars.exe"
STATE = os.path.join(OUT, ".ghidra_pushed")
def reva(tool, payload):
r = subprocess.run(
["uv", "run", "python3", os.path.join(HERE, "reva_call.py"), tool,
json.dumps(payload)],
cwd=REPO, capture_output=True, text=True, timeout=180)
return r.returncode, (r.stdout or r.stderr).strip()
def done_set():
if not os.path.exists(STATE):
return set()
return set(open(STATE).read().split("\n"))
def mark(key):
with open(STATE, "a") as fh:
fh.write(key + "\n")
def structs():
text = open(os.path.join(OUT, "layouts.h")).read()
defs = re.findall(r"struct \w+ \{.*?\};", text, re.S)
have = done_set()
ok = fail = skip = 0
for d in defs:
name = d.split()[1]
key = "struct:" + name
if key in have:
skip += 1
continue
rc, out = reva("parse-c-structure",
{"programPath": PROG, "cDefinition": d})
if rc == 0 and '"message"' in out:
ok += 1
mark(key)
else:
fail += 1
print(f" FAIL {name}: {out[:160]}")
if (ok + fail) % 25 == 0:
print(f" {ok} ok, {fail} failed, {skip} already there")
print(f"structures: {ok} pushed, {fail} failed, {skip} skipped")
def labels():
lay = json.load(open(os.path.join(OUT, "layouts.json")))
have = done_set()
ok = fail = skip = 0
for c, L in sorted(lay.items()):
if L["grade"] not in ("verified", "clean"):
continue
nm = c.replace("::", "_")
for kind in ("write", "read"):
va = L.get(kind)
if not va:
continue
label = f"{nm}_{kind.capitalize()}"
key = f"label:{label}:{va:x}"
if key in have:
skip += 1
continue
rc, out = reva("create-label",
{"programPath": PROG, "labelName": label,
"address": f"0x{va:08x}"})
if rc == 0:
ok += 1
mark(key)
else:
fail += 1
print(f" FAIL {label}: {out[:120]}")
print(f"labels: {ok} created, {fail} failed, {skip} skipped")
def addresses():
"""Merge the verified-tier serializers and corroborated sizeofs into
ghidra/addresses.json (then run tools/gen_addresses.py -- never hand-edit
the header)."""
path = os.path.join(REPO, "ghidra", "addresses.json")
j = json.load(open(path))
base = int(j["image_base"], 16)
have = {e["name"] for e in j["entries"]}
lay = json.load(open(os.path.join(OUT, "layouts.json")))
added = 0
for c, L in sorted(lay.items()):
if L["grade"] != "verified":
continue
nm = c.replace("::", "_")
cr = f"{L['read_agree']}/{L['read_comparable']} field offsets agree " \
f"between Read and Write"
for kind in ("write", "read"):
va = L.get(kind)
name = f"{nm}_{kind.capitalize()}"
if not va or name in have or not (base <= va < base + 0x1000000):
continue
j["entries"].append({
"name": name, "addr": f"0x{va:08x}", "convention": "thiscall",
"prototype": f"void ({nm}* this, Mars::Stream* s) "
f"/* IStreamable slot {2 if kind == 'write' else 1}"
f", vftable 0x{L['vftable']:08x}, COL offset "
f"+0x{L['col_offset']:x}; {len(L['fields'])} "
f"member fields; {cr} */",
"status": "verified",
"source": "findings/objects/serializer-struct-recovery.md"})
have.add(name)
added += 1
if L["sizeof"]:
name = f"sizeof_{nm}"
if name not in have:
j["entries"].append({
"name": name, "offset": f"0x{L['sizeof']:x}",
"convention": "layout",
"prototype": f"sizeof({c}) -- {L['sizeof_by']}",
"status": "verified",
"source": "findings/objects/serializer-struct-recovery.md"})
have.add(name)
added += 1
with open(path, "w") as fh:
json.dump(j, fh, indent=1, ensure_ascii=False)
fh.write("\n")
print(f"addresses.json: +{added} entries, {len(j['entries'])} total")
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else "structs"
{"structs": structs, "labels": labels, "addresses": addresses}[cmd]()

275
tools/serializers_golden.py Normal file
View file

@ -0,0 +1,275 @@
#!/usr/bin/env python3
"""Golden layouts for `tools/serializers.py --validate`.
Transcribed by hand from the layouts this campaign had already recovered and
cross-checked against real saves *before* this tool existed:
* `findings/objects/struct-recovery.md` sections 1-4 (member tables read out
of the decompiled `Write`/`Read` pairs, COL offsets from RTTI)
* `findings/subsystems/observedtech-append.md` section 4 + 9 (`ObservedTech`,
`sizeof` pinned three independent ways)
* `verify/save-reader/SAVE_FORMAT.md` / `save_reader.py` (on-disk tag order,
`--strict` clean against three real saves, 36 tests)
Offsets are **absolute** (object base), i.e. the decompiled `this`-relative
offset plus the IStreamable COL offset. Kinds use the tool's vocabulary.
Nothing here is derived from the tool: it is the independent answer the tool
has to reproduce.
"""
# tag, absolute offset, kind
GOLDEN = {
"Game::ObservedTech": {
"write": 0x00817CF0, "sizeof": 0x2C,
"fields": [("otnF", 0x04, "int16"), ("otnL", 0x06, "int16"),
("odet", 0x08, "bool"), ("otch", 0x0C, "string"),
("owith", 0x28, "int")],
},
"Game::ObservedWeapon": {
"write": 0x00817BC0, "sizeof": 0x2C,
"fields": [("otnF", 0x04, "int16"), ("otnL", 0x06, "int16"),
("odet", 0x08, "bool"), ("owep", 0x0C, "string"),
("owith", 0x28, "int")],
},
"Game::StarSystem::OutputRates": {
"write": 0x00745190, "sizeof": 0x1C,
"fields": [("SRt", 0x00, "float"), ("SRsc", 0x04, "float"),
("SRtf", 0x08, "float"), ("SRi", 0x0C, "float"),
("SRoh", 0x10, "float"), ("SRs", 0x14, "float"),
("SRnr", 0x18, "int")],
},
"Game::ServerSystem": {
"write": 0x00749630, "sizeof": None,
"fields": [
("Pos", 0x18, "object"),
("R", 0x4C, "float"), ("G", 0x50, "float"), ("B", 0x54, "float"),
("A", 0x58, "float"), ("Idx", 0x5C, "int"), ("Size", 0x60, "int"),
("Suit", 0x64, "float"), ("Res", 0x68, "int"),
("ARes2", 0x6C, "int"), ("MRes", 0x70, "int"),
("TRes", 0x74, "int"), ("haltv", 0x78, "bool"),
("OutMod", 0x7C, "float"), ("TAcq", 0x80, "int"),
("TFAcq", 0x84, "int"), ("Rts", 0x88, "object"),
("BQ", 0xA4, "object"), ("Name", 0xA8, "string"),
("Abdn", 0xC4, "bool"), ("Dstyd", 0xC5, "bool"),
("vnh", 0xC6, "bool"), ("vnd", 0xC7, "bool"),
("vnex3", 0xC8, "bool"), ("vnpex3", 0xC9, "bool"),
("VFlags", 0xCC, "int"), ("EFlags", 0xD0, "int"),
("AFlags", 0xD4, "int"), ("FFlags", 0xD8, "int"),
("GFlags", 0xDC, "int"), ("MnRFlags", 0xE0, "int"),
("RfRFlags", 0xE4, "int"), ("ClkFlags", 0xE8, "int"),
("Bats2", 0xF0, "int64"), ("rcex", 0xF8, "int64"),
("PID", 0x100, "handle"), ("dcs", 0x104, "object"),
("dsu", 0x118, "float"), ("cm", 0x11C, "object"),
("cme2", 0x13C, "vector"), ("PvCM", 0x14C, "object"),
("NumFlts", 0x16C, "vector"), ("RepCur", 0x17C, "float"),
("RepMax", 0x180, "float"), ("EggScio", 0x184, "int"),
("NoRebAI", 0x188, "bool"), ("PvNoRebAI", 0x189, "bool"),
("Pop", 0x18C, "int"), ("Infra", 0x190, "float"),
("pbon", 0x194, "int"), ("ibon", 0x198, "float"),
("TerrFl", 0x19C, "int"), ("Pop2", 0x1A0, "object"),
("pbon2", 0x1B4, "object"), ("indi", 0x1C8, "object"),
("spies2", 0x1CC, "vector"), ("rbfl", 0x1DC, "int"),
("hsrg", 0x1E0, "bool"), ("adt", 0x1E4, "int"),
("PvPop", 0x200, "int"), ("PvInfra", 0x204, "float"),
("PvSuit", 0x208, "float"), ("PvRes", 0x20C, "int"),
("PvARes2", 0x210, "int"), ("PvMRes", 0x214, "int"),
("PvPop2", 0x218, "object"), ("DefF", 0x238, "handle"),
("DefSF", 0x23C, "handle"), ("NumGFs", 0x240, "vector"),
("NumSnF", 0x250, "vector"), ("NumMnF", 0x260, "vector"),
("NumPlgs2", 0x2A8, "vector"), ("TnsOH", 0x2B8, "int"),
("TDst", 0x2BC, "int"), ("ntdev", 0x2C4, "int"),
("ltis", 0x2C8, "int"), ("rbtn", 0x2CC, "int"),
("rbfr", 0x2D0, "int"), ("rbwn", 0x2D4, "int"),
],
},
"Game::StarSystem::PlayerView": {
"write": 0x007492D0, "sizeof": None,
"fields": [("VTrn", 0x08, "int"), ("Pop", 0x0C, "int"),
("Pop2", 0x10, "object"), ("Infra", 0x24, "float"),
("Suit", 0x28, "float"), ("Res", 0x2C, "int"),
("ARes2", 0x30, "int"), ("MRes", 0x34, "int"),
("NoRebAI", 0x38, "bool"), ("pbon", 0x3C, "int"),
("pbon2", 0x40, "object"), ("ibon", 0x54, "float"),
("TerrFl", 0x58, "int")],
},
"Game::PopulationGroup": {
"write": 0x00536AF0, "sizeof": 0x18,
"fields": [("PopT", 0x04, "int"), ("PopS", 0x08, "int"),
("PopC", 0x10, "int64")],
},
"Game::IndependenceInfo": {
"write": 0x00748EE0, "sizeof": 0x70,
"fields": [("indsp", 0x04, "int"), ("indcl", 0x08, "object"),
("indnm", 0x1C, "string"), ("indav", 0x38, "string"),
("indba", 0x54, "string")],
},
"Game::MoraleEvent": {
"write": 0x007491B0, "sizeof": 0x50,
"fields": [("mid", 0x04, "int"), ("mtr", 0x08, "int"),
("mn", 0x0C, "int"), ("mtp", 0x10, "int"),
("mfx", 0x14, "object"), ("mdsc", 0x34, "string")],
},
"Game::ShipBuildOrder": {
"write": 0x00813800, "sizeof": None,
"fields": [("desID", 0x04, "int"), ("con", 0x08, "int"),
("sav", 0x0C, "int"), ("conleft", 0x10, "int"),
("ordID", 0x14, "int")],
},
"Game::DiplomacyStats": {
"write": 0x00818CB0, "sizeof": 0x24,
"fields": [("other", 0x04, "int"),
("lastnap", 0x08, "int16"), ("lastnapbty", 0x0A, "int16"),
("bknnap", 0x0C, "int16"), ("btynap", 0x0E, "int16"),
("lastally", 0x10, "int16"), ("lastallybty", 0x12, "int16"),
("bknally", 0x14, "int16"), ("btyally", 0x16, "int16"),
("lastcf", 0x18, "int16"), ("lastcfbty", 0x1A, "int16"),
("bkncf", 0x1C, "int16"), ("btycf", 0x1E, "int16"),
("deadhome", 0x20, "int16")],
},
"Game::PlayerReport": {
"write": 0x00817480, "sizeof": 0x30,
"fields": [("oid", 0x04, "int"), ("pid", 0x08, "int"),
("flds", 0x0C, "int"), ("sav", 0x10, "int"),
("home", 0x14, "int"), ("ncol", 0x18, "int"),
("mpwr", 0x1C, "int"), ("mcls", 0x20, "int"),
("mmsl", 0x24, "int"), ("nshp", 0x28, "int"),
("nsat", 0x2C, "int")],
},
"Game::FlightPlan": {
"write": 0x00700F60, "sizeof": 0x38,
"fields": [("wpts", 0x04, "vector"), ("FPsp2", 0x14, "float"),
("FPeta2", 0x18, "int"), ("FPogn2", 0x1C, "object"),
("FPdpos", 0x28, "object"), ("pnd", 0x34, "int")],
},
"Game::FlightPlan::Waypoint": {
"write": 0x00700ED0, "sizeof": None,
"fields": [("Wpt", 0x04, "int"), ("Tp", 0x08, "int"),
("nrt", 0x0C, "object")],
},
"Game::NodeRoute": {
"write": 0x006E22E0, "sizeof": None,
"fields": [("nrp", 0x04, "int"), ("nrf", 0x08, "int"),
("nrt", 0x0C, "int")],
},
"Game::StarFleet": {
"write": 0x00701070, "sizeof": None,
"fields": [("Pos", 0x18, "object"), ("PrvPos", 0x4C, "object"),
("PID", 0x58, "handle"), ("FtName", 0x5C, "string"),
("Perm", 0x78, "bool"), ("Lay", 0x7C, "object"),
("LocID", 0xA0, "handle"), ("NShips", 0xA4, "vector"),
("FPlan", 0xC4, "object"), ("FtTrans", 0xFC, "int"),
("FtOrig", 0x100, "object"), ("FtFlg", 0x10C, "int"),
("Ftae", 0x110, "int"), ("Ftpae", 0x114, "int"),
("FtEnc", 0x118, "int"), ("FtMS", 0x11C, "int")],
},
"Game::StarShip": {
"write": 0x008291F0, "sizeof": None,
"fields": [("PlrID", 0x10, "handle"), ("DesID", 0x14, "int"),
("Range", 0x20, "float"), ("Health", 0x24, "object"),
("MineCap", 0x34, "int"), ("NTH", 0x38, "vector"),
("Plg", 0x48, "int"), ("Act", 0x4C, "int"),
("Dep", 0x50, "bool"), ("Atq", 0x51, "bool"),
("LCT", 0x5C, "int"), ("tsd", 0x60, "int"),
("FltID", 0x64, "handle"), ("ConCap", 0x68, "int"),
("RefCap", 0x6C, "float"), ("RepCap", 0x70, "float"),
("EncID", 0x7C, "int"), ("PrisH", 0x80, "object"),
("BQ2", 0x98, "object"), ("pop", 0x9C, "object"),
("ppop", 0xA0, "object"), ("atsp", 0xA8, "int"),
("tblt", 0xAC, "int")],
},
"Game::ServerPlayer": {
"write": 0x008563E0, "sizeof": None,
"fields": [
("PlyrIdx", 0x28, "int"), ("HomeSys", 0x2C, "handle"),
("NumOwn", 0x30, "vector"), ("PlryName", 0x40, "string"),
("Species", 0x5C, "int"), ("ClrID", 0x60, "object"),
("Bdg", 0x74, "string"), ("Avt", 0x90, "string"),
("Team", 0xAC, "int"), ("IdealSuit", 0xB0, "float"),
("SuitTol", 0xB4, "float"), ("MaxOH", 0xB8, "float"),
("ResRate", 0xBC, "float"), ("ResMod", 0xC0, "float"),
("ResScl", 0xC4, "float"), ("TRM", 0xD0, "float"),
("TRA", 0xD4, "int"), ("TRP", 0xD8, "int"),
("NumDes", 0xE4, "vector"), ("TechTree", 0xF4, "object"),
("Elim", 0xF8, "bool"), ("NPC", 0xFB, "bool"),
("RebAI", 0xFC, "bool"), ("ReqCL", 0xFD, "bool"),
("AIBn", 0xFE, "bool"), ("CnTrd", 0xFF, "bool"),
("CnRad", 0x100, "bool"), ("CnVItl", 0x101, "bool"),
("hgs", 0x102, "bool"), ("hadvs", 0x103, "bool"),
("harcc", 0x104, "bool"), ("pddm", 0x108, "float"),
("OutMod", 0x124, "float"), ("RebOutMod", 0x128, "float"),
("ScOutMod", 0x12C, "float"), ("PopMod", 0x130, "float"),
("TerraMod", 0x134, "float"), ("AMine", 0x138, "bool"),
("MinPure", 0x13C, "float"), ("MinRate", 0x140, "float"),
("NGts", 0x144, "int"), ("PrGtTrf", 0x148, "int"),
("GTraf", 0x14C, "int"), ("CstR", 0x150, "float"),
("CstE", 0x154, "float"), ("CstT", 0x158, "float"),
("Maint", 0x15C, "int"), ("shrm", 0x160, "float"),
("Status", 0x164, "int"), ("Team", 0x168, "object"),
("NumLeg", 0x178, "vector"), ("PvSav", 0x188, "int"),
("PvMA", 0x18C, "bool"), ("HasDisc", 0x19C, "int"),
("HasDiscSp", 0x1A0, "int"), ("HasDiscCl", 0x1A4, "int"),
("HasEnc", 0x1A8, "int"), ("HasEng", 0x1AC, "int"),
("ShipRecs", 0x1B0, "object"), ("Ojvs", 0x1F4, "vector"),
("Nexp", 0x204, "vector"), ("NWeapXcl", 0x214, "vector"),
("dipstats", 0x230, "vector"), ("comms", 0x240, "object"),
("preps", 0x244, "vector"), ("odes", 0x254, "vector"),
("owep", 0x264, "vector"), ("otch", 0x274, "vector"),
("Sav", 0x284, "int"), ("HasImm", 0x288, "int"),
("HasVac", 0x28C, "int"), ("NPTrk", 0x290, "int"),
("ResTNm", 0x294, "object"), ("FNG", 0x298, "object"),
("Events", 0x29C, "object"), ("BnkWrn", 0x2C4, "int"),
("BnkTrn", 0x2C8, "int"), ("BnkEl", 0x2CC, "int"),
("BnkPr", 0x2D0, "int"), ("plcy", 0x2D8, "int"),
("pswd", 0x2DC, "string"), ("Srn", 0x2F8, "bool"),
("SrnTo", 0x2FC, "handle"), ("lboid", 0x300, "int"),
("lcid2", 0x304, "int"), ("IncMod", 0x30C, "float"),
("aid", 0x310, "vector"), ("ndeflay", 0x320, "vector"),
("cdp", 0x330, "bool"), ("spy2", 0x334, "object"),
("rdtc", 0x338, "vector"), ("aidf", 0x368, "int"),
("civr", 0x370, "object"), ("tnc", 0x39C, "int"),
("NumPR", 0x3A4, "vector"), ("ResErrRoll", 0x3B4, "bool"),
("cta", 0x3B5, "bool"), ("AIR", 0x3B8, "object"),
("AIEnf", 0x3BC, "object"), ("NSprj", 0x3C0, "vector"),
("NextPrjID", 0x3D0, "int"), ("lret", 0x3D4, "int"),
("nmeid", 0x3DC, "int"),
],
},
}
# sizeof values the campaign pinned independently of any serializer walk
GOLDEN_SIZEOF = {
"Game::ObservedTech": 0x2C, # magic divide + imul + search stride
"Game::ObservedWeapon": 0x2C,
"Game::PopulationGroup": 0x18, # Population's own vector stride
"Game::MoraleEvent": 0x50,
"Game::PlayerReport": 0x30,
"Game::DiplomacyStats": 0x24,
"Game::StarSystem::OutputRates": 0x1C,
}
# on-disk tag order, from save_reader.py shapes -> the serializer that writes it
DISK_ORDER = {
0x00829960: "Summary", # StrategyGameInfo::Write
0x008276D0: "Slot", # SlotDef::Write
0x0082AE40: "CreateParams", # StrategyGameCreateParams::Write
0x0082AD40: "Scrp", # StrategyScriptParams::Write
0x0056EC00: "PrisonerHold",
0x008189A0: "SystemEvent",
0x0082C290: "PlayerTurnStats",
0x0082C4A0: "PlayerTurnHistory",
0x0082C5A0: "TurnStats",
0x0079FA70: "Sim", # StrategyServer::Write
0x00749630: "Sys", # ServerSystem::Write
0x008563E0: "Player", # ServerPlayer::Write
0x00701070: "Flt", # StarFleet::Write
0x008291F0: "Ship", # StarShip::Write
0x00700F60: "FlightPlan",
0x00817CF0: "Otch",
0x00817BC0: "Owep",
0x00745190: "Rts",
0x00748EE0: "IndependenceInfo",
0x00813800: "BuildOrder",
0x00817480: "Prep",
0x00818CB0: "DipStat",
0x007492D0: "PlayerView",
}