stream: the name-gen used-slot pair, and the Sprj and FTPnts bodies
Three bodies, one reason: the corpus grew from 22 saves to 43 and started carrying content the shapes did not name. ProjectName read `usnc` as a count of ONE item, on the strength of a comment saying "usnc is 0 in every save available". Game::SpecialProjectNameGen::Write (0x008147e0) says otherwise: each 88-byte record opens with a 32-byte table of per-suffix use counts, `usnc` is the number of NON-ZERO slots, and the loop then writes, for every non-zero slot, its INDEX as `usp` and its count byte -- movzx widened -- as `usc`. Both go through WriteInt, so both are i32 on the wire. Twelve corpus saves have one such slot and were round-tripping exactly 12 bytes short: one i32 item, 4 length + 3 tag + 4 value + 1 pad. `Sprj` is a polymorphic frame and both halves of its mapping are now measured. ServerPlayer::Write emits `SprjT` from the plain member at project+0x3c, and ServerPlayer::Read feeds that value to the factory at 0x008610a0, whose table at 0x008611e8 is 0 BackEngProject / 1 MonitorProject / 2 JewelsProject / 3 TechOfferProject -- and each of those constructors stores its own index back at +0x3c. Only SprjT 0 is exercised by any save; the other three arms are typed from the recovered schema and labelled as the hypotheses they are, and an unknown SprjT falls to rest() so it shows up as opaque coverage rather than being mis-read in silence. FieldTemplate carried its points because no save had ever put one on the wire. Six now do, so Game::FieldTemplate::Point is typed and bound. FTPPosX/PosY/Sqd read 0 in every observed point, so their i32 disk type is still the schema's word and not the corpus's, and the comment says so. All 43 saves now round-trip byte-identically; the wire-schema conformance test binds six new shapes with 0 MISMATCH and every one matching item for item.
This commit is contained in:
parent
e7e2bd6f62
commit
076f71439d
2 changed files with 215 additions and 19 deletions
|
|
@ -961,13 +961,154 @@ struct PrEntry {
|
|||
ar.i32(A("PRBt"), prbt);
|
||||
}
|
||||
};
|
||||
// ---- special projects: Game::SpecialProjectImpl and the four subclasses -----------
|
||||
//
|
||||
// `Sprj` is a POLYMORPHIC frame and `SprjT`, written immediately before it, is the
|
||||
// discriminator. Both halves of the mapping are measured, not guessed:
|
||||
//
|
||||
// * ServerPlayer::Write (0x856fe4) writes `SprjT` from the plain member at
|
||||
// `project+0x3c` -- not from a virtual call -- and then writes the object as
|
||||
// the `Sprj` frame;
|
||||
// * ServerPlayer::Read (0x881aa7) reads `SprjT` and passes it as the first
|
||||
// argument to the factory at 0x8610a0, whose 4-entry jump table (0x8611e8) is
|
||||
//
|
||||
// 0 -> new 0x54, ctor(0, ...), vftable 0xa3185c Game::BackEngProject
|
||||
// 1 -> new 0x54, ctor(...), vftable 0xa3180c Game::MonitorProject
|
||||
// 2 -> new 0x4c, ctor(...), vftable 0x9fa438 Game::JewelsProject
|
||||
// 3 -> new 0x60, ctor(3, ...), vftable 0xa31884 Game::TechOfferProject
|
||||
//
|
||||
// and each of those constructors stores that same index at `+0x3c`, which
|
||||
// closes the loop: the value the writer emits is the index the factory takes.
|
||||
// Any other value creates nothing, so a save carrying one is malformed.
|
||||
//
|
||||
// Game::TechProject (vftable 0xa31834) has NO factory entry and cannot come off a
|
||||
// save; it shares BackEngProject's serializer (Read 0x853280 / Write 0x853390)
|
||||
// byte for byte, so the two are indistinguishable on the wire anyway.
|
||||
//
|
||||
// ONLY SprjT 0 IS EXERCISED by the corpus (one record, in the 12 `ad-*`/`ar-*`/`bp-*`
|
||||
// saves: a back-engineering project on "Magonian Prophicies" for `DRV_RecFiss`).
|
||||
// The other three arms are typed from the recovered schema and the factory, and
|
||||
// are HYPOTHESES until a save exercises them (rule 6). An unknown SprjT falls to
|
||||
// `rest()`, which shows up as opaque coverage rather than as a silent mis-read.
|
||||
struct SpecialProject { // Game::SpecialProject -- the `SPi` sub-frame, common to all four
|
||||
static constexpr const char* kStreamName = "SPi";
|
||||
int32_t spid = 0, sts = 0;
|
||||
float cst = 0;
|
||||
int32_t mxc = 0;
|
||||
std::string nm;
|
||||
int32_t trns = 0;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("SPid"), spid);
|
||||
ar.i32(A("Sts"), sts);
|
||||
ar.f32(A("Cst"), cst);
|
||||
ar.i32(A("MxC"), mxc);
|
||||
ar.str(A("Nm"), nm);
|
||||
ar.i32(A("Trns"), trns);
|
||||
}
|
||||
};
|
||||
struct BackEngProject { // Game::BackEngProject (SprjT 0); Game::TechProject shares this serializer
|
||||
static constexpr const char* kStreamName = "Sprj";
|
||||
int32_t stp = 0;
|
||||
SpecialProject spi;
|
||||
float aOdd = 0, aInc = 0;
|
||||
std::string tch;
|
||||
int32_t rCst = 0, rDn = 0;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("Stp"), stp);
|
||||
ar.obj(A("SPi"), spi);
|
||||
ar.f32(A("AOdd"), aOdd);
|
||||
ar.f32(A("AInc"), aInc);
|
||||
ar.str(A("Tch"), tch);
|
||||
ar.i32(A("RCst"), rCst);
|
||||
ar.i32(A("RDn"), rDn);
|
||||
}
|
||||
};
|
||||
struct MonitorProject { // Game::MonitorProject (SprjT 1) -- no save exercises it
|
||||
static constexpr const char* kStreamName = "Sprj";
|
||||
int32_t stp = 0;
|
||||
SpecialProject spi;
|
||||
int32_t sys = 0, rMn = 0, rMx = 0, rDn = 0;
|
||||
float rMd = 0;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("Stp"), stp);
|
||||
ar.obj(A("SPi"), spi);
|
||||
ar.i32(A("Sys"), sys);
|
||||
ar.i32(A("RMn"), rMn);
|
||||
ar.i32(A("RMx"), rMx);
|
||||
ar.i32(A("RDn"), rDn);
|
||||
ar.f32(A("RMd"), rMd);
|
||||
}
|
||||
};
|
||||
struct JewelsProject { // Game::JewelsProject (SprjT 2) -- no save exercises it
|
||||
static constexpr const char* kStreamName = "Sprj";
|
||||
int32_t stp = 0;
|
||||
SpecialProject spi;
|
||||
int32_t einv = 0, eapp = 0;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("Stp"), stp);
|
||||
ar.obj(A("SPi"), spi);
|
||||
ar.i32(A("einv"), einv);
|
||||
ar.i32(A("eapp"), eapp);
|
||||
}
|
||||
};
|
||||
struct TechOfferProject { // Game::TechOfferProject (SprjT 3) -- no save exercises it
|
||||
static constexpr const char* kStreamName = "Sprj";
|
||||
int32_t stp = 0;
|
||||
SpecialProject spi;
|
||||
float aOdd = 0, aInc = 0;
|
||||
std::string tch;
|
||||
int32_t rCst = 0, rDn = 0;
|
||||
int32_t giv = 0, rcp = 0;
|
||||
bool rcpd2 = false;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("Stp"), stp);
|
||||
ar.obj(A("SPi"), spi);
|
||||
ar.f32(A("AOdd"), aOdd);
|
||||
ar.f32(A("AInc"), aInc);
|
||||
ar.str(A("Tch"), tch);
|
||||
ar.i32(A("RCst"), rCst);
|
||||
ar.i32(A("RDn"), rDn);
|
||||
ar.i32(A("giv"), giv);
|
||||
ar.i32(A("rcp"), rcp);
|
||||
ar.b(A("rcpd2"), rcpd2);
|
||||
}
|
||||
};
|
||||
// The `Sprj` body, selected by the `SprjT` beside it. Same idiom as EncounterObject:
|
||||
// a switch, not a `when`, so SchemaProbe does not concatenate the arms -- each arm is
|
||||
// bound to its own wire class in tests/mars_stream/test_wire_schema.cpp.
|
||||
struct SpecialProjectBody {
|
||||
static constexpr const char* kStreamName = "Sprj";
|
||||
int32_t type = 0;
|
||||
BackEngProject backEng; // 0
|
||||
MonitorProject monitor; // 1
|
||||
JewelsProject jewels; // 2
|
||||
TechOfferProject offer; // 3
|
||||
std::vector<Node> unknown; // no factory entry: carried, and counted as opaque
|
||||
void select(int32_t sprjT) { type = sprjT; }
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
switch (type) {
|
||||
case 0: backEng.io(ar); break;
|
||||
case 1: monitor.io(ar); break;
|
||||
case 2: jewels.io(ar); break;
|
||||
case 3: offer.io(ar); break;
|
||||
default: ar.rest(unknown); break;
|
||||
}
|
||||
}
|
||||
};
|
||||
struct SprjEntry {
|
||||
int32_t sprjT = 0;
|
||||
Node sprj;
|
||||
SpecialProjectBody sprj;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("SprjT"), sprjT);
|
||||
ar.any(A("Sprj"), sprj);
|
||||
sprj.select(sprjT);
|
||||
ar.obj(A("Sprj"), sprj);
|
||||
}
|
||||
};
|
||||
struct NexpEntry {
|
||||
|
|
@ -1351,12 +1492,37 @@ struct SpyManager { // Game::ServerSpyManager
|
|||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
// One used-suffix slot of a name-generator record: the `usp`/`usc` PAIR.
|
||||
//
|
||||
// MEASURED from the writer (Game::SpecialProjectNameGen::Write, 0x8147e0). Each
|
||||
// record is 0x58 bytes and opens with a 32-byte table of per-suffix use counts.
|
||||
// The writer counts the NON-ZERO entries (the unrolled byte loop at 0x814850) and
|
||||
// emits that as `usnc`; it then walks slots 0..31 and, for every non-zero one,
|
||||
// writes the SLOT INDEX as `usp` (0x8148b1) and that slot's count byte, widened by
|
||||
// `movzx`, as `usc` (0x8148c2). Both go through WriteInt, so both are i32 on the
|
||||
// wire (rule 5). `Nm` and `Ntg` follow from record+0x20 and record+0x3c.
|
||||
//
|
||||
// The old shape read `usnc` as a count of ONE item and named only `usc`, on the
|
||||
// strength of "usnc is 0 in every save available". That was rule 6's hypothesis
|
||||
// and `ad-turn27-two-raiders.sav` falsified it: 102 of its 103 records have an
|
||||
// empty table and one has `usnc == 1`, so the writer dropped that record's `usp`
|
||||
// and the save round-tripped exactly 12 bytes short (one i32 item: 4-byte length +
|
||||
// 3-byte tag + 4-byte value + 1 pad). 12 of the 43 corpus saves carry that record.
|
||||
struct UsedName {
|
||||
int32_t slot = 0; // usp -- index into the record's 32-slot use table
|
||||
int32_t count = 0; // usc -- that slot's use count (a byte in memory, i32 on the wire)
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("usp"), slot);
|
||||
ar.i32(A("usc"), count);
|
||||
}
|
||||
};
|
||||
struct ProjectName { // one entry of Game::SpecialProjectNameGen
|
||||
std::vector<Node> used; // usnc is 0 in every save available
|
||||
std::vector<UsedName> used;
|
||||
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.narr(A("usnc"), used, [](Ar& a, UsedName& e) { e.io(a); });
|
||||
ar.str(A("Nm"), nm);
|
||||
ar.str(A("Ntg"), ntg);
|
||||
}
|
||||
|
|
@ -1692,19 +1858,40 @@ struct ShipEntry {
|
|||
ar.obj(A("Ship"), ship);
|
||||
}
|
||||
};
|
||||
struct FieldPoint { // Game::FieldTemplate::Point -- one ship's slot in a stored formation
|
||||
static constexpr const char* kStreamName = "";
|
||||
int32_t shipID = 0, desID = 0, posX = 0, posY = 0, sqd = 0;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("FTPShID"), shipID);
|
||||
ar.i32(A("FTPDesID"), desID);
|
||||
ar.i32(A("FTPPosX"), posX);
|
||||
ar.i32(A("FTPPosY"), posY);
|
||||
ar.i32(A("FTPSqd"), sqd);
|
||||
}
|
||||
};
|
||||
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;
|
||||
// FTPnts was the ONE item 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 was 0 in every Lay in the
|
||||
// 22-save corpus. The elements were therefore carried as Nodes rather than
|
||||
// typed against a framing no save had ever shown -- the trap that mis-typed
|
||||
// SysMem, mts and nalat.
|
||||
//
|
||||
// The workload that note asked for now EXISTS. Six saves in the 43-save corpus
|
||||
// carry stored tactical formations (`ap-turn22` 2 points, `az-turn23` 3, and
|
||||
// `ap-turn25` / `ar-oracle-A-pre` / `ar-r1-turn43-post` / `ar-turn37` 1 each),
|
||||
// and the framing is the ordinary CArr: a `FTPnts` frame holding a "." count
|
||||
// and that many NULL-named element frames of five i32 items. Typed, and
|
||||
// byte-identical on all six.
|
||||
//
|
||||
// FTPPosX/FTPPosY/FTPSqd read 0 in every point observed so far, so their DISK
|
||||
// type (i32, from the WriteInt vftable slot) is the schema's word and not the
|
||||
// corpus's -- a formation with a ship moved off the origin is what would tell
|
||||
// an i32 grid coordinate from a float one by value.
|
||||
std::vector<FieldPoint> points;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
|
|
|
|||
|
|
@ -305,6 +305,15 @@ int main() {
|
|||
check<sh::SpyReport>("SpyReport", "Game::SpyReport");
|
||||
check<sh::SpyManager>("SpyManager", "Game::ServerSpyManager");
|
||||
check<sh::ProjectNames>("ProjectNames", "Game::SpecialProjectNameGen");
|
||||
// The `Sprj` variant: the SELECTION (SprjT -> class) is not a wire item and so
|
||||
// is not checkable here; it came from the game's own factory at 0x8610a0 (see
|
||||
// the note on sh::SpecialProjectBody). What is checked is that each arm agrees
|
||||
// with its class's serializer. Only SprjT 0 is exercised by any save.
|
||||
check<sh::SpecialProject>("SpecialProject", "Game::SpecialProject");
|
||||
check<sh::BackEngProject>("BackEngProject", "Game::BackEngProject");
|
||||
check<sh::MonitorProject>("MonitorProject", "Game::MonitorProject");
|
||||
check<sh::JewelsProject>("JewelsProject", "Game::JewelsProject");
|
||||
check<sh::TechOfferProject>("TechOfferProject", "Game::TechOfferProject");
|
||||
check<sh::TradeManager>("TradeManager", "Game::ServerTradeManagerImpl");
|
||||
check<sh::TradeSector>("TradeSector", "Game::ServerTradeSector");
|
||||
check<sh::ShipSectionID>("ShipSectionID", "Game::ShipSectionID");
|
||||
|
|
@ -319,11 +328,11 @@ int main() {
|
|||
check<sh::TradeRoute>("TradeRoute", "Game::TradeRoute");
|
||||
check<sh::SpyCraft>("SpyCraft", "Game::SpyCraft");
|
||||
check<sh::FleetLayout>("FleetLayout", "Game::FleetLayout");
|
||||
// Game::FieldTemplate::Point is deliberately NOT bound: FTPnts is empty in
|
||||
// every save and the recovery marks the item unresolved, so its element
|
||||
// framing is a hypothesis and FieldTemplate carries the elements (see the
|
||||
// note on sh::FieldTemplate).
|
||||
// Game::FieldTemplate::Point is bound now that the corpus exercises it: six
|
||||
// saves carry stored tactical formations, so the element framing is measured
|
||||
// rather than assumed (see the note on sh::FieldTemplate).
|
||||
check<sh::FieldTemplate>("FieldTemplate", "Game::FieldTemplate");
|
||||
check<sh::FieldPoint>("FieldPoint", "Game::FieldTemplate::Point");
|
||||
check<sh::Crep>("Crep", "Game::CombatReport");
|
||||
check<sh::CrepPrep>("CrepPrep", "Game::CombatPlayerReport");
|
||||
check<sh::Srep>("Srep", "Game::CombatShipReport");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue