Blocker #4 on lane Y's path to a byte-matching turn. HOW DESIGNS PERSIST, correcting a published finding. Game::ShipDesign::Write is 0x008325e0 and makes four stream calls. The recorded address 0x008747a0 is in NO vftable at all (lane V2's inversion), so "ShipDesign::Write makes no stream call" was a misattribution, not a fact about the class. Game::ShipDesign derives from Game::ShipDesignDef and inherits IStreamable second, so its writer is reached through an adjustor thunk -- which is what the slot-indexed serializer sweep found instead. A design persists as two serializers, base and derived: ShipDesignDef::Write 0x00827390 emits FAIDes/DHide/DWep/DName then exactly three DSec frames (+0x4c command, +0x24 mission, +0x74 engine), ShipDesign::Write appends Dtc, the Dwgv flag and a conditional Dwg frame. THREE sections, not five. The campaign's "slots 3-4 reserved and always empty" was save_reader.py's trailing Rest("sections") sweeping Dtc and Dwgv into the section list, and stock_designs.py decoding them as two empty sections -- rule 8 in its exact form, reader and consumer agreeing with each other and both wrong. Two independent enumerations say three: the writer's straight-line body, and the ctor's eh_vector_constructor_iterator(this+0x24, 0x28, 3) closing at 0x9c = sizeof(ShipDesignDef). DWep and Dwgv are BOOLs, not ints -- byte-indistinguishable from ints at a four-character tag, the same class of defect as ObservedTech.odet. THE 0x400 FLAG IS `defence_platform`, read off the .shipsection parser's own bit setter at 0x005749b7. NOT lane B5's 0x400: that one is a fleet flag, on the wire as FtFlg. The full role-flag table is in the finding. HULL SIZE is section_class through a three-name stricmp table (Destroyer/Cruiser/Dreadnought -> 0/1/2), absent or unrecognised meaning 0 with a log line rather than an error. Both words are recomputed from the data files by ShipDesign::UpdateDerivedStats 0x0087e7c0 and neither is on the wire. Corroborated by the default hull-health table the same bit picks: 500/3000/15000 without it, 100/500/1000 with. MEASURED: the census rebuilt from each save's own state matches the record the game archived, 480 leaves / 0 mismatched over 11 saves and 503 designs, computed independently in Python and in C++. COVERAGE REPORTED AS LOUDLY: only 32 of the 480 leaves are nonzero, and three of the six census leaves (both cruiser rows, dreadnought platforms) are unexercised by every save in the corpus. Closed 0 / regressed 0 against the standalone's divergence list, reported separately: the census leaves live in src/app's turn record, which lane A2 holds this cycle, so this lane evaluated and reported rather than writing. Oracles fixed openly (rule 12): save_reader.py's Des shape, 49/49 with three corrected tests and one added that pins "exactly three DSec" against real saves; stock_designs.json regenerated, whose diff is only raw_slots 5->3 and dWep int->bool across all 127 designs with every other field identical; test_design_rules.py still 32/32 with the same ground truth. 19 addresses in ghidra/addresses.d/lane-d2.json, no collision; the generated header was validated to a scratch path, never written in place.
156 lines
12 KiB
JSON
156 lines
12 KiB
JSON
{
|
|
"entries": [
|
|
{
|
|
"name": "Game_ShipDesignDef_Write",
|
|
"addr": "0x00827390",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game_ShipDesignDef* this, Mars::Stream* s) // THE DESIGN SERIALIZER LANE D SAID DID NOT EXIST. Slot 1 of the ShipDesignDef vftable 0x009fef64. Writes, in DISK order: WriteBool 'FAIDes' this+0x4, WriteBool 'DHide' this+0x5, WriteBool 'DWep' this+0x6 (a BOOL, not an int -- the campaign schema had it as int; byte-neutral because a 4-char tag makes both items 12 bytes), WriteString 'DName' this+0x8, then THREE 'DSec' frames through StreamableHelper<ShipDesignDef::Section> at this+0x4c, this+0x24, this+0x74 in that order. THREE sections, not five: the ctor 0x00874c70 runs eh_vector_constructor_iterator(this+0x24, stride 0x28, count 3). MEMORY ORDER != WRITE ORDER: the array is [+0x24, +0x4c, +0x74] and the wire is [+0x4c (command), +0x24 (mission), +0x74 (engine)]",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ShipDesignDef_Read",
|
|
"addr": "0x00827240",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game_ShipDesignDef* this, Mars::Stream* s) // slot 0 of vftable 0x009fef64. Mirrors Write field for field, same tags, same three DSec frames in the same order",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ShipDesign_Write",
|
|
"addr": "0x008325e0",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game_ShipDesign* this, Mars::Stream* s) // CORRECTS 'Game::ShipDesign::Write (0x008747a0) makes no stream call at all': 0x008747a0 is in NO vftable and is not this class's writer. Game::ShipDesign derives from Game::ShipDesignDef (RTTI 0x00a894a8: ShipDesign, ShipDesignDef, IStreamable, RefCounted, NetworkObject; IStreamable sub-object at +0x9c, NetworkObject at +0xa0). Primary vftable 0x00a32720 slot 1; the +0x9c IStreamable vftable 0x00a32710 reaches it through an adjustor thunk at 0x00874de0. Body: direct call to ShipDesignDef::Write 0x00827390 (the base part), then WriteInt 'Dtc' this+0x134, WriteBool 'Dwgv' this+0x16c, and ONLY IF that flag is set a 'Dwg' frame through StreamableHelper<Game::WeaponGroups> at this+0x170. Dwgv is false in all 11 saves, so the Dwg branch is unexercised (rule 6)",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ShipDesign_Read",
|
|
"addr": "0x008324f0",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game_ShipDesign* this, Mars::Stream* s) // primary vftable 0x00a32720 slot 0. Calls ShipDesignDef::Read, then ReadInt 'DRefCnt' into a NULL destination (read and discarded; the WRITER never emits it, so it is a network-stream field the tag-addressed reader tolerates), then 'Dtc', 'Dwgv' and the conditional 'Dwg'",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ShipDesign_ctor",
|
|
"addr": "0x00874c70",
|
|
"convention": "thiscall",
|
|
"prototype": "Game_ShipDesign* (Game_ShipDesign* this) // installs ShipDesignDef::vftable at +0, then IStreamable/NetworkObject at +0x9c/+0xa0 and the three ShipDesign vftables. Constructs the DName string at +0x8 and eh_vector_constructor_iterator(this+0x24, 0x28, 3) -- the three section records. Enumerates sizeof(Game_ShipDesignDef) == 0x9c",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ShipDesign_UpdateDerivedStats",
|
|
"addr": "0x0087e7c0",
|
|
"convention": "fastcall",
|
|
"prototype": "void (Game_ShipDesign* this) // THE RECOMPUTE THAT PRODUCES BOTH CENSUS WORDS. 16 call sites; one of them stamps this+0x134 (Dtc) and calls straight in. (1) resolves each of the three section records this+0x24/+0x4c/+0x74 to a ShipSectionDef* through the catalog lookup 0x0056edd0 and caches them at this+0xac/+0xb0/+0xb4, IN MEMORY SLOT ORDER; (2) builds a 3-element context array (stride 0x124) and hands it to the aggregator 0x00826af0 together with this+0x130; (3) copies the aggregator's ~0x7c-byte output struct into the design: struct+0x10 -> this+0xb8 (role flags, low dword), struct+0x14 -> this+0xbc (high dword), struct+0x18..+0x64 -> this+0xc0..+0x118, struct+0x74 -> this+0x12c (HULL SIZE), struct+0x78 -> this+0x1a4, struct+0x00..+0x0c -> this+0x138..+0x144. Neither this+0xb8 nor this+0x12c is on the wire: they are rebuilt from the data files whenever a design changes or is loaded",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ShipDesign_AggregateSectionStats",
|
|
"addr": "0x00826af0",
|
|
"convention": "cdecl",
|
|
"prototype": "void (DesignStatBlock* out, SectionContext ctx[3], void* techCtx, char flag) // walks the three section contexts (stride 0x124, ctx[i]+0 = the ShipSectionDef*, null = empty slot). out[4]/out[5] (the 64-bit role-flag word) |= ShipSectionDef::GetRoleFlags 0x0056ee80 per section -- an OR, so ONE flagged section flags the whole design. out[0x1d] (hull size) = sectionDef+0x260 -- an ASSIGNMENT, so the LAST resolved section in memory slot order wins; every shipped design is section_class-homogeneous so first-wins and last-wins agree on all 503 design records in the corpus. Also: out[6] x5 when any section carries tech 0x2756; out[4] &= ~0x80 for tech 0x2757; out[4] |= 0x100000 when EVERY section carries tech 0x2742. Default hull health (out[2]) is chosen by the SAME 0x400 bit when no section overrides it at +0x2ac: without 0x400 hull 0/1/2 -> 500/3000/15000, with 0x400 -> 100/500/1000",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ShipSectionDef_GetRoleFlags",
|
|
"addr": "0x0056ee80",
|
|
"convention": "thiscall",
|
|
"prototype": "unsigned __int64 (Game_ShipSectionDef* this, void* ctx) // returns CONCAT(this+0x29c, this+0x298) -- the section's 64-bit role-flag word straight out of the parsed .shipsection -- with bit 0x20 of the low dword OR-ed in when ctx is non-null, ctx+0xfc is set and this+0x304 > 0. That conditional bit is the only part of the word that is not pure file data",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ShipSectionCatalog_FindByID",
|
|
"addr": "0x0056edd0",
|
|
"convention": "thiscall",
|
|
"prototype": "Game_ShipSectionDef* (SectionCatalog* this, Game_ShipSectionID* id) // linear scan of the vector at this+0x8/+0xc matching def+0x4 == id->species and def+0x8 == id->sectionId. Returns null for the (0,0) empty slot without scanning",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ParseShipClassName",
|
|
"addr": "0x0056e1c0",
|
|
"convention": "cdecl",
|
|
"prototype": "bool (int* out, const char* name) // THE HULL-SIZE DEFINITION. _stricmp against \"Destroyer\", \"Cruiser\", \"Dreadnought\" in that order and stores the index it stopped at: 0, 1, 2. Returns false without writing on no match",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ShipSectionDef_ParseSectionClass",
|
|
"addr": "0x0056e250",
|
|
"convention": "cdecl",
|
|
"prototype": "bool (int* out, const char* name) // wraps 0x0056e1c0 for the `section_class` key; on failure logs \" [%s] unrecognized ship class\" and stores 0, so an unknown OR ABSENT section_class is a destroyer, not an error",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_SetRoleFlagBit",
|
|
"addr": "0x0056e780",
|
|
"convention": "cdecl",
|
|
"prototype": "void (unsigned __int64* flags, unsigned int loMask, unsigned int hiMask, bool value) // the .shipsection parser's flag setter. Every boolean role key in the section parser 0x005744e0 is one call to this with its own mask pair; `defence_platform` is (lo 0x400, hi 0) at 0x005749b7, `monitor` is (lo 0, hi 0x4), `refinery` 0x8, `mining_capacity` 0x10, `scanrange`/`rebelai_scanrange` 0x20, `gateship` 0x40, `ewar` 0x800, `ramscoop` 0x1000, `aicontrol` 0x2000, `command_quota` 0x10000, `node_bore` 0x20000, `prisoner_capacity` 0x40000, `freighter` 0x80000, `gravboat_bonus` 0x200000, `construction_capacity` 0x400000, `science` 0x1000000, `tradingpost` 0x2000000, `freighterQ` 0x8000000, `police` 0x10000000, `spy` 0x40000000, `spytender` 0x80000000, `refueling_capacity` 0x2, `repair_capacity` 0x4; high dword: `colony_trap` 0x1, `mining_trap` 0x2, `monitor` 0x4, `propaganda` 0x10",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ServerPlayer_ShipCensusByHullClass",
|
|
"addr": "0x00818a50",
|
|
"convention": "cdecl",
|
|
"prototype": "void (ServerPlayer* p, int out[8]) // THE CENSUS. Zeroes out[0..7], then walks the server's fleet vector (p+0x8 -> S, S+0x60/+0x64), keeps fleets whose owner (fleet+0x58) is p, and for every ship in fleet+0xa4/+0xa8 takes design = ship+0x14. If (design+0xb8 & 0x400) == 0: ++out[0] and ++out[2 + design->hullSize(+0x12c)]; else ++out[1] and ++out[5 + hullSize]. So out[0]/out[1] are the two grand totals (computed and DISCARDED by the caller), out[2..4] are ships by hull size 0/1/2 and out[5..7] are defence platforms by hull size. The six land at turnRecord+0x2a..+0x34 and reach the wire as the three `cls` groups' `shpt` and `satt`",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "Game_ShipDesign_IsMobileWarshipClass",
|
|
"addr": "0x0081a430",
|
|
"convention": "cdecl",
|
|
"prototype": "int (Game_ShipDesign* design) // a SECOND classifier over the same two words, kept because it shows the flag word is a role set and not a single bit: returns -1 when the design lacks flag 0x80000 (`freighter`), else for hull size 1 returns 0 when 0x8000000 (`freighterQ`) is set and 1 otherwise, and 2 for any other hull size. 0x0082c7c0 is the matching counter over a fleet list. NOT the census -- neither reads 0x400",
|
|
"status": "unverified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "ShipDesign_off_RoleFlagsLow",
|
|
"offset": "0xb8",
|
|
"convention": "offset",
|
|
"prototype": "unsigned int // low dword of the design's 64-bit role-flag word, the OR of its sections'. Bit 0x400 = `defence_platform`. High dword at +0xbc. NOT on the wire; rebuilt by 0x0087e7c0",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "ShipDesign_off_HullSize",
|
|
"offset": "0x12c",
|
|
"convention": "offset",
|
|
"prototype": "int // 0 destroyer / 1 cruiser / 2 dreadnought, from the last resolved section's `section_class`. NOT on the wire; rebuilt by 0x0087e7c0",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "ShipDesign_off_Dtc",
|
|
"offset": "0x134",
|
|
"convention": "offset",
|
|
"prototype": "int // the wire field `Dtc`, written by ShipDesign::Write and stamped by at least one caller immediately before it calls the recompute 0x0087e7c0",
|
|
"status": "unverified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "ShipSectionDef_off_SectionClass",
|
|
"offset": "0x260",
|
|
"convention": "offset",
|
|
"prototype": "int // parsed `section_class`: 0 destroyer / 1 cruiser / 2 dreadnought, 0 when absent or unrecognised",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
},
|
|
{
|
|
"name": "ShipSectionDef_off_RoleFlagsLow",
|
|
"offset": "0x298",
|
|
"convention": "offset",
|
|
"prototype": "unsigned int // low dword of the section's 64-bit role-flag word (high dword at +0x29c), one bit per boolean role key in the .shipsection file",
|
|
"status": "verified",
|
|
"source": "findings/objects/ship-design-catalogue.md"
|
|
}
|
|
]
|
|
}
|