# 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` / `Mars::VectorHelper` 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{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 (MSVC10 `_Bx` union@0, size@0x14, res@0x18; `FUN_008b9d70` does the `res>=16 ? heap : sso` check); `std::vector` = {begin@0, end@4, cap@8}; `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` | `cme2` | VectorHelper | | 0x14c..0x16b | 0x144 | `Morale` | `PvCM` | | | 0x16c..0x177 | 0x164 | `vector` | `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` | `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` | `NumGFs` + n×`GF` | gates | | 0x250..0x25b | 0x248 | `vector` | `NumSnF` + n×`SnF` | stations | | 0x260..0x26b | 0x258 | `vector` | `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` | `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`; 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` | `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` | `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` | `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` | `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` | `NWeapXcl` + n×`WeapXcl` | | | 0x230 | -0x170 | `vector` | `dipstats` | §2.2 | | 0x240 | -0x160 | `CommMessageContainer*` | `comms` | | | 0x244 | -0x15c | `vector` | `preps` | §2.3 | | 0x254 | -0x14c | `vector` | `odes` | | | 0x264 | -0x13c | `vector` | `owep` | | | 0x274 | -0x12c | `vector` | `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` 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` | `aid` | | | 0x320/0x324 | -0x80/-0x7c | `vector` | `ndeflay` + n×`deflay` | | | 0x330 | -0x70 | bool | `cdp` | | | 0x334 | -0x6c | `SpyReport*` | `spy2` | §2.4 | | 0x338/0x33c | -0x68/-0x64 | `vector` (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` | `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::list`s: `+4 list` (count `defc2`@+8, items `def`), `+0x10 list` (`rtc`@+0x14, `strd`), `+0x1c list` (`evc`@+0x20, `evs`), `+0x28 list` (`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"). --- ## 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` | `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 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`, ship `RefCap/RepCap`, PlayerView `Infra` are **floats**; `pswd` is a **string**; `PvMA`, `AIBn` are bools; `FtOrig` is a Vector3 (3 floats). - Widths: `Bats2`/`rcex` are true int64 (R2 int is wrong); `Abdn`/`Dstyd` are bools, `ltis` an 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: `Nexp` entries carry (`xid`,`xmin`,`xmax`,`xper`); SpyReport lists have bodies; Morale is a sparse (msp,mv) table; `nadct` is 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 `StarSystem` vs `ServerSystem` (serializer is `ServerSystem`'s; `ClientSystem` shares the StarSystem vftables and only streams `Pos`). - **Open**: `FUN_008b9cb0` exact on-disk width for `PlayerColorID` (R1 says int32 — plausible); `ServerSystem+0x10` owner type (StrategyServer? its `+0x50` is a player-object table); `OID` allocation (R2's ×16); per-tech body of `TechTree::Write`; `CdPlayer` block not attempted (names are R1's `unknownN`, nothing to xref). No types were written back into Ghidra (notes only).