sots-re/tools/serializers_golden.py
alex d7ea0a048c lane D: automated struct recovery from the IStreamable serializers
Every serializable class carries an enumeration of its own fields -- its
Write(Stream&), walking the members in order with a 4-char tag. This decodes
that idiom mechanically for the whole binary in 0.35 s.

Validation first (tools/serializers.py validate), against answers the campaign
already had before the tool existed:
  A  305/307 field offsets+kinds exact across 17 classes, 0 WRONG, vs
     struct-recovery.md 1-4 and observedtech-append.md
  B  sizeof from the container-stride divides: ObservedTech 0x2c, MoraleEvent
     0x50, PlayerReport 0x30, DiplomacyStats 0x24 -- all matching
  C  22 of save_reader.py's shapes, tag order identical (Sys 78 tags,
     Player 104, CreateParams 25, Ship 22): 22 agree, 0 disagree
  D  Read/Write cross-check on every class: 437/437 field offsets agree

At scale: 386 classes with a Write, 1,682 member fields.
  verified 87 (542 fields) | clean 77 (328) | unnamed 176 (471)
  partial 31 (341) | empty 15
  58 classes with a sizeof corroborated by a second line of evidence
  (45 container stride, 13 enumeration meeting the embedding bound); the rest
  report a lower bound and say so.

Four things each worth 10-170 classes: the RTTI class hierarchy descriptor as
the only honest "is this an IStreamable" test (a 3-slot vftable also matches
TacAISquadRule_* and the row parsers); mod=0 memory operands, which x86disp.py
cannot index and which hide every field at offset 0; the member->id pointer
idiom behind every handle field; and sub-writers, both base-class and private
(StrategyServer's six id lists live in FUN_00794cd0).

Failure classes are enumerated in the finding -- 176 anonymous-tag classes are
a hard limit on names but not on layout, and the other 64 are bounded
mechanical fixes. Two fields lost to a value assembled across a branch were
left unrecovered rather than patched with an unverifiable heuristic.

Write-back: 288 structures + 328 labels into Ghidra (0 failures), +201
addresses.json entries, header regenerated with tools/gen_addresses.py.

Note: ghidra/addresses.json also carries lane V's already-written live
confirmation text on ObservedTech_sizeof and ServerPlayer_off_ObservedTechs --
their edit, swept in only because we share the file.
2026-09-08 05:51:37 -04:00

275 lines
14 KiB
Python

#!/usr/bin/env python3
"""Golden layouts for `tools/serializers.py --validate`.
Transcribed by hand from the layouts this campaign had already recovered and
cross-checked against real saves *before* this tool existed:
* `findings/objects/struct-recovery.md` sections 1-4 (member tables read out
of the decompiled `Write`/`Read` pairs, COL offsets from RTTI)
* `findings/subsystems/observedtech-append.md` section 4 + 9 (`ObservedTech`,
`sizeof` pinned three independent ways)
* `verify/save-reader/SAVE_FORMAT.md` / `save_reader.py` (on-disk tag order,
`--strict` clean against three real saves, 36 tests)
Offsets are **absolute** (object base), i.e. the decompiled `this`-relative
offset plus the IStreamable COL offset. Kinds use the tool's vocabulary.
Nothing here is derived from the tool: it is the independent answer the tool
has to reproduce.
"""
# tag, absolute offset, kind
GOLDEN = {
"Game::ObservedTech": {
"write": 0x00817CF0, "sizeof": 0x2C,
"fields": [("otnF", 0x04, "int16"), ("otnL", 0x06, "int16"),
("odet", 0x08, "bool"), ("otch", 0x0C, "string"),
("owith", 0x28, "int")],
},
"Game::ObservedWeapon": {
"write": 0x00817BC0, "sizeof": 0x2C,
"fields": [("otnF", 0x04, "int16"), ("otnL", 0x06, "int16"),
("odet", 0x08, "bool"), ("owep", 0x0C, "string"),
("owith", 0x28, "int")],
},
"Game::StarSystem::OutputRates": {
"write": 0x00745190, "sizeof": 0x1C,
"fields": [("SRt", 0x00, "float"), ("SRsc", 0x04, "float"),
("SRtf", 0x08, "float"), ("SRi", 0x0C, "float"),
("SRoh", 0x10, "float"), ("SRs", 0x14, "float"),
("SRnr", 0x18, "int")],
},
"Game::ServerSystem": {
"write": 0x00749630, "sizeof": None,
"fields": [
("Pos", 0x18, "object"),
("R", 0x4C, "float"), ("G", 0x50, "float"), ("B", 0x54, "float"),
("A", 0x58, "float"), ("Idx", 0x5C, "int"), ("Size", 0x60, "int"),
("Suit", 0x64, "float"), ("Res", 0x68, "int"),
("ARes2", 0x6C, "int"), ("MRes", 0x70, "int"),
("TRes", 0x74, "int"), ("haltv", 0x78, "bool"),
("OutMod", 0x7C, "float"), ("TAcq", 0x80, "int"),
("TFAcq", 0x84, "int"), ("Rts", 0x88, "object"),
("BQ", 0xA4, "object"), ("Name", 0xA8, "string"),
("Abdn", 0xC4, "bool"), ("Dstyd", 0xC5, "bool"),
("vnh", 0xC6, "bool"), ("vnd", 0xC7, "bool"),
("vnex3", 0xC8, "bool"), ("vnpex3", 0xC9, "bool"),
("VFlags", 0xCC, "int"), ("EFlags", 0xD0, "int"),
("AFlags", 0xD4, "int"), ("FFlags", 0xD8, "int"),
("GFlags", 0xDC, "int"), ("MnRFlags", 0xE0, "int"),
("RfRFlags", 0xE4, "int"), ("ClkFlags", 0xE8, "int"),
("Bats2", 0xF0, "int64"), ("rcex", 0xF8, "int64"),
("PID", 0x100, "handle"), ("dcs", 0x104, "object"),
("dsu", 0x118, "float"), ("cm", 0x11C, "object"),
("cme2", 0x13C, "vector"), ("PvCM", 0x14C, "object"),
("NumFlts", 0x16C, "vector"), ("RepCur", 0x17C, "float"),
("RepMax", 0x180, "float"), ("EggScio", 0x184, "int"),
("NoRebAI", 0x188, "bool"), ("PvNoRebAI", 0x189, "bool"),
("Pop", 0x18C, "int"), ("Infra", 0x190, "float"),
("pbon", 0x194, "int"), ("ibon", 0x198, "float"),
("TerrFl", 0x19C, "int"), ("Pop2", 0x1A0, "object"),
("pbon2", 0x1B4, "object"), ("indi", 0x1C8, "object"),
("spies2", 0x1CC, "vector"), ("rbfl", 0x1DC, "int"),
("hsrg", 0x1E0, "bool"), ("adt", 0x1E4, "int"),
("PvPop", 0x200, "int"), ("PvInfra", 0x204, "float"),
("PvSuit", 0x208, "float"), ("PvRes", 0x20C, "int"),
("PvARes2", 0x210, "int"), ("PvMRes", 0x214, "int"),
("PvPop2", 0x218, "object"), ("DefF", 0x238, "handle"),
("DefSF", 0x23C, "handle"), ("NumGFs", 0x240, "vector"),
("NumSnF", 0x250, "vector"), ("NumMnF", 0x260, "vector"),
("NumPlgs2", 0x2A8, "vector"), ("TnsOH", 0x2B8, "int"),
("TDst", 0x2BC, "int"), ("ntdev", 0x2C4, "int"),
("ltis", 0x2C8, "int"), ("rbtn", 0x2CC, "int"),
("rbfr", 0x2D0, "int"), ("rbwn", 0x2D4, "int"),
],
},
"Game::StarSystem::PlayerView": {
"write": 0x007492D0, "sizeof": None,
"fields": [("VTrn", 0x08, "int"), ("Pop", 0x0C, "int"),
("Pop2", 0x10, "object"), ("Infra", 0x24, "float"),
("Suit", 0x28, "float"), ("Res", 0x2C, "int"),
("ARes2", 0x30, "int"), ("MRes", 0x34, "int"),
("NoRebAI", 0x38, "bool"), ("pbon", 0x3C, "int"),
("pbon2", 0x40, "object"), ("ibon", 0x54, "float"),
("TerrFl", 0x58, "int")],
},
"Game::PopulationGroup": {
"write": 0x00536AF0, "sizeof": 0x18,
"fields": [("PopT", 0x04, "int"), ("PopS", 0x08, "int"),
("PopC", 0x10, "int64")],
},
"Game::IndependenceInfo": {
"write": 0x00748EE0, "sizeof": 0x70,
"fields": [("indsp", 0x04, "int"), ("indcl", 0x08, "object"),
("indnm", 0x1C, "string"), ("indav", 0x38, "string"),
("indba", 0x54, "string")],
},
"Game::MoraleEvent": {
"write": 0x007491B0, "sizeof": 0x50,
"fields": [("mid", 0x04, "int"), ("mtr", 0x08, "int"),
("mn", 0x0C, "int"), ("mtp", 0x10, "int"),
("mfx", 0x14, "object"), ("mdsc", 0x34, "string")],
},
"Game::ShipBuildOrder": {
"write": 0x00813800, "sizeof": None,
"fields": [("desID", 0x04, "int"), ("con", 0x08, "int"),
("sav", 0x0C, "int"), ("conleft", 0x10, "int"),
("ordID", 0x14, "int")],
},
"Game::DiplomacyStats": {
"write": 0x00818CB0, "sizeof": 0x24,
"fields": [("other", 0x04, "int"),
("lastnap", 0x08, "int16"), ("lastnapbty", 0x0A, "int16"),
("bknnap", 0x0C, "int16"), ("btynap", 0x0E, "int16"),
("lastally", 0x10, "int16"), ("lastallybty", 0x12, "int16"),
("bknally", 0x14, "int16"), ("btyally", 0x16, "int16"),
("lastcf", 0x18, "int16"), ("lastcfbty", 0x1A, "int16"),
("bkncf", 0x1C, "int16"), ("btycf", 0x1E, "int16"),
("deadhome", 0x20, "int16")],
},
"Game::PlayerReport": {
"write": 0x00817480, "sizeof": 0x30,
"fields": [("oid", 0x04, "int"), ("pid", 0x08, "int"),
("flds", 0x0C, "int"), ("sav", 0x10, "int"),
("home", 0x14, "int"), ("ncol", 0x18, "int"),
("mpwr", 0x1C, "int"), ("mcls", 0x20, "int"),
("mmsl", 0x24, "int"), ("nshp", 0x28, "int"),
("nsat", 0x2C, "int")],
},
"Game::FlightPlan": {
"write": 0x00700F60, "sizeof": 0x38,
"fields": [("wpts", 0x04, "vector"), ("FPsp2", 0x14, "float"),
("FPeta2", 0x18, "int"), ("FPogn2", 0x1C, "object"),
("FPdpos", 0x28, "object"), ("pnd", 0x34, "int")],
},
"Game::FlightPlan::Waypoint": {
"write": 0x00700ED0, "sizeof": None,
"fields": [("Wpt", 0x04, "int"), ("Tp", 0x08, "int"),
("nrt", 0x0C, "object")],
},
"Game::NodeRoute": {
"write": 0x006E22E0, "sizeof": None,
"fields": [("nrp", 0x04, "int"), ("nrf", 0x08, "int"),
("nrt", 0x0C, "int")],
},
"Game::StarFleet": {
"write": 0x00701070, "sizeof": None,
"fields": [("Pos", 0x18, "object"), ("PrvPos", 0x4C, "object"),
("PID", 0x58, "handle"), ("FtName", 0x5C, "string"),
("Perm", 0x78, "bool"), ("Lay", 0x7C, "object"),
("LocID", 0xA0, "handle"), ("NShips", 0xA4, "vector"),
("FPlan", 0xC4, "object"), ("FtTrans", 0xFC, "int"),
("FtOrig", 0x100, "object"), ("FtFlg", 0x10C, "int"),
("Ftae", 0x110, "int"), ("Ftpae", 0x114, "int"),
("FtEnc", 0x118, "int"), ("FtMS", 0x11C, "int")],
},
"Game::StarShip": {
"write": 0x008291F0, "sizeof": None,
"fields": [("PlrID", 0x10, "handle"), ("DesID", 0x14, "int"),
("Range", 0x20, "float"), ("Health", 0x24, "object"),
("MineCap", 0x34, "int"), ("NTH", 0x38, "vector"),
("Plg", 0x48, "int"), ("Act", 0x4C, "int"),
("Dep", 0x50, "bool"), ("Atq", 0x51, "bool"),
("LCT", 0x5C, "int"), ("tsd", 0x60, "int"),
("FltID", 0x64, "handle"), ("ConCap", 0x68, "int"),
("RefCap", 0x6C, "float"), ("RepCap", 0x70, "float"),
("EncID", 0x7C, "int"), ("PrisH", 0x80, "object"),
("BQ2", 0x98, "object"), ("pop", 0x9C, "object"),
("ppop", 0xA0, "object"), ("atsp", 0xA8, "int"),
("tblt", 0xAC, "int")],
},
"Game::ServerPlayer": {
"write": 0x008563E0, "sizeof": None,
"fields": [
("PlyrIdx", 0x28, "int"), ("HomeSys", 0x2C, "handle"),
("NumOwn", 0x30, "vector"), ("PlryName", 0x40, "string"),
("Species", 0x5C, "int"), ("ClrID", 0x60, "object"),
("Bdg", 0x74, "string"), ("Avt", 0x90, "string"),
("Team", 0xAC, "int"), ("IdealSuit", 0xB0, "float"),
("SuitTol", 0xB4, "float"), ("MaxOH", 0xB8, "float"),
("ResRate", 0xBC, "float"), ("ResMod", 0xC0, "float"),
("ResScl", 0xC4, "float"), ("TRM", 0xD0, "float"),
("TRA", 0xD4, "int"), ("TRP", 0xD8, "int"),
("NumDes", 0xE4, "vector"), ("TechTree", 0xF4, "object"),
("Elim", 0xF8, "bool"), ("NPC", 0xFB, "bool"),
("RebAI", 0xFC, "bool"), ("ReqCL", 0xFD, "bool"),
("AIBn", 0xFE, "bool"), ("CnTrd", 0xFF, "bool"),
("CnRad", 0x100, "bool"), ("CnVItl", 0x101, "bool"),
("hgs", 0x102, "bool"), ("hadvs", 0x103, "bool"),
("harcc", 0x104, "bool"), ("pddm", 0x108, "float"),
("OutMod", 0x124, "float"), ("RebOutMod", 0x128, "float"),
("ScOutMod", 0x12C, "float"), ("PopMod", 0x130, "float"),
("TerraMod", 0x134, "float"), ("AMine", 0x138, "bool"),
("MinPure", 0x13C, "float"), ("MinRate", 0x140, "float"),
("NGts", 0x144, "int"), ("PrGtTrf", 0x148, "int"),
("GTraf", 0x14C, "int"), ("CstR", 0x150, "float"),
("CstE", 0x154, "float"), ("CstT", 0x158, "float"),
("Maint", 0x15C, "int"), ("shrm", 0x160, "float"),
("Status", 0x164, "int"), ("Team", 0x168, "object"),
("NumLeg", 0x178, "vector"), ("PvSav", 0x188, "int"),
("PvMA", 0x18C, "bool"), ("HasDisc", 0x19C, "int"),
("HasDiscSp", 0x1A0, "int"), ("HasDiscCl", 0x1A4, "int"),
("HasEnc", 0x1A8, "int"), ("HasEng", 0x1AC, "int"),
("ShipRecs", 0x1B0, "object"), ("Ojvs", 0x1F4, "vector"),
("Nexp", 0x204, "vector"), ("NWeapXcl", 0x214, "vector"),
("dipstats", 0x230, "vector"), ("comms", 0x240, "object"),
("preps", 0x244, "vector"), ("odes", 0x254, "vector"),
("owep", 0x264, "vector"), ("otch", 0x274, "vector"),
("Sav", 0x284, "int"), ("HasImm", 0x288, "int"),
("HasVac", 0x28C, "int"), ("NPTrk", 0x290, "int"),
("ResTNm", 0x294, "object"), ("FNG", 0x298, "object"),
("Events", 0x29C, "object"), ("BnkWrn", 0x2C4, "int"),
("BnkTrn", 0x2C8, "int"), ("BnkEl", 0x2CC, "int"),
("BnkPr", 0x2D0, "int"), ("plcy", 0x2D8, "int"),
("pswd", 0x2DC, "string"), ("Srn", 0x2F8, "bool"),
("SrnTo", 0x2FC, "handle"), ("lboid", 0x300, "int"),
("lcid2", 0x304, "int"), ("IncMod", 0x30C, "float"),
("aid", 0x310, "vector"), ("ndeflay", 0x320, "vector"),
("cdp", 0x330, "bool"), ("spy2", 0x334, "object"),
("rdtc", 0x338, "vector"), ("aidf", 0x368, "int"),
("civr", 0x370, "object"), ("tnc", 0x39C, "int"),
("NumPR", 0x3A4, "vector"), ("ResErrRoll", 0x3B4, "bool"),
("cta", 0x3B5, "bool"), ("AIR", 0x3B8, "object"),
("AIEnf", 0x3BC, "object"), ("NSprj", 0x3C0, "vector"),
("NextPrjID", 0x3D0, "int"), ("lret", 0x3D4, "int"),
("nmeid", 0x3DC, "int"),
],
},
}
# sizeof values the campaign pinned independently of any serializer walk
GOLDEN_SIZEOF = {
"Game::ObservedTech": 0x2C, # magic divide + imul + search stride
"Game::ObservedWeapon": 0x2C,
"Game::PopulationGroup": 0x18, # Population's own vector stride
"Game::MoraleEvent": 0x50,
"Game::PlayerReport": 0x30,
"Game::DiplomacyStats": 0x24,
"Game::StarSystem::OutputRates": 0x1C,
}
# on-disk tag order, from save_reader.py shapes -> the serializer that writes it
DISK_ORDER = {
0x00829960: "Summary", # StrategyGameInfo::Write
0x008276D0: "Slot", # SlotDef::Write
0x0082AE40: "CreateParams", # StrategyGameCreateParams::Write
0x0082AD40: "Scrp", # StrategyScriptParams::Write
0x0056EC00: "PrisonerHold",
0x008189A0: "SystemEvent",
0x0082C290: "PlayerTurnStats",
0x0082C4A0: "PlayerTurnHistory",
0x0082C5A0: "TurnStats",
0x0079FA70: "Sim", # StrategyServer::Write
0x00749630: "Sys", # ServerSystem::Write
0x008563E0: "Player", # ServerPlayer::Write
0x00701070: "Flt", # StarFleet::Write
0x008291F0: "Ship", # StarShip::Write
0x00700F60: "FlightPlan",
0x00817CF0: "Otch",
0x00817BC0: "Owep",
0x00745190: "Rts",
0x00748EE0: "IndependenceInfo",
0x00813800: "BuildOrder",
0x00817480: "Prep",
0x00818CB0: "DipStat",
0x007492D0: "PlayerView",
}