diff --git a/campaign/board.md b/campaign/board.md index 09eb89f..71defc4 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -9,11 +9,11 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | `.gob` format | subsystem | mapped | high | 100% | 2026-09-07 | renamed uncompressed ZIP (community-known) | | `Mars::AppStartup` (entry) | control-flow | backlog | — | 0% | 2026-09-07 | bootstrap → main loop | | main loop / tick dispatch | control-flow | backlog | — | 0% | 2026-09-07 | turn SM known from names: strategic round -> SETurnEndPending -> SEProcessTurn -> querying round -> combat round -> SNMAllCombatDone. Ghidra: StrategyMapScreen ctor, ScreenBar::ToggleList, CombatScreen load | -| `Game::ClientPlayer` / `AIPlayer` | object | in-progress | low | 30% | 2026-09-07 | R2: struct recovery via save-name xrefs (SimPlayerDetails); vft ClientPlayer@0x00a32b68 : StrategyPlayer, NetworkObject | -| `Game::StarSystem` | object | in-progress | low | 30% | 2026-09-07 | R2: struct recovery via save-name xrefs; vft@0x00a200e4 : StarMapNode | -| `Game::Planet` / `DOPlanet` | object | in-progress | low | 30% | 2026-09-07 | R2: struct recovery via save-name xrefs; vft@0x009ef144 (36) : Actor chain | +| `Game::ClientPlayer` / `AIPlayer` | object | mapped | high | 80% | 2026-09-07 | empire STATE = Game::ServerPlayer (~110 members, abs 0x28..0x3dc; IStreamable +0x3a0; read FUN_008804d0 / write FUN_008563e0). ClientPlayer = client view. findings/objects/struct-recovery.md. Verify vs real save pending | +| `Game::StarSystem` | object | mapped | high | 80% | 2026-09-07 | Game::ServerSystem = star system + colony (~90 members, abs 0x18..0x2d4; read FUN_0075d4b0 / write FUN_00749630). PlayerView per-player fog view. Verify vs real save pending | +| `Game::Planet` / `DOPlanet` | object | mapped | high | 100% | 2026-09-07 | Planet : Actor is a RENDER actor, not streamed; all colony state is in ServerSystem (+PlayerView in NVs map) | | `Game::ShipDesignDef` / `DesignList` | object | in-progress | low | 30% | 2026-09-07 | vft@0x009fef64 (3), leaf (0 bases); mostly data-defined via .shipsection. fields TBD | -| `Game::FleetLayout` | object | backlog | — | 0% | 2026-09-07 | fleet composition | +| `Game::FleetLayout` | object | mapped | high | 80% | 2026-09-07 | StarFleet (read FUN_00702470/write FUN_00701070) + StarShip (FUN_00853fa0/FUN_008291f0) layouts recovered incl. FlightPlan/Waypoint/NodeRoute; FleetLayout = UI. Verify vs real save pending | | Mars entity chain | object | mapped | high | 100% | 2026-09-07 | EntityBase@0x00a36fd0 (10) -> PhysicalEntity@0x00a3710c (12) -> TargetableEntity -> Actor@0x009e2114 (36); mixins RigidBody/Body/NetworkObject/HandleObject/IScriptable | | `Game::GameCombatSim` + `CombatCommand*` | subsystem | backlog | — | 0% | 2026-09-07 | tactical combat (~40 command types) | | D3D9 device init | subsystem | backlog | — | 0% | 2026-09-07 | `Mars::DrawDevice`/`Camera` → Direct3DCreate9/CreateDevice | @@ -27,3 +27,8 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | networking (SNM/FNM + GameSpy) | subsystem | backlog | — | 0% | 2026-09-07 | ~60 SNM strategy msgs, FNM file-xfer/host-migration; lockstep sim | | battle-load: thread contention | subsystem | backlog | — | 0% | 2026-09-07 | root cause hunt: CreateThread sites FUN_00902350, FUN_00736e30, FUN_008a0e50; streaming-sound thread; lockstep sync waits | | UI screen & flow map | meta | mapped | high | 100% | 2026-09-07 | findings/subsystems/ui-screen-map.md - 36 screens; screens are C++ on Mars controls (NOT data); turn state machine recovered | +| `Game::StrategyServer` (sim block) | object | mapped | high | 80% | 2026-09-07 | top-level sim state; read FUN_007d27a0 / write FUN_0079fa70; IStreamable +0 | +| stream primitive API | subsystem | mapped | high | 100% | 2026-09-07 | IStreamable vft: +0x18 string, +0x1c bool, +0x20 float, +0x24 int, +0x28 nested, +0x30 raw; FUN_00816490 = NetworkObject handle id | +| real save for verification | verify | backlog | — | 0% | 2026-09-07 | need the game running (DXVK+lavapipe on VM140) -> autosave -> parse with recovered layouts | +| save_reader.py | verify | backlog | — | 0% | 2026-09-07 | Streamable reader (gzip, name-tagged, BEEFBEEF framing) from reference + recovered layouts | +| Ghidra type write-back | meta | backlog | — | 0% | 2026-09-07 | create struct datatypes + rename serializers in the sots project so ReVa sessions inherit the map | diff --git a/campaign/open-questions.md b/campaign/open-questions.md index 22fed1f..5ef434e 100644 --- a/campaign/open-questions.md +++ b/campaign/open-questions.md @@ -18,3 +18,5 @@ Each links to the finding that raised it. Promoted to backlog or closed by **re- - **Missing HUD scripts** — exe references `GUI/Combat/CombatHUD.script`, `SensorHUD.script`, `NoHUD.script` but none ship in the gobs or loose; likely dev-only overrides via the gobio native-FS fallback. Confirm via `CombatScreen` load path. Also: `.script` files are display configs, not widget layouts (corrects round-one note). (from [[ui-screen-map]]) - **Tech `allows` default per-race %** — `tech_tree.json` edges only carry races written in the `allows` string; the default for an unlisted race (believed 100%) is engine code. Ghidra target in the tech loader. (from [[data-parsers]]) - **Engine parser leniency** — 12 shipped shipsections are syntactically broken (unclosed `{`, extra `}`) yet load; keys and identifiers are case-insensitive. Reimplementation must match this leniency. `.effect` is its own `TXT`/`BEGIN-END` format, not brace-block (corrects round one). (from [[data-parsers]]) +- **Struct recovery leftovers** — `PlayerColorID` exact on-disk width (writer `FUN_008b9cb0` undecompiled); `ServerSystem+0x10` owner type; `TechTree` per-tech body; `CdPlayer` block. R2's `OID = PID*16` is an id-allocation pattern, not in this code. (from [[struct-recovery]]) +- **Resolved (R1/R2 contradictions)** — `Bats2`/`rcex` are int64 (R2 wrong); `Abdn`/`Dstyd` bools, `ltis` int; `TRM`/`CstR/E/T`/`shrm`/`RefCap`/`RepCap`/PlayerView `Infra` are floats; `pswd` string; `TShn`/`ETS`/diplomacy counters int16 in memory, int32 on disk; `Nexp` carries `xid/xmin/xmax/xper`; `FtOrig` is Vector3. (from [[struct-recovery]]) diff --git a/findings/objects/struct-recovery.md b/findings/objects/struct-recovery.md new file mode 100644 index 0000000..f7d5cb9 --- /dev/null +++ b/findings/objects/struct-recovery.md @@ -0,0 +1,461 @@ +# 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).