// mars::stream — typed shapes of the SotS 1.8 save stream. // // Field orders and types follow verify/save-reader/SAVE_FORMAT.md (the // confirmed format). A("x") tags are on-disk names; R("x") fields are written // by the game with a NULL name ("." on disk) or carry a reference name whose // disk spelling differs, and are matched by position. Bodies the format keeps // opaque are held as generic Nodes so a shape can be re-emitted byte-for-byte. // After the AIAgent round only two regions are still carried that way: the RNG // state block (correctly opaque) and the one CD block per save whose CDT id ends // ".TurnCommands_v5", which needs a save with issued orders before it can be typed. #pragma once #include #include #include #include #include "archive.h" namespace mars::stream::shapes { // Game::PlayerColorID — "." int index; iff -1 the three "." ints r,g,b follow. struct PlayerColor { static constexpr const char* kStreamName = ""; int32_t idx = 0; int32_t r = 0, g = 0, b = 0; std::vector extra; template void io(Ar& ar) { ar.i32(R("idx"), idx); ar.when(idx == -1, [&](Ar& a) { a.i32(R("r"), r); a.i32(R("g"), g); a.i32(R("b"), b); }); ar.rest(extra); } }; // ---- Summary (StrategyGameInfo) --------------------------------------------- struct PlayerSettings { // StrategyPlayerGameSettings: 4 x "." int static constexpr const char* kStreamName = "Settings"; int32_t treasury = 0, colonies = 0, techs = 0, difficulty = 0; std::vector extra; template void io(Ar& ar) { ar.i32(R("treasury"), treasury); ar.i32(R("colonies"), colonies); ar.i32(R("techs"), techs); ar.i32(R("difficulty"), difficulty); ar.rest(extra); } }; struct Slot { // SlotDef static constexpr const char* kStreamName = "Slot"; bool isPlay = false, isDead = false, isReq = false, isRec = false; bool isFxNm = false; std::string fxNm; bool isFxSp = false; int32_t fxSp = 0; bool isFxCr = false; PlayerColor fxCrID; bool isFxBd = false; std::string fxBd; bool isFxAv = false; std::string fxAv; int32_t tag = 0; std::string pwd; int32_t team = 0; PlayerSettings settings; std::vector extra; template void io(Ar& ar) { ar.b(A("IsPlay"), isPlay); ar.b(A("IsDead"), isDead); ar.b(A("IsReq"), isReq); ar.b(A("IsRec"), isRec); ar.b(A("IsFxNm"), isFxNm); ar.str(A("FxNm"), fxNm); ar.b(A("IsFxSp"), isFxSp); ar.i32(A("FxSp"), fxSp); ar.b(A("IsFxCr"), isFxCr); ar.obj(A("FxCrID"), fxCrID); ar.b(A("IsFxBd"), isFxBd); ar.str(A("FxBd"), fxBd); ar.b(A("IsFxAv"), isFxAv); ar.str(A("FxAv"), fxAv); ar.i32(A("Tag"), tag); ar.str(A("Pwd"), pwd); ar.i32(A("Team"), team); ar.obj(A("Settings"), settings); ar.rest(extra); } }; struct PlayerInfo { // element of Summary.Players ("." frame) static constexpr const char* kStreamName = ""; Slot slot; int32_t rank = 0; std::vector extra; template void io(Ar& ar) { ar.obj(A("Slot"), slot); ar.i32(A("Rank"), rank); ar.rest(extra); } }; struct Tmrs { static constexpr const char* kStreamName = "TMRS"; float tstl = 0, tctl = 0, tqtl = 0, tqtle = 0; std::vector extra; template void io(Ar& ar) { ar.f32(A("TSTL"), tstl); ar.f32(A("TCTL"), tctl); ar.f32(A("TQTL"), tqtl); ar.f32(A("TQTLE"), tqtle); ar.rest(extra); } }; struct Session { static constexpr const char* kStreamName = "Session"; Tmrs tmrs; std::vector extra; template void io(Ar& ar) { ar.obj(A("TMRS"), tmrs); ar.rest(extra); } }; struct Summary { static constexpr const char* kStreamName = "Summary"; std::string gameName; int32_t turn = 0, numSys = 0, checksum = 0; std::vector players; Session session; int32_t mapShape = 0; float incMod = 0, resMod = 0; bool alliances = false, teams = false, encounters = false; std::string scenario; std::vector extra; template void io(Ar& ar) { ar.str(A("GameName"), gameName); ar.i32(A("Turn"), turn); ar.i32(A("NumSys"), numSys); ar.i32(A("Checksum"), checksum); ar.carr(A("Players"), players); ar.obj(A("Session"), session); ar.i32(A("MapShape"), mapShape); ar.f32(A("IncMod"), incMod); ar.f32(A("ResMod"), resMod); ar.b(A("Alliances"), alliances); ar.b(A("Teams"), teams); ar.b(A("Encounters"), encounters); ar.str(A("Scenario"), scenario); ar.rest(extra); } }; // ---- CreateParams (StrategyGameCreateParams) ---------------------------------- struct Planet { // SystemParams element, all tags "." static constexpr const char* kStreamName = ""; Vec3 pos; // p1 is a std::string, not an int: SystemParams::Write calls the string // writer for it (sots-re objects/streams.json, Game::SystemParams item 1). // It is the empty string in every real save, and an empty string is four // zero bytes -- byte-identical to the int 0 the community reader assumed, // which is why reading it as an int has never broken a round trip. A save // carrying a non-empty name here would have desynchronised the parse. std::string p1; int32_t p2 = 0, p3 = 0; float p4 = 0; std::vector extra; template void io(Ar& ar) { ar.vec3(R("pos"), pos); ar.str(R("p1"), p1); ar.i32(R("p2"), p2); ar.i32(R("p3"), p3); ar.f32(R("p4"), p4); ar.rest(extra); } }; struct MapP { // StarMapParams, all tags "." static constexpr const char* kStreamName = "MapP"; int32_t mapType = 0; std::vector planets; // VectorHelper std::vector> players; // "." count, n x "." frame{ "." count, n x "." int } std::vector nodePaths; // VectorHelper, empty in the real saves std::vector extra; template void io(Ar& ar) { ar.i32(R("mapType"), mapType); ar.carr(R("planets"), planets); ar.narr(R("players"), players, [](Ar& a, std::vector& e) { a.carr(R("."), e); }); ar.carr(R("nodePaths"), nodePaths); ar.rest(extra); } }; struct ScriptParam { std::string spsn, sppn, sppv; template void io(Ar& ar) { ar.str(A("spsn"), spsn); ar.str(A("sppn"), sppn); ar.str(A("sppv"), sppv); } }; struct Scrp { // StrategyScriptParams static constexpr const char* kStreamName = "scrp"; std::vector params; std::vector extra; template void io(Ar& ar) { ar.narr(A("spc"), params, [](Ar& a, ScriptParam& e) { e.io(a); }); ar.rest(extra); } }; struct CreateParams { static constexpr const char* kStreamName = "CreateParams"; std::string name; int32_t id = 0, rseed = 0, aid = 0; std::string key; MapP mapP; int32_t mapS = 0; std::string mapF; int32_t nSys = 0; float rEnc = 0, sDist = 0, sSize = 0, sRes = 0, sSuit = 0; int32_t maxP = 0, aSpec = 0; bool bAlly = false; int32_t nTeam = 0; bool tmgrp = false; int32_t pSav = 0, pCol = 0, pTech = 0; float incM = 0, resM = 0; Scrp scrp; std::vector extra; template void io(Ar& ar) { ar.str(A("Name"), name); ar.i32(A("ID"), id); ar.i32(A("RSeed"), rseed); ar.i32(A("AID"), aid); ar.str(A("Key"), key); ar.obj(A("MapP"), mapP); ar.i32(A("MapS"), mapS); ar.str(A("MapF"), mapF); ar.i32(A("NSys"), nSys); ar.f32(A("REnc"), rEnc); ar.f32(A("SDist"), sDist); ar.f32(A("SSize"), sSize); ar.f32(A("SRes"), sRes); ar.f32(A("SSuit"), sSuit); ar.i32(A("MaxP"), maxP); ar.i32(A("ASpec"), aSpec); ar.b(A("bAlly"), bAlly); ar.i32(A("NTeam"), nTeam); ar.b(A("tmgrp"), tmgrp); ar.i32(A("PSav"), pSav); ar.i32(A("PCol"), pCol); ar.i32(A("PTech"), pTech); ar.f32(A("IncM"), incM); ar.f32(A("ResM"), resM); ar.obj(A("scrp"), scrp); ar.rest(extra); } }; // ---- shared sim types --------------------------------------------------------- struct PopG { static constexpr const char* kStreamName = "PopG"; int32_t popT = 0, popS = 0; int64_t popC = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("PopT"), popT); ar.i32(A("PopS"), popS); ar.i64(A("PopC"), popC); ar.rest(extra); } }; struct Population { static constexpr const char* kStreamName = ""; std::vector groups; std::vector extra; template void io(Ar& ar) { ar.narr(A("PopNG"), groups, [](Ar& a, PopG& e) { a.obj(A("PopG"), e); }); ar.rest(extra); } }; struct MoraleEntry { int32_t msp = 0, mv = 0; template void io(Ar& ar) { ar.i32(A("msp"), msp); ar.i32(A("mv"), mv); } }; struct Morale { static constexpr const char* kStreamName = ""; std::vector entries; std::vector extra; template void io(Ar& ar) { ar.narr(A("mnsp"), entries, [](Ar& a, MoraleEntry& e) { e.io(a); }); ar.rest(extra); } }; struct MoraleEvent { static constexpr const char* kStreamName = ""; int32_t mid = 0, mtr = 0, mn = 0, mtp = 0; Morale mfx; std::string mdsc; std::vector extra; template void io(Ar& ar) { ar.i32(A("mid"), mid); ar.i32(A("mtr"), mtr); ar.i32(A("mn"), mn); ar.i32(A("mtp"), mtp); ar.obj(A("mfx"), mfx); ar.str(A("mdsc"), mdsc); ar.rest(extra); } }; struct BuildOrder { static constexpr const char* kStreamName = ""; int32_t desID = 0, con = 0, conleft = 0, sav = 0, ordID = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("desID"), desID); ar.i32(A("con"), con); ar.i32(A("conleft"), conleft); ar.i32(A("sav"), sav); ar.i32(A("ordID"), ordID); ar.rest(extra); } }; struct BuildQueue { static constexpr const char* kStreamName = "BQ"; std::vector orders; bool ordersFramed = true; std::vector extra; template void io(Ar& ar) { ar.carr_flex(R("ords", "ords"), orders, ordersFramed); ar.rest(extra); } }; struct IndependenceInfo { static constexpr const char* kStreamName = "indi"; int32_t indsp = 0; PlayerColor indcl; std::string indnm, indav, indba; std::vector extra; template void io(Ar& ar) { ar.i32(A("indsp"), indsp); ar.obj(A("indcl"), indcl); ar.str(A("indnm"), indnm); ar.str(A("indav"), indav); ar.str(A("indba"), indba); ar.rest(extra); } }; struct Rts { static constexpr const char* kStreamName = "Rts"; float srs = 0, srt = 0, srsc = 0, srtf = 0, sri = 0, sroh = 0; int32_t srnr = 0; std::vector extra; template void io(Ar& ar) { ar.f32(A("SRs"), srs); ar.f32(A("SRt"), srt); ar.f32(A("SRsc"), srsc); ar.f32(A("SRtf"), srtf); ar.f32(A("SRi"), sri); ar.f32(A("SRoh"), sroh); ar.i32(A("SRnr"), srnr); ar.rest(extra); } }; struct PlayerView { static constexpr const char* kStreamName = "pview"; int32_t vtrn = 0, pop = 0; Population pop2; float infra = 0, suit = 0; int32_t res = 0; std::optional aRes; int32_t aRes2 = 0, mRes = 0; bool noRebAI = false; int32_t pbon = 0; Population pbon2; float ibon = 0; int32_t terrFl = 0; bool footer = false; std::vector extra; template void io(Ar& ar) { ar.i32(A("VTrn"), vtrn); ar.i32(A("Pop"), pop); ar.obj(A("Pop2"), pop2); ar.f32(A("Infra"), infra); ar.f32(A("Suit"), suit); ar.i32(A("Res"), res); ar.opt_i32(A("ARes"), aRes); ar.i32(A("ARes2"), aRes2); ar.i32(A("MRes"), mRes); ar.b(A("NoRebAI"), noRebAI); ar.i32(A("pbon"), pbon); ar.obj(A("pbon2"), pbon2); ar.f32(A("ibon"), ibon); ar.i32(A("TerrFl"), terrFl); ar.b(A("footer"), footer); ar.rest(extra); } }; // ---- star system (ServerSystem) ----------------------------------------------- struct HaltEntry { int32_t haltt = 0; bool haltv = false; template void io(Ar& ar) { ar.i32(A("haltt"), haltt); ar.b(A("haltv"), haltv); } }; struct AdctEntry { int32_t ads = 0, adt = 0; template void io(Ar& ar) { ar.i32(A("ads"), ads); ar.i32(A("adt"), adt); } }; struct PlagueEntry { int32_t plgT = 0; Node plg; template void io(Ar& ar) { ar.i32(A("PlgT"), plgT); ar.any(A("Plg"), plg); } }; struct Colony { // NVO entry; indi is an inline member, always present int32_t pid = 0, tshn = 0, oid = 0; bool isind = false; IndependenceInfo indi; template void io(Ar& ar) { ar.i32(A("PID"), pid); ar.i32(A("TShn"), tshn); ar.i32(A("OID"), oid); ar.b(A("isind"), isind); ar.obj(A("indi"), indi); } }; struct NveEntry { int32_t ePid = 0, ets = 0, eid = 0; template void io(Ar& ar) { ar.i32(A("EPid"), ePid); ar.i32(A("ETS"), ets); ar.i32(A("Eid"), eid); } }; // One `NVs` element. `NVs` had count 0 in every save the campaign held until // zuul-turn23-fleet23, so the element's leading id was typed positionally on the // assumption that it, like most anonymous elements, carries a "." tag. The first // save that actually populates the list shows the tag is `PID`, and writing "." // broke the typed round trip at the first element. Exactly the case rule 6 // describes: a path no save exercised was a hypothesis, not a fact. struct ViewEntry { int32_t pid = 0; PlayerView pview; template void io(Ar& ar) { ar.i32(A("PID"), pid); ar.obj(R("pview", "pview"), pview); } }; struct Sys { static constexpr const char* kStreamName = "Sys"; Vec3 pos; float r = 0, g = 0, b = 0, a = 0; int32_t idx = 0, size = 0; std::optional iSuit; float suit = 0; int32_t res = 0; std::optional aRes; int32_t aRes2 = 0, mRes = 0; bool noRebAI = false; int32_t tRes = 0, pop = 0; Population pop2; float infra = 0; int32_t pvPop = 0; Population pvPop2; float pvInfra = 0, pvSuit = 0; int32_t pvRes = 0, pvARes2 = 0, pvMRes = 0; bool pvNoRebAI = false; Rts rts; bool abdn = false, dstyd = false; int32_t tnsOH = 0; float outMod = 0, repCur = 0, repMax = 0; int32_t ntdev = 0, pbon = 0; Population pbon2; float ibon = 0; int32_t ltis = 0, rbfl = 0, rbtn = 0, rbfr = 0, rbwn = 0; bool hsrg = false; std::vector halt; bool vnh = false; bool vnd = false, vnex3 = false, vnpex3 = false; std::string name; int32_t vFlags = 0, eFlags = 0, aFlags = 0, fFlags = 0, gFlags = 0; int64_t bats2 = 0, rcex = 0; int32_t mnRFlags = 0, rfRFlags = 0, clkFlags = 0; int32_t eggScio = 0, terrFl = 0, tAcq = 0, tfAcq = 0, tDst = 0; Population dcs; float dsu = 0; Morale cm, pvCM; std::vector cme2; bool cme2Framed = true; // `spies2` is VectorHelper on the original (the helper's own decorated // type names the element), so it is a framed array of NULL-named ints. The // count is 0 in every system of every save we hold, so the element TYPE is // certain but no element VALUE has ever been observed. std::vector spies2; int32_t pid = 0, defF = 0, defSF = 0; std::optional bq; std::vector adct; std::vector plagues; std::vector fleets, gates, stations, monitors; std::vector colonies; std::vector nve; std::vector views; bool hindi = false; IndependenceInfo indi; std::vector extra; template void io(Ar& ar) { ar.vec3(A("Pos"), pos); ar.f32(A("R"), r); ar.f32(A("G"), g); ar.f32(A("B"), b); ar.f32(A("A"), a); ar.i32(A("Idx"), idx); ar.i32(A("Size"), size); ar.opt_f32(A("ISuit"), iSuit); ar.f32(A("Suit"), suit); ar.i32(A("Res"), res); ar.opt_i32(A("ARes"), aRes); ar.i32(A("ARes2"), aRes2); ar.i32(A("MRes"), mRes); ar.b(A("NoRebAI"), noRebAI); ar.i32(A("TRes"), tRes); ar.i32(A("Pop"), pop); ar.obj(A("Pop2"), pop2); ar.f32(A("Infra"), infra); ar.i32(A("PvPop"), pvPop); ar.obj(A("PvPop2"), pvPop2); ar.f32(A("PvInfra"), pvInfra); ar.f32(A("PvSuit"), pvSuit); ar.i32(A("PvRes"), pvRes); ar.i32(A("PvARes2"), pvARes2); ar.i32(A("PvMRes"), pvMRes); ar.b(A("PvNoRebAI"), pvNoRebAI); ar.obj(A("Rts"), rts); ar.b(A("Abdn"), abdn); ar.b(A("Dstyd"), dstyd); ar.i32(A("TnsOH"), tnsOH); ar.f32(A("OutMod"), outMod); ar.f32(A("RepCur"), repCur); ar.f32(A("RepMax"), repMax); ar.i32(A("ntdev"), ntdev); ar.i32(A("pbon"), pbon); ar.obj(A("pbon2"), pbon2); ar.f32(A("ibon"), ibon); ar.i32(A("ltis"), ltis); ar.i32(A("rbfl"), rbfl); ar.i32(A("rbtn"), rbtn); ar.i32(A("rbfr"), rbfr); ar.i32(A("rbwn"), rbwn); ar.b(A("hsrg"), hsrg); ar.narr(A("haltc"), halt, [](Ar& a, HaltEntry& e) { e.io(a); }); ar.b(A("vnh"), vnh); ar.when(vnh, [&](Ar& a) { a.b(A("vnd"), vnd); a.b(A("vnex3"), vnex3); a.b(A("vnpex3"), vnpex3); }); ar.str(A("Name"), name); ar.i32(A("VFlags"), vFlags); ar.i32(A("EFlags"), eFlags); ar.i32(A("AFlags"), aFlags); ar.i32(A("FFlags"), fFlags); ar.i32(A("GFlags"), gFlags); ar.i64(A("Bats2"), bats2); ar.i64(A("rcex"), rcex); ar.i32(A("MnRFlags"), mnRFlags); ar.i32(A("RfRFlags"), rfRFlags); ar.i32(A("ClkFlags"), clkFlags); ar.i32(A("EggScio"), eggScio); ar.i32(A("TerrFl"), terrFl); ar.i32(A("TAcq"), tAcq); ar.i32(A("TFAcq"), tfAcq); ar.i32(A("TDst"), tDst); ar.obj(A("dcs"), dcs); ar.f32(A("dsu"), dsu); ar.obj(A("cm"), cm); ar.obj(A("PvCM"), pvCM); ar.carr_flex(A("cme2"), cme2, cme2Framed); ar.carr(A("spies2"), spies2); ar.i32(A("PID"), pid); ar.i32(A("DefF"), defF); ar.i32(A("DefSF"), defSF); ar.opt_obj(A("BQ"), bq); ar.narr(A("nadct"), adct, [](Ar& a, AdctEntry& e) { e.io(a); }); ar.narr(A("NumPlgs2"), plagues, [](Ar& a, PlagueEntry& e) { e.io(a); }); ar.narr(A("NumFlts"), fleets, [](Ar& a, int32_t& e) { a.i32(A("Flt"), e); }); ar.narr(A("NumGFs"), gates, [](Ar& a, int32_t& e) { a.i32(A("GF"), e); }); ar.narr(A("NumSnF"), stations, [](Ar& a, int32_t& e) { a.i32(A("SnF"), e); }); ar.narr(A("NumMnF"), monitors, [](Ar& a, int32_t& e) { a.i32(A("MnF"), e); }); ar.narr(A("NVO"), colonies, [](Ar& a, Colony& e) { e.io(a); }); ar.narr(A("NVE"), nve, [](Ar& a, NveEntry& e) { e.io(a); }); ar.narr(A("NVs"), views, [](Ar& a, ViewEntry& e) { e.io(a); }); ar.b(A("hindi"), hindi); ar.when(hindi, [&](Ar& a) { a.obj(A("indi"), indi); }); ar.rest(extra); } }; // ---- player / empire ------------------------------------------------------------ struct Alliances { // the second "Team" item in Player static constexpr const char* kStreamName = "Team"; int32_t alid = 0, al = 0, na = 0, cf = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("ALid"), alid); ar.i32(A("AL"), al); ar.i32(A("NA"), na); ar.i32(A("CF"), cf); ar.rest(extra); } }; struct DipStat { static constexpr const char* kStreamName = ""; int32_t other = 0; int32_t lastnap = 0, lastnapbty = 0, bknnap = 0, btynap = 0; int32_t lastally = 0, lastallybty = 0, bknally = 0, btyally = 0; int32_t lastcf = 0, lastcfbty = 0, bkncf = 0, btycf = 0; int32_t deadhome = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("other"), other); ar.i32(A("lastnap"), lastnap); ar.i32(A("lastnapbty"), lastnapbty); ar.i32(A("bknnap"), bknnap); ar.i32(A("btynap"), btynap); ar.i32(A("lastally"), lastally); ar.i32(A("lastallybty"), lastallybty); ar.i32(A("bknally"), bknally); ar.i32(A("btyally"), btyally); ar.i32(A("lastcf"), lastcf); ar.i32(A("lastcfbty"), lastcfbty); ar.i32(A("bkncf"), bkncf); ar.i32(A("btycf"), btycf); ar.i32(A("deadhome"), deadhome); ar.rest(extra); } }; struct Prep { static constexpr const char* kStreamName = ""; int32_t oid = 0, pid = 0, flds = 0, sav = 0, home = 0, ncol = 0, mpwr = 0, mcls = 0, mmsl = 0, nshp = 0, nsat = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("oid"), oid); ar.i32(A("pid"), pid); ar.i32(A("flds"), flds); ar.i32(A("sav"), sav); ar.i32(A("home"), home); ar.i32(A("ncol"), ncol); ar.i32(A("mpwr"), mpwr); ar.i32(A("mcls"), mcls); ar.i32(A("mmsl"), mmsl); ar.i32(A("nshp"), nshp); ar.i32(A("nsat"), nsat); ar.rest(extra); } }; struct Odes { static constexpr const char* kStreamName = ""; int32_t ontF = 0, otnL = 0, odid = 0, opid = 0; std::vector extra; template void io(Ar& ar) { ar.i32(R("ontF", "otnF"), ontF); ar.i32(R("otnL", "otnL"), otnL); ar.i32(R("odid", "odid"), odid); ar.i32(R("opid", "opid"), opid); ar.rest(extra); } }; // `odet` is a bool. ObservedWeapon::Write / ObservedTech::Write call the bool // writer, and the campaign's hand-recovered struct table agrees (odet @+0x08, // bool). The community reader models it as an int; that is byte-safe here only // by coincidence -- with a 4-char tag a bool item and an int item both occupy 12 // bytes, and the bool's three pad bytes are zeroed, so the two encodings are // identical. A shorter tag would not have been so forgiving. struct Owep { static constexpr const char* kStreamName = ""; int32_t ontF = 0, otnL = 0; bool odet = false; std::string owep; int32_t owith = 0; std::vector extra; template void io(Ar& ar) { ar.i32(R("ontF", "otnF"), ontF); ar.i32(R("otnL", "otnL"), otnL); ar.b(R("odet", "odet"), odet); ar.str(R("owep", "owep"), owep); ar.i32(R("owith", "owith"), owith); ar.rest(extra); } }; struct Otch { // see the note on Owep::odet static constexpr const char* kStreamName = ""; int32_t ontF = 0, otnL = 0; bool odet = false; std::string otch; int32_t owith = 0; std::vector extra; template void io(Ar& ar) { ar.i32(R("ontF", "otnF"), ontF); ar.i32(R("otnL", "otnL"), otnL); ar.b(R("odet", "odet"), odet); ar.str(R("otch", "otch"), otch); ar.i32(R("owith", "owith"), owith); ar.rest(extra); } }; struct Note { static constexpr const char* kStreamName = "Nts"; int32_t ntSys = 0; std::string ntTxt; int32_t ntTrn = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("NtSys"), ntSys); ar.str(A("NtTxt"), ntTxt); ar.i32(A("NtTrn"), ntTrn); ar.rest(extra); } }; struct ShipSectionID { // Game::ShipSectionID: two "." ints static constexpr const char* kStreamName = ""; int32_t a = 0, b = 0; std::vector extra; template void io(Ar& ar) { ar.i32(R("a"), a); ar.i32(R("b"), b); ar.rest(extra); } }; // Mars::StreamableEnum: a framed wrapper whose body is a single "." int. It // is how an enum-valued key or element reaches the stream, so it is a frame and // not a bare int -- a distinction our saves cannot show, because every container // that holds one has count 0. struct StreamEnum { static constexpr const char* kStreamName = ""; int32_t value = 0; std::vector extra; template void io(Ar& ar) { ar.i32(R("value"), value); ar.rest(extra); } }; struct GunWeapon { // DW2: the weapon in a gun bank, named either by id or by name static constexpr const char* kStreamName = "DW2"; bool bID = false; // true: the weapon is identified by id, false: by family name int32_t wid = 0; std::string wfn; int32_t did = 0; std::vector extra; template void io(Ar& ar) { ar.b(A("bID"), bID); ar.when(bID, [&](Ar& a) { a.i32(A("wid"), wid); }); ar.when(!bID, [&](Ar& a) { a.str(A("wfn"), wfn); }); ar.i32(A("did"), did); ar.rest(extra); } }; struct GunBank { // one DGbnk2 element: an anonymous frame holding one DW2 static constexpr const char* kStreamName = ""; GunWeapon w; std::vector extra; template void io(Ar& ar) { ar.obj(A("DW2"), w); ar.rest(extra); } }; struct DesignSection { // Game::ShipDesignDef::Section, one DSec of a Des frame static constexpr const char* kStreamName = "DSec"; ShipSectionID sec; std::vector gunBanks; // DOpts is VectorHelper, so its elements are NULL-named strings. std::vector opts; std::vector extra; template void io(Ar& ar) { ar.obj(A("DSec"), sec); ar.carr(A("DGbnk2"), gunBanks); ar.carr(A("DOpts"), opts); ar.rest(extra); } }; // Game::GunBankSelection -- the `wgb` frame of a weapon group. The recovery // resolves ONE NULL-named int here, which is a loop body: the only record in the // corpus (human-turn15-spyprogram, the first save with Dwgv set) carries three. // Neither a count word nor a fixed arity is on the wire, so this is read as an // uncounted run of "." ints rather than as a fixed three -- a design with a // different bank arity cannot desynchronise the frame. What settles the arity is // a save whose design has a non-three gun-bank layout. struct GunBankSelection { static constexpr const char* kStreamName = ""; std::vector banks; template void io(Ar& ar) { ar.repeat(".", banks, [](Ar& a, int32_t& e) { a.i32(R("bank"), e); }); } }; struct WeaponGroup { // one wgng element: the group id and its bank selection static constexpr const char* kStreamName = ""; int32_t wgid = 0; GunBankSelection wgb; template void io(Ar& ar) { ar.i32(A("wgid"), wgid); ar.obj(A("wgb"), wgb); } }; struct WeaponGroups { // Game::WeaponGroups, the Dwg frame static constexpr const char* kStreamName = "Dwg"; std::vector groups; std::vector extra; template void io(Ar& ar) { ar.narr(A("wgng"), groups, [](Ar& a, WeaponGroup& e) { e.io(a); }); ar.rest(extra); } }; // A design is written by TWO serializers, a base and a derived one. The base // emits FAIDes / DHide / DWep / DName and then EXACTLY THREE section frames; // the derived one appends Dtc, the Dwgv flag and -- only when that flag is set // -- the weapon-group frame. An earlier note here said the derived writer made // no stream call at all; that was a misattributed address, and both writers are // recovered. See findings/objects/ship-design-catalogue.md in the RE repo. // // THREE section frames, not five: the "two extra reserved slots" the reference // Python reader reports are Dtc and Dwgv swept in by its catch-all tail, and // the base writer's own loop runs three times over a three-element array. // The run is still read as a run rather than as a fixed three, so a malformed // record cannot desynchronise the rest of the player block. struct Design { // on-disk tags are case variants of the reference names (FAIDes, DHide, DWep, DName) static constexpr const char* kStreamName = "Des"; bool faiDes = false, dHide = false; // DWep and Dwgv are BOOLs, not ints -- both writers call the bool // primitive. With their four-character tags a bool item and an int item are // the same size on the wire and the values 0/1 are the same bytes, so the // corpus cannot tell them apart; the writers can. bool dWep = false; std::string dName; std::vector sections; int32_t dtc = 0; bool dwgv = false; // Dwg; present only when dwgv, which is set on exactly one design in the // corpus (human-turn15-spyprogram). Typed from that record plus the // recovered Game::WeaponGroups serializer. WeaponGroups weaponGroups; std::vector extra; template void io(Ar& ar) { ar.b(R("faiDes", "FAIDes"), faiDes); ar.b(R("dHide", "DHide"), dHide); ar.b(R("dWep", "DWep"), dWep); ar.str(R("dName", "DName"), dName); ar.repeat("DSec", sections, [](Ar& a, DesignSection& e) { a.obj(A("DSec"), e); }); ar.i32(A("Dtc"), dtc); ar.b(A("Dwgv"), dwgv); ar.when(dwgv, [&](Ar& a) { a.obj(A("Dwg"), weaponGroups); }); ar.rest(extra); } }; struct ConMods { static constexpr const char* kStreamName = "ConMods"; float conMod0 = 0, savMod0 = 0, conMod1 = 0, savMod1 = 0, conMod2 = 0, savMod2 = 0; template void io(Ar& ar) { ar.f32(A("ConMod"), conMod0); ar.f32(A("SavMod"), savMod0); ar.f32(A("ConMod"), conMod1); ar.f32(A("SavMod"), savMod1); ar.f32(A("ConMod"), conMod2); ar.f32(A("SavMod"), savMod2); } }; struct DesignEntry { int32_t desID = 0; Design des; template void io(Ar& ar) { ar.i32(A("DesID"), desID); ar.obj(A("Des"), des); } }; struct PrEntry { float prm = 0; int32_t prbt = 0; template void io(Ar& ar) { ar.f32(A("PRm"), prm); ar.i32(A("PRBt"), prbt); } }; struct SprjEntry { int32_t sprjT = 0; Node sprj; template void io(Ar& ar) { ar.i32(A("SprjT"), sprjT); ar.any(A("Sprj"), sprj); } }; struct NexpEntry { int32_t xid = 0, xmin = 0, xmax = 0; float xper = 0; template void io(Ar& ar) { ar.i32(A("xid"), xid); ar.i32(A("xmin"), xmin); ar.i32(A("xmax"), xmax); ar.f32(A("xper"), xper); } }; // ---- bodies typed from the recovered wire schema --------------------------------- // Each of these was an `ar.any` blob until the serializer recovery named its items. // The class each shape corresponds to is in the comment; tests/mars_stream/test_wire_schema.cpp // checks the field list against include/generated/sots_stream_schema.h. struct EventRec { // Game::EventStorage::Event static constexpr const char* kStreamName = ""; int32_t evEID = 0; std::string evDsc, evMsg, evImg; int32_t evLoc = 0; Vec3 evPos; int32_t evAct = 0, evCID = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("EvEID"), evEID); ar.str(A("EvDsc"), evDsc); ar.str(A("EvMsg"), evMsg); ar.str(A("EvImg"), evImg); ar.i32(A("EvLoc"), evLoc); ar.vec3(A("EvPos"), evPos); ar.i32(A("EvAct"), evAct); ar.i32(A("EvCID"), evCID); ar.rest(extra); } }; struct TurnEvents { // Game::EventStorage::TurnEvents static constexpr const char* kStreamName = ""; int32_t evTurn = 0; std::vector events; std::vector extra; template void io(Ar& ar) { ar.i32(A("EvTurn"), evTurn); ar.carr(A("Events"), events); ar.rest(extra); } }; struct EventStorage { // Game::EventStorage static constexpr const char* kStreamName = "Events"; int32_t evNxID = 0; std::vector turns; std::vector extra; template void io(Ar& ar) { ar.i32(A("EvNxID"), evNxID); ar.carr(A("Events"), turns); ar.rest(extra); } }; struct FleetNameGen { // Game::FleetNameGenerator static constexpr const char* kStreamName = "FNG"; int32_t fngNum = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("FNGNum"), fngNum); ar.rest(extra); } }; struct SpeciesRatios { // Game::SpeciesRatios static constexpr const char* kStreamName = "spe"; // `nv` is the count, not a field: the recovery marks sp and va2 as loop-body // writes, and the saves agree -- nv==1 frames carry one (sp, va2) pair and // nv==0 frames carry nothing at all. struct Entry { int32_t sp = 0, va2 = 0; template void io(Ar& ar) { ar.i32(A("sp"), sp); ar.i32(A("va2"), va2); } }; std::vector ratios; std::vector extra; template void io(Ar& ar) { ar.narr(A("nv"), ratios, [](Ar& a, Entry& e) { e.io(a); }); ar.rest(extra); } }; struct CivilianRatios { // Game::CivilianRatios static constexpr const char* kStreamName = "civr"; float smx = 0; SpeciesRatios spe; std::vector extra; template void io(Ar& ar) { ar.f32(A("smx"), smx); ar.obj(A("spe"), spe); ar.rest(extra); } }; struct TechNode { // one entry of Game::TechTree: a name and its branch names int32_t numBrs = 0; std::string tNm; std::vector branches; template void io(Ar& ar) { ar.str(A("TNm"), tNm); ar.narr(A("NumBrs"), branches, [](Ar& a, std::string& e) { a.str(A("TNm"), e); }); } }; struct TechState { // per-tech state, second NumTechs section of Game::TechTree std::string tNm; int32_t st = 0, tResCost = 0, tResDone = 0, tAcq = 0, tiAcq = 0, tbd = 0; bool tfc = false; int32_t tUnlck = 0; template void io(Ar& ar) { ar.str(A("TNm"), tNm); ar.i32(A("St"), st); ar.i32(A("TResCost"), tResCost); ar.i32(A("TResDone"), tResDone); ar.i32(A("TAcq"), tAcq); ar.i32(A("TiAcq"), tiAcq); ar.i32(A("Tbd"), tbd); ar.b(A("Tfc"), tfc); ar.i32(A("TUnlck"), tUnlck); } }; struct TechTree { // Game::TechTree static constexpr const char* kStreamName = "TechTree"; // TechTree::Write emits *two* NumTechs-counted sections: the tree topology // (each tech's name and its branch names) and then the per-tech state. The // recovery sees only the first under Game::TechTree; the second section's // field list and types are the ones Game::SpyReportTechTree names -- the spy // report streams the same per-tech record -- which is where Tfc being a bool // rather than an int comes from. std::vector techs; std::vector state; std::vector extra; template void io(Ar& ar) { ar.narr(A("NumTechs"), techs, [](Ar& a, TechNode& e) { e.io(a); }); ar.narr(A("NumTechs"), state, [](Ar& a, TechState& e) { e.io(a); }); ar.rest(extra); } }; struct ShipRecord { // one entry of Game::ShipRecords int32_t srb = 0, srl = 0, srk = 0, sri = 0; template void io(Ar& ar) { ar.i32(A("srb"), srb); ar.i32(A("srl"), srl); ar.i32(A("srk"), srk); ar.i32(A("sri"), sri); } }; struct DesignRecord { // second section of Game::ShipRecords -- note: no srk int32_t srd = 0, src = 0, srb = 0, srl = 0, sri = 0; template void io(Ar& ar) { ar.i32(A("srd"), srd); ar.i32(A("src"), src); ar.i32(A("srb"), srb); ar.i32(A("srl"), srl); ar.i32(A("sri"), sri); } }; struct ShipRecords { // Game::ShipRecords static constexpr const char* kStreamName = "ShipRecs"; // Two counted sections. `srbd` is the second COUNT, not a field: the recovery // lists srd/src/srb/srl/sri right after it, and the saves confirm srbd records // follow (srbd is 0, 1, 3 or 4 across the players available). std::vector recs; std::vector designs; std::vector extra; template void io(Ar& ar) { ar.narr(A("srnc"), recs, [](Ar& a, ShipRecord& e) { e.io(a); }); ar.narr(A("srbd"), designs, [](Ar& a, DesignRecord& e) { e.io(a); }); ar.rest(extra); } }; struct AIEncounterFlags { // Game::AIEncounterFlags static constexpr const char* kStreamName = "AIEnf"; struct Entry { int32_t fid = 0, ast = 0; template void io(Ar& ar) { ar.i32(A("Fid"), fid); ar.i32(A("Ast"), ast); } }; std::vector entries; std::vector extra; template void io(Ar& ar) { ar.narr(A("Nas"), entries, [](Ar& a, Entry& e) { e.io(a); }); ar.rest(extra); } }; struct PlayerAid { // Game::PlayerAid, one element of the "aid" array static constexpr const char* kStreamName = ""; int32_t aidto = 0, aidsa = 0, aidst = 0, aidrai = 0, aidrt = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("aidto"), aidto); ar.i32(A("aidsa"), aidsa); ar.i32(A("aidst"), aidst); ar.i32(A("aidrai"), aidrai); ar.i32(A("aidrt"), aidrt); ar.rest(extra); } }; struct CommMessages { // Game::CommMessageContainer static constexpr const char* kStreamName = "comms"; struct Entry { int32_t msgt = 0; Node msg; // Game::ICommMessage: polymorphic, the subtype is msgt template void io(Ar& ar) { ar.i32(A("msgt"), msgt); ar.any(A("msg"), msg); } }; std::vector msgs; std::vector extra; template void io(Ar& ar) { ar.narr(A("nmsg"), msgs, [](Ar& a, Entry& e) { e.io(a); }); ar.rest(extra); } }; struct SpyReport { // Game::SpyReport: four counted lists, one per report kind static constexpr const char* kStreamName = "spy2"; std::vector defences, trade, events, techTree; std::vector extra; template void io(Ar& ar) { // Every count is 0 in every save available, so the element shapes named by // the recovery (SpyReportDefences / …Trade / …Events / …TechTree) are // unverified; the counts and the tags of the element frames are not. ar.narr(A("defc2"), defences, [](Ar& a, Node& e) { a.any(A("def"), e); }); ar.narr(A("rtc"), trade, [](Ar& a, Node& e) { a.any(A("strd"), e); }); ar.narr(A("evc"), events, [](Ar& a, Node& e) { a.any(A("evs"), e); }); ar.narr(A("ttc"), techTree, [](Ar& a, Node& e) { a.any(A("tt"), e); }); ar.rest(extra); } }; struct FreighterWarning { // Game::ServerTradeSector::FreighterWarning, one fwarn element static constexpr const char* kStreamName = ""; int32_t pid = 0, ntrns = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("pid"), pid); ar.i32(A("ntrns"), ntrns); ar.rest(extra); } }; struct TradeSector { // Game::ServerTradeSector static constexpr const char* kStreamName = "Trade"; Vec3 pos, ctr; int32_t gridID = 0, tssec = 0, tsct = 0, tscr = 0, ptssec = 0, ptsct = 0, ptscr = 0; // ServerTradeSector::FreighterWarning. The count was 0 in every save until // human-turn11-spytechs, which raided a route and produced one. std::vector fwarn; std::vector systems, fleets; std::vector extra; template void io(Ar& ar) { ar.vec3(A("Pos"), pos); ar.i32(A("tsgridID"), gridID); ar.vec3(A("tsctr"), ctr); ar.i32(A("tssec"), tssec); ar.i32(A("tsct"), tsct); ar.i32(A("tscr"), tscr); ar.i32(A("ptssec"), ptssec); ar.i32(A("ptsct"), ptsct); ar.i32(A("ptscr"), ptscr); ar.carr(A("fwarn"), fwarn); ar.narr(A("tsnumsys"), systems, [](Ar& a, int32_t& e) { a.i32(A("tssys"), e); }); ar.narr(A("tsnumflt"), fleets, [](Ar& a, int32_t& e) { a.i32(A("tsflt"), e); }); ar.rest(extra); } }; struct TradeRoute { // Game::TradeRoute, one `rt` frame of the trade manager static constexpr const char* kStreamName = "rt"; int32_t tro = 0, trfow = 0, trfr = 0, trfrs = 0, trtow = 0, trto = 0, trtos = 0, trtc = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("tro"), tro); ar.i32(A("trfow"), trfow); ar.i32(A("trfr"), trfr); ar.i32(A("trfrs"), trfrs); ar.i32(A("trtow"), trtow); ar.i32(A("trto"), trto); ar.i32(A("trtos"), trtos); ar.i32(A("trtc"), trtc); ar.rest(extra); } }; struct TradeManager { // Game::ServerTradeManagerImpl // The `trdmgr` member is declared as Game::ServerTradeManager, whose Read and // Write really are the inherited no-op -- the campaign recorded that as an open // trap. The call is virtual: the object is a ServerTradeManagerImpl, and *that* // class has a real serializer, which is where this field list comes from. The // same shape resolves Game::IServerSpyManager -> Game::ServerSpyManager below. static constexpr const char* kStreamName = "trdmgr"; struct Entry { int32_t tradeID = 0; TradeSector trade; template void io(Ar& ar) { ar.i32(A("TradeID"), tradeID); ar.obj(A("Trade"), trade); } }; std::vector sectors; float sctSize = 0; // `rt` is a container write with NO count word: the recovery marks it a loop // body (member == false) and the wire has the frames back to back after // SctSize, so it is read as an uncounted run keyed on the tag. Every save // that has any route at all (human-turn11, human-turn15) carries exactly one // here, so "uncounted" is the framing the evidence supports, not "one". std::vector routes; std::vector extra; template void io(Ar& ar) { ar.narr(A("NumTradeSectors"), sectors, [](Ar& a, Entry& e) { e.io(a); }); ar.f32(A("SctSize"), sctSize); ar.repeat("rt", routes, [](Ar& a, TradeRoute& e) { a.obj(A("rt"), e); }); ar.rest(extra); } }; struct SpyCraft { // Game::SpyCraft, one `spy` frame static constexpr const char* kStreamName = "spy"; int32_t sid = 0, sown = 0, atto = 0, deat = 0, tdep = 0; float cbh = 0, sdo = 0; int32_t sdet = 0, spyon = 0, spyat = 0, cm = 0; float cmo = 0; std::vector cpl; // ncp: compromised players; 0 in the one record we hold std::vector extra; template void io(Ar& ar) { ar.i32(A("sid"), sid); ar.i32(A("sown"), sown); ar.i32(A("atto"), atto); ar.i32(A("deat"), deat); ar.i32(A("tdep"), tdep); ar.f32(A("cbh"), cbh); ar.f32(A("sdo"), sdo); ar.i32(A("sdet"), sdet); ar.i32(A("spyon"), spyon); ar.i32(A("spyat"), spyat); ar.i32(A("cm"), cm); ar.f32(A("cmo"), cmo); ar.narr(A("ncp"), cpl, [](Ar& a, int32_t& e) { a.i32(A("cpl"), e); }); ar.rest(extra); } }; struct SpyManager { // Game::ServerSpyManager static constexpr const char* kStreamName = "spymgr"; int32_t xsid = 0; // nspy was 0 in every save until human-turn15-spyprogram, which ran a spy // program and produced one craft. std::vector spies; std::vector extra; template void io(Ar& ar) { ar.i32(A("xsid"), xsid); ar.narr(A("nspy"), spies, [](Ar& a, SpyCraft& e) { a.obj(A("spy"), e); }); ar.rest(extra); } }; struct ProjectName { // one entry of Game::SpecialProjectNameGen std::vector used; // usnc is 0 in every save available std::string nm, ntg; template void io(Ar& ar) { ar.narr(A("usnc"), used, [](Ar& a, Node& e) { a.any(A("usc"), e); }); ar.str(A("Nm"), nm); ar.str(A("Ntg"), ntg); } }; struct ProjectNames { // Game::SpecialProjectNameGen static constexpr const char* kStreamName = "sprjs"; std::vector names; std::vector extra; template void io(Ar& ar) { ar.narr(A("NNms2"), names, [](Ar& a, ProjectName& e) { e.io(a); }); ar.rest(extra); } }; struct Player { static constexpr const char* kStreamName = "Player"; TechTree techTree; int32_t homeSys = 0, plyrIdx = 0; std::string plryName; int32_t species = 0; PlayerColor clrID; std::string bdg, avt; int32_t team = 0, sav = 0; float idealSuit = 0, suitTol = 0, maxOH = 0, resRate = 0, resMod = 0, resScl = 0, trm = 0; int32_t trp = 0, tra = 0; float outMod = 0, rebOutMod = 0, scOutMod = 0, incMod = 0; std::optional sensMod; std::optional exPopSys; float popMod = 0, terraMod = 0; bool aMine = false; float minPure = 0, minRate = 0; int32_t nGts = 0, prGtTrf = 0, gTraf = 0; float cstR = 0, cstE = 0, cstT = 0; int32_t maint = 0; float shrm = 0; int32_t status = 0; bool elim = false, npc = false, rebAI = false, reqCL = false; Alliances alliances; int32_t hasVac = 0, hasImm = 0, npTrk = 0, hasDisc = 0, hasDiscSp = 0, hasDiscCl = 0, hasEnc = 0, hasEng = 0; EventStorage events; FleetNameGen fng; int32_t pvSav = 0; bool pvMA = false, aiBn = false, cnTrd = false, cnRad = false, hgs = false, hadvs = false, harcc = false, cnVItl = false; float pddm = 0; int32_t bnkWrn = 0, bnkTrn = 0, bnkPr = 0, bnkEl = 0; ShipRecords shipRecs; int32_t nextPrjID = 0, plcy = 0; std::string pswd; int32_t lret = 0, nmeid = 0; bool cdp = false; SpyReport spy2; CivilianRatios civr; int32_t aidf = 0; bool srn = false; int32_t srnTo = 0, lboid = 0; std::optional lcid; int32_t lcid2 = 0; std::string resTNm; bool resErrRoll = false; ConMods conMods; bool conModsFramed = false; std::vector owners; std::vector designs, legacyDesigns; std::vector notes; std::vector pr; bool hasAIR = false; Node air; bool cta = false; AIEncounterFlags aiEnf; std::vector specialProjects; std::vector nexp; std::vector weapXcl; std::vector ojvs; // Game::Objective elements; the count is 0 in every save available std::vector dipstats; CommMessages comms; std::vector preps; std::vector odes; std::vector owep; std::vector otch; std::vector aid; std::vector defLayouts, raidTargets; int32_t tnc = 0; std::vector extra; template void io(Ar& ar) { ar.obj(A("TechTree"), techTree); ar.i32(A("HomeSys"), homeSys); ar.i32(A("PlyrIdx"), plyrIdx); ar.str(A("PlryName"), plryName); ar.i32(A("Species"), species); ar.obj(A("ClrID"), clrID); ar.str(A("Bdg"), bdg); ar.str(A("Avt"), avt); ar.i32(A("Team"), team); ar.i32(A("Sav"), sav); ar.f32(A("IdealSuit"), idealSuit); ar.f32(A("SuitTol"), suitTol); ar.f32(A("MaxOH"), maxOH); ar.f32(A("ResRate"), resRate); ar.f32(A("ResMod"), resMod); ar.f32(A("ResScl"), resScl); ar.f32(A("TRM"), trm); ar.i32(A("TRP"), trp); ar.i32(A("TRA"), tra); ar.f32(A("OutMod"), outMod); ar.f32(A("RebOutMod"), rebOutMod); ar.f32(A("ScOutMod"), scOutMod); ar.f32(A("IncMod"), incMod); ar.opt_f32(A("SensMod"), sensMod); ar.opt_i32(A("ExPopSys"), exPopSys); ar.f32(A("PopMod"), popMod); ar.f32(A("TerraMod"), terraMod); ar.b(A("AMine"), aMine); ar.f32(A("MinPure"), minPure); ar.f32(A("MinRate"), minRate); ar.i32(A("NGts"), nGts); ar.i32(A("PrGtTrf"), prGtTrf); ar.i32(A("GTraf"), gTraf); ar.f32(A("CstR"), cstR); ar.f32(A("CstE"), cstE); ar.f32(A("CstT"), cstT); ar.i32(A("Maint"), maint); ar.f32(A("shrm"), shrm); ar.i32(A("Status"), status); ar.b(A("Elim"), elim); ar.b(A("NPC"), npc); ar.b(A("RebAI"), rebAI); ar.b(A("ReqCL"), reqCL); ar.obj(A("Team"), alliances); ar.i32(A("HasVac"), hasVac); ar.i32(A("HasImm"), hasImm); ar.i32(A("NPTrk"), npTrk); ar.i32(A("HasDisc"), hasDisc); ar.i32(A("HasDiscSp"), hasDiscSp); ar.i32(A("HasDiscCl"), hasDiscCl); ar.i32(A("HasEnc"), hasEnc); ar.i32(A("HasEng"), hasEng); ar.obj(A("Events"), events); ar.obj(A("FNG"), fng); ar.i32(A("PvSav"), pvSav); ar.b(A("PvMA"), pvMA); ar.b(A("AIBn"), aiBn); ar.b(A("CnTrd"), cnTrd); ar.b(A("CnRad"), cnRad); ar.b(A("hgs"), hgs); ar.b(A("hadvs"), hadvs); ar.b(A("harcc"), harcc); ar.b(A("CnVItl"), cnVItl); ar.f32(A("pddm"), pddm); ar.i32(A("BnkWrn"), bnkWrn); ar.i32(A("BnkTrn"), bnkTrn); ar.i32(A("BnkPr"), bnkPr); ar.i32(A("BnkEl"), bnkEl); ar.obj(A("ShipRecs"), shipRecs); ar.i32(A("NextPrjID"), nextPrjID); ar.i32(A("plcy"), plcy); ar.str(A("pswd"), pswd); ar.i32(A("lret"), lret); ar.i32(A("nmeid"), nmeid); ar.b(A("cdp"), cdp); ar.obj(A("spy2"), spy2); ar.obj(A("civr"), civr); ar.i32(A("aidf"), aidf); ar.b(A("Srn"), srn); ar.i32(A("SrnTo"), srnTo); ar.i32(A("lboid"), lboid); ar.opt_i32(A("lcid"), lcid); ar.i32(A("lcid2"), lcid2); ar.str(A("ResTNm"), resTNm); ar.b(A("ResErrRoll"), resErrRoll); ar.obj_flex(R("conMods", "ConMods"), conMods, conModsFramed); ar.narr(A("NumOwn"), owners, [](Ar& a, int32_t& e) { a.i32(A("OwnId"), e); }); ar.narr(A("NumDes"), designs, [](Ar& a, DesignEntry& e) { e.io(a); }); ar.narr(A("NumLeg"), legacyDesigns, [](Ar& a, DesignEntry& e) { e.io(a); }); ar.narr(A("NumNotes"), notes, [](Ar& a, Note& e) { a.obj(A("Nts"), e); }); ar.narr(A("NumPR"), pr, [](Ar& a, PrEntry& e) { e.io(a); }); ar.b(A("HasAIR"), hasAIR); ar.when(hasAIR, [&](Ar& a) { a.any(A("AIR"), air); }); ar.b(A("cta"), cta); ar.obj(A("AIEnf"), aiEnf); ar.narr(A("NSprj"), specialProjects, [](Ar& a, SprjEntry& e) { e.io(a); }); ar.narr(A("Nexp"), nexp, [](Ar& a, NexpEntry& e) { e.io(a); }); ar.narr(A("NWeapXcl"), weapXcl, [](Ar& a, int32_t& e) { a.i32(A("WeapXcl"), e); }); ar.carr(A("Ojvs"), ojvs); ar.carr(A("dipstats"), dipstats); ar.obj(A("comms"), comms); ar.carr(A("preps"), preps); ar.carr(A("odes"), odes); ar.carr(A("owep"), owep); ar.carr(A("otch"), otch); ar.carr(A("aid"), aid); ar.narr(A("ndeflay"), defLayouts, [](Ar& a, Node& e) { a.any(A("deflay"), e); }); ar.narr(A("rdtc"), raidTargets, [](Ar& a, Node& e) { a.any(A("rdt"), e); }); ar.i32(A("tnc"), tnc); ar.rest(extra); } }; // ---- fleets & ships -------------------------------------------------------------- struct NodeRoute { static constexpr const char* kStreamName = "nrt"; int32_t nrp = 0, nrf = 0, nrt = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("nrp"), nrp); ar.i32(A("nrf"), nrf); ar.i32(A("nrt"), nrt); ar.rest(extra); } }; struct Waypoint { static constexpr const char* kStreamName = ""; int32_t wpt = 0, tp = 0; NodeRoute nrt; bool nrtFramed = true; std::vector extra; template void io(Ar& ar) { ar.i32(A("Wpt"), wpt); ar.i32(A("Tp"), tp); ar.obj_flex(R("nrt", "nrt"), nrt, nrtFramed); ar.rest(extra); } }; struct FlightPlan { static constexpr const char* kStreamName = "FPlan"; std::vector wpts; bool wptsFramed = true; float fpsp2 = 0; int32_t fpeta2 = 0; Vec3 fpogn2, fpdpos; int32_t pnd = 0; std::vector extra; template void io(Ar& ar) { ar.carr_flex(R("wpts", "wpts"), wpts, wptsFramed); ar.f32(A("FPsp2"), fpsp2); ar.i32(A("FPeta2"), fpeta2); ar.vec3(A("FPogn2"), fpogn2); ar.vec3(A("FPdpos"), fpdpos); ar.i32(A("pnd"), pnd); ar.rest(extra); } }; struct PrisonerEntry { int32_t prSp = 0, prNum = 0; template void io(Ar& ar) { ar.i32(A("PrSp"), prSp); ar.i32(A("PrNum"), prNum); } }; struct PrisonerHold { static constexpr const char* kStreamName = "PrisH"; int32_t prMax = 0; std::vector prisoners; std::vector extra; template void io(Ar& ar) { ar.i32(A("PrMax"), prMax); ar.when(prMax > 0, [&](Ar& a) { a.narr(A("PrNSp"), prisoners, [](Ar& b, PrisonerEntry& e) { e.io(b); }); }); ar.rest(extra); } }; struct Thruster { float th = 0, thm = 0; template void io(Ar& ar) { ar.f32(A("TH"), th); ar.f32(A("THM"), thm); } }; struct Ship { static constexpr const char* kStreamName = "Ship"; int32_t desID = 0, fltID = 0, plrID = 0; float range = 0; Vec3 health; int32_t conCap = 0; float refCap = 0, repCap = 0; int32_t mineCap = 0, plg = 0, act = 0; bool dep = false, atq = false; int32_t encID = 0; PrisonerHold prisH; int32_t lct = 0, tsd = 0, atsp = 0, tblt = 0; bool hbq = false; BuildQueue bq2; bool hsp = false; Population pop, ppop; std::vector thrusters; std::vector extra; template void io(Ar& ar) { ar.i32(A("DesID"), desID); ar.i32(A("FltID"), fltID); ar.i32(A("PlrID"), plrID); ar.f32(A("Range"), range); ar.vec3(A("Health"), health); ar.i32(A("ConCap"), conCap); ar.f32(A("RefCap"), refCap); ar.f32(A("RepCap"), repCap); ar.i32(A("MineCap"), mineCap); ar.i32(A("Plg"), plg); ar.i32(A("Act"), act); ar.b(A("Dep"), dep); ar.b(A("Atq"), atq); ar.i32(A("EncID"), encID); ar.obj(A("PrisH"), prisH); ar.i32(A("LCT"), lct); ar.i32(A("tsd"), tsd); ar.i32(A("atsp"), atsp); ar.i32(A("tblt"), tblt); ar.b(A("hbq"), hbq); ar.when(hbq, [&](Ar& a) { a.obj(A("BQ2"), bq2); }); ar.b(A("hsp"), hsp); ar.when(hsp, [&](Ar& a) { a.obj(A("pop"), pop); a.obj(A("ppop"), ppop); }); ar.narr(A("NTH"), thrusters, [](Ar& a, Thruster& e) { e.io(a); }); ar.rest(extra); } }; struct ShipEntry { int32_t shipID = 0; Ship ship; template void io(Ar& ar) { ar.i32(A("ShipID"), shipID); ar.obj(A("Ship"), ship); } }; struct FieldTemplate { // Game::FieldTemplate, the NULL-named first frame of a Lay static constexpr const char* kStreamName = ""; // FTPnts is the ONE item in this round's work that the recovery itself marks // unresolved: it names the element class from the decorated helper name // (Game::FieldTemplate::Point, whose own serializer is fully recovered -- // FTPShID/FTPDesID/FTPPosX/FTPPosY/FTPSqd) but could not type the item, and // the count is 0 in every Lay in the corpus. That is exactly the pair of // conditions under which SysMem, mts and nalat were all typed wrong: the // element FRAMING is a property of the helper, not of the element class, and // no save has ever shown one. So the elements stay carried. What settles it // is a save whose fleet has a stored tactical formation -- set a fleet's // combat layout in the tactical setup screen, then save. std::vector points; std::vector extra; template void io(Ar& ar) { ar.carr(A("FTPnts"), points); ar.rest(extra); } }; // Game::FleetLayout, the `Lay` frame gated by HLay. Every item in it is // NULL-named: a FieldTemplate frame, then a count, then that many ship ids. struct FleetLayout { static constexpr const char* kStreamName = "Lay"; FieldTemplate tmpl; std::vector shipIDs; std::vector extra; template void io(Ar& ar) { ar.obj(R("tmpl"), tmpl); ar.narr(R("nships"), shipIDs, [](Ar& a, int32_t& e) { a.i32(R("shipID"), e); }); ar.rest(extra); } }; struct Fleet { static constexpr const char* kStreamName = "Flt"; Vec3 pos; int32_t pid = 0, locID = 0; std::optional sysID, trdID; bool hfPlan = false; FlightPlan fplan; std::string ftName; std::optional caps, gtTrf; std::optional ftSens; std::optional ftInc; int32_t ftTrans = 0; Vec3 ftOrig; int32_t ftFlg = 0, ftae = 0, ftpae = 0, ftEnc = 0, ftMS = 0; bool perm = false; Vec3 prvPos; bool hLay = false; FleetLayout lay; std::vector ships; std::vector extra; template void io(Ar& ar) { ar.vec3(A("Pos"), pos); ar.i32(A("PID"), pid); ar.i32(A("LocID"), locID); ar.opt_i32(A("SysID"), sysID); ar.opt_i32(A("TrdID"), trdID); ar.b(A("HFPlan"), hfPlan); ar.when(hfPlan, [&](Ar& a) { a.obj(A("FPlan"), fplan); }); ar.str(A("FtName"), ftName); ar.opt_i32(A("Caps"), caps); ar.opt_i32(A("GtTrf"), gtTrf); ar.opt_f32(A("FtSens"), ftSens); ar.opt_i32(A("FtInc"), ftInc); ar.i32(A("FtTrans"), ftTrans); ar.vec3(A("FtOrig"), ftOrig); ar.i32(A("FtFlg"), ftFlg); ar.i32(A("Ftae"), ftae); ar.i32(A("Ftpae"), ftpae); ar.i32(A("FtEnc"), ftEnc); ar.i32(A("FtMS"), ftMS); ar.b(A("Perm"), perm); ar.vec3(A("PrvPos"), prvPos); ar.b(A("HLay"), hLay); ar.when(hLay, [&](Ar& a) { a.obj(A("Lay"), lay); }); ar.narr(A("NShips"), ships, [](Ar& a, ShipEntry& e) { e.io(a); }); ar.rest(extra); } }; // ---- combat reports, node grid --------------------------------------------------- // // The damage quartet is written by TWO classes, Game::CombatWeaponReport and // Game::CombatShipReport, and both recoveries agree: dams and damp are ints, dami // and damt are FLOATS. An earlier `Dams` sub-struct read all four as ints and was // reached through obj_flex, i.e. it modelled a nested `dams` frame that the // binary does not write and that no save in the corpus contains. Both are // corrected here: the quartet is flat, and the last two are f32. The int reading // round-tripped because the bytes are the bytes; the values it produced // (dami = 1033392620 for what is 0.0787) were nonsense. struct Wrep { // Game::CombatWeaponReport static constexpr const char* kStreamName = ""; std::string wep; int32_t dams = 0, damp = 0; float dami = 0, damt = 0; std::vector extra; template void io(Ar& ar) { ar.str(R("wep", "wep"), wep); ar.i32(R("dams", "dams"), dams); ar.i32(R("damp", "damp"), damp); ar.f32(R("dami", "dami"), dami); ar.f32(R("damt", "damt"), damt); ar.rest(extra); } }; struct Srep { // Game::CombatShipReport, one srep element static constexpr const char* kStreamName = ""; std::string name; int32_t did = 0, cls = 0; int64_t caps2 = 0; int32_t nshp = 0, nfld = 0, nlst = 0, dtak = 0, dams = 0, damp = 0; float dami = 0, damt = 0; std::vector extra; template void io(Ar& ar) { ar.str(A("name"), name); ar.i32(A("did"), did); ar.i32(A("cls"), cls); ar.i64(A("caps2"), caps2); ar.i32(A("nshp"), nshp); ar.i32(A("nfld"), nfld); ar.i32(A("nlst"), nlst); ar.i32(A("dtak"), dtak); ar.i32(A("dams"), dams); ar.i32(A("damp"), damp); ar.f32(A("dami"), dami); ar.f32(A("damt"), damt); ar.rest(extra); } }; struct CrepClass { // one ncls loop body of a CombatPlayerReport (per ship class) static constexpr const char* kStreamName = ""; int32_t cls = 0, nshp = 0, nsat = 0, nshfld = 0, nshlst = 0, nsatl = 0; template void io(Ar& ar) { ar.i32(A("cls"), cls); ar.i32(A("nshp"), nshp); ar.i32(A("nsat"), nsat); ar.i32(A("nshfld"), nshfld); ar.i32(A("nshlst"), nshlst); ar.i32(A("nsatl"), nsatl); } }; struct CrepSector { // one nsec loop body of a CombatPlayerReport (per ship section) static constexpr const char* kStreamName = ""; int32_t ssec = 0, nshp = 0; template void io(Ar& ar) { ar.i32(A("ssec"), ssec); ar.i32(A("nshp"), nshp); } }; struct CrepPrep { // Game::CombatPlayerReport static constexpr const char* kStreamName = ""; int32_t plr = 0; bool ai = false; int32_t ally = 0, status = 0, mxeng = 0, mxcls = 0, mxmsl = 0; std::vector classes; std::vector sectors; int32_t ndam = 0; std::vector srep; std::vector extra; template void io(Ar& ar) { ar.i32(R("plr", "plr"), plr); ar.b(R("ai", "ai"), ai); ar.i32(R("ally", "ally"), ally); ar.i32(R("status", "status"), status); ar.i32(R("mxeng", "mxeng"), mxeng); ar.i32(R("mxcls", "mxcls"), mxcls); ar.i32(R("mxmsl", "mxmsl"), mxmsl); ar.narr(A("ncls"), classes, [](Ar& a, CrepClass& e) { e.io(a); }); ar.narr(A("nsec"), sectors, [](Ar& a, CrepSector& e) { e.io(a); }); ar.i32(A("ndam"), ndam); ar.carr(A("srep"), srep); ar.rest(extra); } }; // Game::CombatReport. auto and cdst are BOOLs and dur, cdt and cdi are FLOATS -- // all five were read as ints here. Four of them are 0 or 1 in every save, so the // bytes never moved; cdt is not (0x3FCC... = 1.598 turns of combat), and reading // it as an int gave 1070805848. struct Crep { static constexpr const char* kStreamName = "crep"; int32_t cid = 0, trn = 0; Vec3 pos; int32_t sid = 0; bool autoF = false; float dur = 0; int32_t cow = 0; bool cdst = false; int32_t cpk = 0, cpt = 0; float cdt = 0, cdi = 0; std::vector prep; bool prepFramed = true; std::vector wrep; bool wrepFramed = true; std::vector extra; template void io(Ar& ar) { ar.i32(R("cid", "cid"), cid); ar.i32(R("trn", "trn"), trn); ar.vec3(R("pos", "pos"), pos); ar.i32(R("sid", "sid"), sid); ar.b(R("auto", "auto"), autoF); ar.f32(R("dur", "dur"), dur); ar.i32(R("cow", "cow"), cow); ar.b(R("cdst", "cdst"), cdst); ar.i32(R("cpk", "cpk"), cpk); ar.i32(R("cpt", "cpt"), cpt); ar.f32(R("cdt", "cdt"), cdt); ar.f32(R("cdi", "cdi"), cdi); ar.carr_flex(R("prep", "prep"), prep, prepFramed); ar.carr_flex(R("wrep", "wrep"), wrep, wrepFramed); ar.rest(extra); } }; struct NodePath { static constexpr const char* kStreamName = ""; int32_t npt = 0, npid = 0, npfr = 0, npto = 0, npctm = 0, npcby = 0, npdtn = 0, npdtf = 0, npenp = 0, npuse = 0, nptf = 0; std::vector extra; template void io(Ar& ar) { ar.i32(R("npt", "npt"), npt); ar.i32(R("npid", "npid"), npid); ar.i32(R("npfr", "npfr"), npfr); ar.i32(R("npto", "npto"), npto); ar.i32(R("npctm", "npctm"), npctm); ar.i32(R("npcby", "npcby"), npcby); ar.i32(R("npdtn", "npdtn"), npdtn); ar.i32(R("npdtf", "npdtf"), npdtf); ar.i32(R("npenp", "npenp"), npenp); ar.i32(R("npuse", "npuse"), npuse); ar.i32(R("nptf", "nptf"), nptf); ar.rest(extra); } }; struct NodeGrid { static constexpr const char* kStreamName = "NdGr2"; std::vector paths; bool pathsFramed = true; int32_t nextId = 0; std::vector extra; template void io(Ar& ar) { ar.carr_flex(R("paths", "paths"), paths, pathsFramed); ar.i32(R("nextId", "nextid"), nextId); ar.rest(extra); } }; // ---- turn statistics --------------------------------------------------------------- struct SystemEvent { static constexpr const char* kStreamName = ""; int32_t set = 0, ses = 0, seop = 0, senp = 0; std::vector others; std::vector extra; template void io(Ar& ar) { ar.i32(A("set"), set); ar.i32(A("ses"), ses); ar.i32(A("seop"), seop); ar.i32(A("senp"), senp); ar.narr(A("seno2"), others, [](Ar& a, int32_t& e) { a.i32(A("seot2"), e); }); ar.rest(extra); } }; struct ClassStats { int32_t cls = 0, shpt = 0, shpl = 0, shpk = 0, satt = 0, satl = 0, satk = 0; template void io(Ar& ar) { ar.i32(A("cls"), cls); ar.i32(A("shpt"), shpt); ar.i32(A("shpl"), shpl); ar.i32(A("shpk"), shpk); ar.i32(A("satt"), satt); ar.i32(A("satl"), satl); ar.i32(A("satk"), satk); } }; struct PlayerTurnStats { static constexpr const char* kStreamName = "stats"; int64_t pop = 0; std::vector sacq, slost; int32_t trn = 0, almem = 0, inc = 0, tdinc = 0, sav = 0, col = 0, bat = 0, tch = 0; std::vector classes; std::vector extra; template void io(Ar& ar) { ar.i64(A("pop"), pop); ar.carr(A("sacq"), sacq); ar.carr(A("slost"), slost); ar.i32(A("trn"), trn); ar.i32(A("almem"), almem); ar.i32(A("inc"), inc); ar.i32(A("tdinc"), tdinc); ar.i32(A("sav"), sav); ar.i32(A("col"), col); ar.i32(A("bat"), bat); ar.i32(A("tch"), tch); ar.narr(A("ncls"), classes, [](Ar& a, ClassStats& e) { e.io(a); }); ar.rest(extra); } }; struct PlayerTurnHistory { static constexpr const char* kStreamName = "hist"; int32_t ply = 0; std::vector stats; // no count: until the frame END template void io(Ar& ar) { ar.i32(A("ply"), ply); ar.repeat("stats", stats, [](Ar& a, PlayerTurnStats& e) { a.obj(A("stats"), e); }); } }; struct TurnHistoryEntry { int32_t ply = 0; PlayerTurnHistory hist; template void io(Ar& ar) { ar.i32(A("ply"), ply); ar.obj(A("hist"), hist); } }; struct TurnStats { static constexpr const char* kStreamName = "turnstats"; std::vector players; std::vector extra; template void io(Ar& ar) { ar.narr(A("nply"), players, [](Ar& a, TurnHistoryEntry& e) { e.io(a); }); ar.rest(extra); } }; // ---- Sim block (StrategyServer) ------------------------------------------------------ struct Invasion { int32_t invs = 0, inve = 0, invt = 0, invtb = 0; template void io(Ar& ar) { ar.i32(A("invs"), invs); ar.i32(A("inve"), inve); ar.i32(A("invt"), invt); ar.i32(A("invtb"), invtb); } }; struct IntPair { int32_t a = 0, b = 0; }; struct Species { std::string issp; float issu = 0; template void io(Ar& ar) { ar.str(A("ISsp"), issp); ar.f32(A("ISsu"), issu); } }; struct PlayerEntry { int32_t playerID = 0; Player player; template void io(Ar& ar) { ar.i32(A("PlayerID"), playerID); ar.obj(A("Player"), player); } }; struct SysEntry { int32_t sysID = 0; Sys sys; template void io(Ar& ar) { ar.i32(A("SysID"), sysID); ar.obj(A("Sys"), sys); } }; struct FleetEntry { int32_t fltID = 0; Fleet flt; template void io(Ar& ar) { ar.i32(A("FltID"), fltID); ar.obj(A("Flt"), flt); } }; struct ZoneDefence { int32_t zdsi = 0, zdst = 0; template void io(Ar& ar) { ar.i32(A("zdsi"), zdsi); ar.i32(A("zdst"), zdst); } }; // ---- SvSctOb: the script-object tree ------------------------------------------------- // // `SvSctOb` is a StreamableHelper -- a polymorphic pointer. // In every save it holds a Game::SVSOSots, which writes two variant lists: // // numx x { xscn (a scenario NAME) , xsc (SVScriptObject*) } // NEncObjs x { EncID (an encounter ID), EncObj (SVScriptObject*) } // // so both bodies are dispatched by the key item that precedes them. Neither the // name->class nor the id->class map is anywhere on the wire; both were read out of // the game: the ids from a 23-entry dword jump table (indexed by EncID-1) and the // names from a flat _stricmp chain. See findings/objects/svsctob-variants.md in // the notes repo for the addresses. An id or name we do not model still round // trips, because the fallback carries the body as a generic Node. struct SwarmInfestation { // Game::SVSOSwarm::Infestation static constexpr const char* kStreamName = ""; int32_t sysid = 0, stg = 0, strn = 0, mtrn = 0, trgtrn = 0; bool canh = false; template void io(Ar& ar) { ar.i32(A("sysid"), sysid); ar.i32(A("stg"), stg); ar.i32(A("strn"), strn); ar.i32(A("mtrn"), mtrn); ar.i32(A("trgtrn"), trgtrn); ar.b(A("canh"), canh); } }; struct FleetAssignment { // the (Eflt, Esys) pair the NAsg loop writes int32_t eflt = 0, esys = 0; }; struct DesignWeight { // the (DsnID, Dwght) pair the NDsn loop writes int32_t dsnID = 0, dwght = 0; }; struct SVSOSwarm { // EncID 3 static constexpr const char* kStreamName = ""; std::vector asg; std::vector infest; int32_t deshive = 0, deslarva = 0; std::vector extra; template void io(Ar& ar) { ar.narr(A("NAsg"), asg, [](Ar& a, FleetAssignment& e) { a.i32(A("Eflt"), e.eflt); a.i32(A("Esys"), e.esys); }); ar.carr(A("infest"), infest); ar.i32(A("deshive"), deshive); ar.i32(A("deslarva"), deslarva); ar.rest(extra); } }; struct SVSODerelict { // EncID 4 -- also the base Game::SVSOMonitor::Write starts with static constexpr const char* kStreamName = ""; std::vector designs; std::vector asg; template void io(Ar& ar) { ar.narr(A("NDsn"), designs, [](Ar& a, DesignWeight& e) { a.i32(A("DsnID"), e.dsnID); a.i32(A("Dwght"), e.dwght); }); ar.narr(A("NAsg"), asg, [](Ar& a, FleetAssignment& e) { a.i32(A("Eflt"), e.eflt); a.i32(A("Esys"), e.esys); }); } }; struct MonitorSpawn { // one `nt` element of Game::SVSOMonitor std::string scnm; int32_t spwt = 0; float rsmd = 0; int32_t dsgn = 0; }; struct SVSOMonitor { // EncID 5 -- Game::SVSOMonitor derives from Game::SVSODerelict static constexpr const char* kStreamName = ""; SVSODerelict base; std::vector spawns; std::vector extra; template void io(Ar& ar) { base.io(ar); // Monitor::Write calls Derelict::Write first ar.narr(A("nt"), spawns, [](Ar& a, MonitorSpawn& e) { a.str(A("scnm"), e.scnm); a.i32(A("spwt"), e.spwt); a.f32(A("rsmd"), e.rsmd); a.i32(A("dsgn"), e.dsgn); }); ar.rest(extra); } }; struct SVSOSlaversRefuel { // EncID 9 static constexpr const char* kStreamName = ""; std::vector asg; int32_t cdiff = 0; std::vector tdids, adids; std::vector extra; template void io(Ar& ar) { ar.narr(A("NAsg"), asg, [](Ar& a, FleetAssignment& e) { a.i32(A("Eflt"), e.eflt); a.i32(A("Esys"), e.esys); }); ar.i32(A("CDiff"), cdiff); ar.narr(A("NTD"), tdids, [](Ar& a, int32_t& e) { a.i32(A("TDID"), e); }); ar.narr(A("NAD"), adids, [](Ar& a, int32_t& e) { a.i32(A("ADID"), e); }); ar.rest(extra); } }; struct SVSOSwarmQueenHive { // Game::SVSOSwarmQueen::HiveInfo static constexpr const char* kStreamName = ""; int32_t hiveID = 0, queenID = 0, nextQ = 0; template void io(Ar& ar) { ar.i32(A("HiveID"), hiveID); ar.i32(A("QueenID"), queenID); ar.i32(A("NextQ"), nextQ); } }; struct SVSOSwarmQueenQueen { // Game::SVSOSwarmQueen::QueenInfo static constexpr const char* kStreamName = ""; int32_t queenID = 0, qDstID = 0; template void io(Ar& ar) { ar.i32(A("QueenID"), queenID); ar.i32(A("QDstID"), qDstID); } }; struct SVSOSwarmQueen { // EncID 10 static constexpr const char* kStreamName = ""; // QDesignID is written from a computed handle id, and the recovery cannot say // whether the writer emits one or several, so this consumes the whole run. std::vector designIds; std::vector hives; std::vector queens; // VectorHelper>: each element is a "." FRAME holding one // "." int, not a bare int. Count 0 in every save, so nothing exercises it. std::vector sysMem; std::vector extra; template void io(Ar& ar) { ar.repeat("QDesignID", designIds, [](Ar& a, int32_t& e) { a.i32(A("QDesignID"), e); }); ar.carr(A("Hives"), hives); ar.carr(A("Queens"), queens); ar.carr(A("SysMem"), sysMem); ar.rest(extra); } }; struct SVSOCrowRuins { // EncID 17 static constexpr const char* kStreamName = ""; std::vector asg; std::vector wids; std::vector extra; template void io(Ar& ar) { ar.narr(A("NAsg"), asg, [](Ar& a, FleetAssignment& e) { a.i32(A("Eflt"), e.eflt); a.i32(A("Esys"), e.esys); }); ar.narr(A("NWD"), wids, [](Ar& a, int32_t& e) { a.i32(A("wid"), e); }); ar.rest(extra); } }; struct RefugeeStatus { // Game::SVSORefugees::PlayerStatus -- the `rsu` frame static constexpr const char* kStreamName = "rsu"; int32_t lr = 0, lrt = 0, llc = 0; bool atr = false, asy = false; std::vector extra; template void io(Ar& ar) { ar.i32(A("lr"), lr); ar.i32(A("lrt"), lrt); ar.i32(A("llc"), llc); ar.b(A("atr"), atr); ar.b(A("asy"), asy); ar.rest(extra); } }; struct RefugeePlayer { // one `rsuc` element: (pid, rsu) int32_t pid = 0; RefugeeStatus status; }; struct SVSORefugees { // EncID 20 static constexpr const char* kStreamName = ""; std::vector players; std::vector dids; bool ini = false; std::vector extra; template void io(Ar& ar) { ar.narr(A("rsuc"), players, [](Ar& a, RefugeePlayer& e) { a.i32(A("pid"), e.pid); a.obj(A("rsu"), e.status); }); ar.narr(A("didc"), dids, [](Ar& a, int32_t& e) { a.i32(A("did"), e); }); ar.b(A("ini"), ini); ar.rest(extra); } }; struct VonNeumannDefeat { // Game::SVSOVonNeumann::DefeatRecord static constexpr const char* kStreamName = ""; int32_t sys = 0, ndft = 0, nxrv = 0, lstd = 0; bool bkdf = false; template void io(Ar& ar) { ar.i32(A("sys"), sys); ar.i32(A("ndft"), ndft); ar.i32(A("nxrv"), nxrv); ar.i32(A("lstd"), lstd); ar.b(A("bkdf"), bkdf); } }; struct SVSOVonNeumann { // EncID 1 static constexpr const char* kStreamName = ""; std::vector defeats; int32_t rusav = 0, nenc = 0, nmb = 0, nml = 0, nbb = 0, nbl = 0, nskb = 0, nskl = 0; std::vector mts; // VectorHelper>: framed elements int32_t ntm = 0, ntb = 0; bool sken = false; int32_t skdid = 0, skhid = 0, skfid = 0, sktq = 0, skt = 0, sktc = 0; int32_t mbid = 0, mbgid = 0, skgid = 0; int64_t vnhp = 0; int32_t vnhw = 0; std::vector trev; // VectorHelper std::vector extra; template void io(Ar& ar) { ar.carr(A("dfts"), defeats); ar.i32(A("rusav"), rusav); ar.i32(A("nenc"), nenc); ar.i32(A("nmb"), nmb); ar.i32(A("nml"), nml); ar.i32(A("nbb"), nbb); ar.i32(A("nbl"), nbl); ar.i32(A("nskb"), nskb); ar.i32(A("nskl"), nskl); ar.carr(A("mts"), mts); ar.i32(A("ntm"), ntm); ar.i32(A("ntb"), ntb); ar.b(A("sken"), sken); ar.i32(A("skdid"), skdid); ar.i32(A("skhid"), skhid); ar.i32(A("skfid"), skfid); ar.i32(A("sktq"), sktq); ar.i32(A("skt"), skt); ar.i32(A("sktc"), sktc); ar.i32(A("mbid"), mbid); ar.i32(A("mbgid"), mbgid); ar.i32(A("skgid"), skgid); ar.i64(A("vnhp"), vnhp); ar.i32(A("vnhw"), vnhw); ar.carr(A("trev"), trev); ar.rest(extra); } }; // --- the four named scenario objects (`xsc`, keyed by `xscn`) ------------------- struct SVSOTrap { // Game::SVSOTraps::Trap static constexpr const char* kStreamName = ""; int32_t sys = 0, pid = 0, trenc = 0, trgenc = 0; template void io(Ar& ar) { ar.i32(A("sys"), sys); ar.i32(A("pid"), pid); ar.i32(A("trenc"), trenc); ar.i32(A("trgenc"), trgenc); } }; struct SVSOTraps { // xscn "traps" static constexpr const char* kStreamName = ""; std::vector traps; std::vector extra; template void io(Ar& ar) { ar.carr(A("traps"), traps); ar.rest(extra); } }; struct SVSOCrowDefenders { // xscn "crowdefs" static constexpr const char* kStreamName = ""; int32_t sys = 0; // `dsys` is a loop element of `ndsys`, not a plain member: the recovery said // member, but Game::SVSOCrowDefenders::Write emits it inside the ndsys loop. // Both counts are 0 in every save, so only the binary settles this. std::vector dsys, des; float drad = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("sys"), sys); ar.narr(A("ndsys"), dsys, [](Ar& a, int32_t& e) { a.i32(A("dsys"), e); }); ar.narr(A("ndes"), des, [](Ar& a, int32_t& e) { a.i32(A("des"), e); }); ar.f32(A("drad"), drad); ar.rest(extra); } }; struct SVSOGrandMenaceTrigger { // xscn "gmtrigger" static constexpr const char* kStreamName = ""; int32_t gmch = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("gmch"), gmch); ar.rest(extra); } }; struct SVSOIndependentSystems { // xscn "indsys" -- Read and Write are the `ret 4` stub static constexpr const char* kStreamName = ""; std::vector extra; template void io(Ar& ar) { ar.rest(extra); // the frame is genuinely empty on disk } }; // One `xsc` body. `which` is set from the preceding `xscn` before io() runs, in // both directions, so a body written back goes out as whatever it came in as. struct ScenarioObject { static constexpr const char* kStreamName = "xsc"; enum class Which { Unknown, Traps, CrowDefenders, GrandMenaceTrigger, IndependentSystems }; Which which = Which::Unknown; SVSOTraps traps; SVSOCrowDefenders crowDefenders; SVSOGrandMenaceTrigger grandMenace; SVSOIndependentSystems indSys; std::vector unknown; // a name we do not model: carried verbatim void select(const std::string& name) { if (name == "traps") which = Which::Traps; else if (name == "crowdefs") which = Which::CrowDefenders; else if (name == "gmtrigger") which = Which::GrandMenaceTrigger; else if (name == "indsys") which = Which::IndependentSystems; else which = Which::Unknown; } template void io(Ar& ar) { switch (which) { case Which::Traps: traps.io(ar); break; case Which::CrowDefenders: crowDefenders.io(ar); break; case Which::GrandMenaceTrigger: grandMenace.io(ar); break; case Which::IndependentSystems: indSys.io(ar); break; case Which::Unknown: ar.rest(unknown); break; } } }; // One `EncObj` body, keyed by the preceding `EncID`. The full jump table is // 1 VonNeumann, 3 Swarm, 4 Derelict, 5 Monitor, 7 SystemKiller, 8 PuppetMaster, // 9 SlaversRefuel, 10 SwarmQueen, 14 Locust, 17 CrowRuins, 20 Refugees, // 21 Ortgay; every other id in 0..23 creates nothing. The four ids no save we // hold exercises (7, 8, 14, 21) are deliberately NOT modelled -- their bodies are // carried as Nodes rather than typed against a shape nothing can check. struct EncounterObject { static constexpr const char* kStreamName = "EncObj"; int32_t id = 0; SVSOVonNeumann vonNeumann; // 1 SVSOSwarm swarm; // 3 SVSODerelict derelict; // 4 SVSOMonitor monitor; // 5 SVSOSlaversRefuel slavers; // 9 SVSOSwarmQueen swarmQueen; // 10 SVSOCrowRuins crowRuins; // 17 SVSORefugees refugees; // 20 std::vector unknown; // 7, 8, 14, 21 and any id with no factory entry void select(int32_t encID) { id = encID; } template void io(Ar& ar) { switch (id) { case 1: vonNeumann.io(ar); break; case 3: swarm.io(ar); break; case 4: derelict.io(ar); break; case 5: monitor.io(ar); break; case 9: slavers.io(ar); break; case 10: swarmQueen.io(ar); break; case 17: crowRuins.io(ar); break; case 20: refugees.io(ar); break; default: ar.rest(unknown); break; } } }; struct ScriptObjects { // Game::SVSOSots -- the SvSctOb body static constexpr const char* kStreamName = "SvSctOb"; struct Extra { std::string name; ScenarioObject obj; }; struct Enc { int32_t id = 0; EncounterObject obj; }; int32_t scnID = 0; std::optional scnObj; // StreamableHelper, NULL in our saves std::vector extras; std::vector encounters; std::vector extra; template void io(Ar& ar) { ar.i32(A("ScnID"), scnID); ar.opt_any(A("ScnObj"), scnObj); ar.narr(A("numx"), extras, [](Ar& a, Extra& e) { a.str(A("xscn"), e.name); e.obj.select(e.name); a.obj(A("xsc"), e.obj); }); ar.narr(A("NEncObjs"), encounters, [](Ar& a, Enc& e) { a.i32(A("EncID"), e.id); e.obj.select(e.id); a.obj(A("EncObj"), e.obj); }); ar.rest(extra); } }; // --- the strategy AI agent's cache, one CD block per AI player ------------------ // // `CDT` lists the custom-data ids in order and one `CD` frame follows per id. An // id ending in ".AIAgent" selects Game::StrategyAIAgent::Streamable, whose whole // Write is unconditional -- every item below is present in every block. The // shapes here follow that writer item for item; the notes repo carries the // derivation (findings/objects/aiagent-block.md). // Game::AttribMap: a string->string map, written as a "." count followed by two // "." strings per entry. Empty in every save we hold, so the pair layout is a // hypothesis from the writer, not something the data exercises. struct AttribMap { static constexpr const char* kStreamName = ""; struct Entry { std::string key, value; }; std::vector entries; std::vector extra; template void io(Ar& ar) { ar.narr(R("n"), entries, [](Ar& a, Entry& e) { a.str(R("key"), e.key); a.str(R("value"), e.value); }); ar.rest(extra); } }; // Game::AIWeightMap: a "." count then, per entry, a "." key frame and a "." // float weight. Four instantiations exist and all four share this wire shape; // only the key body differs (StreamEnum, or ShipSectionID's two ints). template struct AIWeightMap { static constexpr const char* kStreamName = ""; struct Entry { Key key; float weight = 0; }; std::vector entries; std::vector extra; template void io(Ar& ar) { ar.narr(R("n"), entries, [](Ar& a, Entry& e) { a.obj(R("key"), e.key); a.f32(R("w"), e.weight); }); ar.rest(extra); } }; using AIWeightMapEnum = AIWeightMap; using AIWeightMapSection = AIWeightMap; struct AISituation { // Game::AISituation static constexpr const char* kStreamName = "AISit"; AIWeightMapSection sections; AIWeightMapEnum weaponFamilies; std::vector extra; template void io(Ar& ar) { ar.obj(A("AISitSecs"), sections); ar.obj(A("AISitWeapFams"), weaponFamilies); ar.rest(extra); } }; // Game::AISystem writes one AISituation and nothing else -- and the situation it // writes is default-constructed at the call, not read from the object, so every // AISys body on disk is an empty pair of weight maps regardless of game state. struct AISystem { static constexpr const char* kStreamName = "AISys"; AISituation situation; std::vector extra; template void io(Ar& ar) { ar.obj(A("AISysSit"), situation); ar.rest(extra); } }; struct DesignNameGen { // Game::StrategyAIAgent::DesignNameGen static constexpr const char* kStreamName = "AIDNG"; struct Name { ShipSectionID section; std::string name; }; std::vector names; // dnnc counts the (dnid, dnnm) pairs std::vector extra; template void io(Ar& ar) { ar.narr(A("dnnc"), names, [](Ar& a, Name& e) { a.obj(A("dnid"), e.section); a.str(A("dnnm"), e.name); }); ar.rest(extra); } }; struct AIAutoPeaceRun { // Game::AIAutoPeaceRun static constexpr const char* kStreamName = ""; int32_t sid = 0, tn0 = 0, tn1 = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("sid"), sid); ar.i32(A("tn0"), tn0); ar.i32(A("tn1"), tn1); ar.rest(extra); } }; struct TacReportEvents { // Game::TacReportEvents, the TRby / TRto frames static constexpr const char* kStreamName = ""; int32_t hd = 0, hi = 0; float d = 0, dp = 0, di = 0, dt = 0; bool b = false; std::vector extra; template void io(Ar& ar) { ar.i32(A("TREhd"), hd); ar.i32(A("TREhi"), hi); ar.f32(A("TREd"), d); ar.f32(A("TREdp"), dp); ar.f32(A("TREdi"), di); ar.f32(A("TREdt"), dt); ar.b(A("TREb"), b); ar.rest(extra); } }; struct TacClass { // one TRnc loop body: the per-ship-class tail of a TacReport static constexpr const char* kStreamName = ""; int32_t ships = 0, sats = 0, shipsL = 0; template void io(Ar& ar) { ar.i32(A("TRships"), ships); ar.i32(A("TRsats"), sats); ar.i32(A("TRshipsL"), shipsL); } }; // Game::TacReport. Two lanes left this carried: the recovery ends with a computed // count (TRnc) and three trailing scalar items, and with TRnc == 0 in every save // held at the time there was no way to tell three trailing scalars from a loop // body of three. The trade/spy saves settle it -- TRnc == 3 and the wire carries // TRships, TRsats, TRshipsL three times, INTERLEAVED, so the recovery's three // items are one loop body, not three runs. struct TacReport { static constexpr const char* kStreamName = ""; TacReportEvents by, to; // `mine` not `min`: windows.h defines min as a macro and this header is // compiled by the MinGW cross-build too. int32_t id = 0, al = 0, bal = 0, las = 0, mis = 0, mine = 0, nrg = 0, bio = 0, brd = 0; bool sld = false, sldd = false, sldc = false, sldi = false, sldr = false; std::vector classes; std::vector extra; template void io(Ar& ar) { ar.obj(A("TRby"), by); ar.obj(A("TRto"), to); ar.i32(A("TRid"), id); ar.i32(A("TRal"), al); ar.i32(A("TRbal"), bal); ar.i32(A("TRlas"), las); ar.i32(A("TRmis"), mis); ar.i32(A("TRmin"), mine); ar.i32(A("TRnrg"), nrg); ar.i32(A("TRbio"), bio); ar.i32(A("TRbrd"), brd); ar.b(A("TRsld"), sld); ar.b(A("TRsldd"), sldd); ar.b(A("TRsldc"), sldc); ar.b(A("TRsldi"), sldi); ar.b(A("TRsldr"), sldr); ar.narr(A("TRnc"), classes, [](Ar& a, TacClass& e) { e.io(a); }); ar.rest(extra); } }; struct CombatPlayerStats { // Game::CombatPlayerStats static constexpr const char* kStreamName = "CRPlSv2"; float rpBon = 0; int32_t rpBonT = 0, savBonus = 0; bool maintHF = false; std::vector tacReports; std::vector extra; template void io(Ar& ar) { ar.f32(A("RPBon"), rpBon); ar.i32(A("RPBonT"), rpBonT); ar.i32(A("SavBonus"), savBonus); ar.b(A("MaintHF"), maintHF); ar.carr(A("TacReports"), tacReports); ar.rest(extra); } }; struct AICombatReport { // Game::AICombatReport static constexpr const char* kStreamName = "CmbR"; int32_t crTrn = 0; bool crPce = false; StreamEnum crSys; CombatPlayerStats crPlSv2; std::vector extra; template void io(Ar& ar) { ar.i32(A("CRTrn"), crTrn); ar.b(A("CRPce"), crPce); ar.obj(A("CRSys"), crSys); ar.obj(A("CRPlSv2"), crPlSv2); ar.rest(extra); } }; // Game::AIPlayerRequestStamp is a POD reached only through a specialised helper, // so it has no RTTI class and no entry in the wire table; the two named ints come // straight from the helper's own writer. struct AIRequestStamp { static constexpr const char* kStreamName = ""; int32_t pid = 0, trn = 0; std::vector extra; template void io(Ar& ar) { ar.i32(A("pid"), pid); ar.i32(A("trn"), trn); ar.rest(extra); } }; struct StrategyAIAgent { // Game::StrategyAIAgent::Streamable static constexpr const char* kStreamName = "CD"; struct DesignShare { // one entry of the map `dsh` counts int32_t pid = 0; std::vector turns; }; struct AISysEntry { int32_t id = 0; AISystem sys; }; struct ColonyLoss { int32_t turn = 0, sysID = 0, plID = 0; }; struct Provocation { int32_t id = 0; float value = 0; }; AttribMap attrib; AIWeightMapEnum turnPriorities; AISituation situation; AIWeightMapEnum playerHate; std::vector requestStamps; // `dsh` is the _Mysize of the map the (pid, trns) pairs are walked out of, so // it is the element count and not a scalar field. std::vector designShares; std::vector borderStability; // NBStab x (BStabPl, BStabTn) std::vector menaceBlacklist; // NMBlst x (MBlstSy, MBlstTn) DesignNameGen designNames; int32_t aiHivJ = 0, sdFlT = 0; std::vector newAlliances; // nalat int32_t lnat = 0, lat = 0; std::vector systems; // AINumSys x (AISysID, AISys) std::vector combatReports; std::vector colonyLosses; std::vector provocations; std::vector techScores; int32_t fct = 0; std::vector autoPeaceRuns; std::vector extra; template void io(Ar& ar) { ar.obj(A("AIAttr"), attrib); ar.obj(A("AITurnPris"), turnPriorities); ar.obj(A("AISit"), situation); ar.obj(A("AIPlyHat"), playerHate); ar.carr(A("prs2"), requestStamps); ar.narr(A("dsh"), designShares, [](Ar& a, DesignShare& e) { a.i32(A("pid"), e.pid); a.carr(A("trns"), e.turns); }); ar.narr(A("NBStab"), borderStability, [](Ar& a, IntPair& e) { a.i32(A("BStabPl"), e.a); a.i32(A("BStabTn"), e.b); }); ar.narr(A("NMBlst"), menaceBlacklist, [](Ar& a, IntPair& e) { a.i32(A("MBlstSy"), e.a); a.i32(A("MBlstTn"), e.b); }); ar.obj(A("AIDNG"), designNames); ar.i32(A("AIHivJ"), aiHivJ); ar.i32(A("SDFlT"), sdFlT); ar.carr(A("nalat"), newAlliances); ar.i32(A("lnat"), lnat); ar.i32(A("lat"), lat); ar.narr(A("AINumSys"), systems, [](Ar& a, AISysEntry& e) { a.i32(A("AISysID"), e.id); a.obj(A("AISys"), e.sys); }); ar.narr(A("NCmbR"), combatReports, [](Ar& a, AICombatReport& e) { a.obj(A("CmbR"), e); }); ar.narr(A("NumCL"), colonyLosses, [](Ar& a, ColonyLoss& e) { a.i32(A("CLTn"), e.turn); a.i32(A("CLSyID"), e.sysID); a.i32(A("CLPlID"), e.plID); }); ar.narr(A("NPrv"), provocations, [](Ar& a, Provocation& e) { a.i32(A("NPrvId"), e.id); a.f32(A("NPrvVa"), e.value); }); ar.narr(A("NTecS"), techScores, [](Ar& a, int32_t& e) { a.i32(A("TecS"), e); }); ar.i32(A("fct"), fct); ar.carr(A("apr"), autoPeaceRuns); ar.rest(extra); } }; // ---- Game::TurnCommands: the ".TurnCommands_v5" custom-data block ---------------- // // The pending client->server order queue, drained by ApplyTurnCommands; it is NOT // applied state (a save taken after issuing orders still shows the old research // rate on the ServerPlayer while this block already carries the new one). // // Every item is written with a NULL name, so the whole block is positional and // nothing here can be matched by tag. The writer (0x00842540) has two halves: // // * a prologue of six flag-gated groups. Each group is a `bool` followed, only // when the bool is set, by that command's payload. A turn with no orders // writes the six bools and nothing else, which is why every save the campaign // held before lane O's was bit-identical here at 35 items. // * twenty-seven `std::list` members, each written by its own helper as // WriteInt(size) followed by `size` element records. All twenty-seven are // always written, so an empty list still costs one zero int. 8 prologue items // + 27 zero counts = the 35-item empty block exactly. // // The recovered wire table lists 44 items for this class: the 17 prologue writes // (all branches, as the recovery always presents them) plus one item per list // helper, with the element kind guessed and the count word dropped. That tail is // a flattening artifact, not a description of the wire -- see the note in // tests/mars_stream/test_wire_schema.cpp, which checks the prologue against the // table item for item and checks the two counts (17 + 27) instead of pretending // the tail aligns. // // Only five of the twenty-seven lists have ever been observed non-empty (3, 5, 7, // 8 and 14 below). The rest are typed from the writer's instruction stream alone // and are HYPOTHESES: the scalar sequence is read directly off the helper, but no // save exercises it. Where an element holds a nested object whose own body is not // otherwise modelled here, it is carried as a Node rather than guessed at. struct TcDesignCmd { // list 1: a StreamableHelper frame plus an int Node design; int32_t value = 0; }; struct TcInt1 { int32_t a = 0; }; struct TcInt2 { int32_t a = 0, b = 0; }; struct TcInt3 { int32_t a = 0, b = 0, c = 0; }; struct TcInt4 { int32_t a = 0, b = 0, c = 0, d = 0; }; struct TcIntBool { int32_t a = 0; bool b = false; }; struct TcInt2Bool { int32_t a = 0, b = 0; bool c = false; }; struct TcIntFloat { int32_t a = 0; float b = 0; }; struct TcIntStr { int32_t a = 0; std::string b; }; struct TcIntNode { // an id plus a nested object this codec does not model int32_t a = 0; Node body; }; struct TcSysRates { // list 5: system id + Game::StarSystem::OutputRates int32_t sysID = 0; Rts rates; }; struct TcNotes { // list 6: Game::PlayerNotes Note note; }; struct TcFleetMove { // list 8: fleet id + the route it was given, as system ids int32_t fleetID = 0; std::vector route; }; struct TcCmd09 { int32_t a = 0; bool b = false; int32_t c = 0, d = 0; float e = 0; }; struct TcCmd10 { int32_t a = 0, b = 0; std::vector ids; }; struct TcCmd21 { int32_t a = 0, b = 0; std::vector values; // VectorHelper> }; struct TcPopulationCmd { // list 23: id + Game::Population int32_t a = 0; Population pop; }; struct TcRaidCmd { // list 26: Game::RaidTargets Node targets; }; struct TurnCommands { static constexpr const char* kStreamName = "CD"; // --- prologue: six flag-gated groups ------------------------------------------ int32_t playerID = 0; bool hasResearchRate = false; float researchRate = 0; // the empire savings/research slider, 0.25 by default bool hasResearchTarget = false; int32_t researchTarget = 0; // tech id picked on the research screen bool hasResearchBoost = false; int32_t researchBoostSpend = 0; // savings spent by the Boost Research panel float researchBoostFraction = 0; bool hasGroup4 = false; bool group4Flag = false; int32_t group4Value = 0; bool hasGroup5 = false; float group5a = 0, group5b = 0, group5c = 0; bool hasCivilianRatios = false; CivilianRatios civilianRatios; // --- twenty-seven counted command lists ---------------------------------------- std::vector newDesigns; // 1 std::vector list02; // 2 std::vector buildOrders; // 3 observed: ordinal, designID, systemID, 0 std::vector list04; // 4 std::vector systemRates; // 5 observed: the planetary budget sliders std::vector playerNotes; // 6 std::vector colonizeOrders; // 7 observed: shipID, 1 std::vector fleetMoves; // 8 observed: fleetID, route std::vector list09; // 9 std::vector list10; // 10 std::vector list11; // 11 std::vector fleetLayouts; // 12 Game::FleetLayout std::vector list13; // 13 an id and a string std::vector list14; // 14 observed once, alongside a fleet move std::vector list15; // 15 std::vector list16; // 16 std::vector list17; // 17 std::vector list18; // 18 std::vector list19; // 19 std::vector list20; // 20 std::vector list21; // 21 std::vector weaponGroups; // 22 Game::WeaponGroups std::vector popCmds; // 23 Game::Population std::vector list24; // 24 std::vector defenceLayouts; // 25 Game::DefenceLayout std::vector raidTargets; // 26 Game::RaidTargets std::vector list27; // 27 std::vector extra; // The number of list members is a fact about the writer, not a guess, and the // conformance test checks it against the wire table's own tail length. static constexpr int kListCount = 27; template void io(Ar& ar) { ar.i32(R("playerID"), playerID); ar.b(R("hasResearchRate"), hasResearchRate); ar.when(hasResearchRate, [&](Ar& a) { a.f32(R("researchRate"), researchRate); }); ar.b(R("hasResearchTarget"), hasResearchTarget); ar.when(hasResearchTarget, [&](Ar& a) { a.i32(R("researchTarget"), researchTarget); }); ar.b(R("hasResearchBoost"), hasResearchBoost); ar.when(hasResearchBoost, [&](Ar& a) { a.i32(R("researchBoostSpend"), researchBoostSpend); a.f32(R("researchBoostFraction"), researchBoostFraction); }); ar.b(R("hasGroup4"), hasGroup4); ar.when(hasGroup4, [&](Ar& a) { a.b(R("group4Flag"), group4Flag); a.i32(R("group4Value"), group4Value); }); ar.b(R("hasGroup5"), hasGroup5); ar.when(hasGroup5, [&](Ar& a) { a.f32(R("group5a"), group5a); a.f32(R("group5b"), group5b); a.f32(R("group5c"), group5c); }); ar.b(R("hasCivilianRatios"), hasCivilianRatios); ar.when(hasCivilianRatios, [&](Ar& a) { a.obj(R("civilianRatios"), civilianRatios); }); ar.narr(R("nDesigns"), newDesigns, [](Ar& a, TcDesignCmd& e) { a.any(R("design"), e.design); a.i32(R("value"), e.value); }); ar.narr(R("n02"), list02, [](Ar& a, TcInt1& e) { a.i32(R("a"), e.a); }); ar.narr(R("nBuild"), buildOrders, [](Ar& a, TcInt4& e) { a.i32(R("ordinal"), e.a); a.i32(R("designID"), e.b); a.i32(R("systemID"), e.c); a.i32(R("d"), e.d); }); ar.narr(R("n04"), list04, [](Ar& a, TcInt3& e) { a.i32(R("a"), e.a); a.i32(R("b"), e.b); a.i32(R("c"), e.c); }); ar.narr(R("nSysRates"), systemRates, [](Ar& a, TcSysRates& e) { a.i32(R("systemID"), e.sysID); a.obj(R("rates"), e.rates); }); ar.narr(R("nNotes"), playerNotes, [](Ar& a, TcNotes& e) { a.obj(R("note"), e.note); }); ar.narr(R("nColonize"), colonizeOrders, [](Ar& a, TcInt2& e) { a.i32(R("shipID"), e.a); a.i32(R("b"), e.b); }); ar.narr(R("nFleetMoves"), fleetMoves, [](Ar& a, TcFleetMove& e) { a.i32(R("fleetID"), e.fleetID); a.narr(R("nRoute"), e.route, [](Ar& b, int32_t& s) { b.i32(R("systemID"), s); }); }); ar.narr(R("n09"), list09, [](Ar& a, TcCmd09& e) { a.i32(R("a"), e.a); a.b(R("b"), e.b); a.i32(R("c"), e.c); a.i32(R("d"), e.d); a.f32(R("e"), e.e); }); ar.narr(R("n10"), list10, [](Ar& a, TcCmd10& e) { a.i32(R("a"), e.a); a.i32(R("b"), e.b); a.narr(R("nIDs"), e.ids, [](Ar& b, int32_t& s) { b.i32(R("id"), s); }); }); ar.narr(R("n11"), list11, [](Ar& a, TcIntBool& e) { a.i32(R("a"), e.a); a.b(R("b"), e.b); }); ar.narr(R("nFleetLayouts"), fleetLayouts, [](Ar& a, TcIntNode& e) { a.i32(R("a"), e.a); a.any(R("layout"), e.body); }); ar.narr(R("n13"), list13, [](Ar& a, TcIntStr& e) { a.i32(R("a"), e.a); a.str(R("b"), e.b); }); ar.narr(R("n14"), list14, [](Ar& a, TcInt2Bool& e) { a.i32(R("a"), e.a); a.i32(R("b"), e.b); a.b(R("c"), e.c); }); ar.narr(R("n15"), list15, [](Ar& a, TcIntBool& e) { a.i32(R("a"), e.a); a.b(R("b"), e.b); }); ar.narr(R("n16"), list16, [](Ar& a, TcIntBool& e) { a.i32(R("a"), e.a); a.b(R("b"), e.b); }); ar.narr(R("n17"), list17, [](Ar& a, TcInt3& e) { a.i32(R("a"), e.a); a.i32(R("b"), e.b); a.i32(R("c"), e.c); }); ar.narr(R("n18"), list18, [](Ar& a, TcInt3& e) { a.i32(R("a"), e.a); a.i32(R("b"), e.b); a.i32(R("c"), e.c); }); ar.narr(R("n19"), list19, [](Ar& a, TcInt1& e) { a.i32(R("a"), e.a); }); ar.narr(R("n20"), list20, [](Ar& a, TcInt1& e) { a.i32(R("a"), e.a); }); ar.narr(R("n21"), list21, [](Ar& a, TcCmd21& e) { a.i32(R("a"), e.a); a.i32(R("b"), e.b); a.carr(R("values"), e.values); }); ar.narr(R("nWeaponGroups"), weaponGroups, [](Ar& a, TcIntNode& e) { a.i32(R("a"), e.a); a.any(R("groups"), e.body); }); ar.narr(R("nPop"), popCmds, [](Ar& a, TcPopulationCmd& e) { a.i32(R("a"), e.a); a.obj(R("pop"), e.pop); }); ar.narr(R("n24"), list24, [](Ar& a, TcIntFloat& e) { a.i32(R("a"), e.a); a.f32(R("b"), e.b); }); ar.narr(R("nDefenceLayouts"), defenceLayouts, [](Ar& a, TcIntNode& e) { a.i32(R("a"), e.a); a.any(R("layout"), e.body); }); ar.narr(R("nRaidTargets"), raidTargets, [](Ar& a, TcRaidCmd& e) { a.any(R("targets"), e.targets); }); ar.narr(R("n27"), list27, [](Ar& a, TcInt1& e) { a.i32(R("a"), e.a); }); ar.rest(extra); } }; // One `CD` frame, keyed by the id at the same ordinal in `CDT`. Both id suffixes // the campaign has seen are now typed; anything else is still carried verbatim. struct CustomDataBlock { static constexpr const char* kStreamName = "CD"; enum class Which { Unknown, AIAgent, TurnCmds }; Which which = Which::Unknown; StrategyAIAgent aiAgent; TurnCommands turnCommands; std::vector unknown; static bool ends_with(const std::string& s, const char* suf) { const size_t n = std::char_traits::length(suf); return s.size() >= n && s.compare(s.size() - n, n, suf) == 0; } void select(const std::string& id) { // The version is part of the id, so a future ".TurnCommands_v6" falls back // to the carried Node instead of being decoded with a stale layout. if (ends_with(id, ".AIAgent")) which = Which::AIAgent; else if (ends_with(id, ".TurnCommands_v5")) which = Which::TurnCmds; else which = Which::Unknown; } template void io(Ar& ar) { switch (which) { case Which::AIAgent: aiAgent.io(ar); break; case Which::TurnCmds: turnCommands.io(ar); break; case Which::Unknown: ar.rest(unknown); break; } } }; struct Sim { static constexpr const char* kStreamName = "Sim"; std::string keyPath; int32_t nmSz = 0, nmLc = 0, nmnx = 0; std::vector playerIds, designIds, systemIds, fleetIds, shipIds, tradeIds; int32_t modCount = 0, frame = 0, gameID = 0; std::optional aiDifficultyID; AttribMap attrib; // StreamableHelper: a "." count, 0 in every save std::optional rand; Node rng; // opaque MT19937 blob: frame with one "." raw item (mt[624] + left, 3 pad bytes) std::string gameName; int32_t map = 0; float incMod = 0, resMod = 0; std::optional randEnc; bool enAl = false, enTm = false; int32_t goTurn = 0; std::vector goWinPly; int32_t npcm = 0, npco = 0, npci = 0, npcv = 0, npca = 0; std::optional npc; float szadj = 0, rsadj = 0, suadj = 0; ProjectNames sprjs; float randEncAdj = 0; int32_t cmbtid = 0; TurnStats turnstats; std::vector combatReports; std::vector invasions; std::vector exclusions3, exclusions2, exclusionsCF; std::vector players; std::vector species; std::vector systems; NodeGrid ndGr2; TradeManager trdmgr; SpyManager spymgr; std::vector fleets; std::vector acts; std::optional svSctOb; std::vector zoneDefence; std::vector extra; template void io(Ar& ar) { auto idlist = [](Ar& a, int32_t& e) { a.i32(R("."), e); }; ar.str(A("KeyPath"), keyPath); ar.i32(A("NMSz"), nmSz); ar.i32(A("NMLc"), nmLc); ar.i32(A("NMnx"), nmnx); ar.narr(A("PlayerIDs"), playerIds, idlist); ar.narr(A("DesignIDs"), designIds, idlist); ar.narr(A("SystemIDs"), systemIds, idlist); ar.narr(A("FleetIDs"), fleetIds, idlist); ar.narr(A("ShipIDs"), shipIds, idlist); ar.narr(A("TradeIDs"), tradeIds, idlist); ar.i32(A("ModCount"), modCount); ar.i32(A("Frame"), frame); ar.i32(A("GameID"), gameID); ar.opt_i32(A("AIDifficultyID"), aiDifficultyID); ar.obj(A("Attrib"), attrib); ar.opt_i32(A("Rand"), rand); ar.raw_frame(A("RNG"), rng); ar.str(A("GameName"), gameName); ar.i32(A("Map"), map); ar.f32(A("IncMod"), incMod); ar.f32(A("ResMod"), resMod); ar.opt_b(A("RandEnc"), randEnc); ar.b(A("EnAl"), enAl); ar.b(A("EnTm"), enTm); ar.i32(A("GOTurn"), goTurn); ar.carr(A("GOWinPly"), goWinPly); ar.i32(A("NPCm"), npcm); ar.i32(A("NPCo"), npco); ar.i32(A("NPCi"), npci); ar.i32(A("NPCv"), npcv); ar.i32(A("NPCa"), npca); ar.opt_i32(A("NPC"), npc); ar.f32(A("szadj"), szadj); ar.f32(A("rsadj"), rsadj); ar.f32(A("suadj"), suadj); ar.obj(A("sprjs"), sprjs); ar.f32(A("RandEncAdj"), randEncAdj); ar.i32(A("cmbtid"), cmbtid); ar.obj(A("turnstats"), turnstats); ar.narr(A("numcreps"), combatReports, [](Ar& a, Crep& e) { a.obj(A("crep"), e); }); ar.narr(A("ninv"), invasions, [](Ar& a, Invasion& e) { e.io(a); }); auto excl = [](Ar& a, IntPair& e) { a.i32(A("AllExc"), e.a); a.i32(A("AllExc"), e.b); }; ar.narr(A("AllExc"), exclusions3, excl); ar.narr(A("AllExc"), exclusions2, excl); ar.narr(A("AllExcCF"), exclusionsCF, [](Ar& a, IntPair& e) { a.i32(A("AllExcCFp"), e.a); a.i32(A("AllExcCFp"), e.b); }); ar.narr(A("NumPlrs"), players, [](Ar& a, PlayerEntry& e) { e.io(a); }); ar.repeat("ISsp", species, [](Ar& a, Species& e) { e.io(a); }); // 7 pairs, no count ar.narr(A("NumSys"), systems, [](Ar& a, SysEntry& e) { e.io(a); }); ar.obj(A("NdGr2"), ndGr2); ar.obj(A("trdmgr"), trdmgr); ar.obj(A("spymgr"), spymgr); ar.narr(A("NumFlts"), fleets, [](Ar& a, FleetEntry& e) { e.io(a); }); ar.narr(A("NumActs"), acts, [](Ar& a, int32_t& e) { a.i32(A("Act"), e); }); ar.opt_obj(A("SvSctOb"), svSctOb); // only if the pointer was non-NULL ar.narr(A("zdsc"), zoneDefence, [](Ar& a, ZoneDefence& e) { e.io(a); }); ar.rest(extra); } }; // ---- custom-data table + file root ----------------------------------------------------- struct CdTable { static constexpr const char* kStreamName = "CDT"; std::vector ids; std::vector extra; template void io(Ar& ar) { ar.narr(A("NumIDs"), ids, [](Ar& a, std::string& e) { a.str(A("ID"), e); }); ar.rest(extra); } }; struct SaveGame { // root: Summary, CreateParams, Sim, CDT, then one opaque CD frame per id with data static constexpr const char* kStreamName = ""; Summary summary; CreateParams createParams; Sim sim; CdTable cdTable; std::vector customData; template void io(Ar& ar) { ar.obj(A("Summary"), summary); ar.obj(A("CreateParams"), createParams); ar.obj(A("Sim"), sim); ar.obj(A("CDT"), cdTable); // The CD frames carry no key of their own: the id at the same ordinal in // CDT selects the body, in both directions. size_t k = 0; ar.repeat("CD", customData, [&](Ar& a, CustomDataBlock& e) { e.select(k < cdTable.ids.size() ? cdTable.ids[k] : std::string()); ++k; a.obj(A("CD"), e); }); } }; } // namespace mars::stream::shapes