merge lane Q: TurnCommands_v5 typed; named coverage 100% on all 11 saves; NVs PID tag fix (round-trip defect present at main)

This commit is contained in:
alex 2026-09-08 09:04:20 -04:00
commit 7f0b2a7c0a
4 changed files with 433 additions and 15 deletions

View file

@ -485,12 +485,18 @@ struct NveEntry {
ar.i32(A("Eid"), eid); 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 { struct ViewEntry {
int32_t pid = 0; int32_t pid = 0;
PlayerView pview; PlayerView pview;
template <class Ar> template <class Ar>
void io(Ar& ar) { void io(Ar& ar) {
ar.i32(R("pid"), pid); ar.i32(A("PID"), pid);
ar.obj(R("pview", "pview"), pview); ar.obj(R("pview", "pview"), pview);
} }
}; };
@ -2533,27 +2539,317 @@ struct StrategyAIAgent { // Game::StrategyAIAgent::Streamable
} }
}; };
// One `CD` frame, keyed by the id at the same ordinal in `CDT`. The other id // ---- Game::TurnCommands: the ".TurnCommands_v5" custom-data block ----------------
// suffix in our saves, ".TurnCommands_v5", stays carried: its writer's recovered //
// sequence cannot be aligned to a no-orders save even as a subsequence, so it // The pending client->server order queue, drained by ApplyTurnCommands; it is NOT
// needs a save with issued orders before anything can be typed. // 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 { struct CustomDataBlock {
static constexpr const char* kStreamName = "CD"; static constexpr const char* kStreamName = "CD";
enum class Which { Unknown, AIAgent }; enum class Which { Unknown, AIAgent, TurnCmds };
Which which = Which::Unknown; Which which = Which::Unknown;
StrategyAIAgent aiAgent; StrategyAIAgent aiAgent;
TurnCommands turnCommands;
std::vector<Node> unknown; 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) { void select(const std::string& id) {
static const std::string suffix = ".AIAgent"; // The version is part of the id, so a future ".TurnCommands_v6" falls back
which = (id.size() >= suffix.size() && id.compare(id.size() - suffix.size(), suffix.size(), suffix) == 0) // to the carried Node instead of being decoded with a stale layout.
? Which::AIAgent if (ends_with(id, ".AIAgent")) which = Which::AIAgent;
: Which::Unknown; else if (ends_with(id, ".TurnCommands_v5")) which = Which::TurnCmds;
else which = Which::Unknown;
} }
template <class Ar> template <class Ar>
void io(Ar& ar) { void io(Ar& ar) {
switch (which) { switch (which) {
case Which::AIAgent: aiAgent.io(ar); break; case Which::AIAgent: aiAgent.io(ar); break;
case Which::TurnCmds: turnCommands.io(ar); break;
case Which::Unknown: ar.rest(unknown); break; case Which::Unknown: ar.rest(unknown); break;
} }
} }

View file

@ -84,6 +84,22 @@ static void check_save(const std::string& path, const char* dump_dir) {
for (const auto& fe : sim.fleets) CHECK(fe.flt.ships.size() >= 1); for (const auto& fe : sim.fleets) CHECK(fe.flt.ships.size() >= 1);
CHECK(!doc.game.cdTable.ids.empty()); CHECK(!doc.game.cdTable.ids.empty());
// Every ".TurnCommands_v5" block must be consumed by the prologue plus the 27
// counted lists with nothing left over. `extra` is the tail `ar.rest()` would
// absorb, so an empty `extra` is the item-granular statement that the model
// accounts for the whole frame -- on the 35-item empty blocks and on the
// 123-item one alike. It is what a wrong list count or a wrong element width
// would break first.
for (const auto& cd : doc.game.customData) {
if (cd.which != shapes::CustomDataBlock::Which::TurnCmds) continue;
CHECK(cd.turnCommands.extra.empty());
CHECK(cd.turnCommands.playerID != 0);
std::printf(" TurnCommands: player %d, rate %.4g, %zu build, %zu sysRate, %zu colonize, %zu move\n",
cd.turnCommands.playerID, double(cd.turnCommands.researchRate),
cd.turnCommands.buildOrders.size(), cd.turnCommands.systemRates.size(),
cd.turnCommands.colonizeOrders.size(), cd.turnCommands.fleetMoves.size());
}
// --- RNG blob: mt[624] + left, produced by our MT19937 from RSeed -------------- // --- RNG blob: mt[624] + left, produced by our MT19937 from RSeed --------------
CHECK(sim.rng.is_complex() && sim.rng.children.size() == 1); CHECK(sim.rng.is_complex() && sim.rng.children.size() == 1);
const Node& blob = sim.rng.children[0]; const Node& blob = sim.rng.children[0];
@ -123,7 +139,9 @@ static void check_save(const std::string& path, const char* dump_dir) {
std::printf(" %s=%zu", worst[i].second.c_str(), worst[i].first); std::printf(" %s=%zu", worst[i].second.c_str(), worst[i].first);
std::printf("\n"); std::printf("\n");
// Ratchet, not a target: typing a body must never silently regress. // Ratchet, not a target: typing a body must never silently regress.
CHECK(pct >= 99.8); // With TurnCommands_v5 typed, the only items left carried as Nodes are the
// two of the MT19937 block, which is deliberately opaque.
CHECK(pct >= 99.99);
} }
// --- round trips ------------------------------------------------------------------ // --- round trips ------------------------------------------------------------------

View file

@ -720,9 +720,18 @@ struct CdOnly {
static void test_customdata_dispatch() { static void test_customdata_dispatch() {
CdOnly doc; CdOnly doc;
doc.cdTable.ids = {"Player.00000016.TurnCommands_v5", "Player.00000032.AIAgent"}; doc.cdTable.ids = {"Player.00000016.TurnCommands_v5", "Player.00000032.AIAgent"};
shapes::CustomDataBlock tc; // not modelled: carried verbatim shapes::CustomDataBlock tc;
tc.select(doc.cdTable.ids[0]); tc.select(doc.cdTable.ids[0]);
tc.unknown.push_back(Node::int32(".", 42)); tc.turnCommands.playerID = 16;
tc.turnCommands.hasResearchRate = true;
tc.turnCommands.researchRate = 0.97f;
tc.turnCommands.buildOrders.push_back({1, 608, 384, 0});
// A two-hop route: the fleet-move element ends in a COUNTED vector of system
// ids, not a fixed pair, so a multi-hop order is longer than a single-hop one.
shapes::TcFleetMove mv;
mv.fleetID = 688;
mv.route = {432, 512};
tc.turnCommands.fleetMoves.push_back(mv);
doc.customData.push_back(tc); doc.customData.push_back(tc);
shapes::CustomDataBlock agent; shapes::CustomDataBlock agent;
agent.select(doc.cdTable.ids[1]); agent.select(doc.cdTable.ids[1]);
@ -740,8 +749,16 @@ static void test_customdata_dispatch() {
back.io(ra); back.io(ra);
CHECK_EQ(count(issues, Issue::Error), size_t(0)); CHECK_EQ(count(issues, Issue::Error), size_t(0));
CHECK(back.customData.size() == 2); CHECK(back.customData.size() == 2);
CHECK(back.customData[0].which == shapes::CustomDataBlock::Which::Unknown); CHECK(back.customData[0].which == shapes::CustomDataBlock::Which::TurnCmds);
CHECK(back.customData[0].unknown.size() == 1); {
const shapes::TurnCommands& t = back.customData[0].turnCommands;
CHECK(t.playerID == 16 && t.hasResearchRate && t.researchRate == 0.97f);
CHECK(!t.hasResearchTarget && !t.hasResearchBoost && !t.hasCivilianRatios);
CHECK(t.buildOrders.size() == 1 && t.buildOrders[0].b == 608 && t.buildOrders[0].c == 384);
CHECK(t.fleetMoves.size() == 1 && t.fleetMoves[0].fleetID == 688);
CHECK(t.fleetMoves[0].route.size() == 2 && t.fleetMoves[0].route[1] == 512);
CHECK(t.extra.empty()); // the 27 lists account for the whole tail
}
CHECK(back.customData[1].which == shapes::CustomDataBlock::Which::AIAgent); CHECK(back.customData[1].which == shapes::CustomDataBlock::Which::AIAgent);
CHECK(back.customData[1].aiAgent.fct == 9 && back.customData[1].aiAgent.techScores.size() == 2); CHECK(back.customData[1].aiAgent.fct == 9 && back.customData[1].aiAgent.techScores.size() == 2);
Writer w2; Writer w2;
@ -756,6 +773,12 @@ static void test_customdata_dispatch() {
CHECK(probe.which == shapes::CustomDataBlock::Which::Unknown); CHECK(probe.which == shapes::CustomDataBlock::Which::Unknown);
probe.select(".AIAgent"); probe.select(".AIAgent");
CHECK(probe.which == shapes::CustomDataBlock::Which::AIAgent); CHECK(probe.which == shapes::CustomDataBlock::Which::AIAgent);
probe.select("Player.00000016.TurnCommands_v5");
CHECK(probe.which == shapes::CustomDataBlock::Which::TurnCmds);
// The layout is only known for _v5. A different version must fall back to the
// carried Node rather than be decoded with a stale shape.
probe.select("Player.00000016.TurnCommands_v6");
CHECK(probe.which == shapes::CustomDataBlock::Which::Unknown);
} }
// --- 9. gzip container --------------------------------------------------------------- // --- 9. gzip container ---------------------------------------------------------------

View file

@ -160,6 +160,81 @@ static void check(const char* shape_name, const char* cls) {
CHECK(r.mismatch == 0); CHECK(r.mismatch == 0);
} }
// --- Game::TurnCommands -------------------------------------------------------
//
// The generic check above cannot be used here, and the reason is the finding.
//
// Every item in this class is written with a NULL name, so the LCS degenerates:
// with all tags equal to "." the gap branches are unreachable and the walk becomes
// a strict positional comparison in which every primitive disagreement is a
// MISMATCH. That is fine when the two sequences describe the same thing. They do
// not. The writer's 44 recovered items are 17 member writes plus ONE ITEM PER
// CONTAINER CALL SITE -- there are exactly 27 std::list members, each written by
// its own helper as WriteInt(size) followed by `size` element records. The
// recovery, being a linear pass, keeps the call site, guesses its kind from an
// element field, and drops the count word. So the table's tail names 27 things
// that are on the wire as 27 counts (plus elements), and comparing an element kind
// against a count is comparing two different items.
//
// What IS checkable, and is checked here:
// * the 17-item prologue, item for item and primitive for primitive. Our shape
// models the six conditional groups the writer really has; SchemaProbe takes
// every branch, which is the same view the recovery has, so the two must agree
// exactly. They do -- 17/17 -- which is the evidence that the branch structure
// read out of the instruction stream is the one the recovery flattened.
// * the tail COUNT: the shape has exactly 27 top-level counted lists and the
// table has exactly 27 tail items. 27 == 27 is the whole content of that half
// of the table, and on a no-orders save it is also why the block is 8 + 27 = 35
// items: every list is empty and still writes its zero count.
static void check_turn_commands() {
using S = SchemaProbe::S;
const char* cls = "Game::TurnCommands";
const sots::wire::Class* c = sots::wire::find(cls);
if (!c) {
std::printf(" %-18s -> %-34s NOT IN TABLE\n", "TurnCommands", cls);
++fails;
return;
}
SchemaProbe p = SchemaProbe::of<sh::TurnCommands>();
size_t prologue = 0;
while (prologue < p.items.size() && !(p.items[prologue].member && p.items[prologue].shape == S::NArr)) ++prologue;
int lists = 0, opaque = 0;
for (const SchemaProbe::Item& i : p.items) {
if (i.member && i.shape == S::NArr) ++lists;
opaque += i.opaque;
}
int matched = 0, mismatch = 0;
const size_t n = std::min(prologue, size_t(c->count));
for (size_t i = 0; i < n; ++i) {
if (p.items[i].tag == c->fields[i].tag && prim_ok(p.items[i].prim, c->fields[i].prim)) {
++matched;
} else {
++mismatch;
std::printf(" MISMATCH prologue[%zu] shape %-6s vs wire %-6s\n", i,
probe_prim_name(p.items[i].prim), sots::wire::prim_name(c->fields[i].prim));
}
}
const int tail = int(c->count) - int(prologue);
++bound;
total_matched += matched;
total_mismatch += mismatch;
total_wire_only += tail; // the flattened container tail: reported, never claimed as matched
total_opaque += opaque;
std::printf(" %-18s -> %-34s [%-8s %2u/%-2u] shape %2zu wire %2u match %2d"
" wire-only %2d shape-only %2d opaque %d\n",
"TurnCommands", cls, c->grade, c->read_agree, c->read_comparable, p.items.size(), c->count,
matched, tail, 0, opaque);
std::printf(" prologue %d/%zu items agree; tail: %d wire item(s) vs %d container writer(s)"
" (one per call site, counts dropped by the linear recovery)\n",
matched, prologue, tail, lists);
CHECK(mismatch == 0);
CHECK(prologue == 17); // six flag-gated groups, all branches
CHECK(lists == sh::TurnCommands::kListCount); // 27 std::list members
CHECK(tail == lists); // and the table has one item per list
}
int main() { int main() {
std::printf("wire schema: %zu classes, generated from the game's serializers\n\n", std::printf("wire schema: %zu classes, generated from the game's serializers\n\n",
sots::wire::kClassCount); sots::wire::kClassCount);
@ -280,6 +355,12 @@ int main() {
"Game::Mars::Game::W4WeaponFamilyID::U?$StreamableEnum::?$AIWeightMap"); "Game::Mars::Game::W4WeaponFamilyID::U?$StreamableEnum::?$AIWeightMap");
check<sh::AIWeightMapEnum>("AIWeightMap<uint>", "Game::Mars::I::U?$StreamableEnum::?$AIWeightMap"); check<sh::AIWeightMapEnum>("AIWeightMap<uint>", "Game::Mars::I::U?$StreamableEnum::?$AIWeightMap");
// --- the TurnCommands custom-data block ---------------------------------------
// Bound by a dedicated check; see the note above check_turn_commands(). The
// element bodies it reaches are already bound above (OutputRates as Rts,
// Population, CivilianRatios, PlayerNotes as Note).
check_turn_commands();
std::printf( std::printf(
"\ntotals: %d shapes bound, %d items matched, %d MISMATCH, %d wire-only, " "\ntotals: %d shapes bound, %d items matched, %d MISMATCH, %d wire-only, "
"%d opaque item(s) in bound shapes\n", "%d opaque item(s) in bound shapes\n",