recon: RTTI hierarchy + vftables, affinity pin has no topology branch, save-struct reference
This commit is contained in:
parent
994b182f7a
commit
3bdc83fe29
4 changed files with 771 additions and 8 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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]])
|
||||
|
|
|
|||
209
findings/objects/ghidra-recon.md
Normal file
209
findings/objects/ghidra-recon.md
Normal 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.
|
||||
550
findings/objects/save-editor-structs.md
Normal file
550
findings/objects/save-editor-structs.md
Normal 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.
|
||||
Loading…
Add table
Reference in a new issue