Compare commits

..

No commits in common. "0d84eb2d4cedcf68d6e9467db73d6fe38d4df1f1" and "795aa471d45132d01b8d18f8c18ef4cc12e531f2" have entirely different histories.

12 changed files with 5 additions and 45575 deletions

2
.gitignore vendored
View file

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

View file

@ -96,6 +96,3 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
| std::string 0x18 vs 0x1c CONTRADICTION - RESOLVED | objects | verified | high | 100% | 2026-09-08 | 0x1c WAS RIGHT ALL ALONG; lane X mis-attributed ObservedTech+0x24, which is the string's trailing empty-allocator word. ONE layout binary-wide: _Bx@0 (16-byte SSO union), _Mysize@0x10, _Myres@0x14, _Alval@0x18. Proof by three COMPLETE ENUMERATIONS of ObservedTech (Write 0x00817cf0, ctor 0x008562a0, inlined copy ctor 0x0079a184) each of which skips +0x24, then generalised by new `tools/strfootprint.py`: 65 std::string members off a non-stack base, ZERO with a sibling inside the 0x1c span, 51 of 52 measurable inter-member gaps exactly 0x1c (the one 0x20 is StrategyServer::KeyPath, 0x1c on its own Read side - the WRITER skips a member). No 0x18 instantiation, no EBO variant, no custom allocator, no game-local string class. BLAST RADIUS: 65 layouts audited, ZERO were wrong - including ServerPlayer::pswd, the row lane X flagged. Only PROSE carried the 0x18 number. Separately found+fixed: struct-recovery.md 0 had _Mysize/_Myres TRANSPOSED while every table in the same file used the correct offsets |
| ObservedTech on-disk mapping | objects | verified | high | 100% | 2026-09-08 | READ, NOT GUESSED, from ObservedTech::Write 0x00817cf0 / Read 0x00817c40: +0x00 vptr, +0x04 u16 otnF, +0x06 u16 otnL, +0x08 bool odet (ONE byte, WriteBool), +0x0c std::string otch (0x1c), +0x28 int owith = 0x2c exactly, nothing unaccounted. Lane X's flagged-as-hypothesis first-seen/last-seen pair CONFIRMED by the tag names. Game::ObservedWeapon (0x00817bc0/0x00817b10) is the identical element with tag owep. Matches save_reader.py's on-disk order exactly - independent agreement between disassembly and the save oracle. `odet` typed int in the reader is BENIGN (for a 4-char tag a bool item and an int item are both 12 bytes, same value); all 60/61/61 real-save values are 00000000 so the SAVES do not discriminate - the binary does |
| MY BRIEFING ERROR: fpu_cw triple was under-powered | meta | verified | high | 100% | 2026-09-08 | I specified the experiment as 0x027f / 0x127f / 0x137f. WRONG ON BOTH AXES: 0x027f is 53-bit (it differs from 0x127f only in bit 12, infinity control, ignored since the 387) and 0x137f is 64-bit EXTENDED, not a rounding change. Run literally it returns "all three identical" - TRUE, but it tests single precision not at all and rounding not at all, so the conclusion drawn would have been wrong. Lane F caught it and added the genuine probes 0x007f (24-bit) and 0x1a7f (round-up). RULE: derive experiment parameters from the ISA definition, never from assumed mnemonics - and brief lanes to challenge the parameters, not just execute them |
| serializer struct recovery (automated) | objects | verified | high | 90% | 2026-09-08 | **Lane D: struct recovery is now MECHANICAL.** `tools/serializers.py` decodes the `Mars::IStreamable` `Write` idiom for the whole binary in **0.35 s**; `tools/rtti_map.py` walks type-descriptor <- COL <- vftable[-1] (1,924 TDs = the inventory's count, 2,172 COLs/vftables) to attribute each serializer to its class and its COL this-adjustment. **VALIDATION RUN BEFORE ANY NEW CLAIM, four ways: (A) 305/307 field offsets+kinds exact across 17 already-known classes, ZERO WRONG** vs struct-recovery.md 1-4 (ServerSystem 79/79, StarShip 23/23, StarFleet 16/16, ServerPlayer 101/103, PlayerView 13/13, DiplomacyStats 14/14 - including every trap: ObservedTech+0x24 correctly NOT a field, Bats2/rcex int64, TShn/ETS/DipStats int16, ltis int, pswd string, TRM/CstR/E/T/shrm/RefCap/RepCap/PlayerView.Infra float); **(B) sizeof read off the VectorHelper element-stride divide** (invert MSVC's magic `M = ceil(2^(32+k)/s)`) reproduces ObservedTech **0x2c** independently of lane X's three proofs, plus MoraleEvent 0x50, PlayerReport 0x30, DiplomacyStats 0x24, ObservedDesign 0x10; **(C) 22 of save_reader.py's shapes, recovered tag order identical, 22 agree / 0 disagree** (Sys 78 tags, Player 104, CreateParams 25, Ship 22, Slot 18); **(D) automated Read/Write cross-check on every class, 437/437 field offsets agree**. SCALE: **386 classes with a Write, 1,682 member fields** - verified 87 (542 fields) / clean 77 (328) / unnamed 176 (471) / partial 31 / empty 15; **58 sizeofs corroborated** by a second line of evidence, the rest reported as LOWER BOUNDS and labelled as such (a serializer enumerates only *serialised* members - a non-streamed tail is invisible). ~10x more classes with a known layout, reproducible from the exe in under a second. FOUR ENABLERS each worth 10-170 classes: (1) the RTTI **ClassHierarchyDescriptor** is the only honest "is this an IStreamable" test - a 3-slot vftable also matches TacAISquadRule_*, the CSV row parsers and ~100 others, and using it dropped a bogus 116-class "empty" bucket to 11; (2) **mod=0 memory operands** carry no displacement so `tools/x86disp.py` cannot index them - every field at offset 0 was invisible (OutputRates.SRt, every container element read through a bare iterator); (3) the **member->id pointer idiom** behind every handle field (HomeSys/PlrID/DesID/FltID/SrnTo) - and the base's this-ness must be read BEFORE retiring the destination, because `mov edi,[edi+0x1c0]` overwrites the this-register with the member it is reading; (4) **sub-writers**, base-class (StarMapNode inside ServerSystem = `Pos`) and private-same-class (StrategyServer's six id lists live in FUN_00794cd0 - without it PlayerIDs/DesignIDs/SystemIDs/FleetIDs/ShipIDs/TradeIDs are simply absent), spliced at the call site so disk order survives. FAILURE CLASSES, enumerated: 176 anonymous-tag classes (writer passes NULL, so offsets+types are complete but there are NO on-disk names - a hard limit on names, not on layout; combat commands + network messages); 19 unsized nested types; 9 map/list-node-only; 3 untyped sub-writers; 2 fields lost to a value assembled ACROSS A BRANCH (`tech ? tech->name : ""`, `max(v,1)`) - **left unrecovered on purpose rather than patched with a backward-search heuristic that has no oracle on the other 1,600 classes**; std::map/list members recovered only as their `_Mysize` int; 1 runtime-built tag (Mars::ParticleSystem). Everything but the first is a bounded mechanical fix. WRITE-BACK: 288 structures + 328 labels into Ghidra (0 failures), +201 addresses.json entries, header regenerated with gen_addresses.py. `findings/objects/serializer-struct-recovery.md`, `objects/layouts.{json,md,h}` |
| Game::ShipDesign is NOT streamed through IStreamable | objects | mapped | high | 100% | 2026-09-08 | Lane D, from the serializer sweep: `ShipDesign`'s IStreamable slots are inert - `Write` 0x008747a0 makes no stream call at all. Same for `Game::ServerTradeManager` (Read and Write are BOTH the inherited no-op 0x924fb0) and 13 others incl. the whole `Mars::Particle*` family and `Mars::DecalFile`. So designs persist by some other path (`ShipDesignDef` is already verified separately and may be the whole answer) - worth confirming before anyone goes looking for a ShipDesign serializer that does not exist |
| SVSOJewelsOfTheCrown writes a DUPLICATE tag | objects | mapped | high | 100% | 2026-09-08 | Lane D: `SVSOJewelsOfTheCrown::Write` emits the tag `JEWELLOCATIONID` TWICE, at +0x8 and +0x10. Not a tool error - the Write really does it. Any save reader matching that shape BY NAME will bind the wrong field. Same shape as ServerPlayer's two `Team` members (int @0xac, PlayerAlliances @0x168), which is why the Read/Write cross-check has to exclude duplicated tags rather than report them as conflicts |

View file

@ -1,248 +0,0 @@
# 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).
// Source: sots-re ghidra/addresses.json @ 795aa47, generated 2026-09-08 by tools/gen_addresses.py
// Source: sots-re ghidra/addresses.json @ d523d27, generated 2026-09-08 by tools/gen_addresses.py
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
#pragma once
#include <cstdint>
@ -369,7 +369,7 @@ constexpr uint32_t ServerPlayer_off_SetupResearchMult = 0x0000022c;
constexpr uint32_t ServerPlayer_off_Sav = 0x00000284;
// offset Tech* current research target (ResT); NULL = none [verified-by-save]
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. 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]
// 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]
constexpr uint32_t ServerPlayer_off_ObservedTechs = 0x00000274;
// offset float IncMod [verified-by-save]
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;
// 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;
// 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]
// 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]
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]
constexpr uint32_t ObservedTech_vftable = 0x0062439c;
@ -835,407 +835,5 @@ constexpr uint32_t ObservedWeapon_Write = 0x00417bc0;
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]
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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,261 +0,0 @@
#!/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)

File diff suppressed because it is too large Load diff

View file

@ -1,152 +0,0 @@
#!/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]()

View file

@ -1,275 +0,0 @@
#!/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",
}