Compare commits
No commits in common. "aae2369a47fee8ecc24568e365191c06cb5bc421" and "2bf38bc379b4b7cb041bca0bc6aa0a0e14730e3d" have entirely different histories.
aae2369a47
...
2bf38bc379
5 changed files with 16 additions and 446 deletions
|
|
@ -1,5 +1,5 @@
|
|||
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
||||
// Source: sots-re ghidra/addresses.json @ f9b744e, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ 612bfe6, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
|
@ -1351,18 +1351,6 @@ constexpr uint32_t ServerSystem_ComputeMaxIncome = 0x003521c0;
|
|||
constexpr uint32_t SNMAllCombatDone_layout = 0x00624758;
|
||||
// data const float 0.5f -- the progress-ratio threshold in ServerPlayer::ProcessTurn's `ResT != NULL && ResErrRoll != 0 && CONST < progressRatio` gate (events.md §3, window 0x008915ec-0x00891624). Read out of dumps/sots.exe: .rdata bytes at 0x00a2c788 are 00 00 00 3f (float 0.5); the following word 0x00a2c78c is float 100.0, so this is a float32, NOT the double an 8-byte read would suggest (that reads as 5.28e13). CONSEQUENCE, measured on the live game: ResErrRoll survives into ProcessResearch only while progress/cost <= 0.5 at the START of the turn, so the OnTechResearched draw (0x0088df20) can fire only when a single turn supplies more than half the target tech's remaining cost. See the board's `research_roll_pending save` row [verified]
|
||||
constexpr uint32_t ResearchRollProgressThreshold = 0x0062c788;
|
||||
// thiscall void __thiscall Game::TurnCommands::Write(Mars::IStream* s) -- the writer for the `Player.<id>.TurnCommands_v5` custom-data block (CDT id -> one CD frame). 764 bytes, no loops of its own. Two halves. (1) A PROLOGUE of six flag-gated groups, each a WriteBool on a member followed, only when that bool is set, by the group's payload; write order is NOT offset order, which is why the offset-sorted layout view cannot be aligned to the wire: bool@0x0c gates f32@0x08 (research rate); bool@0x14 gates i32@0x10 (research target tech); bool@0x20 gates i32@0x18 + f32@0x1c (research boost spend + fraction); bool@0x2c gates bool@0x24 + i32@0x28; bool@0x3c gates f32@0x30,0x34,0x38; bool@0x6c gates a StreamableHelper<Game::CivilianRatios> frame on the member at 0x40. i32@0x04 (player id) is written first and unconditionally. (2) TWENTY-SEVEN std::list<T> members at 0x70..0x1a8, stride 0x0c ({_Myhead, _Mysize, _Alval}), each passed to its own free-function writer as helper(stream, &list). Every one is written unconditionally, so an empty list still costs one zero int: 8 prologue items + 27 zero counts = the 35-item block every no-orders save carries [verified]
|
||||
constexpr uint32_t TurnCommands_Write = 0x00442540;
|
||||
// thiscall void __thiscall Game::TurnCommands::Read(Mars::IStream* s) -- the reader paired with TurnCommands_Write (0x00842540). 1543 bytes; not decompiled by lane Q, listed so the pair is on the record [mapped]
|
||||
constexpr uint32_t TurnCommands_Read = 0x00492ae0;
|
||||
// cdecl void __cdecl (Mars::IStream* s, std::list<T>* moves) -- writer for TurnCommands member +0xc4, the FLEET MOVE list. Per element: WriteInt(fleetId) then a NESTED counted int vector -- n = (v._Mylast - v._Myfirst) >> 2 written with WriteInt, then n ints. So a fleet move on the wire is {fleetId, hopCount, hopCount x systemId}, NOT a fixed {fleetId, 1, destSystemId, 0} quadruple: a multi-hop route is longer. Observed single-hop in human-turn2-orders.sav {1456, 1, [128]} and zuul-turn15-orders.sav {688, 1, [432]} [verified]
|
||||
constexpr uint32_t TurnCommands_WriteFleetMoveList = 0x0043e550;
|
||||
// cdecl void __cdecl (Mars::IStream* s, std::list<T>* orders) -- writer for TurnCommands member +0x88, the BUILD ORDER list. Per element four WriteInts taken in DESCENDING member order (node+0x14, +0x10, +0xc, +0x8), so the wire order is {ordinal, designId, systemId, w}. Observed in zuul-turn15-orders.sav (5 orders, designs 608/576, system 384) and zuul-turn17-orders2.sav (20 orders, ordinals 6..25) [verified]
|
||||
constexpr uint32_t TurnCommands_WriteBuildOrderList = 0x00422870;
|
||||
// cdecl void __cdecl (Mars::IStream* s, std::list<T>* cmds) -- writer for TurnCommands member +0xa0, the PLANETARY-BUDGET list. Per element WriteInt(systemId) then a StreamableHelper<Game::StarSystem::OutputRates> frame (helper vftable 0x00a1f884), which is the only NAMED sub-frame anywhere in the block (SRs SRt SRsc SRtf SRi SRoh SRnr). Observed once, in zuul-turn17-orders2.sav: system 384 with SRsc = 1.0 [verified]
|
||||
constexpr uint32_t TurnCommands_WriteSystemRatesList = 0x0042e3d0;
|
||||
// cdecl void __cdecl (Mars::IStream* s, std::list<T>* orders) -- writer for TurnCommands member +0xb8, the COLONIZE list. Per element two WriteInts (node+0x8 then node+0xc): {shipId, w}. Observed once, in zuul-turn17-orders2.sav: three ships 2512/2544/2624, each with w = 1 [verified]
|
||||
constexpr uint32_t TurnCommands_WriteColonizeList = 0x00422960;
|
||||
// thiscall void (ServerPlayer* this, float dt) // RET 4. The per-player turn driver, called once per player from StrategyServer::ProcessTurn's player loop. THE dt ARGUMENT IS NEVER READ: the whole 1086-byte body contains zero [ebp+N] references (mechanical check over the full instruction decode), so a reimplementation may ignore it. Order: ComputeBudget -> Sav = SatAdd(Sav, net) -> record aid given -> ProcessSpecialProjects -> (ResT ? RollResearchAccident/ProcessResearch) -> research refund -> zero TRM/TRA/TRP -> RebAI decay -> timed-bonus sweep -> ResearchRollPending site -> EVENT_NO_RESEARCH -> PruneRaidTargets [verified]
|
||||
constexpr uint32_t ServerPlayer_ProcessTurn = 0x00491340;
|
||||
// site site in ServerPlayer::ProcessTurn: `if (this->ResT(+0x294) != 0) { if (!RollResearchAccident(&budget)) TechTree::ProcessResearch(this->TechTree(+0xf4), rng, &budget.researchAlloc, &overBudget); }`. Argument order read off the push order at 0x00891496-0x008914a5: pushes are (edx=&overBudget), (ecx=&allocVector), (eax=rng), so left-to-right the args are (RNG*, vector*, int*). The RNG is `*(ServerPlayer+8 - 4 + 0x16c)`. `overBudget` is a FRESH STACK LOCAL at [ebp-0x14], NOT Budget+0x64 [verified]
|
||||
|
|
|
|||
|
|
@ -485,18 +485,12 @@ struct NveEntry {
|
|||
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.i32(R("pid"), pid);
|
||||
ar.obj(R("pview", "pview"), pview);
|
||||
}
|
||||
};
|
||||
|
|
@ -2539,317 +2533,27 @@ struct StrategyAIAgent { // Game::StrategyAIAgent::Streamable
|
|||
}
|
||||
};
|
||||
|
||||
// ---- 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.
|
||||
// One `CD` frame, keyed by the id at the same ordinal in `CDT`. The other id
|
||||
// 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
|
||||
// needs a save with issued orders before anything can be typed.
|
||||
struct CustomDataBlock {
|
||||
static constexpr const char* kStreamName = "CD";
|
||||
enum class Which { Unknown, AIAgent, TurnCmds };
|
||||
enum class Which { Unknown, AIAgent };
|
||||
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;
|
||||
static const std::string suffix = ".AIAgent";
|
||||
which = (id.size() >= suffix.size() && id.compare(id.size() - suffix.size(), suffix.size(), suffix) == 0)
|
||||
? Which::AIAgent
|
||||
: 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,22 +84,6 @@ static void check_save(const std::string& path, const char* dump_dir) {
|
|||
for (const auto& fe : sim.fleets) CHECK(fe.flt.ships.size() >= 1);
|
||||
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 --------------
|
||||
CHECK(sim.rng.is_complex() && sim.rng.children.size() == 1);
|
||||
const Node& blob = sim.rng.children[0];
|
||||
|
|
@ -139,9 +123,7 @@ 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("\n");
|
||||
// Ratchet, not a target: typing a body must never silently regress.
|
||||
// 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);
|
||||
CHECK(pct >= 99.8);
|
||||
}
|
||||
|
||||
// --- round trips ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -720,18 +720,9 @@ struct CdOnly {
|
|||
static void test_customdata_dispatch() {
|
||||
CdOnly doc;
|
||||
doc.cdTable.ids = {"Player.00000016.TurnCommands_v5", "Player.00000032.AIAgent"};
|
||||
shapes::CustomDataBlock tc;
|
||||
shapes::CustomDataBlock tc; // not modelled: carried verbatim
|
||||
tc.select(doc.cdTable.ids[0]);
|
||||
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);
|
||||
tc.unknown.push_back(Node::int32(".", 42));
|
||||
doc.customData.push_back(tc);
|
||||
shapes::CustomDataBlock agent;
|
||||
agent.select(doc.cdTable.ids[1]);
|
||||
|
|
@ -749,16 +740,8 @@ static void test_customdata_dispatch() {
|
|||
back.io(ra);
|
||||
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
||||
CHECK(back.customData.size() == 2);
|
||||
CHECK(back.customData[0].which == shapes::CustomDataBlock::Which::TurnCmds);
|
||||
{
|
||||
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[0].which == shapes::CustomDataBlock::Which::Unknown);
|
||||
CHECK(back.customData[0].unknown.size() == 1);
|
||||
CHECK(back.customData[1].which == shapes::CustomDataBlock::Which::AIAgent);
|
||||
CHECK(back.customData[1].aiAgent.fct == 9 && back.customData[1].aiAgent.techScores.size() == 2);
|
||||
Writer w2;
|
||||
|
|
@ -773,12 +756,6 @@ static void test_customdata_dispatch() {
|
|||
CHECK(probe.which == shapes::CustomDataBlock::Which::Unknown);
|
||||
probe.select(".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 ---------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -160,81 +160,6 @@ static void check(const char* shape_name, const char* cls) {
|
|||
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() {
|
||||
std::printf("wire schema: %zu classes, generated from the game's serializers\n\n",
|
||||
sots::wire::kClassCount);
|
||||
|
|
@ -355,12 +280,6 @@ int main() {
|
|||
"Game::Mars::Game::W4WeaponFamilyID::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(
|
||||
"\ntotals: %d shapes bound, %d items matched, %d MISMATCH, %d wire-only, "
|
||||
"%d opaque item(s) in bound shapes\n",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue