sots-engine/src/mars/stream/shapes.h
alex 7f04bcc88b lane WS: pay the ratchet debt -- type the five new bodies, do not move the bar
The corpus went 11 -> 19 saves and named coverage fell to 97.6% against a 99.99
bar. Rule 27 says type the content. Typed, all from the recovered wire schema
plus the records the new trade/spy saves finally carry:

  Game::CombatPlayerReport  the <rest:prep> tail -- ncls run of six per ship
                            class, nsec run of two per section, ndam, srep
  Game::CombatShipReport    srep elements (caps2 is an i64; dami/damt f32)
  Game::TacReport           TRnc is a COUNT and TRships/TRsats/TRshipsL are one
                            loop body, INTERLEAVED on the wire -- not three
                            trailing runs. Two lanes could not settle this
                            because TRnc was 0 in every save until tonight.
  Game::TacReportEvents     TRby / TRto
  Game::FleetLayout         Lay: a FieldTemplate frame then a count and its ids
  Game::TradeRoute          rt -- a container write with NO count word, read as
                            an uncounted run keyed on the tag
  Game::ServerTradeSector::FreighterWarning   fwarn elements
  Game::SpyCraft            spy elements
  Game::WeaponGroups        Dwg, and Game::GunBankSelection under it

Corrections found on the way, both silent until a shape was bound to the table:

  * Game::CombatReport: auto and cdst are BOOLs, dur/cdt/cdi are FLOATS. All
    five were ints here. Four are 0/1 everywhere so the bytes never moved; cdt
    is not, and was being read as 1070805848 instead of 1.598.
  * Game::CombatWeaponReport: the damage quartet is flat and dami/damt are
    floats. The old shape reached them through obj_flex, modelling a nested
    `dams` frame the binary does not write. That also made CoverageArchive
    charge one phantom typed item per weapon report -- exactly 8/13/21/32 on the
    four affected saves -- so the pre-fix coverage figures were slightly
    optimistic as well as too low.

FTPnts stays carried: it is the one item the recovery itself marks unresolved,
its count is 0 in every save, and element framing is a property of the helper --
the SysMem / mts / nalat trap. The workload that settles it is a save with a
stored fleet tactical formation.

Named coverage, per save, before -> after:
  human-turn5-traderoutes   99.0003% -> 99.9951%
  human-turn8-traderoutes   98.6349% -> 99.9955%
  human-turn11-spytechs     97.6382% -> 99.9959%
  human-turn15-spyprogram   96.9412% -> 99.9964%
  the other 16 saves        unchanged, 99.9940-99.9953%
  corpus                    99.5021% -> 99.9949%

All 20 saves are above the ratchet; the only opaque items left anywhere are the
two of the RNG blob. 14 new shapes bound to the wire schema, every one a full
match: 100 shapes, 966 items, 0 MISMATCH. ctest 58/58 over all 20 saves, both
round trips byte-identical, 0 errors and 0 warnings.
2026-09-08 20:14:18 -04:00

3280 lines
112 KiB
C++

// 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 <cstdint>
#include <optional>
#include <string>
#include <vector>
#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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<PlayerInfo> players;
Session session;
int32_t mapShape = 0;
float incMod = 0, resMod = 0;
bool alliances = false, teams = false, encounters = false;
std::string scenario;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Planet> planets; // VectorHelper<SystemParams>
std::vector<std::vector<int32_t>> players; // "." count, n x "." frame{ "." count, n x "." int }
std::vector<Node> nodePaths; // VectorHelper<SimpleNodePath>, empty in the real saves
std::vector<Node> extra;
template <class Ar>
void io(Ar& ar) {
ar.i32(R("mapType"), mapType);
ar.carr(R("planets"), planets);
ar.narr(R("players"), players, [](Ar& a, std::vector<int32_t>& e) { a.carr(R("."), e); });
ar.carr(R("nodePaths"), nodePaths);
ar.rest(extra);
}
};
struct ScriptParam {
std::string spsn, sppn, sppv;
template <class Ar>
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<ScriptParam> params;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<PopG> groups;
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
void io(Ar& ar) {
ar.i32(A("msp"), msp);
ar.i32(A("mv"), mv);
}
};
struct Morale {
static constexpr const char* kStreamName = "";
std::vector<MoraleEntry> entries;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<BuildOrder> orders;
bool ordersFramed = true;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<int32_t> 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<Node> extra;
template <class Ar>
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 <class Ar>
void io(Ar& ar) {
ar.i32(A("haltt"), haltt);
ar.b(A("haltv"), haltv);
}
};
struct AdctEntry {
int32_t ads = 0, adt = 0;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("ads"), ads);
ar.i32(A("adt"), adt);
}
};
struct PlagueEntry {
int32_t plgT = 0;
Node plg;
template <class Ar>
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 <class Ar>
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 <class Ar>
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 <class Ar>
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<float> iSuit;
float suit = 0;
int32_t res = 0;
std::optional<int32_t> 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<HaltEntry> 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<MoraleEvent> cme2;
bool cme2Framed = true;
// `spies2` is VectorHelper<int> 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<int32_t> spies2;
int32_t pid = 0, defF = 0, defSF = 0;
std::optional<BuildQueue> bq;
std::vector<AdctEntry> adct;
std::vector<PlagueEntry> plagues;
std::vector<int32_t> fleets, gates, stations, monitors;
std::vector<Colony> colonies;
std::vector<NveEntry> nve;
std::vector<ViewEntry> views;
bool hindi = false;
IndependenceInfo indi;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
void io(Ar& ar) {
ar.i32(R("a"), a);
ar.i32(R("b"), b);
ar.rest(extra);
}
};
// Mars::StreamableEnum<T>: 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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<GunBank> gunBanks;
// DOpts is VectorHelper<Mars::String>, so its elements are NULL-named strings.
std::vector<std::string> opts;
std::vector<Node> extra;
template <class Ar>
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<int32_t> banks;
template <class Ar>
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 <class Ar>
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<WeaponGroup> groups;
std::vector<Node> extra;
template <class Ar>
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<DesignSection> 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<Node> extra;
template <class Ar>
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 <class Ar>
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 <class Ar>
void io(Ar& ar) {
ar.i32(A("DesID"), desID);
ar.obj(A("Des"), des);
}
};
struct PrEntry {
float prm = 0;
int32_t prbt = 0;
template <class Ar>
void io(Ar& ar) {
ar.f32(A("PRm"), prm);
ar.i32(A("PRBt"), prbt);
}
};
struct SprjEntry {
int32_t sprjT = 0;
Node sprj;
template <class Ar>
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 <class Ar>
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<Node> extra;
template <class Ar>
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<EventRec> events;
std::vector<Node> extra;
template <class Ar>
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<TurnEvents> turns;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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 <class Ar>
void io(Ar& ar) {
ar.i32(A("sp"), sp);
ar.i32(A("va2"), va2);
}
};
std::vector<Entry> ratios;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<std::string> branches;
template <class Ar>
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 <class Ar>
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<TechNode> techs;
std::vector<TechState> state;
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
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 <class Ar>
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<ShipRecord> recs;
std::vector<DesignRecord> designs;
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
void io(Ar& ar) {
ar.i32(A("Fid"), fid);
ar.i32(A("Ast"), ast);
}
};
std::vector<Entry> entries;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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 <class Ar>
void io(Ar& ar) {
ar.i32(A("msgt"), msgt);
ar.any(A("msg"), msg);
}
};
std::vector<Entry> msgs;
std::vector<Node> extra;
template <class Ar>
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<Node> defences, trade, events, techTree;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<FreighterWarning> fwarn;
std::vector<int32_t> systems, fleets;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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 <class Ar>
void io(Ar& ar) {
ar.i32(A("TradeID"), tradeID);
ar.obj(A("Trade"), trade);
}
};
std::vector<Entry> 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<TradeRoute> routes;
std::vector<Node> extra;
template <class Ar>
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<int32_t> cpl; // ncp: compromised players; 0 in the one record we hold
std::vector<Node> extra;
template <class Ar>
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<SpyCraft> spies;
std::vector<Node> extra;
template <class Ar>
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<Node> used; // usnc is 0 in every save available
std::string nm, ntg;
template <class Ar>
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<ProjectName> names;
std::vector<Node> extra;
template <class Ar>
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<float> sensMod;
std::optional<int32_t> 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<int32_t> lcid;
int32_t lcid2 = 0;
std::string resTNm;
bool resErrRoll = false;
ConMods conMods;
bool conModsFramed = false;
std::vector<int32_t> owners;
std::vector<DesignEntry> designs, legacyDesigns;
std::vector<Note> notes;
std::vector<PrEntry> pr;
bool hasAIR = false;
Node air;
bool cta = false;
AIEncounterFlags aiEnf;
std::vector<SprjEntry> specialProjects;
std::vector<NexpEntry> nexp;
std::vector<int32_t> weapXcl;
std::vector<Node> ojvs; // Game::Objective elements; the count is 0 in every save available
std::vector<DipStat> dipstats;
CommMessages comms;
std::vector<Prep> preps;
std::vector<Odes> odes;
std::vector<Owep> owep;
std::vector<Otch> otch;
std::vector<PlayerAid> aid;
std::vector<Node> defLayouts, raidTargets;
int32_t tnc = 0;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Waypoint> wpts;
bool wptsFramed = true;
float fpsp2 = 0;
int32_t fpeta2 = 0;
Vec3 fpogn2, fpdpos;
int32_t pnd = 0;
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
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<PrisonerEntry> prisoners;
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
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<Thruster> thrusters;
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
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<Node> points;
std::vector<Node> extra;
template <class Ar>
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<int32_t> shipIDs;
std::vector<Node> extra;
template <class Ar>
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<int32_t> sysID, trdID;
bool hfPlan = false;
FlightPlan fplan;
std::string ftName;
std::optional<int32_t> caps, gtTrf;
std::optional<float> ftSens;
std::optional<int32_t> 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<ShipEntry> ships;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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 <class Ar>
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 <class Ar>
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<CrepClass> classes;
std::vector<CrepSector> sectors;
int32_t ndam = 0;
std::vector<Srep> srep;
std::vector<Node> extra;
template <class Ar>
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<CrepPrep> prep;
bool prepFramed = true;
std::vector<Wrep> wrep;
bool wrepFramed = true;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<NodePath> paths;
bool pathsFramed = true;
int32_t nextId = 0;
std::vector<Node> extra;
template <class Ar>
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<int32_t> others;
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
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<SystemEvent> sacq, slost;
int32_t trn = 0, almem = 0, inc = 0, tdinc = 0, sav = 0, col = 0, bat = 0, tch = 0;
std::vector<ClassStats> classes;
std::vector<Node> extra;
template <class Ar>
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<PlayerTurnStats> stats; // no count: until the frame END
template <class Ar>
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 <class Ar>
void io(Ar& ar) {
ar.i32(A("ply"), ply);
ar.obj(A("hist"), hist);
}
};
struct TurnStats {
static constexpr const char* kStreamName = "turnstats";
std::vector<TurnHistoryEntry> players;
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
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 <class Ar>
void io(Ar& ar) {
ar.str(A("ISsp"), issp);
ar.f32(A("ISsu"), issu);
}
};
struct PlayerEntry {
int32_t playerID = 0;
Player player;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("PlayerID"), playerID);
ar.obj(A("Player"), player);
}
};
struct SysEntry {
int32_t sysID = 0;
Sys sys;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("SysID"), sysID);
ar.obj(A("Sys"), sys);
}
};
struct FleetEntry {
int32_t fltID = 0;
Fleet flt;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("FltID"), fltID);
ar.obj(A("Flt"), flt);
}
};
struct ZoneDefence {
int32_t zdsi = 0, zdst = 0;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("zdsi"), zdsi);
ar.i32(A("zdst"), zdst);
}
};
// ---- SvSctOb: the script-object tree -------------------------------------------------
//
// `SvSctOb` is a StreamableHelper<Game::SVScriptObject> -- 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 <class Ar>
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<FleetAssignment> asg;
std::vector<SwarmInfestation> infest;
int32_t deshive = 0, deslarva = 0;
std::vector<Node> extra;
template <class Ar>
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<DesignWeight> designs;
std::vector<FleetAssignment> asg;
template <class Ar>
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<MonitorSpawn> spawns;
std::vector<Node> extra;
template <class Ar>
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<FleetAssignment> asg;
int32_t cdiff = 0;
std::vector<int32_t> tdids, adids;
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
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 <class Ar>
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<int32_t> designIds;
std::vector<SVSOSwarmQueenHive> hives;
std::vector<SVSOSwarmQueenQueen> queens;
// VectorHelper<StreamableEnum<uint>>: each element is a "." FRAME holding one
// "." int, not a bare int. Count 0 in every save, so nothing exercises it.
std::vector<StreamEnum> sysMem;
std::vector<Node> extra;
template <class Ar>
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<FleetAssignment> asg;
std::vector<int32_t> wids;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<RefugeePlayer> players;
std::vector<int32_t> dids;
bool ini = false;
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
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<VonNeumannDefeat> defeats;
int32_t rusav = 0, nenc = 0, nmb = 0, nml = 0, nbb = 0, nbl = 0, nskb = 0, nskl = 0;
std::vector<StreamEnum> mts; // VectorHelper<StreamableEnum<uint>>: 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<std::string> trev; // VectorHelper<Mars::String>
std::vector<Node> extra;
template <class Ar>
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 <class Ar>
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<SVSOTrap> traps;
std::vector<Node> extra;
template <class Ar>
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<int32_t> dsys, des;
float drad = 0;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> 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 <class Ar>
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<Node> unknown; // 7, 8, 14, 21 and any id with no factory entry
void select(int32_t encID) { id = encID; }
template <class Ar>
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<Node> scnObj; // StreamableHelper<SVSOScenarioBase>, NULL in our saves
std::vector<Extra> extras;
std::vector<Enc> encounters;
std::vector<Node> extra;
template <class Ar>
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<Entry> entries;
std::vector<Node> extra;
template <class Ar>
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<K>: 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 <class Key>
struct AIWeightMap {
static constexpr const char* kStreamName = "";
struct Entry {
Key key;
float weight = 0;
};
std::vector<Entry> entries;
std::vector<Node> extra;
template <class Ar>
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<StreamEnum>;
using AIWeightMapSection = AIWeightMap<ShipSectionID>;
struct AISituation { // Game::AISituation
static constexpr const char* kStreamName = "AISit";
AIWeightMapSection sections;
AIWeightMapEnum weaponFamilies;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Name> names; // dnnc counts the (dnid, dnnm) pairs
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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 <class Ar>
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<TacClass> classes;
std::vector<Node> extra;
template <class Ar>
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<TacReport> tacReports;
std::vector<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<Node> extra;
template <class Ar>
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<int32_t> 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<AIRequestStamp> 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<DesignShare> designShares;
std::vector<IntPair> borderStability; // NBStab x (BStabPl, BStabTn)
std::vector<IntPair> menaceBlacklist; // NMBlst x (MBlstSy, MBlstTn)
DesignNameGen designNames;
int32_t aiHivJ = 0, sdFlT = 0;
std::vector<StreamEnum> newAlliances; // nalat
int32_t lnat = 0, lat = 0;
std::vector<AISysEntry> systems; // AINumSys x (AISysID, AISys)
std::vector<AICombatReport> combatReports;
std::vector<ColonyLoss> colonyLosses;
std::vector<Provocation> provocations;
std::vector<int32_t> techScores;
int32_t fct = 0;
std::vector<AIAutoPeaceRun> autoPeaceRuns;
std::vector<Node> extra;
template <class Ar>
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<T>` 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<ShipDesignDef> 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<int32_t> 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<int32_t> ids;
};
struct TcCmd21 {
int32_t a = 0, b = 0;
std::vector<StreamEnum> values; // VectorHelper<StreamableEnum<uint>>
};
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<TcDesignCmd> newDesigns; // 1
std::vector<TcInt1> list02; // 2
std::vector<TcInt4> buildOrders; // 3 observed: ordinal, designID, systemID, 0
std::vector<TcInt3> list04; // 4
std::vector<TcSysRates> systemRates; // 5 observed: the planetary budget sliders
std::vector<TcNotes> playerNotes; // 6
std::vector<TcInt2> colonizeOrders; // 7 observed: shipID, 1
std::vector<TcFleetMove> fleetMoves; // 8 observed: fleetID, route
std::vector<TcCmd09> list09; // 9
std::vector<TcCmd10> list10; // 10
std::vector<TcIntBool> list11; // 11
std::vector<TcIntNode> fleetLayouts; // 12 Game::FleetLayout
std::vector<TcIntStr> list13; // 13 an id and a string
std::vector<TcInt2Bool> list14; // 14 observed once, alongside a fleet move
std::vector<TcIntBool> list15; // 15
std::vector<TcIntBool> list16; // 16
std::vector<TcInt3> list17; // 17
std::vector<TcInt3> list18; // 18
std::vector<TcInt1> list19; // 19
std::vector<TcInt1> list20; // 20
std::vector<TcCmd21> list21; // 21
std::vector<TcIntNode> weaponGroups; // 22 Game::WeaponGroups
std::vector<TcPopulationCmd> popCmds; // 23 Game::Population
std::vector<TcIntFloat> list24; // 24
std::vector<TcIntNode> defenceLayouts; // 25 Game::DefenceLayout
std::vector<TcRaidCmd> raidTargets; // 26 Game::RaidTargets
std::vector<TcInt1> list27; // 27
std::vector<Node> 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 <class Ar>
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<Node> unknown;
static bool ends_with(const std::string& s, const char* suf) {
const size_t n = std::char_traits<char>::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 <class Ar>
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<int32_t> playerIds, designIds, systemIds, fleetIds, shipIds, tradeIds;
int32_t modCount = 0, frame = 0, gameID = 0;
std::optional<int32_t> aiDifficultyID;
AttribMap attrib; // StreamableHelper<Game::AttribMap>: a "." count, 0 in every save
std::optional<int32_t> 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<bool> randEnc;
bool enAl = false, enTm = false;
int32_t goTurn = 0;
std::vector<int32_t> goWinPly;
int32_t npcm = 0, npco = 0, npci = 0, npcv = 0, npca = 0;
std::optional<int32_t> npc;
float szadj = 0, rsadj = 0, suadj = 0;
ProjectNames sprjs;
float randEncAdj = 0;
int32_t cmbtid = 0;
TurnStats turnstats;
std::vector<Crep> combatReports;
std::vector<Invasion> invasions;
std::vector<IntPair> exclusions3, exclusions2, exclusionsCF;
std::vector<PlayerEntry> players;
std::vector<Species> species;
std::vector<SysEntry> systems;
NodeGrid ndGr2;
TradeManager trdmgr;
SpyManager spymgr;
std::vector<FleetEntry> fleets;
std::vector<int32_t> acts;
std::optional<ScriptObjects> svSctOb;
std::vector<ZoneDefence> zoneDefence;
std::vector<Node> extra;
template <class Ar>
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<std::string> ids;
std::vector<Node> extra;
template <class Ar>
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<CustomDataBlock> customData;
template <class Ar>
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