recon: RTTI hierarchy + vftables, affinity pin has no topology branch, save-struct reference

This commit is contained in:
alex 2026-09-07 15:01:50 -04:00
parent 994b182f7a
commit 3bdc83fe29
4 changed files with 771 additions and 8 deletions

View file

@ -9,19 +9,20 @@ 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 | how subsystems are pumped per frame/turn |
| `Game::ClientPlayer` / `AIPlayer` | object | backlog | — | 0% | 2026-09-07 | the empire object (SOTS calls it "player"); root of game state |
| `Game::StarSystem` | object | backlog | — | 0% | 2026-09-07 | + `PlayerView::StarSystem`; cross-ref save structs |
| `Game::Planet` / `DOPlanet` | object | backlog | — | 0% | 2026-09-07 | colony/economy data |
| `Game::ShipDesignDef` / `DesignList` | object | backlog | — | 0% | 2026-09-07 | ship design model |
| `Game::ClientPlayer` / `AIPlayer` | object | in-progress | low | 30% | 2026-09-07 | vft ClientPlayer@0x00a32b68 (8) : StrategyPlayer, NetworkObject; AIPlayer@0x00a199c4 (11) : AIObject, IStreamable. fields TBD |
| `Game::StarSystem` | object | in-progress | low | 30% | 2026-09-07 | vft@0x00a200e4 (8) : StarMapNode + NetworkObject/HandleObject/IStreamable. fields TBD |
| `Game::Planet` / `DOPlanet` | object | in-progress | low | 30% | 2026-09-07 | vft@0x009ef144 (36) : Actor->TargetableEntity->PhysicalEntity->EntityBase; DOPlanet@0x00a28cfc (7). fields TBD |
| `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 |
| Mars entity chain | object | backlog | — | 0% | 2026-09-07 | EntityBase→PhysicalEntity→TargetableEntity→Actor |
| 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 |
| `Mars::Buffer::gobio` | subsystem | in-progress | — | 0% | 2026-09-07 | .gob I/O; anchors the verify/ parser |
| serializable-types vs save structs | verify | in-progress | — | 0% | 2026-09-07 | cross-ref `serializable-types.txt` with community save editors |
| battle-load path | subsystem | in-progress | — | 0% | 2026-09-07 | SMOKING GUN: ForceSingleCore -> SetProcessAffinityMask; Ghidra agent pinning the fn |
| class hierarchy + key vftables | meta | in-progress | — | — | 2026-09-07 | Ghidra recon: inheritance + vftable sizes for core classes |
| serializable-types vs save structs | verify | mapped | high | 100% | 2026-09-07 | findings/objects/save-editor-structs.md (~130 structs, exact order); verifier: cross-check vs binary next |
| battle-load path | subsystem | mapped | med | 60% | 2026-09-07 | mitigation = blunt affinity pin FUN_0089ee70 (SetProcessAffinityMask); NO topology detection -> no adaptive branch; cause = emergent multi-core contention |
| class hierarchy + key vftables | meta | mapped | high | 100% | 2026-09-07 | findings/objects/ghidra-recon.md - RTTI Base_Class_Array read directly; vftables for 8 core classes |
| data-model (.gob data files) | subsystem | mapped | high | 100% | 2026-09-07 | findings/subsystems/data-model.md - tech/weapons/sections/races/AI all data-driven |
| string / config intel | meta | mapped | high | 100% | 2026-09-07 | findings/subsystems/strings-and-config.md |
| Mars brace-block parser | subsystem | backlog | — | 0% | 2026-09-07 | one recursive KV parser; reimpl lever + feeds verify/parsers |
| 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 |

View file

@ -12,3 +12,6 @@ Each links to the finding that raised it. Promoted to backlog or closed by **re-
state coverage may be partial. Treat as strong hints, verify each field against real saves.
- **Lockstep determinism** — `@SYNC ERROR DETECTED: Frame %d` indicates a deterministic lockstep sim. Big for reimplementation + old-vs-new differential (determinism makes equivalence checkable frame-by-frame). Confirm scope. (from [[strings-and-config]])
- **Mods override mechanism** — `.gob` = `gobio::ZipFileSystem` with a `NativeFileSystem` loose-file override; this is how `./Mods` wins over archived files. Verify load order. (from [[strings-and-config]])
- **Battle-load root cause** — the shipped fix is a blunt process-affinity pin (`FUN_0089ee70`) with NO CPU-topology detection, so there is no adaptive branch. The real cause is emergent multi-core contention: which thread(s)? Investigate the 3 `CreateThread` sites (`FUN_00902350`, `FUN_00736e30`, startup `FUN_008a0e50`), the streaming-sound thread, and lockstep sync waits. (from [[ghidra-recon]])
- **Save-struct contradictions (R1 vs R2)** — field widths (`Abdn`/`Dstyd`/`ltis` Int16 vs Int32; `Bats2`), R2's `OID = PID*16` owner-handle claim, species id 4 (`_NPC` vs 'AI Rebellion'). Resolve against the binary's Streamable read code. (from [[save-editor-structs]])
- **Unlabeled save blocks** — `CdPlayer` (unknown1..35), empty `SimSystemDetailSpy`, opaque ~2500 B RNG blob. Analyst targets once the Streamable readers are located. (from [[save-editor-structs]])

View file

@ -0,0 +1,209 @@
# Ghidra Recon — Sword of the Stars (2006), "Sword of the Stars.exe"
ImageBase 0x00400000, 32-bit MSVC-2010 DX9. Program `sots` in `/srv/re-lab/ghidra-projects`.
All addresses below are Ghidra/virtual addresses (VA = ImageBase + RVA).
Method: headless GhidraScripts (ReVa MCP was down this run). reva-server stopped for runs, restarted after.
---
## 1. Battle-load / many-core lead: ForceSingleCore
### Key strings
| String | Address |
|---|---|
| `ForceSingleCore` | `0x00a36eec` (key literal), config-tag blob at `0x00a36ee8` |
| `Limiting process affinity to CPU-%i...` | `0x00a36880` |
| `SetProcessAffinityMask` (data ref name) | `0x00ad8202` |
### The affinity worker — `FUN_0089ee70` @ `0x0089ee70`
```c
void FUN_0089ee70(void) { // core index arrives in ESI from caller
int unaff_ESI;
FUN_008ba1e0("Limiting process affinity to CPU-%i...", unaff_ESI + 1);
HANDLE hProcess = GetCurrentProcess();
BOOL ok = SetProcessAffinityMask(hProcess, 1 << ((byte)unaff_ESI & 0x1f)); // pin to ONE core
if (ok) FUN_008ba1e0(" OK.\n");
else FUN_008ba1e0(" FAILED!\n");
}
```
This is the whole mitigation: it pins the entire process to a single logical core via a
one-bit affinity mask. `SetProcessAffinityMask` is the ONLY affinity/topology API the
binary uses (see below).
### Where the config is read & branches — `FUN_008a0e50` @ `0x008a0e50`
This is the application startup/init routine (also creates the window, D3D device, reads
display.cfg/audio.cfg, spawns a thread). Relevant excerpt:
```c
// ref site 0x008a0ee6
uVar3 = FUN_00898bc0(&DAT_00a36ee8, "ForceSingleCore", local_64c, local_630); // read config key
FUN_008e7160(uVar3);
...
iVar4 = FUN_008e7140(local_64c); // convert the read value -> int
if (0 < iVar4) { // <-- the many-core branch: any value > 0
FUN_0089ee70(); // pin process to a single core
}
```
`FUN_00898bc0` = generic config/ini lookup (key -> value string); `FUN_008e7140`
extracts an integer from the parsed value. If the integer is > 0 the process is bound
to one core (the core index is the value carried in ESI into `FUN_0089ee70`, and the
`"CPU-%i"` log uses index+1).
### Read on the many-core slowdown
The binary performs **no runtime CPU-topology detection at all** — there is no
`GetSystemInfo`, `GetNativeSystemInfo`, `GetLogicalProcessorInformation(Ex)`,
`GetActiveProcessorCount`, `GetProcessAffinityMask`, or `SetThreadAffinityMask` import
anywhere. So there is **no adaptive "if many cores do X else Y" branch** in the code.
`ForceSingleCore` is a blunt, opt-in override read once at startup that forces the whole
process onto one core.
Implication for the battle-load bug: the slowdown is not gated by a core-count code path
we can flip; it is emergent multi-core behavior (thread contention / lock convoy / false
sharing / scheduler thrash, or the D3D9 driver's own worker threads) that gets worse as
the OS spreads the process's threads across more cores. Pinning to one core removes the
cross-core traffic and masks it. Next place to dig for the actual contention is the
thread creation sites:
### Threading entry points (CreateThread callers) — follow-ups
| Caller | Address | Note |
|---|---|---|
| `FUN_00902350` | `0x00902350` | creates a thread at `0x0090242d` |
| `FUN_008a0e50` | `0x008a0e50` | startup routine, creates a thread at `0x008a14ef` |
| `FUN_00736e30` | `0x00736e30` | creates a thread at `0x00736e84` — likely a game/loader worker; prime suspect for the battle-load path |
`SetProcessAffinityMask` external thunk: `EXTERNAL:00000052`; `CreateThread`: `EXTERNAL:00000019`.
---
## 2. vftable dumps (primary vftable per class)
MSVC layout note: each class has a `..::vftable_meta_ptr` label (0 methods — that slot
holds the RTTI Complete-Object-Locator pointer, sitting one word *before* the real table)
immediately followed by `..::vftable`, the actual function-pointer array. Classes with
multiple `vftable` groups use multiple inheritance (one sub-table per polymorphic base).
Only the *primary* (largest / class-named) table is listed here; secondary tables are
small MI thunks (`IStreamable` etc.). Repeated addresses = shared/thunk stubs:
`0x0080c5a0` and `0x005f8ac0` are stub/return-nullptr style thunks reused across many
slots; `0x00496e00` a shared pure/empty stub; `0x0085b290` a common getter.
### Game::ClientPlayer — vftable @ `0x00a32b68`, 8 methods
```
[0]0x00882630 [1]0x008f6630 [2]0x008188f0 [3]0x00818910
[4]0x0085b290 [5]0x008801d0 [6]0x0080c5a0 [7]0x0080c5a0
```
### Game::AIPlayer — vftable @ `0x00a199c4`, 11 methods
```
[0]0x004f7230 [1]0x005f8ac0 [2]0x005f8ac0 [3]0x00723ed0 [4]0x00692350 [5]0x00496e00
[6]0x00496e00 [7]0x00496e00 [8]0x00496e00 [9]0x00496e00 [10]0x006922e0
```
(secondary MI tables: VectorHelper<AIPlayerRequestStamp> @0x00a1a700, StreamableHelper @0x00a1a730, each 3 methods.)
### Game::StarSystem — primary vftable @ `0x00a200e4`, 8 methods
```
[0]0x00753160 [1]0x00725130 [2]0x00751b30 [3]0x00751b40
[4]0x00751b40 [5]0x005f8ac0 [6]0x0085b290 [7]0x004f41d0
```
(also a 3-method table @0x00a200d4; StreamableHelper<StarSystem> @0x00a23cc4.)
### Game::Planet — primary vftable @ `0x009ef144`, 36 methods
```
[0]0x008a1860 [1]0x004e8960 [2]0x004e72b0 [3]0x0080c5a0 [4]0x004d75a0 [5]0x004d75d0
[6]0x004e16e0 [7]0x004d9cb0 [8]0x004e5e80 [9]0x0080c5a0 [10]0x005f8ac0 [11]0x00423710
[12]0x004229a0 [13]0x004f59c0 [14]0x005f8ac0 [15]0x004ddbf0 [16]0x0085b290 [17]0x00425770
[18]0x00425340 [19]0x00424b70 [20]0x004267f0 [21]0x00426810 [22]0x00426830 [23]0x00422bd0
[24]0x0080d1f0 [25]0x004242b0 [26]0x00496e00 [27]0x00496e00 [28]0x00496e00 [29]0x004d9c20
[30]0x0080c5a0 [31]0x0080c5a0 [32]0x005f8ac0 [33]0x0085b290 [34]0x00422b70 [35]0x005dbb90
```
(Planet also: Creator vftable @0x009ee234 (1); two 2-method tables @0x009ef12c/0x009ef138. DOPlanet is a separate class @0x00a28cfc, 7 methods.)
### Game::ShipDesignDef — vftable @ `0x009fef64`, 3 methods
```
[0]0x00827240 [1]0x00827390 [2]0x0057c250
```
(no deep hierarchy — RTTI shows 0 bases; the many `Mars::StreamableHelper<ShipDesignDef::*>`
and `VectorHelper<...GunBank>` tables are serialization helpers for its member containers.)
### Game::Actor — primary vftable @ `0x009e2114`, 36 methods
```
[0]0x008a1860 [1]0x00426d10 [2]0x004267d0 [3]0x0080c5a0 [4]0x00426c80 [5]0x00426c90
[6]0x0080c5a0 [7]0x005f8ac0 [8]0x0080c5a0 [9]0x0080c5a0 [10]0x005f8ac0 [11]0x00423710
[12]0x004229a0 [13]0x004f59c0 [14]0x00422af0 [15]0x004267e0 [16]0x0085b290 [17]0x00425770
[18]0x00425340 [19]0x00424b70 [20]0x004267f0 [21]0x00426810 [22]0x00426830 [23]0x00422bd0
[24]0x0080d1f0 [25]0x004242b0 [26]0x00496e00 [27]0x005dbbc0 [28]0x00496e00 [29]0x00422830
[30]0x0080c5a0 [31]0x0080c5a0 [32]0x005f8ac0 [33]0x0085b290 [34]0x00422b70 [35]0x005dbb90
```
(Actor also: Creator @0x009e1fa8 (1); two 2-method tables @0x009e20f8/0x009e2104.
Interfaces IActorReference @0x009e604c (4), IActorEvents @0x009e90ac (2).)
**Note:** Planet's primary table shares slots 11-13,16-25,30-35 verbatim with Actor's,
and both are 36-wide — confirming Planet : Actor (Planet overrides slots 1-8,14,15,27-29).
### Mars::EntityBase — primary vftable @ `0x00a36fd0`, 10 methods
```
[0]0x008a1860 [1]0x008a1da0 [2]0x008a1c30 [3]0x0080c5a0 [4]0x0080c5a0
[5]0x0080c5a0 [6]0x0080c5a0 [7]0x005f8ac0 [8]0x0080c5a0 [9]0x0080c5a0
```
(Creator @0x00a36fb0 (1); two 2-method MI tables @0x00a36fb8/0x00a36fc4.)
### Mars::PhysicalEntity — primary vftable @ `0x00a3710c`, 12 methods
```
[0]0x008a1860 [1]0x00423340 [2]0x008a24d0 [3]0x0080c5a0 [4]0x0080c5a0 [5]0x0080c5a0
[6]0x0080c5a0 [7]0x005f8ac0 [8]0x0080c5a0 [9]0x0080c5a0 [10]0x005f8ac0 [11]0x004227a0
```
(Creator @0x00a36f50 (1); two 2-method MI tables @0x00a370f4/0x00a37100.)
**Note:** slot [0]=0x008a1860 shared with EntityBase's slot [0] → PhysicalEntity : EntityBase
(12 = 10 + 2 added slots [1],[2],[11] with 1 EntityBase slot overridden).
---
## 3. RTTI inheritance graph (from Base_Class_Array structures)
RTTI is **fully populated** — namespaces are NOT empty; the "known empty-namespace issue"
does **not** apply to this program. Chains read directly from each class's
`RTTI_Class_Hierarchy_Descriptor` -> `RTTI_Base_Class_Array` -> BaseClassDescriptors ->
TypeDescriptors. Listed most-derived → root; `mdisp` = byte offset of that base subobject.
**ClientPlayer** (3 bases): `ClientPlayer` → `StrategyPlayer` → `Mars::NetworkObject`.
**AIPlayer** (3): `AIPlayer` → `AIObject` → `Mars::IStreamable`.
**StarSystem** (5): `StarSystem` → `StarMapNode`; + `Mars::NetworkObject`(@0),
`Mars::HandleObject`(@12), `Mars::IStreamable`(@8).
**ShipDesignDef** (1): `ShipDesignDef` — no base classes (leaf/standalone data-def class).
**Mars::EntityBase** (7): `EntityBase` → `IScheduledEvent` →
`ListDLI<IScheduledEvent,IScheduledEventScope>::Link`(@4) → `DLink`(@4);
+ `IScriptable`(@16), `NetworkObject`(@20), `HandleObject`(@28).
**Mars::PhysicalEntity** (11): `PhysicalEntity` → `EntityBase` → `IScheduledEvent` →
`ListDLI<...>::Link`(@4) → `DLink`(@4); + `IScriptable`(@16), `NetworkObject`(@20),
`HandleObject`(@28), `RigidBody`(@44) → `Body`(@44), `SceneObject::Owner`(@212).
**Game::Actor** (13): `Actor` → `TargetableEntity` → `Mars::PhysicalEntity` →
`Mars::EntityBase` → `IScheduledEvent` → `ListDLI<...>::Link`(@4) → `DLink`(@4);
+ `IScriptable`(@16), `NetworkObject`(@20), `HandleObject`(@28), `RigidBody`(@44) →
`Body`(@44), `SceneObject::Owner`(@212).
**Game::Planet** (14): `Planet` → `Actor` → `TargetableEntity` → `PhysicalEntity` →
`EntityBase` → `IScheduledEvent` → `ListDLI<...>::Link`(@4) → `DLink`(@4);
+ `IScriptable`(@16), `NetworkObject`(@20), `HandleObject`(@28), `RigidBody`(@44) →
`Body`(@44), `SceneObject::Owner`(@212).
### Consolidated single-inheritance spine (Mars physics/entity → Game gameplay):
```
Body → RigidBody (Mars physics; mixed in at offset 44)
DLink → ListDLI::Link (Mars intrusive list node; offset 4)
IScheduledEvent IScriptable NetworkObject HandleObject IStreamable (Mars interfaces)
\______________ all mixed into ______________/
Mars::EntityBase → Mars::PhysicalEntity → Game::TargetableEntity → Game::Actor → Game::Planet
StrategyPlayer → ... , ClientPlayer : StrategyPlayer, NetworkObject
AIObject → AIPlayer : AIObject, IStreamable
StarMapNode → StarSystem : StarMapNode (+NetworkObject/HandleObject/IStreamable)
```
---
## Artifacts / reproduction
Scripts (pushed to CT111 `/root/`): `CoreRecon.java`, `VtRecon.java`, `RttiHier.java`.
Run pattern: `systemctl stop reva-server; sleep 3;` then
`analyzeHeadless /srv/re-lab/ghidra-projects sots -process "Sword of the Stars.exe" -noanalysis -scriptPath /root -postScript <Script>.java`; `systemctl start reva-server` after.

View file

@ -0,0 +1,550 @@
# Sword of the Stars 1 (SOTS1) — Save-File Struct Reference (Community RE)
Harvested from two community save-game editors for cross-checking against binary-recovered
serializable types. SOTS1 uses a self-describing "Streamable" serialization system: **field
order + type matter**, and most values are preceded by their own name string.
Target game version: **SOTS1 v1.8+** (both editors target the 1.8.x line; repo2 says "1.80 +19").
## Sources
- **[R1] BardezAnAvatar/Sots.Sots1.SavedGameEditor** — C#. A *complete, ordered, typed*
Streamable parser. Migrated from SourceForge `sots-sge`. This is the authoritative layout
evidence (read/write methods reproduce exact on-disk order). Key files under
`Bardez.Project.SwordOfTheStars.DataStructures/`:
- `BaseSaveStructures.cs` (primitives + framing), `SharedSaveStructures.cs` (coords/colors),
`SaveGameDataStructure.cs` (top level), `SummarySaveStructures.cs`,
`CreateParametersSaveStructures.cs`, `SimulationSaveStructures.cs` (11,974 lines — the bulk),
`CdTableSaveStructures.cs` (combat/AI table). IO: `Bardez.Project.SwordOfTheStars.IO/{Gzip,SaveFileIO}.cs`.
- **[R2] ghbplayer/SOTSedit** — C#/WPF. A *name-tag scanner* (does NOT model layout; it searches
the decompressed blob for length-prefixed field-name strings and reads the value that follows).
Value comes from `Parse.cs` + the field catalog `SOTSEdit.cfg` (friendly-name ⇄ serialized-name
⇄ type mapping) and race/semantic hacks. Written 2011 "to learn C#"; author calls the parser weak.
Both editors independently confirm the same primitives, framing markers, race IDs, and color IDs.
---
## 1. File format & serialization mechanics
### 1.1 Container
- **Whole `.sav` file = gzip stream.** Decompress first (`GZipStream`, standard gzip). [R1 Gzip.cs, R2 gzip.cs]
- Editors work on the **decompressed** byte stream (R1 names its test artifacts `*.sav.inflate.dat`).
- No separate magic/version header is decoded by either editor beyond the top-level Summary block;
version is implied by scenario/field presence, not a numeric version field. Endianness: **little-endian**
throughout (`BitConverter` on x86).
### 1.2 Decompressed top-level order [R1 SaveGameData.ReadFromStream]
```
SaveGameData:
1. SummarySaveStruct summary
2. CreateParametersSaveStruct createParams
3. SimSaveStruct sim <-- the giant one (players/systems/fleets/etc.)
4. CdTable cdTable <-- combat / AI state table
```
### 1.3 Primitive encodings [R1 BaseSaveStructures.cs]
Text encoding is **windows-1252** (R1) / ASCII (R2).
- **StringStruct** (raw string): `Int32 length` + `length` bytes (no NUL terminator counted).
R2 calls this a "BStr": 4-byte little-endian length prefix + ASCII bytes.
- **Padding rule (critical):** every *basic* value struct is **NUL-padded to a 4-byte boundary**.
`PaddingSize = 4`. Padding is computed over `(sizeof(Int32 desc-length) + description.Length + valueBytes)`.
- **Named value fields** (`BasicSaveStruct` subclasses) are each laid out as:
`StringStruct description` (a field-name tag, often the non-descriptive `"."`) → then the value →
then NUL padding to 4 bytes. Concrete leaf types:
| Struct | Payload after description tag |
|---|---|
| `Int32SaveStruct` | 4-byte Int32 |
| `Int64SaveStruct` | 8-byte Int64 |
| `FloatSaveStruct` | 4-byte IEEE Single |
| `BooleanSaveStruct` | 1 byte (0/1), padded to 4 |
| `StringSaveStruct` | nested StringStruct (len+bytes) |
| `ByteArraySaveStruct` | raw bytes (length externally known) |
So the on-disk shape of a named scalar is: `[len][name-ascii][pad] [value] [pad]`. R2 exploits exactly
this: it locates a field by searching for `[len][name]` and reads the value immediately after.
### 1.4 Complex-struct framing (the "BEEFBEEF" envelope) [R1 ComplexSaveStruct]
Every **complex** structure is framed:
```
StringStruct description (NUL-padded to 4)
UInt32 0xBEEFBEEF (begin marker)
... body (ordered child fields) ...
UInt32 0x41104110 (end marker = bitwise NOT of 0xBEEFBEEF)
```
`0xBEEFBEEF` / `~0xBEEFBEEF (0x41104110)` bracket every complex object — a reliable resync/validation
signature when scanning the binary. (`ISotsStructure` leaf types are NOT framed; only `ComplexSaveStruct`.)
### 1.5 Array conventions [R1 BaseSaveStructures.cs]
- **ComplexArraySaveStruct<T>**: framed (has description + BEEFBEEF), body = `Int32SaveStruct count`
then `count` × T.
- **NonComplexArraySaveStruct<T>**: NOT framed; body = `Int32SaveStruct count` then `count` × T.
(Distinguishing which arrays are framed vs. not is itself layout evidence — see per-struct notes.)
### 1.6 Conditional & polymorphic reads (watch for these in the binary)
- **Optional-by-flag:** a boolean/int gate precedes an optional sub-object.
- `SimPlayerColorSaveStruct`: `Int32 colorIndex`; **iff `colorIndex == -1`**, an `RgbColorInt32`
(custom RGB) follows. Otherwise palette index only.
- `SimPlayerDesignDw2SaveStruct` (weapon slot): `Boolean bId`; if true → `Int32 wId`, else →
`StringSaveStruct wfn` (weapon full resource path); then `Int32 dId`.
- `SimFleetShipDetails`: `Boolean hbq` gates `bq`; `Boolean hsp` gates `sp`. Fleet flight-plan/lay
gated by `hfPlan` / `hLay` booleans.
- `SimSystemDetailNvo.isInd` gates independent-colony sub-block; `SimSystemDetailsIndi.hindi`,
`SimSystemDetailsVonNeumann.vnh` similar boolean gates.
- **Polymorphism by string tag:** `SimSvSctObXscn` reads `StringSaveStruct xcsn`, then switches:
`"crowdefs"`, `"gmtrigger"`, `"traps"`, `"indsys"`/default → different body subclass.
- **Polymorphism by fixed position:** `SimScSctObEncObjArray` (grand-menace/encounter objects) reads a
count then dispatches subclass **by index 0..8** in fixed order:
`0 Infest, 1 Dsn, 2 AsteroidMonitor, 3 TD, 4 WD, 5 Hives, 6 Rsuc, 7 Dfts, 8 Ini2`.
### 1.7 Write-time quirks worth knowing (R2)
- R2 edits in place and cannot safely change string length (it truncates/space-pads to the original
length). R1 rewrites the whole stream and re-pads. If the binary stores string lengths, the game
reads them dynamically (R1 proves round-trip works when re-padded).
- R2 planet OID/PID hack: on-disk **`OID = PID * 16`** (R2 divides by 16 to show a "PlayerID").
i.e. the raw owner id field is the player index shifted left 4 bits.
---
## 2. Enums / ID tables (agreed by both editors)
### 2.1 Species / race ID [R2 Parse.cs addRace(); R1 PlayerSlot.FxSp comment]
| ID | Species |
|---|---|
| 0 | Human |
| 1 | Hiver |
| 2 | Tarka(s) |
| 3 | Liir |
| 4 | `_NPC` / AI-rebellion / grand-menace player (R1: "??? AI Rebellion") |
| 5 | Zuul |
| 6 | Morrigi |
### 2.2 Player color ID (palette index) [R1 PlayerSlot.FxCrId & SimPlayerColor]
`01 Red, 02 Yellow, 03 Blue, 04 Pink/Magenta, 05 Orange, 06 Green, 07 Aqua, 08 Gray,
09 Dark Green, 10 Purple`. Value **-1 ⇒ custom RGB triplet follows** (see §1.6).
### 2.3 Difficulty [R1 PlayerSettings.Difficulty]
`0 Easy, 1 Normal, 2 Difficult`.
### 2.4 Sentinel values seen in fields
`0x7FFFFFFF (Int32.MaxValue)` used as "tag"/unset (PlayerSlot.tag);
`team = -1 (0xFFFFFFFF)` = no team; R2: value `-1` = field absent in this save.
No named C# enums exist for tech IDs / weapon families — techs and weapons are **string resource
names** (e.g. tech `tNm`, weapon `wfn`), not numeric enums. Weapon *family* enumeration lives only in
the CdAi combat block as `aiSitWepFams` (opaque int set).
---
## 3. Summary block [R1 SummarySaveStructures.cs] (complex)
### SummarySaveStruct (fields in on-disk order)
1. `StringSaveStruct gameName`
2. `Int32 turn`
3. `Int32 numSys` (system count)
4. `Int32 checkSum`
5. `ComplexArray<PlayerSlotWrapper> players`
6. `SessionSaveStruct session`
7. `Int32 mapShape`
8. `Int32 incMod` (income modifier)
9. `Int32 resMod` (research modifier)
10. `Boolean alliances`
11. `Boolean teams`
12. `Boolean encounters`
13. `StringSaveStruct scenario`
### PlayerSlotWrapper (complex): `{ PlayerSlotSaveStruct slot; Int32 rank; }`
### PlayerSlotSaveStruct (complex) — new-game slot definition, ordered:
`Boolean isPlay, isDead, isReq, isRec, isFxNm` → `String fxNm` (fixed name) →
`Boolean isFxSp` → `Int32 fxSp` (species, §2.1) → `Boolean isFxCr` →
`NestedInt32 fxCrId` (color, §2.2) → `Boolean isFxBd` → `String fxBd` (badge) →
`Boolean isFxAv` → `String fxAv` (avatar) → `Int32 tag` (often 0x7FFFFFFF) →
`Int32 pwd` (password) → `Int32 team` (-1=none) → `PlayerSettingsSaveStruct settings`.
### PlayerSettingsSaveStruct (complex):
`Int32 initialTreasury, initialColonies, initialTechnologies, difficulty (§2.3)`.
### SessionSaveStruct (complex) → `TmrsSaveStruct tmrs`:
`Int32 tstl (0x7F7FFFFF), tctl (0x42700000), tqtl (0x7F7FFFFF), tqtle (0)` — timer limits (float bit-patterns stored as int).
---
## 4. CreateParameters block [R1 CreateParametersSaveStructures.cs] (complex)
### CreateParametersSaveStruct (ordered):
`String name; Int32 id; Int32 rSeed (random seed); Int32 aid; String key;`
`MapPSaveStruct mapP;` `Int32 mapS; Int32 mapF; Int32 nSys;` `Float rEnc (random-encounter rate);`
`Int32 sDist;` `Float sSize; Float sRes;` `Int32 sSuit; Int32 maxP; Int32 aSpec;`
`Boolean bAlly; Int32 nTeam; Boolean tmgrp;`
`Int32 pSav (start savings); Int32 pCol (start colonies); Int32 pTech (start techs);`
`Float incM; Float resM; ScrpSaveStruct scrp;`
### MapPSaveStruct (complex) — initial map/galaxy generation:
1. `Int32 unknown1`
2. `ComplexArray<PlanetSaveStruct> planetArray`
3. `NonComplexArray<ComplexArray<Int32>> players` (per-player int arrays; "non-complex array of players")
4. `ComplexArray<MapPNpc> npcArray` (≈ players − 1; independents/NPCs)
### PlanetSaveStruct (complex) — initial star node geometry:
`SpatialCoordinate coordinates (x,y,z floats)`, `Int32 unknown1..4`
(values seen: `0x7FFFFFFF`, `0x7F7FFFFF`). NOTE: this is the *map-generation* planet record; the
*live* planet/colony state lives in `SimSystemDetailsSaveStruct` (§8).
### MapPNpc (complex): `Int32 unknown1, unknown2`. ### ScrpSaveStruct (complex): `Int32 spc`.
### Shared value types [R1 SharedSaveStructures.cs]
- `SpatialCoordinateSaveStruct` (complex): `Float x, y, z`.
- `RgbColorFloat` (leaf): `Float r,g,b`. `RgbaColorFloat` (leaf): `RgbColorFloat rgb; Float a`.
- `RgbColorInt32` (leaf): `Int32 r,g,b`.
---
## 5. Simulation block — top level [R1 SimulationSaveStructures.cs]
### SimSaveStruct (complex) — the master world state, ordered:
```
String keyPath
Int32 nMsz, nMlc, nMnx (next-id / size counters)
NonComplexArray<Int32> playerIds, designIds, systemIds, fleetIds, shipIds, tradeIds
Int32 modCount, frame, gameId
AttributeSaveStruct attribute
RngSaveStruct rng (ByteArray unknownData ~2500 bytes: RNG state)
String gameName
Int32 map, incMod, resMod, enAl, enTm, gOTurn
NestedInt32 gOWinPly
Int32 npcm, npco, npci, npcv, npca, szad, rsad, suad
ComplexArray<ResearchSaveStruct> sprjs (shared/special research projects)
Float randEncAdjustment
Int32 cmbtid (next combat id)
ComplexArray<TurnPly> turnstats (per-turn per-player history)
NonComplexArray<SimCrepSaveStruct> creps (combat reports)
Int32 ninv, allExc1, allExc2, allExcCF
NonComplexArray<SimPlayerSaveStruct> players <-- EMPIRES (§6)
SimSpeciesArraySaveStruct species (galaxy species list)
NonComplexArray<SimSystemSaveStruct> systems <-- STAR SYSTEMS (§8)
SimNodeGrid2 ndgr2 (node-line / warp graph)
SimTradeManager trdmgr (§9)
SimSpyManager spymgr (Int32 xsid, nspy)
NonComplexArray<SimFleet> flt <-- FLEETS (§10)
NonComplexArray<Int32> acts
SimSvSctOb svSctOb (scenario/encounter objects, §11)
Int32 zdsc, zdsi, zdst (Zuul/system-destroyer counters)
```
### Small shared sim types
- `SimPopGSaveStruct` (complex): `Int32 popT; Int32 popS; Int64 popC` (pop type / species / civ count).
- `ResearchSaveStruct` (leaf): `NonComplexArray<ResearchOptionalSaveStruct> us; String nm; String ntg`.
`ResearchOptionalSaveStruct`: `Int32 usc, usp`.
- `TurnPly` (leaf): `Int32 ply; PlyHistSaveStruct hist`.
- `PlyHistSaveStruct` (complex): `Int32 ply; PlyHistStatsSaveStruct[] stats`.
- `PlyHistStatsSaveStruct` (complex): `Int64 pop; ComplexArray<PlyHistStatsSacq> sacq, slost;`
`Int32 trn, almem, inc, tdinc, sav, col, bat, tch; NonComplexArray<PlyHistClsSaveStruct> cls`.
- `PlyHistStatsSacq` (complex): `Int32 set, ses, seop, senp; NonComplexArray<Int32> seo`.
- `PlyHistClsSaveStruct` (leaf): `Int32 cls, shpt, shpl, shpk, satt, satl, satk` (ship/sat built/lost/killed by class).
---
## 6. Player / Empire [R1 SimulationSaveStructures.cs]
### SimPlayerSaveStruct (leaf wrapper): `Int32 playerId; SimPlayerDetailsSaveStruct details`.
### SimPlayerDetailsSaveStruct (complex) — the empire record, on-disk order:
```
SimPlayerTechTree techTree <-- TECH TREE (§7)
Int32 homeSystem, playerIndex
String playerName
Int32 species (§2.1)
SimPlayerColorSaveStruct colorId (palette idx or -1 + RGB, §1.6/§2.2)
String badge, avatar
Int32 team, sav (savings)
Float idealSuit, suitTolerance, maxOH
Float resRate, resModifier, resScl (research)
Int32 trm, trp, tra
Float outMod, rebOutMod, scOutMod, incMod, popMod, terraMod (economy multipliers)
Boolean aMine; Float minPure, minRate; Int32 ngts, prGtTrf, gTraf
Int32 cstR, cstE, cstT, maint, shrm, status, elim
Boolean npc, rebAi, reqCL
SimPlayerTeamSaveStruct teamStruct (Int32 alid, al, na, cf)
Int32 hasVac, hasImm, npTrak, hasDisc, hasDiscSp, hasDiscCl, hasEnc, hasEng
SimPlayerEventsSaveStruct events (Int32 evNxId; ComplexArray<SimPlayerEvent>)
NestedInt32 fngNum
Int32 pvSav, pvMA, aibN
Boolean cnTrd, cnRad, hgs, hadvs, harcc, cnVItl
Float pddm
Int32 bankWrn, bankTrn, bankPr, bankEl
SimPlayerShipRecsEventsSaveStruct shipRecs
Int32 nextPrjId, plcy, pswd, lret, nmeid
Boolean cdp
SimPlayerSpySaveStruct spy2 (Int32 defc2, rtc, evc, ttc)
SimPlayerCivrSaveStruct civR (Float smx; ComplexArray<SimPlayerCivrSpeSpVa {Int32 sp, va2}>)
Int32 aidf
Boolean srn; Int32 srcTo, lboid, lcid2
String resTnm
Boolean resErrRoll
SimPlayerModsSaveStruct conMods (3× SimPlayerConModSaveStruct {Float conMod, savMod})
NonComplexArray<Int32> ownerIds
NonComplexArray<SimPlayerDesignEntrySaveStruct> designs <-- SHIP DESIGNS (§7.2)
NonComplexArray<SimPlayerDesignEntrySaveStruct> droneDesigns
NonComplexArray<SimPlayerNote> notes (Int32 ntSys; String ntTxt; Int32 ntTrn)
NonComplexArray<SimPlayerPr> pr (Float prm; Int32 prbt)
Boolean hasAiRebellion, cta
NestedInt32 aienf
NonComplexArray<SimPlayerDetailsSpecialProjectT> nSprj
Int32 nexp, nWeapXcl
ComplexArray<SimPlayerDetailsOjv> ovjs (objectives: Int32 id; Bool cmp; Int32 spr; String dsc; Int32 nid)
ComplexArray<SimPlayerDipStat> dipStats (diplomacy; see below)
ComplexArray<SimPlayerComm> comms (Int32 msgt; SimPlayerCommMsg msg)
ComplexArray<SimPlayerPrepSaveStruct> preps
ComplexArray<SimPlayerOdesSaveStruct> odes
ComplexArray<SimPlayerOwepSaveStruct> owep
ComplexArray<SimPlayerOtchSaveStruct> otch
NestedInt32 aid
Int32 ndeflay, rdtc, tnc
```
- `SimPlayerDipStat` (complex): `Int32 other; SimPlayerDipStatDetail nap, ally, cf; Int32 deadhome`.
`SimPlayerDipStatDetail` (leaf): `Int32 last_, last_bty, bkn_, bty_`.
- `SimPlayerCommMsg` (complex): `Int32 cid2, snd, rcp, exp, sent, rcpt, sys`.
- `SimPlayerPrepSaveStruct` (complex): `Int32 oid, pid, flds, sav, home, ncol, mpwr, mcls, mmsl, nshp, nsat`.
- `SimPlayerOdes/Owep/Otch` (complex, "old design/weapon/tech" build history):
`Odes {Int32 ontF, otnL, odid, opid}`, `Owep {Int32 ontF, otnL, odet; String owep; Int32 owith}`,
`Otch {Int32 ontF, otnL, odet; String otch; Int32 owith}`.
- `SimPlayerDetailsSpecialProjectT` (leaf): `Int32 sprjT; SimPlayerDetailsSpecialProjectDetails sprj`.
`...Details` (complex): `Int32 stp; SpecialProjectSpi spi; tail (polymorphic AsMon | Tech)`.
`...Spi` (complex): `Int32 sPid, sts; Float cst; Int32 mxC; String name; Int32 trns`.
tail `AsMon`: `Int32 rDn, sys, rMn, rMx; Float rMd`; tail `Tech`: `Int32 rDn; Float aOdd, aInc; String tch; Int32 rCst`.
---
## 7. Tech tree & ship designs
### 7.1 Tech tree [R1]
- `SimPlayerTechTree` (complex): `NonComplexArray<SimPlayerTechTreeBranch> tree; NonComplexArray<SimPlayerTechTreeTech> techs`.
- `SimPlayerTechTreeBranch` (leaf): `String tNm` (tech name); `NonComplexArray<String> branches` (child tech names).
- `SimPlayerTechTreeTech` (leaf), ordered:
`String tNm` (tech name) → `Int32 st` → `Int32 tResCost` (research cost) →
`Int32 tResDone` (progress) → `Int32 tAcq` (turn acquired) → `Int32 tiAcq` (turns-to-acquire) →
`Int32 tbd` → `Boolean tfc` → `Int32 tUnlck` (unlocked flag).
**Techs are identified by string name, not a numeric enum.**
### 7.2 Ship designs [R1]
- `SimPlayerDesignEntrySaveStruct` (leaf): `Int32 designId; SimPlayerDesignSaveStruct design`.
- `SimPlayerDesignSaveStruct` (complex), ordered:
`Boolean faiDes; Boolean dHide; Int32 dWep; String dName;` `SimPlayerDesignSectionArray sections;`
`Int32 dtc; NonComplexArray<SimPlayerDesignDwg> dwgv`.
- `SimPlayerDesignSectionArray` (leaf): `Int32 count` + `count` × `SimPlayerDesignSectionEntrySaveStruct`.
- `SimPlayerDesignSectionEntrySaveStruct` (complex): `SimPlayerDesignSectionSaveStruct dsec` (Int32 unknown1, unknown2);
`ComplexArray<SimPlayerDesignUnknown1SaveStruct> dgbnk2;` `ComplexArray<String> dOpts`.
- `SimPlayerDesignUnknown1SaveStruct` (complex) → `SimPlayerDesignDw2SaveStruct dw2` (weapon slot,
conditional — see §1.6): `Boolean bId; (bId? Int32 wId : String wfn); Int32 dId`.
- `SimPlayerDesignDwg` (complex): `NonComplexArray<SimPlayerDesignGng> wgng`;
`SimPlayerDesignGng` (leaf): `Int32 wgid; SimPlayerDesignDwgWgb wgb` (complex: `Int32 unknown1..3`).
---
## 8. Star systems & planets [R1]
- `SimSystemSaveStruct` (leaf): `Int32 sysId; SimSystemDetailsSaveStruct details`.
- **`SimSystemDetailsSaveStruct` (complex)** — the live system+colony record, on-disk order:
```
SpatialCoordinate pos
RgbaColorFloat starColor
Int32 idx
Int32 size (1-10)
Float suit (climate hazard)
Int32 res, aRes, mRes (resources / asteroid / extra)
Boolean noRebAi
Int32 tRes, pop
ComplexArray<SimPopGSaveStruct> popG
Float infra
Int32 pvPop; ComplexArray<SimPopG> pvPopG; Float pvInfra, pvSuit; Int32 pvRes, pvARes2, pvMRes; Bool pvNoRebAi (previous-turn snapshot)
SimSystemDetailRtsSaveStruct rts (Float sRs, sRt, sRsc, sRtf, sRi, sRoh, sRnr — IO allocations)
Int32 abdn; Boolean dstyd; Int32 tnsOh
Float outMod, repCur, repMax
Int32 ntdev, pbon
ComplexArray<SimPopG> pbon2
Float ibon
Int32 ltis, rbfl, rbtn, rbfr, rbwn, hsrg
NonComplexArray<SimSystemDetailHalt> halt (Int32 haltt; Bool haltv)
SimSystemDetailsVonNeumann vnm (Bool vnh gate → details: Bool vnd, vnex3, vnpex3)
String name
SimSystemDetailFlags1 flags1 (Int32 vFlags, eFlags, aFlags, fFlags, gFlags)
Int64 bats2 (recent battles; larger=more recent)
Int64 rcex
SimSystemDetailFlags2 flags2 (Int32 mnRFlags, rfRFlags, clkFlags)
Int32 eggScio, terrFl, tAcq, tfAcq, tDst
ComplexArray<SimPopG> dcs; Int32 dsu
ComplexArray<SimSystemDetailCm> cm, pvcm (SimSystemDetailCm: Int32 msp, mv)
ComplexArray<SimSystemDetailCme2> cme2 (Int32 mid, mtrT, mn, mtp; ComplexArray<Cm> mfx; String mdsc)
ComplexArray<SimSystemDetailSpy> spies
Int32 pid (owner player id), defF, defSf
SimSystemDetailBq bq (ComplexArray<SimSystemDetailBqOrd> ords; Ord: Int32 desId, con, conleft, sav, ordId)
NonComplexArray<SimSystemDetailAdct> adct (Int32 ads, adt)
Int32 numPlgs2
NonComplexArray<Int32> flts, gfs, snF, mnF (fleets / gates / stations / monitors present)
NonComplexArray<SimSystemDetailNvo> nvos (colonies; see below)
NonComplexArray<SimSystemDetailVe> nve (Int32 ePid, ets, eid)
NonComplexArray<SimSystemDetailVs> nvs (Int32 pid; SimSystemDetailVsPView pview)
SimSystemDetailsIndi indi (Bool hindi gate → indsp, SimPlayerColor indcl, String indnm/indav/indba)
```
- `SimSystemDetailNvo` (leaf): `Int32 pid, tShn, oId; Boolean isInd; SimSystemDetailNvoIndi indi`
(independent-colony sub-block gated by `isInd`).
- `SimSystemDetailVsPView` (complex — per-player *seen* snapshot of a colony): `Int32 vTrn, pop;`
`ComplexArray<SimPopG> pop2; Int32 infra; Float suit; Int32 res, aRes2, mRes; Bool noRebAi;`
`Int32 pbon; ComplexArray<SimPopG> pbon2; Float ibon; Int32 terrFl; Bool footer`.
### 8.1 R2 ⇄ R1 cross-map for planet/colony fields (verifier gold)
R2 finds these serialized tags anywhere in the "Planets" region (between markers `NumSys`…`NdGr2`).
They correspond to fields inside R1's `SimSystemDetailsSaveStruct` / `...VsPView`:
| R2 serialized tag | type | meaning | R1 field |
|---|---|---|---|
| `Idx` | int | planet/system id | `idx` |
| `Name` | string | name | `name` |
| `Size` | int | 1-10 | `size` |
| `Suit` | float | climate hazard | `suit` |
| `Res` / `ARes2` / `MRes` | int | resources | `res` / `aRes` / `mRes` |
| `Infra` | float | infrastructure | `infra` |
| `ibon` | float | infra bonus | `ibon` |
| `Pop` | int | imperial pop | `pop` |
| `pbon` | int | imperial pop bonus | `pbon` |
| `PopC` | long | civilian pop | (SimPopG `popC` Int64) |
| `OID` | int | owner id (**= PID×16**) | `pid` (owner) |
| `PID` | int | derived player id | (OID/16) |
| `SRt/SRsc/SRtf/SRi/SRoh` | int | IO trade/ship/terraform/infra/overharvest | `rts.sRt/sRsc/sRtf/sRi/sRoh` |
| `Abdn` | short | abandon order | `abdn` |
| `Dstyd` | short | star annihilated | `dstyd` |
| `ltis` | short | last-time-seen | `ltis` |
| `VFlags/EFlags/AFlags/FFlags/GFlags` | int | state flags | `flags1.*` |
| `Bats2` | int | recent combat | `bats2` (Int64 in R1) |
| `nadct` | int | addicted (1=yes) | (in `adct` array) |
| `NumFlts/NumGFs/NumSnF/NumMnF` | int | fleets/gates/stations/monitors | `flts/gfs/snF/mnF` counts |
Note the **type disagreements** (verifier flags): R2 reads `Abdn`, `Dstyd`, `ltis` as **short (Int16)**
while R1 models them as framed `Int32SaveStruct`; R2 reads `Bats2` as int while R1 uses Int64. R2's
name-scan reads the value bytes directly after the tag+pad, so R2's width is the more literal
on-value-bytes claim for those specific fields; treat as "value is small, low bytes are the datum."
---
## 9. Trade & node grid [R1]
- `SimNodeGrid2` (complex): `ComplexArray<SimNodeGridPath> paths; Int32 nextId`.
`SimNodeGridPath` (complex): `Int32 npt, npid, npfr(from), npto(to), npctm, npcby, npdtn, npdtf, npenp, npuse, nptf`.
- `SimTradeManager` (complex): `NonComplexArray<SimTradeSector> tradeSectors; Float sctSize; List<SimTradeSectorRt> rt`.
`SimTradeSector` (leaf): `Int32 tradeId; SimTradeSectorTradeSaveStruct trade`.
`SimTradeSectorTradeSaveStruct` (complex): `SpatialCoordinate pos; Int32 tradeSectorGridId;`
`SimTradeSectorTradeCtrSaveStruct tsctr(3 floats); Int32 tssec, tsct, tscr, ptssec, ptsct, ptscr;`
`ComplexArray<...Fwarn {Int32 pId, ntrns}> fwarn; NonComplexArray<Int32> systems, tsflt`.
`SimTradeSectorRt` (complex): `Int32 tro, trfow, trfr, trfrs, trtow, trto, trtos, trtc`.
---
## 10. Fleets & ships [R1]
- `SimFleet` (leaf): `Int32 fltId; SimFleetDetails flt`.
- **`SimFleetDetails` (complex)**, ordered:
`SpatialCoordinate pos; Int32 pId, locId; FlightPlanContainer fplan (Bool hfPlan gate);`
`String ftName; Int32 ftTrans; SimFleetOrigin ftOrig (Int32 ×3);`
`Int32 ftFlag, ftae, ftpae, ftEnc, ftMs, perm; SpatialCoordinate prvPos;`
`LayContainer lay (Bool hLay gate); NonComplexArray<SimFleetShip> ships`.
- `SimFleetDetailsFlightPlan` (complex): `ComplexArray<Wpt> wpts; Float fPsp2; Int32 fPeta2;`
`Fpogn2 (3 floats); SpatialCoordinate fPdpos; Int32 pnd`. `Wpt`: `Int32 wpt, tp; Nrt {Int32 nrp,nrf,nrt}`.
- `SimFleetShip` (leaf): `Int32 shipId; SimFleetShipDetails ship`.
- **`SimFleetShipDetails` (complex)**, ordered:
`Int32 desId (design id), fltId, plrId; Float range; SimFleetShipHealth health (3 floats: command/mission/drive);`
`Int32 conCap, refCap, repCap, mineCap, plg, act; Boolean dep, atq; Int32 encId;`
`SimFleetShipPrish prish (Int32 prMax; NonComplexArray<Int32> prSp); Int32 lct, tsd, atsp, tblt;`
`Boolean hbq; SimFleetShipDetailsBq bq (gated by hbq);`
`Boolean hsp; SimFleetShipDetailsSp sp (gated by hsp; two SpPop pop/pPop);`
`NonComplexArray<SimFleetShipDetailsTh> th (Float th, thm)`.
`SimFleetShipDetailsBqOrd`: `Int32 desId, con, conleft, sav, ordId` (same shape as system BqOrd).
---
## 11. Combat reports & scenario/encounter objects [R1]
- `SimCrepSaveStruct` (combat report, complex): `Int32 cid, trn; SpatialCoordinate pos; Int32 sid, auto, dur, cow, cdst, cpk, cpt, cdt, cdi;`
`ComplexArray<SimCrepPrepSaveStruct> prep; ComplexArray<SimCrepWrepSaveStruct> wrep`.
`SimCrepPrepSaveStruct`: `Int32 plr; Bool ai; Int32 ally, status, mxeng, mxcls, mxmsl;`
`NonComplexArray<Cls> cls; NonComplexArray<Sec> sec; Int32 ndam; ComplexArray<Srep> srep`.
`SimCrepPrepSrepSaveStruct`: `String name; Int32 did, cls; Int64 caps2; Int32 nshp, nfld, nlst, dtak; SimDamsSaveStruct dams`.
`SimDamsSaveStruct`: `Int32 dams, damp, dami, damt`. `SimCrepWrepSaveStruct`: `String wep; SimDams dams`.
- `SimSvSctOb` (complex): `ScnObjStruct scn; NonComplexArray<SimSvSctObXscn> xscn; SimScSctObEncObjArray encObjs`.
`SimSvSctObXscn` = polymorphic-by-string (§1.6): `traps` (ComplexArray<TrapDetails {Int32 sys,pid,trenc,trgenc}>),
`gmtrigger` (Int32 gmch), `crowdefs` (Int32 sys; NonComplexArray<Int32> dsys,des; Float drad), `indsys`/default (empty).
- **`SimScSctObEncObjArray`** = grand-menace/encounter table, dispatched **by fixed index 0-8** (§1.6).
Each subclass carries that menace's state, e.g.:
- `EncInfest` (Hiver infestation): `NonComplexArray<Asg> asg; ComplexArray<Infest> infests; Int32 deshive, deslarva`.
- `EncHives`: `Int32 qDesignId; ComplexArray<Hive {Int32 hiveId,queenId,nextQ}> hives; ComplexArray<Queen {Int32 queenId,qDstId}> queens; ComplexArray<NestedInt32> sysMem`.
- `EncAsteroidMonitor`, `EncTD`, `EncWD`, `EncRsuc`, `EncDfts` (Von Neumann; large), `EncIni2`.
---
## 12. CdTable — combat / AI persistence block [R1 CdTableSaveStructures.cs]
### CdTable (leaf, top of the 4th file-section): `ComplexArray<String> cdt; CdPlayer cdplayer; CdAi[] cdai`.
### CdPlayer (complex) — **entirely reverse-unlabeled** (fields named `unknown1..35`); shape is known:
`Int32 unknown1(=16); Bool unknown2; Float unknown3; Bool unknown4; Int32 unknown4p5; Bool unknown5..8;`
`Int32 unknown9,10; NonComplexArray<CdPlayerUnknown11Item {Int32 unknownId, const1, const2, value1}> unknown11;`
`Int32 unknown12..14; NonComplexArray<Int32> unknown15,16; NonComplexArray<Unknown17Item{Int32 ×2}> unknown17;`
`NonComplexArray<Int32> unknown18; Int32 unknown19..21;`
`NonComplexArray<Unknown22Item{Int32×2,Bool}> unknown22; Int32 unknown23..35`.
### CdAi (complex) — per-AI-player planner state, ordered:
`AttributeSaveStruct aiAttr; NestedInt32 aiTurnPris; CdAiSit aiSit; NestedInt32 aiPlyHat;`
`ComplexArray<CdAiPrsUnknown {Int32 pid,trn}> prs2; Int32 dsh, nbStab, nmBlst; CdAiAidng aidng;`
`Int32 aiHivJ, sdFlT; NestedInt32 nalat; Int32 lnat, lat;`
`NonComplexArray<CdAiSys> aiSys; NonComplexArray<CdAiCmbr> cmbR; NonComplexArray<CdAiCl {Int32 clTn,clSyId,clPlId}> cl;`
`NonComplexArray<CdAiPrv {Int32 nPrvId; Float nPrvVa}> prv; NonComplexArray<Int32> tecs; Int32 fct;`
`ComplexArray<CdAiApr {Int32 sid,tn0,tn1}> apr`.
- `CdAiSit` (complex): `NestedInt32 aiSitSecs; NestedInt32 aiSitWepFams` (**weapon-family set** lives here — opaque ints).
- `CdAiCmbr` (complex): `Int32 crTrnK; Bool crPce; NestedInt32 crSys; CdAiCmbrCrplSv2 crplSv2`.
`...CrplSv2`: `Int32 rpBon, rpBonT, savBonus; Bool maintHf; ComplexArray<TacReport> tacReports`.
`TacReport`: `TrStruct trBy, trTo; TacReportDamage damageStruct; NonComplexArray<TacReportShips> ships`.
`TrStruct`: `Int32 treHd, treHi, treD, treDp, treDi, treDt, treB`.
`TacReportDamage` (leaf): `Int32 tRid, tRal, tRbal, tRlas, tRmis, tRmin, tRnrg, tRbio, tRbrd; Bool tRsld, tRsldd, tRsldc, tRsldi, tRsldr`
— **damage-by-weapon-family breakdown**: bal(listic)/las(er)/mis(sile)/min(e)/nrg(=energy)/bio/brd(=boarding); sld=shields.
`TacReportShips` (leaf): `Int32 tRships, tRsldr, tRshipL`.
---
## 13. Coverage gaps & contradictions
**Coverage (what's decoded):**
- R1 decodes essentially the *entire* file top-to-bottom: summary, create-params/map-gen, full sim
(players, tech, designs, systems/colonies incl. per-player fog-of-war snapshots, fleets, ships,
trade, node grid, combat reports, grand-menace/encounter objects) and the CdTable AI block. This is
the most complete community model and the primary Rosetta source.
- R2 decodes only Summary, Player Settings, Players, Species, and Planets (colony) fields, by tag
search — but adds *friendly semantics* and confirms the value-bytes width of several planet fields.
**Known-unknown fields (labelled `unknown*` in R1 — do NOT treat names as authoritative):**
- All of `CdPlayer` (`unknown1..35`) and `CdAiAidngDnId`, `SimFleetOrigin`, `SimFleetDetailsFlightPlanFpogn2`,
`SimTradeSectorTradeCtr`, `SimPlayerDesignDwgWgb`, `SimSystemDetailNvoIndcl*` bodies.
- `SimFleetShipHealth` three floats guessed as command/mission/drive.
- `PlanetSaveStruct.unknown1..4` (map-gen) unexplained.
**Not decoded / thin:**
- Tactical/real-time combat geometry: only *reports/summaries* are stored (SimCrep*, CdAiCmbr TacReport).
Per-ship in-battle positions/velocities are not in these editors (likely not in the sim save at all).
- `SimSystemDetailSpy` body is empty in R1 (marked "needs to be populated"); spy detail unresolved.
- RNG state (`RngSaveStruct.unknownData`) is an opaque ~2500-byte blob.
- R1 source comments flag `SimSystemDetailSpiesArray`, `SimSvSctObXscnXsc` and
`SimScSctObEncObjDetails` as incomplete/"wrong" in places — verify encounter bodies against binary.
**Contradictions between R1 and R2 (reconcile against binary):**
1. **Field widths on planet flags:** R2 reads `Abdn`, `Dstyd`, `ltis` as Int16 and `Bats2` as Int32;
R1 models `abdn/ltis` as Int32 and `bats2` as Int64. → The datum is small; check the true stored
width in the binary struct.
2. **PID vs OID:** R2 asserts `OID = PID*16` (owner id is player index << 4); R1 stores a single `pid`
owner field and does not model the ×16 relationship. → Confirm whether the binary owner field is a
raw index or a shifted/tagged handle.
3. **`_NPC` species id 4:** R2 names it `_NPC`; R1 comment guesses "AI Rebellion". Same numeric id 4,
different label — likely a shared "non-player/rogue" species slot.
4. R2 treats Players and PlayerSettings as separate flat tab regions bounded by marker strings
(`HomeSys`…`ISsp`, `Slot`…`Session`); R1 shows these are actually nested (settings inside the
Summary PlayerSlot, live player data inside SimPlayerDetails). R2's region boundaries
(`Summary`,`Slot`,`Session`,`HomeSys`,`ISsp`,`NumSys`,`NdGr2`,`PlayerIDs`,`DesignIDs`) are useful
**section-marker strings** to locate blocks in the raw binary.
**High-value binary-scan signatures:**
`0xBEEFBEEF` / `0x41104110` complex-struct brackets; length-prefixed ASCII field-name tags
(`[int32 len][name]`) preceding every named scalar; section marker strings above.