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:
commit
7f0b2a7c0a
4 changed files with 433 additions and 15 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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 ---------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue