Settles the 0x18-vs-0x1c contradiction lane X raised. 0x1c is right, everywhere, and there is exactly one std::string instantiation in this binary: _Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18. ObservedTech+0x24 is that string's trailing empty-allocator word, not the unaccounted data field it was read as. Three complete enumerations of the element each skip it: ObservedTech::Write 0x00817cf0, the ctor 0x008562a0, and the copy ctor inlined at 0x0079a184. Generalised with a new scanner, tools/strfootprint.py, which recovers every (base, disp, tag) handed to the Mars::Stream string helpers: 65 std::string members off a non-stack base across every serializer in the exe, ZERO with a sibling member inside the 0x1c span, and 51 of the 52 measurable inter-member gaps exactly 0x1c. Corroborated by the vector<string> walk stride (add esi,0x1c @0x00699c29), PostEvent's by-value strings at [ebp+8]/[ebp+0x24] with RET 0x4c, and MoraleEvent 0x50 = name@0x34 + 0x1c. Blast radius: zero recovered struct tables were wrong. Every string-bearing layout already used 0x1c spans and 0x1c gaps -- ServerPlayer::pswd @0x2dc..0x2f7, the row flagged for re-checking, included. Only prose carried the 0x18 number: the loader-prototypes conventions line, the GlobalConst_ParseString prototype, and the ObservedTech element table. struct-recovery S0 additionally had _Mysize/_Myres transposed (size@0x14, res@0x18) while every table in the same file used the correct offsets; fixed. ObservedTech's four on-disk fields are now mapped rather than guessed, by reading the serializer as lane X suggested: +0x04 uint16 otnF, +0x06 uint16 otnL, +0x08 bool odet (ONE BYTE, WriteBool), +0x0c std::string otch (0x1c), +0x28 int owith = 0x2c exactly. That matches save_reader.py's on-disk order already. Game:: ObservedWeapon (0x00817bc0/0x00817b10) is the same element with tag owep. Oracles unaffected and re-run: save_reader 36/36 and --strict exit 0 on all three real saves; state_checksum 38 tests OK, coverage PROVED byte-for-byte on turn1 and turn3. sots-engine wip/strings 32d3e36 syncs the header and corrects two stale "unpinned" comments: clean_room_check OK, host ctest 33/33. Standing rule this produced: never size a struct member from the offsets the code touches. This build's STL puts the empty allocator LAST in both string (0x1c) and vector (0x10), and an empty allocator is never loaded or stored, so a touch-scan undercounts by exactly 4 every time. Size from an enumeration instead.
31 KiB
Struct recovery via save-field-name xrefs — Sword of the Stars (2006)
Program sots / "Sword of the Stars.exe", ImageBase 0x00400000, 32-bit MSVC. All addresses are VAs.
Method: every on-disk field is tagged with its name string; the name strings live in .rdata; the
functions that reference dozens of a struct's names in sequence are its IStreamable::Read/Write.
Decompiled those, mapped this+offset → name → type. Scripts (on CT111 /root/): SerFind.java
(string→xref→function ranking), SerDump.java/SerDump2.java (decompile with DAT_ → literal
substitution), VtOwner.java/VtOwner2.java (find the owning vftable + RTTI Complete-Object-Locator
offset), SubWrite.java (sub-struct Read/Write by class name). Raw decompiles: CT111 /tmp/serdump/*.c.
Reference cross-checked: save-editor-structs.md (R1 = Bardez editor, R2 = SOTSedit).
0. Serialization runtime facts (needed to read the tables)
IStreamable vftable shape
Every streamable class has a 3-slot vftable { [0] scalar-deleting dtor, [1] Read(Stream&), [2] Write(Stream&) }
(Mars::IStreamable::vftable @ 0x009e22bc = {0x4f7230, 0x924fb0, 0x924fb0}). Mars::StreamableHelper<T> /
Mars::VectorHelper<T> are thin adaptors: slot [1]/[2] call the object's virtual Read/Write (or, for
POD types like Vector3, a free function).
this-adjustment (IMPORTANT for offsets)
The serializer is called through the class's IStreamable sub-vftable, whose RTTI COL offset field
gives the sub-object offset. The vftable slots point straight at the functions (no adjustor thunks;
prologues verified in the exe: 55 8b ec 6a ff 68 …), so inside each function this = object + COL
offset. Absolute member offset = decompiled offset + COL offset. Tables below give both.
| Class | IStreamable vftable (COL offset) | Read | Write | Primary vftable |
|---|---|---|---|---|
Game::ServerSystem (: StarSystem : StarMapNode) |
0x00a2043c (+8) | FUN_0075d4b0 |
FUN_00749630 |
(StarSystem primary 0x00a200e4) |
Game::StarSystem / ClientSystem (StarMapNode part) |
0x00a200d4 / 0x00a20144 (+8) | FUN_00727790 |
FUN_00727820 |
0x00a200e4 / 0x00a20154 |
Game::StarMapNode |
0x00a1e620 (+8) | FUN_00727790 |
FUN_00727820 |
0x00a1e630 |
Game::ServerPlayer (: StrategyPlayer) |
0x00a32794 (+0x3a0 = 928) | FUN_008804d0 |
FUN_008563e0 |
0x00a327a4 (COL 0, 8 slots) |
Game::StarShip |
0x00a31408 (+8) | FUN_00853fa0 |
FUN_008291f0 |
0x00a31418 (2 slots) |
Game::StarFleet (: StarMapNode) |
0x00a1d5f8 (+8) | FUN_00702470 |
FUN_00701070 |
0x00a1d608 |
Game::StrategyServer (whole sim block) |
0x00a26084 (+0) | FUN_007d27a0 |
FUN_0079fa70 |
0x00a26034 (COL 4) |
Game::StarSystem::PlayerView |
0x00a201ac (+0) | FUN_00752af0 |
FUN_007492d0 |
— |
Game::StarSystem::OutputRates (POD, via helper) |
helper 0x00a1f884 | FUN_007472a0 |
FUN_00745190 |
— |
Game::Population |
0x009f90f0 (+0) | FUN_005390c0 |
FUN_00537ef0 |
— |
Game::PopulationGroup |
0x009f8d50 (+0) | FUN_00536a80 |
FUN_00536af0 |
— |
Game::IndependenceInfo |
0x00a2005c (+0) | FUN_00748df0 |
FUN_00748ee0 |
— |
Game::Morale / MoraleEvent |
0x00a1f7c8 / 0x00a2003c | FUN_00744dd0 / FUN_007490b0 |
FUN_00744ea0 / FUN_007491b0 |
— |
Game::ShipBuildOrder(Def) |
0x00a0c160 / 0x00a0ad08 | FUN_00813770 |
FUN_00813800 |
— |
Game::PlayerNotes |
0x00a21948 | FUN_00813250 |
FUN_008132b0 |
— |
Game::SpyReport |
0x00a32b2c | FUN_008843d0 |
FUN_00828ec0 |
— |
Game::PlayerReport (preps) |
0x00a21440 | FUN_008200a0 |
FUN_00817480 |
— |
Game::DiplomacyStats |
0x00a21430 | — | FUN_00818cb0 |
— |
Game::FlightPlan / ::Waypoint / NodeRoute |
0x00a1d50c / 0x00a1d39c / 0x00a1cbdc | FUN_00704c70 / FUN_00701860 / FUN_006e2260 |
FUN_00700f60 / FUN_00700ed0 / FUN_006e22e0 |
— |
Game::PrisonerHold |
0x009fe130 | FUN_0056eb00 |
FUN_0056ec00 |
— |
Game::EventStorage / PlayerAlliances / ShipHealth / PlayerColorID / Mars::Vector3 |
— | — | FUN_00825cc0 / FUN_006d2e10 / FUN_00813e50 / FUN_0053c080 / FUN_008a60d0 |
— |
Stream primitive API (writer side; Stream object vftable, this = stream)
| call | meaning | wrapper used by serializers |
|---|---|---|
vft+0x18 (name, std::string*) |
write string | FUN_008b9d70(stream,name,std::string*) |
vft+0x1c (name, byte) |
write bool | FUN_008b9c20(stream,name,bool*) |
vft+0x20 (name, float) |
write float | FUN_008b9be0(stream,name,float*) |
vft+0x24 (name, int, default=-1) |
write int32 | FUN_008b9d50(stream,name,int*); FUN_008b9d00(stream,name,int16*) (widens short→int) |
vft+0x28 (name, IStreamable-helper*) |
write nested object (BEEFBEEF frame) | inline StreamableHelper<T>{vft, 0, T*} |
vft+0x30 (name, ptr, nbytes) |
write raw bytes | used for 8-byte Int64s |
FUN_00816490(stream,name,obj*) |
write handle id = obj ? obj->id(+4) : 0 |
NetworkObject id at +4 |
FUN_008b9c60(stream,name,int64*) |
write int64 (PopC) | |
Reader side mirrors: FUN_008b9bc0 float, FUN_008b9d20 int, FUN_008b9c00 bool, FUN_008b9d90 string, |
||
FUN_008b9c40 int64, FUN_008b9cd0 short, FUN_008164d0(stream,name) handle→object* lookup, |
||
| stream vft+0x10 int-by-ref (returns found flag), vft+0x14 nested object (NULL helper = skip/legacy). | ||
Readers accept legacy tags (ISuit, Income, HPop, Bats, Builds, Clr, SensMod, ExPopSys, |
||
NShps, SysID, TrdID, Caps, GtTrf, FtSens, FtInc, Pris, NumPlgs, lcid, morev, cme) |
||
| by reading them into scratch/NULL — these are pre-1.8 fields, NOT members. |
Common Mars/MSVC layouts seen: std::string = 0x1c bytes — _Bx union@0 (16-byte SSO buffer, or a
char* when _Myres >= 16), _Mysize@0x10, _Myres@0x14, _Alval@0x18 (empty allocator, occupies
a word, never read or written). FUN_008b9d70 does the res>=16 ? heap : sso check.
(2026-09-08: this line previously transposed the two to size@0x14, res@0x18; the 0x10/0x14 offsets are the
verified ones — Stream::WriteString cmp [str+0x14],0x10, basic_string::assign 0x00425550.)
The allocator is trailing in this build's STL, in strings and vectors alike, so it is invisible to any
scan of "which offsets does the code touch" and costs exactly 4 bytes if you size a member that way. See
findings/subsystems/observedtech-append.md §9 for the whole-binary audit (tools/strfootprint.py): 65
std::string members across every serializer, zero with a sibling field inside the 0x1c span, 51 of 52
measurable inter-member gaps exactly 0x1c. There is one string layout in this binary.
std::vector<T> = {begin@0, end@4, cap@8, _Alval@0xc — 0x10 bytes};
std::map/set node = {left@0, parent@4, right@8, key@0xc, value@0x10, …, color/isnil bytes at tail};
std::list = {head*@0, size@4}. Mars::NetworkObject = {vptr@0, int id@4}.
1. Game::ServerSystem (= live star system + colony record; R1 SimSystemDetailsSaveStruct)
Serializers: Write FUN_00749630 @ 0x00749630 (3453 B), Read FUN_0075d4b0 @ 0x0075d4b0 (8320 B, has
legacy branches). Both begin with StarMapNode::Write/Read (FUN_00727820/FUN_00727790) which
emits Pos. this = obj+8. Base layout: +0 primary vptr (StarSystem 0x00a200e4), +4 NetworkObject id,
+8 IStreamable vptr, +0xc HandleObject vptr, +0x10 owner pointer (*(+0x10)->+0x50 = player-object
table indexed by map key), +0x18 Pos.
| abs off | rel(this+8) | type | save name | notes |
|---|---|---|---|---|
| 0x18 | 0x10 | Mars::Vector3 (3 floats) |
Pos |
via StarMapNode; on disk 3 unnamed floats |
| 0x4c | 0x44 | float | R |
starColor.r |
| 0x50 | 0x48 | float | G |
|
| 0x54 | 0x4c | float | B |
|
| 0x58 | 0x50 | float | A |
|
| 0x5c | 0x54 | int | Idx |
system index |
| 0x60 | 0x58 | int | Size |
1–10 |
| 0x64 | 0x5c | float | Suit |
climate hazard (legacy ISuit discarded) |
| 0x68 | 0x60 | int | Res |
|
| 0x6c | 0x64 | int | ARes2 |
(legacy ARes read then overwritten) |
| 0x70 | 0x68 | int | MRes |
|
| 0x74 | 0x6c | int | TRes |
|
| 0x78..0x7a | 0x70..0x72 | bool[3] | haltv |
written as haltc=3, then 3×(haltt=i, haltv=v[i]) |
| 0x7c | 0x74 | float | OutMod |
|
| 0x80 | 0x78 | int | TAcq |
|
| 0x84 | 0x7c | int | TFAcq |
|
| 0x88..0xa3 | 0x80 | StarSystem::OutputRates (0x1c) |
Rts |
see §1.1; nested object |
| 0xa4 | 0x9c | BuildQueue* |
BQ |
written only if owner (PID) non-null |
| 0xa8..0xc3 | 0xa0 | std::string |
Name |
|
| 0xc4 | 0xbc | bool | Abdn |
bool in memory (R2 "short" is just the value byte; R1 Int32 is the framing) |
| 0xc5 | 0xbd | bool | Dstyd |
|
| 0xc6 | 0xbe | bool | vnh |
gate: if true → vnd,vnex3,vnpex3 |
| 0xc7 | 0xbf | bool | vnd |
|
| 0xc8 | 0xc0 | bool | vnex3 |
|
| 0xc9 | 0xc1 | bool | vnpex3 |
|
| 0xcc | 0xc4 | int | VFlags |
written by value (int) |
| 0xd0 | 0xc8 | int | EFlags |
|
| 0xd4 | 0xcc | int | AFlags |
|
| 0xd8 | 0xd0 | int | FFlags |
|
| 0xdc | 0xd4 | int | GFlags |
|
| 0xe0 | 0xd8 | int | MnRFlags |
|
| 0xe4 | 0xdc | int | RfRFlags |
|
| 0xe8 | 0xe0 | int | ClkFlags |
|
| 0xf0 | 0xe8 | int64 | Bats2 |
raw 8 bytes (R1 correct; R2 Int32 reads low half). Legacy Bats int |
| 0xf8 | 0xf0 | int64 | rcex |
raw 8 bytes |
| 0x100 | 0xf8 | ServerPlayer* |
PID |
owner; written as handle id (->+4) |
| 0x104..0x117 | 0xfc | Population (0x14) |
dcs |
nested |
| 0x118 | 0x110 | float | dsu |
|
| 0x11c..0x13b | 0x114 | Morale (0x20: vptr + int[7]) |
cm |
nested |
| 0x13c..0x147 | 0x134 | vector<MoraleEvent> |
cme2 |
VectorHelper |
| 0x14c..0x16b | 0x144 | Morale |
PvCM |
|
| 0x16c..0x177 | 0x164 | vector<StarFleet*> |
NumFlts + n×Flt |
ids via handle |
| 0x17c | 0x174 | float | RepCur |
|
| 0x180 | 0x178 | float | RepMax |
|
| 0x184 | 0x17c | int | EggScio |
|
| 0x188 | 0x180 | bool | NoRebAI |
|
| 0x189 | 0x181 | bool | PvNoRebAI |
|
| 0x18c | 0x184 | int | Pop |
imperial pop |
| 0x190 | 0x188 | float | Infra |
|
| 0x194 | 0x18c | int | pbon |
|
| 0x198 | 0x190 | float | ibon |
|
| 0x19c | 0x194 | int | TerrFl |
|
| 0x1a0..0x1b3 | 0x198 | Population |
Pop2 |
civilian pop groups (R1 popG) |
| 0x1b4..0x1c7 | 0x1ac | Population |
pbon2 |
|
| 0x1c8 | 0x1c0 | IndependenceInfo* |
hindi + indi |
hindi = ptr!=NULL |
| 0x1cc..0x1d7 | 0x1c4 | vector<int> |
spies2 |
VectorHelper |
| 0x1dc | 0x1d4 | int | rbfl |
written by value |
| 0x1e0 | 0x1d8 | bool | hsrg |
|
| 0x1e4..0x1ff | 0x1dc | int[7] | nadct,(ads=i,adt=v) |
addiction table: count of non-zero entries then sparse (index,value) pairs |
| 0x200 | 0x1f8 | int | PvPop |
previous-turn snapshot block |
| 0x204 | 0x1fc | float | PvInfra |
|
| 0x208 | 0x200 | float | PvSuit |
|
| 0x20c | 0x204 | int | PvRes |
|
| 0x210 | 0x208 | int | PvARes2 |
|
| 0x214 | 0x20c | int | PvMRes |
|
| 0x218..0x22b | 0x210 | Population |
PvPop2 |
|
| 0x238 | 0x230 | StarFleet* |
DefF |
handle id |
| 0x23c | 0x234 | StarFleet* |
DefSF |
handle id |
| 0x240..0x24b | 0x238 | vector<obj*> |
NumGFs + n×GF |
gates |
| 0x250..0x25b | 0x248 | vector<obj*> |
NumSnF + n×SnF |
stations |
| 0x260..0x26b | 0x258 | vector<obj*> |
NumMnF + n×MnF |
monitors |
| 0x274 / 0x278 | 0x26c / 0x270 | std::map head / size |
NVO + entries |
colonies, see §1.2 |
| 0x284 / 0x288 | 0x27c / 0x280 | std::map head / size |
NVE + entries |
§1.2 |
| 0x294 / 0x298 | 0x28c / 0x290 | std::map head / size |
NVs + entries |
per-player pview, §1.3 |
| 0x2a8..0x2b3 | 0x2a0 | vector<Plague*> |
NumPlgs2 + n×(PlgT=plg->+4, Plg obj) |
|
| 0x2b8 | 0x2b0 | int | TnsOH |
|
| 0x2bc | 0x2b4 | int | TDst |
|
| 0x2c4 | 0x2bc | int | ntdev |
|
| 0x2c8 | 0x2c0 | int | ltis |
int (R2 "short" wrong width) |
| 0x2cc | 0x2c4 | int | rbtn |
|
| 0x2d0 | 0x2c8 | int | rbfr |
by value |
| 0x2d4 | 0x2cc | int | rbwn |
Object size ≥ 0x2d8. On-disk order = R1 §8 exactly (Pos, RGBA, Idx, Size, Suit, Res, ARes2, MRes, NoRebAI, TRes, Pop, Pop2, Infra, PvPop, PvPop2, PvInfra, PvSuit, PvRes, PvARes2, PvMRes, PvNoRebAI, Rts, Abdn, Dstyd, TnsOH, OutMod, RepCur, RepMax, ntdev, pbon, pbon2, ibon, ltis, rbfl, rbtn, rbfr, rbwn, hsrg, halt*, vn*, Name, *Flags, Bats2, rcex, Mn/Rf/ClkFlags, EggScio, TerrFl, TAcq, TFAcq, TDst, dcs, dsu, cm, PvCM, cme2, spies2, PID, DefF, DefSF, BQ, nadct/ads/adt, NumPlgs2…, NumFlts/GFs/SnF/MnF, NVO, NVE, NVs, hindi/indi).
Members that are only ever read with a NULL/scratch target (not stored): ISuit, Income, HPop, Builds
(+Con,Sav,ConLeft,OrID,DesID — old inline build queue), Slvs, dct, cme, Bats, NumPlgs.
1.1 Game::StarSystem::OutputRates (POD, 0x1c) — Write FUN_00745190
Memory order ≠ disk order: +0x00 float SRt, +0x04 SRsc, +0x08 SRtf, +0x0c SRi, +0x10 SRoh,
+0x14 SRs, +0x18 int SRnr. Disk order: SRs, SRt, SRsc, SRtf, SRi, SRoh, SRnr. Reader: if SRs tag is
absent, reads 5 unnamed floats (legacy).
1.2 Colony maps NVO / NVE (std::map keyed by player-table index)
Write emits NVO=size, then per node: PID = handle id of owner->+0x50[key] (player object table),
TShn = int16 at node+0x12 (value+2), OID = int at node+0x14 (value+4) by value, isind bool at
node+0x18 (value+8), indi = inline IndependenceInfo at node+0x1c (value+0xc, 0x70 bytes). Node isnil
byte at +0x8d ⇒ value size 0x7c. NVE nodes: EPid handle (key→player), ETS int16 @ node+0x12, Eid
int @ node+0x14 (isnil @ +0x19 ⇒ value 8 bytes).
OID is a stored int, distinct from PID; the R2 claim "OID = PID×16" is an id-allocation pattern, not
a derivation in this code (open question — check the HandleObject id allocator).
1.3 Game::StarSystem::PlayerView (per-player seen snapshot) — Write FUN_007492d0, Read FUN_00752af0
Stored inline as map value at node+0x10 (NVs; node isnil @ +0xad ⇒ value ≈ 0x9c).
+0 vptr (0x00a201ac), +8 int VTrn, +0xc int Pop, +0x10 Population Pop2 (0x14), +0x24 float Infra,
+0x28 float Suit, +0x2c int Res, +0x30 int ARes2, +0x34 int MRes, +0x38 bool NoRebAI, +0x3c int pbon,
+0x40 Population pbon2, +0x54 float ibon, +0x58 int TerrFl; trailer bool footer=1. Reader also accepts
legacy ARes, PvPop/PvInfra/PvSuit/PvRes/PvARes/PvARes2/PvMRes/PvNoRebAI into the same slots.
(R1 lists Int32 infra — it is a float.)
1.4 Game::Population (0x14) / Game::PopulationGroup (0x18 stride)
Population: +0 vptr, +4/+8/+0xc vector<PopulationGroup>; Write emits PopNG = count of groups with
PopC>0 (or ≥0 with low word ≠0), then each as nested PopG. PopulationGroup: +4 int PopT, +8 int PopS,
+0x10 int64 PopC (R1 popT,popS,popC ✓).
1.5 Game::IndependenceInfo (0x70) — Write FUN_00748ee0
+4 int indsp, +8 PlayerColorID indcl (nested), +0x1c string indnm, +0x38 string indav, +0x54 string indba.
1.6 Game::Morale / Game::MoraleEvent
Morale (0x20): +0 vptr, +4 int[7]; disk: mnsp=n then n×(msp=index, mv=value) (reader tolerates
missing mnsp → 7 fixed entries, skipping index 4). MoraleEvent: +4 mid, +8 mtr, +0xc mn, +0x10 mtp
(ints), +0x14 Morale mfx, +0x34 string mdsc.
1.7 Game::ShipBuildOrder (build-queue entry) — Write FUN_00813800
+4 int desID, +8 int con, +0xc int sav, +0x10 int conleft, +0x14 int ordID; disk order desID, con,
conleft, sav, ordID (R1 ✓).
Where is "Planet"?
Game::Planet : Actor (vft 0x009ef144) is a render/scene actor and is not streamed. The colony/planet
state the save calls "planet" (R2's Idx/Name/Size/Suit/Res/Infra/Pop/OID…) is entirely in ServerSystem
above plus PlayerView. The CreateParameters PlanetSaveStruct (x,y,z + 4 ints) is map-gen input
(StarMapParams), not touched here.
2. Game::ServerPlayer (empire; R1 SimPlayerDetailsSaveStruct)
Serializers: Write FUN_008563e0 @ 0x008563e0 (4040 B), Read FUN_008804d0 @ 0x008804d0 (7647 B).
IStreamable sub-object at +0x3a0 (COL offset 928); this = obj+0x3a0, so decompiled offsets are
negative for most members. Primary vptr @+0 (0x00a327a4, StrategyPlayer shape), NetworkObject id @+4.
| abs off | rel(this+0x3a0) | type | save name | notes |
|---|---|---|---|---|
| 0x28 | -0x378 | int | PlyrIdx |
|
| 0x2c | -0x374 | ServerSystem* |
HomeSys |
handle id |
| 0x30/0x34 | -0x370/-0x36c | vector<ServerPlayer*> |
NumOwn + n×OwnId |
handle ids (R1 ownerIds) |
| 0x40..0x5b | -0x360 | std::string |
PlryName |
|
| 0x5c | -0x344 | int | Species |
0 Human … 6 Morrigi |
| 0x60 | -0x340 | PlayerColorID (4 B) |
ClrID |
nested; §2.1 (legacy Clr int skipped) |
| 0x74..0x8f | -0x32c | std::string |
Bdg |
badge |
| 0x90..0xab | -0x310 | std::string |
Avt |
avatar |
| 0xac | -0x2f4 | int | Team |
|
| 0xb0 | -0x2f0 | float | IdealSuit |
|
| 0xb4 | -0x2ec | float | SuitTol |
|
| 0xb8 | -0x2e8 | float | MaxOH |
|
| 0xbc | -0x2e4 | float | ResRate |
|
| 0xc0 | -0x2e0 | float | ResMod |
|
| 0xc4 | -0x2dc | float | ResScl |
|
| 0xd0 | -0x2d0 | float | TRM |
R1 says Int32 — it is float |
| 0xd4 | -0x2cc | int | TRA |
note memory order TRA before TRP |
| 0xd8 | -0x2c8 | int | TRP |
|
| 0xe4/0xe8 | -0x2bc/-0x2b8 | vector<ShipDesign*> |
NumDes + n×(DesID=d->+0xa4, Des obj) |
current designs |
| 0xf4 | -0x2ac | TechTree* |
TechTree |
first field on disk |
| 0xf8 | -0x2a8 | bool | Elim |
|
| 0xfb | -0x2a5 | bool | NPC |
|
| 0xfc | -0x2a4 | bool | RebAI |
|
| 0xfd | -0x2a3 | bool | ReqCL |
|
| 0xfe | -0x2a2 | bool | AIBn |
|
| 0xff | -0x2a1 | bool | CnTrd |
|
| 0x100 | -0x2a0 | bool | CnRad |
|
| 0x101 | -0x29f | bool | CnVItl |
|
| 0x102 | -0x29e | bool | hgs |
|
| 0x103 | -0x29d | bool | hadvs |
|
| 0x104 | -0x29c | bool | harcc |
|
| 0x108 | -0x298 | float | pddm |
|
| 0x10c..0x117 | -0x294 | float[3] | ConMod ×3 |
interleaved on disk as 3×(ConMod[i], SavMod[i]) |
| 0x118..0x123 | -0x288 | float[3] | SavMod ×3 |
|
| 0x124 | -0x27c | float | OutMod |
|
| 0x128 | -0x278 | float | RebOutMod |
|
| 0x12c | -0x274 | float | ScOutMod |
|
| 0x130 | -0x270 | float | PopMod |
(legacy SensMod float, ExPopSys int skipped between IncMod/PopMod/TerraMod) |
| 0x134 | -0x26c | float | TerraMod |
|
| 0x138 | -0x268 | bool | AMine |
|
| 0x13c | -0x264 | float | MinPure |
|
| 0x140 | -0x260 | float | MinRate |
|
| 0x144 | -0x25c | int | NGts |
|
| 0x148 | -0x258 | int | PrGtTrf |
|
| 0x14c | -0x254 | int | GTraf |
|
| 0x150 | -0x250 | float | CstR |
R1 Int32 → float |
| 0x154 | -0x24c | float | CstE |
|
| 0x158 | -0x248 | float | CstT |
|
| 0x15c | -0x244 | int | Maint |
(legacy NShps skipped) |
| 0x160 | -0x240 | float | shrm |
|
| 0x164 | -0x23c | int | Status |
by value |
| 0x168..0x177 | -0x238 | PlayerAlliances {int ALid, AL, NA, CF} |
Team (2nd) |
nested (R1 teamStruct) |
| 0x178/0x17c | -0x228/-0x224 | vector<ShipDesign*> |
NumLeg + n×(DesID,Des) |
legacy/drone designs (R1 droneDesigns) |
| 0x188 | -0x218 | int | PvSav |
|
| 0x18c | -0x214 | bool | PvMA |
|
| 0x19c | -0x204 | int | HasDisc |
by value |
| 0x1a0 | -0x200 | int | HasDiscSp |
|
| 0x1a4 | -0x1fc | int | HasDiscCl |
|
| 0x1a8 | -0x1f8 | int | HasEnc |
|
| 0x1ac | -0x1f4 | int | HasEng |
|
| 0x1b0 | -0x1f0 | ShipRecords (inline) |
ShipRecs |
Write FUN_008176a0 |
| 0x1f4 | -0x1ac | vector<Objective> |
Ojvs |
VectorHelper |
| 0x204/0x208 | -0x19c/-0x198 | vector<{int xid,xmin,xmax; float xper}> (16 B) |
Nexp + n×(xid,xmin,xmax,xper) |
R1 misses the per-entry body |
| 0x214/0x218 | -0x18c/-0x188 | vector<int> |
NWeapXcl + n×WeapXcl |
|
| 0x230 | -0x170 | vector<DiplomacyStats> |
dipstats |
§2.2 |
| 0x240 | -0x160 | CommMessageContainer* |
comms |
|
| 0x244 | -0x15c | vector<PlayerReport> |
preps |
§2.3 |
| 0x254 | -0x14c | vector<ObservedDesign> |
odes |
|
| 0x264 | -0x13c | vector<ObservedWeapon> |
owep |
|
| 0x274 | -0x12c | vector<ObservedTech> |
otch |
|
| 0x284 | -0x11c | int | Sav |
savings |
| 0x288 | -0x118 | int | HasImm |
by value |
| 0x28c | -0x114 | int | HasVac |
|
| 0x290 | -0x110 | int | NPTrk |
|
| 0x294 | -0x10c | Tech* (current research) |
ResTNm |
writes tech ? tech->name(+4) : "" |
| 0x298 | -0x108 | FleetNameGenerator* |
FNG |
|
| 0x29c | -0x104 | EventStorage (inline) |
Events |
{EvNxID@+0x14, Events vector@+4} |
| 0x2b8/0x2bc | -0xe8/-0xe4 | std::list<PlayerNotes> head/size |
NumNotes + n×Nts |
node value at +0x10: NtSys@+4,NtTxt str@+8,NtTrn@+0x24 |
| 0x2c4 | -0xdc | int | BnkWrn |
by value |
| 0x2c8 | -0xd8 | int | BnkTrn |
|
| 0x2cc | -0xd4 | int | BnkEl |
|
| 0x2d0 | -0xd0 | int | BnkPr |
|
| 0x2d8 | -0xc8 | int | plcy |
by value |
| 0x2dc..0x2f7 | -0xc4 | std::string |
pswd |
R1 says Int32 — it is a string |
| 0x2f8 | -0xa8 | bool | Srn |
|
| 0x2fc | -0xa4 | obj* |
SrnTo |
handle id (R1 srcTo) |
| 0x300 | -0xa0 | int | lboid |
|
| 0x304 | -0x9c | int | lcid2 |
by value (legacy lcid) |
| 0x30c | -0x94 | float | IncMod |
|
| 0x310 | -0x90 | vector<PlayerAid> |
aid |
|
| 0x320/0x324 | -0x80/-0x7c | vector<DefenceLayout*> |
ndeflay + n×deflay |
|
| 0x330 | -0x70 | bool | cdp |
|
| 0x334 | -0x6c | SpyReport* |
spy2 |
§2.4 |
| 0x338/0x33c | -0x68/-0x64 | vector<RaidTargets> (0x20 stride) |
rdtc + n×rdt |
|
| 0x368 | -0x38 | int | aidf |
by value |
| 0x370 | -0x30 | CivilianRatios (inline) |
civr |
Write FUN_0082c740 |
| 0x39c | -4 | int | tnc |
written as max(v,1) |
| 0x3a0 | 0 | vptr | — | IStreamable sub-vftable 0x00a32794 |
| 0x3a4/0x3a8 | +4/+8 | vector<{float PRm; int PRBt}> |
NumPR + n×(PRm,PRBt) |
|
| 0x3b4 | +0x14 | bool | ResErrRoll |
|
| 0x3b5 | +0x15 | bool | cta |
|
| 0x3b8 | +0x18 | AIRebellion* |
HasAIR + AIR |
gate = ptr!=NULL |
| 0x3bc | +0x1c | AIEncounterFlags* |
AIEnf |
|
| 0x3c0/0x3c4 | +0x20/+0x24 | vector<SpecialProjectImpl*> |
NSprj + n×(SprjT=p->+0x3c, Sprj) |
|
| 0x3d0 | +0x30 | int | NextPrjID |
|
| 0x3d4 | +0x34 | int | lret |
|
| 0x3dc | +0x3c | int | nmeid |
Object size ≥ 0x3e0. Disk order = R1 §6 (TechTree, HomeSys, PlyrIdx, PlryName, Species, ClrID, Bdg, Avt,
Team, Sav, IdealSuit, SuitTol, MaxOH, ResRate, ResMod, ResScl, TRM, TRP, TRA, OutMod, RebOutMod, ScOutMod,
IncMod, PopMod, TerraMod, AMine, MinPure, MinRate, NGts, PrGtTrf, GTraf, CstR/E/T, Maint, shrm, Status, Elim,
NPC, RebAI, ReqCL, Team{ALid,AL,NA,CF}, HasVac, HasImm, NPTrk, HasDisc, HasDiscSp, HasDiscCl, HasEnc, HasEng,
Events, FNG, PvSav, PvMA, AIBn, CnTrd, CnRad, hgs, hadvs, harcc, CnVItl, pddm, BnkWrn/Trn/Pr/El, ShipRecs,
NextPrjID, plcy, pswd, lret, nmeid, cdp, spy2, civr, aidf, Srn, SrnTo, lboid, lcid2, ResTNm, ResErrRoll,
3×(ConMod,SavMod), NumOwn/OwnId, NumDes/DesID/Des, NumLeg/DesID/Des, NumNotes/Nts, NumPR/PRm/PRBt, HasAIR/AIR,
cta, AIEnf, NSprj/SprjT/Sprj, Nexp/xid/xmin/xmax/xper, NWeapXcl/WeapXcl, Ojvs, dipstats, comms, preps, odes,
owep, otch, aid, ndeflay/deflay, rdtc/rdt, tnc). Note: on-disk Sav comes right after Team though it
lives at 0x284 in memory.
2.1 Game::PlayerColorID (4 bytes) — Write FUN_0053c080
+0 int8 index; +1,+2,+3 uint8 r,g,b; writer emits index via FUN_008b9cb0 (char→int on disk), and
iff index == -1 the three r,g,b bytes (matches R1 §1.6). Same struct used by IndependenceInfo.indcl.
2.2 Game::DiplomacyStats (0x24) — Write FUN_00818cb0
+4 int other; then int16 fields at +8 lastnap, +0xa lastnapbty, +0xc bknnap, +0xe btynap,
+0x10 lastally, +0x12 lastallybty, +0x14 bknally, +0x16 btyally, +0x18 lastcf, +0x1a lastcfbty,
+0x1c bkncf, +0x1e btycf, +0x20 deadhome (all widened to int32 on disk; R1 nap/ally/cf{last_,last_bty,bkn_,bty_} ✓).
2.3 Game::PlayerReport (preps, 0x30) — Write FUN_00817480
ints +4 oid, +8 pid, +0xc flds, +0x10 sav, +0x14 home, +0x18 ncol, +0x1c mpwr, +0x20 mcls, +0x24 mmsl, +0x28 nshp, +0x2c nsat (R1 ✓).
2.4 Game::SpyReport — Write FUN_00828ec0
Four std::lists: +4 list<SpyReportDefences> (count defc2@+8, items def), +0x10 list<SpyReportTrade>
(rtc@+0x14, strd), +0x1c list<SpyReportEvents> (evc@+0x20, evs), +0x28 list<SpyReportTechTree>
(ttc@+0x2c, tt). R1 only kept the four counts.
2.5 Game::TechTree — Write FUN_005890a0 (tags NumTechs, TNm, NumBrs; per-tech body in a
sub-writer not decompiled here; logs "TechTree: Tech %d not found saving tech tree").
2.6 Game::ObservedTech (0x2c) — Write 0x00817cf0, Read 0x00817c40
Elements of ServerPlayer::otch, the vector<ObservedTech> at ServerPlayer+0x274. Polymorphic:
vftable 0x00a2439c, RTTI .?AVObservedTech@Game@@, slots {[0] 0x00793610 dtor, [1] Read, [2] Write}.
| off | type | save name | notes |
|---|---|---|---|
| 0x00 | vptr | — | 0x00a2439c |
| 0x04 | uint16 |
otnF |
turn first observed; widened to int32 on disk |
| 0x06 | uint16 |
otnL |
turn last observed; widened to int32 on disk |
| 0x08 | bool |
odet |
one byte (+3 pad); WriteBool/ReadBool |
| 0x0c..0x27 | std::string |
otch |
tech name; _Mysize@0x1c, _Myres@0x20, _Alval@0x24 |
| 0x28 | int |
owith |
last member; 0x28 + 4 = 0x2c = sizeof, no slack |
sizeof = 0x2c (44), pinned three ways by lane X (magic divide 0x2e8ba2e9 sar 3 at 0x0087239f,
imul reg,reg,0x2c, search stride add edi,0x2c); the member map is the serializer's own order.
Game::ObservedWeapon (Write 0x00817bc0, Read 0x00817b10) is the identical element with tag owep
in place of otch; odes elements are the smaller otnF otnL odid opid record.
Appended by RecordObservedTech 0x007ba1a0, which de-duplicates by tech name — a reimplementation
that just push_backs will diverge on re-observation. It writes otnF and otnL from the same source
word on first sighting, so first-seen == last-seen initially.
Do not read +0x24 as a field. It is the name string's trailing _Alval. See §0 and
findings/subsystems/observedtech-append.md §9 — this is the class that produced the 0x18-vs-0x1c
std::string scare, and the resolution is that 0x1c was right everywhere.
3. Game::StarFleet (R1 SimFleetDetails) — Write FUN_00701070, Read FUN_00702470; this = obj+8
| abs | rel | type | save name | notes |
|---|---|---|---|---|
| 0x18 | 0x10 | Vector3 | Pos |
via StarMapNode |
| 0x4c | 0x44 | Vector3 | PrvPos |
|
| 0x58 | 0x50 | ServerPlayer* |
PID |
handle |
| 0x5c..0x77 | 0x54 | string | FtName |
|
| 0x78 | 0x70 | bool | Perm |
|
| 0x7c | 0x74 | FleetLayout (inline, ~0x24) |
HLay + Lay |
gate = either of its two vectors (+4/+8, +0x14/+0x18) non-empty |
| 0xa0 | 0x98 | StarSystem* |
LocID |
handle (legacy SysID,TrdID ints skipped) |
| 0xa4/0xa8 | 0x9c/0xa0 | vector<StarShip*> |
NShips + n×(ShipID handle, Ship obj) |
|
| 0xc4..0xfb | 0xbc | FlightPlan (inline, 0x38) |
HFPlan + FPlan |
gate = wpts non-empty; §3.1 |
| 0xfc | 0xf4 | int | FtTrans |
by value (legacy Caps int, GtTrf short, FtSens float, FtInc int skipped) |
| 0x100 | 0xf8 | Vector3 | FtOrig |
(R1 "3 ints" → 3 floats) |
| 0x10c | 0x104 | int | FtFlg |
|
| 0x110 | 0x108 | int | Ftae |
|
| 0x114 | 0x10c | int | Ftpae |
|
| 0x118 | 0x110 | int | FtEnc |
|
| 0x11c | 0x114 | int | FtMS |
3.1 Game::FlightPlan (0x38) / Waypoint / NodeRoute
FlightPlan: +0 vptr, +4 vector<Waypoint> wpts, +0x14 float FPsp2, +0x18 int FPeta2, +0x1c Vector3 FPogn2,
+0x28 Vector3 FPdpos, +0x34 int pnd. Waypoint: +4 int Wpt, +8 int Tp, +0xc NodeRoute nrt.
NodeRoute: +4 nrp, +8 nrf, +0xc nrt (ints). Reader also handles legacy NumWpt/path/FPognid.
4. Game::StarShip (R1 SimFleetShipDetails) — Write FUN_008291f0, Read FUN_00853fa0; this = obj+8
| abs | rel | type | save name | notes |
|---|---|---|---|---|
| 0x10 | 0x8 | ServerPlayer* |
PlrID |
handle |
| 0x14 | 0xc | ShipDesign* |
DesID |
writes design->+0xa4 (design id) |
| 0x20 | 0x18 | float | Range |
|
| 0x24..0x33 | 0x1c | ShipHealth {vptr; float[3]} |
Health |
3 unnamed floats (R1 guess command/mission/drive) |
| 0x34 | 0x2c | int | MineCap |
|
| 0x38/0x3c | 0x30/0x34 | vector<{float th, thm}> |
NTH + n×(TH,THM) |
|
| 0x48 | 0x40 | int | Plg |
|
| 0x4c | 0x44 | int | Act |
|
| 0x50 | 0x48 | bool | Dep |
|
| 0x51 | 0x49 | bool | Atq |
|
| 0x5c | 0x54 | int | LCT |
|
| 0x60 | 0x58 | int | tsd |
|
| 0x64 | 0x5c | StarFleet* |
FltID |
handle |
| 0x68 | 0x60 | int | ConCap |
|
| 0x6c | 0x64 | float | RefCap |
R1 Int32 → float |
| 0x70 | 0x68 | float | RepCap |
R1 Int32 → float |
| 0x7c | 0x74 | int | EncID |
|
| 0x80 | 0x78 | PrisonerHold (inline) |
PrisH |
+0x14 int* → [0]=PrMax, [2..8] per-species counts; disk PrMax,PrNSp,(PrSp=idx,PrNum) |
| 0x98 | 0x90 | BuildQueue* |
hbq + BQ2 |
gate = ptr!=NULL |
| 0x9c | 0x94 | Population* |
hsp + pop |
gate = ptr!=NULL |
| 0xa0 | 0x98 | Population* |
ppop |
|
| 0xa8 | 0xa0 | int | atsp |
by value |
| 0xac | 0xa4 | int | tblt |
5. Top-level sim block — Game::StrategyServer Write FUN_0079fa70 / Read FUN_007d27a0
Tag order confirms R1 §5: KeyPath, NMSz, NMLc, NMnx, ModCount, Frame, GameID, [AIDifficultyID legacy], Attrib, RNG, GameName, Map, IncMod, ResMod, EnAl, EnTm, GOTurn, GOWinPly, NPCm/o/i/v/a, szadj, rsadj, suadj, sprjs, RandEncAdj, cmbtid, turnstats, numcreps/crep, ninv/invs/inve/invt/invtb, AllExc×6, AllExcCF, AllExcCFp×2, NumPlrs/PlayerID/Player, ISsp, ISsu, NumSys/SysID/Sys, NdGr2, trdmgr, spymgr, NumFlts/FltID/Flt, NumActs/Act, SvSctOb, zdsc, zdsi, zdst. (Offsets not tabulated — out of scope this round.)
6. Corrections to the community reference (R1/R2)
- Types:
TRM,CstR/E/T,shrm, shipRefCap/RepCap, PlayerViewInfraare floats;pswdis a string;PvMA,AIBnare bools;FtOrigis a Vector3 (3 floats). - Widths:
Bats2/rcexare true int64 (R2 int is wrong);Abdn/Dstydare bools,ltisan int (R2 "short" is an artefact of reading value bytes);TShn,ETS, all DiplomacyStats counters are int16 in memory but int32 on disk. - Missing in R1:
Nexpentries carry (xid,xmin,xmax,xper); SpyReport lists have bodies; Morale is a sparse (msp,mv) table;nadctis followed by sparse (ads,adt) pairs over a 7-int table. Plg/Act/EncID/OID/flags are written by value → plain ints (not handles).
7. Confidence & open questions
- High: all offsets/types in §1–§4 (direct from Write functions; Read functions agree on every member address; COL offsets from RTTI; no adjustor thunks).
- Medium: nested struct sizes inferred from neighbouring offsets (Population 0x14, Morale 0x20,
FlightPlan 0x38, PlayerView ≈0x9c, IndependenceInfo 0x70); which members belong to
StarSystemvsServerSystem(serializer isServerSystem's;ClientSystemshares the StarSystem vftables and only streamsPos). - Open:
FUN_008b9cb0exact on-disk width forPlayerColorID(R1 says int32 — plausible);ServerSystem+0x10owner type (StrategyServer? its+0x50is a player-object table);OIDallocation (R2's ×16); per-tech body ofTechTree::Write;CdPlayerblock not attempted (names are R1'sunknownN, nothing to xref). No types were written back into Ghidra (notes only).