Part 1 (notes side): findings/objects/svsctob-variants.md records the two maps
that are nowhere on the wire and were read out of the game -- EncID -> class
from a 23-entry dword jump table at 0x0052bf60 indexed by EncID-1 (0x0052bf00),
and xscn -> class from an exhaustive four-way _stricmp chain at 0x005a7050.
Twelve live EncIDs, four scenario names, and "indsys" =
Game::SVSOIndependentSystems whose Read and Write are both the shared `ret 4`
stub at 0x005f8ac0, so its empty frame is correct output rather than a
truncation. Also: SVSOSots::Read accepts NPCPlr and hastraps, which Write never
emits -- read-only backward compatibility, not a hole in the recovery. And a
correction to the recovery itself: SVSOCrowDefenders writes `dsys` INSIDE the
ndsys loop; layouts.json calls it a plain member and no save can settle it
because both counts are 0 everywhere. 13 addresses in ghidra/addresses.d/
lane-w.json; gen_addresses.py merges to 640 with no duplicate name.
Part 2: lane G found four defects in both readers and deliberately did not
patch the oracle mid-campaign. Fixed now, with tests, and byte-neutral.
1. Game::SystemParams field 1 is a string, not an int (empty string == four
zero bytes == int 0, so it round-tripped by luck).
2. ObservedTech/ObservedWeapon odet is a bool, not an int (byte-safe only
because a 4-char tag makes both items 12 bytes; 3 chars would not).
3. SpeciesRatios nv is a count, not a field.
4. ShipRecords srbd is a count, not a field -- and this one is behaviourally
confirmed, not inferred: srbd takes 0, 1, 3 and 4 across the players and
every non-zero count is followed by exactly srbd x 5 scalars.
Note that 3 and 4 were an ABSENCE in save_reader.py, not an error: ShipRecs and
civr were both A(..., "any"), so the fix had to add the shapes rather than
retype a field.
Byte-neutrality: every item's inflated offset is unchanged on all four saves
(38,933 / 39,843 / 40,300 / 35,771 offsets, sequences identical), so no item
boundary moved. state_checksum.py still reports coverage: PROVED on all four
with the same rebuilt byte counts. The /CreateParams and /Sim/players digests do
change, because they hash typed VALUES and two fixes change what a value is --
and the value-byte deltas balance exactly: odet items x 3 plus p1 items x 4.
--strict exit 0 on all four saves; tests 36 -> 48.
findings/objects/wire-schema-closeout.md carries the whole account, including
the proof that CD/TurnCommands_v5 cannot be typed without a save that has
issued orders.
108 lines
5.9 KiB
JSON
108 lines
5.9 KiB
JSON
{
|
|
"entries": [
|
|
{
|
|
"name": "SVScriptObject_FactoryByEncID",
|
|
"addr": "0x0052bf00",
|
|
"convention": "cdecl",
|
|
"prototype": "Game::SVScriptObject* (int encID) // The EncObj factory. `dec eax; cmp eax,0x16; ja <null>; jmp dword [eax*4 + 0x0052bf60]` -- a 23-entry dword jump table indexed by encID-1. Live ids: 1 VonNeumann, 3 Swarm, 4 Derelict, 5 Monitor, 7 SystemKiller, 8 PuppetMaster, 9 SlaversRefuel, 10 SwarmQueen, 14 Locust, 17 CrowRuins, 20 Refugees, 21 Ortgay. Ids 2, 6, 11-13, 15, 16, 18, 19, 22, 23 and everything outside 1..23 return NULL. Class names read off the vftable store in each ctor",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVScriptObject_EncIDJumpTable",
|
|
"addr": "0x0052bf60",
|
|
"convention": "data",
|
|
"prototype": "void* [23] // the jump table SVScriptObject_FactoryByEncID indexes with encID-1",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSots_GetOrCreateEncObj",
|
|
"addr": "0x005a4450",
|
|
"convention": "thiscall",
|
|
"prototype": "Game::SVScriptObject* (Game::SVSOSots* this, int encID) // scans the loaded (id, obj) pair vector at this+0x0C..0x10 first and only calls SVScriptObject_FactoryByEncID on a miss",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVScriptObject_FactoryByScenarioName",
|
|
"addr": "0x005a7050",
|
|
"convention": "cdecl",
|
|
"prototype": "Game::SVScriptObject* (const char* xscn) // the xsc factory: a flat four-way _stricmp chain, EXHAUSTIVE. \"traps\" -> Game::SVSOTraps (ctor 0x0052cbb0), \"crowdefs\" -> Game::SVSOCrowDefenders (0x0052b3c0), \"indsys\" -> Game::SVSOIndependentSystems (0x0075b530), \"gmtrigger\" -> Game::SVSOGrandMenaceTrigger (0x004f5630). Falls through with \"Error creating extra script %s.\"",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSots_SeedNewGameScripts",
|
|
"addr": "0x005a7d70",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOSots* this) // registers exactly traps, crowdefs, indsys and -- only when ScnObj == NULL -- gmtrigger. Corroborates that the four scenario names are the whole set",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSots_Write",
|
|
"addr": "0x0059ddf0",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOSots* this, Mars::IStream* s) // slot 2 of vftable 0x00A063C4. Emits ScnID, ScnObj (only when non-NULL), numx x (xscn, xsc), NEncObjs x (EncID, EncObj)",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSots_Read",
|
|
"addr": "0x005a7a40",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOSots* this, Mars::IStream* s) // slot 1. Accepts two tags Write never emits -- NPCPlr (int, first) and hastraps (bool) with a following traps object -- read-only backward compatibility; a writer that omits them is correct",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSots_vftable",
|
|
"addr": "0x00a063c4",
|
|
"convention": "data",
|
|
"prototype": "Game::SVSOSots vftable (slot0 dtor, slot1 Read 0x005a7a40, slot2 Write 0x0059ddf0)",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOIndependentSystems_vftable",
|
|
"addr": "0x00a20314",
|
|
"convention": "data",
|
|
"prototype": "Game::SVSOIndependentSystems vftable. slot0 dtor 0x0075afb0; slot1 Read AND slot2 Write are both 0x005f8ac0, the shared `ret 4` no-op stub -- so the \"indsys\" frame is genuinely empty on disk and the class's 0x1c4-byte body is runtime-only state",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "Streamable_NoOpStub",
|
|
"addr": "0x005f8ac0",
|
|
"convention": "thiscall",
|
|
"prototype": "void (void*, Mars::IStream*) // `C2 04 00` -- a bare RET 4 shared as the inherited Read/Write for classes that serialize nothing",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOCrowDefenders_Write",
|
|
"addr": "0x004f8c90",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOCrowDefenders* this, Mars::IStream* s) // sys; ndsys count then a loop writing dsys; ndes count then a loop writing des; drad. NOTE: `dsys` is INSIDE the ndsys loop -- objects/layouts.json records it as a plain member, which is wrong, and no save can settle it because both counts are 0 everywhere",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOMonitor_Write",
|
|
"addr": "0x004fd810",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOMonitor* this, Mars::IStream* s) // calls SVSODerelict::Write (0x004fc2b0) as its first act -- Monitor derives from Derelict, which is why its tag run starts NDsn/DsnID/Dwght + NAsg/Eflt/Esys before nt/scnm/spwt/rsmd/dsgn",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSODerelict_Write",
|
|
"addr": "0x004fc2b0",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSODerelict* this, Mars::IStream* s) // NDsn count then a loop of (DsnID, Dwght); NAsg count then a loop of (Eflt, Esys). Two fields per iteration in each, confirmed by the 8-byte element strides",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-variants.md (lane W 2026-09-08)"
|
|
}
|
|
]
|
|
}
|