Compare commits
No commits in common. "f4f3a7aae7197a65f980c146dfefc9123ebc5f45" and "4a4c7bc2495ad22c1306cd1e010bbe2f89bc6068" have entirely different histories.
f4f3a7aae7
...
4a4c7bc249
8 changed files with 12 additions and 620 deletions
|
|
@ -76,23 +76,3 @@ cannot supply — and the table is what proves they agree with the binary.
|
|||
3. Add a `check<sh::Shape>("Shape", "Game::TheClass")` line to `test_wire_schema.cpp`.
|
||||
4. `ctest` — the conformance test fails on a type disagreement, and `test_save` fails if the round
|
||||
trip breaks or coverage regresses.
|
||||
|
||||
## What the table cannot tell you: polymorphic members
|
||||
|
||||
`Mars::StreamableHelper<Base>` is a **pointer**, and the wire schema only records that a frame goes
|
||||
here — never which derived class wrote it. `Game::SVSOSots` writes two such lists, each preceded by
|
||||
the key that selects the body (`xscn`, a scenario name, for `xsc`; `EncID`, an int, for `EncObj`).
|
||||
Those maps are not on the wire at all; they were read out of the game's own factories and are
|
||||
recorded in the notes repo (`findings/objects/svsctob-variants.md`). The shapes apply the key in
|
||||
both directions — the reader `select()`s from what it just read, the writer from what it is about
|
||||
to write — so a body goes back out as whatever it came in as, and a key with no shape falls to a
|
||||
generic `Node` and still round-trips.
|
||||
|
||||
Two more things the table's `prim` column will not save you from:
|
||||
|
||||
* **A `bool` and an `int` item are the same size only when the tag length makes the padding agree.**
|
||||
A 4-character tag gives 4 + 4 + 1 → 12 and 4 + 4 + 4 = 12; a 3-character tag gives 8 and 12. That
|
||||
coincidence is what hid `odet` being a bool for a whole campaign round.
|
||||
* **An empty string is four zero bytes**, byte-identical to the int 0, so a string field that is
|
||||
empty in all available data round-trips perfectly while typed as an int. That is what hid
|
||||
`Game::SystemParams`'s name field.
|
||||
|
|
|
|||
|
|
@ -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 @ 2308b6e, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ 5a3f986, 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>
|
||||
|
|
@ -1261,31 +1261,5 @@ constexpr uint32_t TechTree_ProcessResearch_TechsUnlockedCollector = 0x00187cc3;
|
|||
constexpr uint32_t ServerPlayer_OnTechResearched_RecordObservedTech = 0x00491790;
|
||||
// site site in ServerPlayer::OnTechResearched, second statement: `if (this->ResT(+0x294) == def) { if (this->ResearchRollPending(+0x3b4)) RollResearchEvent(this); this->ResearchRollPending = 0; this->ResT = 0; }`. RollResearchEvent (0x0088df20) draws EXACTLY ONE NextFloat unconditionally and then enters 0x00889d60 only when roll < ResearchEventOdds -- the odds are 0 for every tech outside the plague and AI-rebellion families, so that branch is normally dead. This is the one extra RNG word a completion consumes, and clearing ResT means a second completion in the same pass consumes none [verified]
|
||||
constexpr uint32_t ServerPlayer_OnTechResearched_ResearchRollBlock = 0x00491790;
|
||||
// cdecl Game::SVScriptObject* (int encID) // The EncObj factory. `dec eax; cmp eax,0x16; ja <null>; jmp dword [eax*4 + 0x0052bf60]` -- a 23-entry dword jump table indexed by encID-1. Live ids: 1 VonNeumann, 3 Swarm, 4 Derelict, 5 Monitor, 7 SystemKiller, 8 PuppetMaster, 9 SlaversRefuel, 10 SwarmQueen, 14 Locust, 17 CrowRuins, 20 Refugees, 21 Ortgay. Ids 2, 6, 11-13, 15, 16, 18, 19, 22, 23 and everything outside 1..23 return NULL. Class names read off the vftable store in each ctor [verified]
|
||||
constexpr uint32_t SVScriptObject_FactoryByEncID = 0x0012bf00;
|
||||
// data void* [23] // the jump table SVScriptObject_FactoryByEncID indexes with encID-1 [verified]
|
||||
constexpr uint32_t SVScriptObject_EncIDJumpTable = 0x0012bf60;
|
||||
// thiscall Game::SVScriptObject* (Game::SVSOSots* this, int encID) // scans the loaded (id, obj) pair vector at this+0x0C..0x10 first and only calls SVScriptObject_FactoryByEncID on a miss [verified]
|
||||
constexpr uint32_t SVSOSots_GetOrCreateEncObj = 0x001a4450;
|
||||
// cdecl Game::SVScriptObject* (const char* xscn) // the xsc factory: a flat four-way _stricmp chain, EXHAUSTIVE. "traps" -> Game::SVSOTraps (ctor 0x0052cbb0), "crowdefs" -> Game::SVSOCrowDefenders (0x0052b3c0), "indsys" -> Game::SVSOIndependentSystems (0x0075b530), "gmtrigger" -> Game::SVSOGrandMenaceTrigger (0x004f5630). Falls through with "Error creating extra script %s." [verified]
|
||||
constexpr uint32_t SVScriptObject_FactoryByScenarioName = 0x001a7050;
|
||||
// thiscall void (Game::SVSOSots* this) // registers exactly traps, crowdefs, indsys and -- only when ScnObj == NULL -- gmtrigger. Corroborates that the four scenario names are the whole set [verified]
|
||||
constexpr uint32_t SVSOSots_SeedNewGameScripts = 0x001a7d70;
|
||||
// thiscall void (Game::SVSOSots* this, Mars::IStream* s) // slot 2 of vftable 0x00A063C4. Emits ScnID, ScnObj (only when non-NULL), numx x (xscn, xsc), NEncObjs x (EncID, EncObj) [verified]
|
||||
constexpr uint32_t SVSOSots_Write = 0x0019ddf0;
|
||||
// thiscall void (Game::SVSOSots* this, Mars::IStream* s) // slot 1. Accepts two tags Write never emits -- NPCPlr (int, first) and hastraps (bool) with a following traps object -- read-only backward compatibility; a writer that omits them is correct [verified]
|
||||
constexpr uint32_t SVSOSots_Read = 0x001a7a40;
|
||||
// data Game::SVSOSots vftable (slot0 dtor, slot1 Read 0x005a7a40, slot2 Write 0x0059ddf0) [verified]
|
||||
constexpr uint32_t SVSOSots_vftable = 0x006063c4;
|
||||
// data Game::SVSOIndependentSystems vftable. slot0 dtor 0x0075afb0; slot1 Read AND slot2 Write are both 0x005f8ac0, the shared `ret 4` no-op stub -- so the "indsys" frame is genuinely empty on disk and the class's 0x1c4-byte body is runtime-only state [verified]
|
||||
constexpr uint32_t SVSOIndependentSystems_vftable = 0x00620314;
|
||||
// thiscall void (void*, Mars::IStream*) // `C2 04 00` -- a bare RET 4 shared as the inherited Read/Write for classes that serialize nothing [verified]
|
||||
constexpr uint32_t Streamable_NoOpStub = 0x001f8ac0;
|
||||
// thiscall void (Game::SVSOCrowDefenders* this, Mars::IStream* s) // sys; ndsys count then a loop writing dsys; ndes count then a loop writing des; drad. NOTE: `dsys` is INSIDE the ndsys loop -- objects/layouts.json records it as a plain member, which is wrong, and no save can settle it because both counts are 0 everywhere [verified]
|
||||
constexpr uint32_t SVSOCrowDefenders_Write = 0x000f8c90;
|
||||
// thiscall void (Game::SVSOMonitor* this, Mars::IStream* s) // calls SVSODerelict::Write (0x004fc2b0) as its first act -- Monitor derives from Derelict, which is why its tag run starts NDsn/DsnID/Dwght + NAsg/Eflt/Esys before nt/scnm/spwt/rsmd/dsgn [verified]
|
||||
constexpr uint32_t SVSOMonitor_Write = 0x000fd810;
|
||||
// thiscall void (Game::SVSODerelict* this, Mars::IStream* s) // NDsn count then a loop of (DsnID, Dwght); NAsg count then a loop of (Eflt, Esys). Two fields per iteration in each, confirmed by the 8-byte element strides [verified]
|
||||
constexpr uint32_t SVSODerelict_Write = 0x000fc2b0;
|
||||
|
||||
} // namespace sots::addr
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// GENERATED -- do not edit. Wire schema of Sword of the Stars.exe (GOG 1.8.1).
|
||||
// Source: sots-re objects/streams.json @ 2308b6e, generated 2026-09-08 by tools/gen_stream_schema.py
|
||||
// Source: sots-re objects/streams.json @ 5a3f986, generated 2026-09-08 by tools/gen_stream_schema.py
|
||||
//
|
||||
// For each serializable class, the ordered sequence of items its Mars::IStreamable
|
||||
// Write() puts on the stream. Recovered mechanically from the serializers; the
|
||||
|
|
|
|||
|
|
@ -264,12 +264,11 @@ public:
|
|||
sub.finish(&n);
|
||||
}
|
||||
|
||||
// one array element: struct -> "." frame, int32/string -> "." scalar, Node -> any
|
||||
// one array element: struct -> "." frame, int32 -> "." int, Node -> any
|
||||
template <class T>
|
||||
void read_elem(T& e, const std::string& p) {
|
||||
const Node* n = take();
|
||||
if constexpr (std::is_same_v<T, int32_t>) e = coerce_int(*n, p);
|
||||
else if constexpr (std::is_same_v<T, std::string>) e = coerce_string(*n, p);
|
||||
else if constexpr (std::is_same_v<T, Node>) e = *n;
|
||||
else read_frame(*n, e, p);
|
||||
}
|
||||
|
|
@ -494,7 +493,6 @@ public:
|
|||
template <class T>
|
||||
void write_elem(T& e) {
|
||||
if constexpr (std::is_same_v<T, int32_t>) w_.int32(".", e);
|
||||
else if constexpr (std::is_same_v<T, std::string>) w_.string(".", e);
|
||||
else if constexpr (std::is_same_v<T, Node>) w_.node(e);
|
||||
else {
|
||||
w_.begin(".");
|
||||
|
|
@ -576,7 +574,6 @@ public:
|
|||
Desc d;
|
||||
d.type = Desc::CArr;
|
||||
if constexpr (std::is_same_v<T, int32_t>) d.elem.kind = Prim::Int;
|
||||
else if constexpr (std::is_same_v<T, std::string>) d.elem.kind = Prim::String;
|
||||
else if constexpr (std::is_same_v<T, Node>) d.elem = Hint{};
|
||||
else d.elem.sub = describe<T>();
|
||||
const Desc* cd = ctx_.reg.add(std::move(d));
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@
|
|||
// confirmed format). A("x") tags are on-disk names; R("x") fields are written
|
||||
// by the game with a NULL name ("." on disk) or carry a reference name whose
|
||||
// disk spelling differs, and are matched by position. Bodies the format keeps
|
||||
// opaque are held as generic Nodes so a shape can be re-emitted byte-for-byte.
|
||||
// After the SvSctOb round only three regions are still carried that way: the CD
|
||||
// custom-data blocks, the RNG state block (correctly opaque) and Attrib (an empty
|
||||
// frame in every save).
|
||||
// opaque (TechTree, Events, ShipRecs, spy2, civr, comms, Ojvs, Attrib, sprjs,
|
||||
// SvSctOb, trdmgr, spymgr, CD, ...) are held as generic Nodes so a shape can
|
||||
// be re-emitted byte-for-byte.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
|
@ -536,11 +535,7 @@ struct Sys {
|
|||
Morale cm, pvCM;
|
||||
std::vector<MoraleEvent> cme2;
|
||||
bool cme2Framed = true;
|
||||
// `spies2` is VectorHelper<int> on the original (the helper's own decorated
|
||||
// type names the element), so it is a framed array of NULL-named ints. The
|
||||
// count is 0 in every system of every save we hold, so the element TYPE is
|
||||
// certain but no element VALUE has ever been observed.
|
||||
std::vector<int32_t> spies2;
|
||||
Node spies2;
|
||||
int32_t pid = 0, defF = 0, defSF = 0;
|
||||
std::optional<BuildQueue> bq;
|
||||
std::vector<AdctEntry> adct;
|
||||
|
|
@ -626,7 +621,7 @@ struct Sys {
|
|||
ar.obj(A("cm"), cm);
|
||||
ar.obj(A("PvCM"), pvCM);
|
||||
ar.carr_flex(A("cme2"), cme2, cme2Framed);
|
||||
ar.carr(A("spies2"), spies2);
|
||||
ar.any(A("spies2"), spies2);
|
||||
ar.i32(A("PID"), pid);
|
||||
ar.i32(A("DefF"), defF);
|
||||
ar.i32(A("DefSF"), defSF);
|
||||
|
|
@ -820,8 +815,7 @@ struct DesignSection { // Game::ShipDesignDef::Section, one DSec of a Des frame
|
|||
static constexpr const char* kStreamName = "DSec";
|
||||
ShipSectionID sec;
|
||||
std::vector<GunBank> gunBanks;
|
||||
// DOpts is VectorHelper<Mars::String>, so its elements are NULL-named strings.
|
||||
std::vector<std::string> opts;
|
||||
std::vector<Node> opts; // DOpts: the recovery says carr<String>
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
|
|
@ -1855,419 +1849,6 @@ struct ZoneDefence {
|
|||
}
|
||||
};
|
||||
|
||||
// ---- SvSctOb: the script-object tree -------------------------------------------------
|
||||
//
|
||||
// `SvSctOb` is a StreamableHelper<Game::SVScriptObject> -- a polymorphic pointer.
|
||||
// In every save it holds a Game::SVSOSots, which writes two variant lists:
|
||||
//
|
||||
// numx x { xscn (a scenario NAME) , xsc (SVScriptObject*) }
|
||||
// NEncObjs x { EncID (an encounter ID), EncObj (SVScriptObject*) }
|
||||
//
|
||||
// so both bodies are dispatched by the key item that precedes them. Neither the
|
||||
// name->class nor the id->class map is anywhere on the wire; both were read out of
|
||||
// the game: the ids from a 23-entry dword jump table (indexed by EncID-1) and the
|
||||
// names from a flat _stricmp chain. See findings/objects/svsctob-variants.md in
|
||||
// the notes repo for the addresses. An id or name we do not model still round
|
||||
// trips, because the fallback carries the body as a generic Node.
|
||||
struct SwarmInfestation { // Game::SVSOSwarm::Infestation
|
||||
static constexpr const char* kStreamName = "";
|
||||
int32_t sysid = 0, stg = 0, strn = 0, mtrn = 0, trgtrn = 0;
|
||||
bool canh = false;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("sysid"), sysid);
|
||||
ar.i32(A("stg"), stg);
|
||||
ar.i32(A("strn"), strn);
|
||||
ar.i32(A("mtrn"), mtrn);
|
||||
ar.i32(A("trgtrn"), trgtrn);
|
||||
ar.b(A("canh"), canh);
|
||||
}
|
||||
};
|
||||
struct FleetAssignment { // the (Eflt, Esys) pair the NAsg loop writes
|
||||
int32_t eflt = 0, esys = 0;
|
||||
};
|
||||
struct DesignWeight { // the (DsnID, Dwght) pair the NDsn loop writes
|
||||
int32_t dsnID = 0, dwght = 0;
|
||||
};
|
||||
struct SVSOSwarm { // EncID 3
|
||||
static constexpr const char* kStreamName = "";
|
||||
std::vector<FleetAssignment> asg;
|
||||
std::vector<SwarmInfestation> infest;
|
||||
int32_t deshive = 0, deslarva = 0;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.narr(A("NAsg"), asg, [](Ar& a, FleetAssignment& e) {
|
||||
a.i32(A("Eflt"), e.eflt);
|
||||
a.i32(A("Esys"), e.esys);
|
||||
});
|
||||
ar.carr(A("infest"), infest);
|
||||
ar.i32(A("deshive"), deshive);
|
||||
ar.i32(A("deslarva"), deslarva);
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
struct SVSODerelict { // EncID 4 -- also the base Game::SVSOMonitor::Write starts with
|
||||
static constexpr const char* kStreamName = "";
|
||||
std::vector<DesignWeight> designs;
|
||||
std::vector<FleetAssignment> asg;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.narr(A("NDsn"), designs, [](Ar& a, DesignWeight& e) {
|
||||
a.i32(A("DsnID"), e.dsnID);
|
||||
a.i32(A("Dwght"), e.dwght);
|
||||
});
|
||||
ar.narr(A("NAsg"), asg, [](Ar& a, FleetAssignment& e) {
|
||||
a.i32(A("Eflt"), e.eflt);
|
||||
a.i32(A("Esys"), e.esys);
|
||||
});
|
||||
}
|
||||
};
|
||||
struct MonitorSpawn { // one `nt` element of Game::SVSOMonitor
|
||||
std::string scnm;
|
||||
int32_t spwt = 0;
|
||||
float rsmd = 0;
|
||||
int32_t dsgn = 0;
|
||||
};
|
||||
struct SVSOMonitor { // EncID 5 -- Game::SVSOMonitor derives from Game::SVSODerelict
|
||||
static constexpr const char* kStreamName = "";
|
||||
SVSODerelict base;
|
||||
std::vector<MonitorSpawn> spawns;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
base.io(ar); // Monitor::Write calls Derelict::Write first
|
||||
ar.narr(A("nt"), spawns, [](Ar& a, MonitorSpawn& e) {
|
||||
a.str(A("scnm"), e.scnm);
|
||||
a.i32(A("spwt"), e.spwt);
|
||||
a.f32(A("rsmd"), e.rsmd);
|
||||
a.i32(A("dsgn"), e.dsgn);
|
||||
});
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
struct SVSOSlaversRefuel { // EncID 9
|
||||
static constexpr const char* kStreamName = "";
|
||||
std::vector<FleetAssignment> asg;
|
||||
int32_t cdiff = 0;
|
||||
std::vector<int32_t> tdids, adids;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.narr(A("NAsg"), asg, [](Ar& a, FleetAssignment& e) {
|
||||
a.i32(A("Eflt"), e.eflt);
|
||||
a.i32(A("Esys"), e.esys);
|
||||
});
|
||||
ar.i32(A("CDiff"), cdiff);
|
||||
ar.narr(A("NTD"), tdids, [](Ar& a, int32_t& e) { a.i32(A("TDID"), e); });
|
||||
ar.narr(A("NAD"), adids, [](Ar& a, int32_t& e) { a.i32(A("ADID"), e); });
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
struct SVSOSwarmQueenHive { // Game::SVSOSwarmQueen::HiveInfo
|
||||
static constexpr const char* kStreamName = "";
|
||||
int32_t hiveID = 0, queenID = 0, nextQ = 0;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("HiveID"), hiveID);
|
||||
ar.i32(A("QueenID"), queenID);
|
||||
ar.i32(A("NextQ"), nextQ);
|
||||
}
|
||||
};
|
||||
struct SVSOSwarmQueenQueen { // Game::SVSOSwarmQueen::QueenInfo
|
||||
static constexpr const char* kStreamName = "";
|
||||
int32_t queenID = 0, qDstID = 0;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("QueenID"), queenID);
|
||||
ar.i32(A("QDstID"), qDstID);
|
||||
}
|
||||
};
|
||||
struct SVSOSwarmQueen { // EncID 10
|
||||
static constexpr const char* kStreamName = "";
|
||||
// QDesignID is written from a computed handle id, and the recovery cannot say
|
||||
// whether the writer emits one or several, so this consumes the whole run.
|
||||
std::vector<int32_t> designIds;
|
||||
std::vector<SVSOSwarmQueenHive> hives;
|
||||
std::vector<SVSOSwarmQueenQueen> queens;
|
||||
std::vector<int32_t> sysMem; // VectorHelper<StreamableEnum<uint>>
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.repeat("QDesignID", designIds, [](Ar& a, int32_t& e) { a.i32(A("QDesignID"), e); });
|
||||
ar.carr(A("Hives"), hives);
|
||||
ar.carr(A("Queens"), queens);
|
||||
ar.carr(A("SysMem"), sysMem);
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
struct SVSOCrowRuins { // EncID 17
|
||||
static constexpr const char* kStreamName = "";
|
||||
std::vector<FleetAssignment> asg;
|
||||
std::vector<int32_t> wids;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.narr(A("NAsg"), asg, [](Ar& a, FleetAssignment& e) {
|
||||
a.i32(A("Eflt"), e.eflt);
|
||||
a.i32(A("Esys"), e.esys);
|
||||
});
|
||||
ar.narr(A("NWD"), wids, [](Ar& a, int32_t& e) { a.i32(A("wid"), e); });
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
struct RefugeeStatus { // Game::SVSORefugees::PlayerStatus -- the `rsu` frame
|
||||
static constexpr const char* kStreamName = "rsu";
|
||||
int32_t lr = 0, lrt = 0, llc = 0;
|
||||
bool atr = false, asy = false;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("lr"), lr);
|
||||
ar.i32(A("lrt"), lrt);
|
||||
ar.i32(A("llc"), llc);
|
||||
ar.b(A("atr"), atr);
|
||||
ar.b(A("asy"), asy);
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
struct RefugeePlayer { // one `rsuc` element: (pid, rsu)
|
||||
int32_t pid = 0;
|
||||
RefugeeStatus status;
|
||||
};
|
||||
struct SVSORefugees { // EncID 20
|
||||
static constexpr const char* kStreamName = "";
|
||||
std::vector<RefugeePlayer> players;
|
||||
std::vector<int32_t> dids;
|
||||
bool ini = false;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.narr(A("rsuc"), players, [](Ar& a, RefugeePlayer& e) {
|
||||
a.i32(A("pid"), e.pid);
|
||||
a.obj(A("rsu"), e.status);
|
||||
});
|
||||
ar.narr(A("didc"), dids, [](Ar& a, int32_t& e) { a.i32(A("did"), e); });
|
||||
ar.b(A("ini"), ini);
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
struct VonNeumannDefeat { // Game::SVSOVonNeumann::DefeatRecord
|
||||
static constexpr const char* kStreamName = "";
|
||||
int32_t sys = 0, ndft = 0, nxrv = 0, lstd = 0;
|
||||
bool bkdf = false;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("sys"), sys);
|
||||
ar.i32(A("ndft"), ndft);
|
||||
ar.i32(A("nxrv"), nxrv);
|
||||
ar.i32(A("lstd"), lstd);
|
||||
ar.b(A("bkdf"), bkdf);
|
||||
}
|
||||
};
|
||||
struct SVSOVonNeumann { // EncID 1
|
||||
static constexpr const char* kStreamName = "";
|
||||
std::vector<VonNeumannDefeat> defeats;
|
||||
int32_t rusav = 0, nenc = 0, nmb = 0, nml = 0, nbb = 0, nbl = 0, nskb = 0, nskl = 0;
|
||||
std::vector<int32_t> mts; // VectorHelper<StreamableEnum<uint>>
|
||||
int32_t ntm = 0, ntb = 0;
|
||||
bool sken = false;
|
||||
int32_t skdid = 0, skhid = 0, skfid = 0, sktq = 0, skt = 0, sktc = 0;
|
||||
int32_t mbid = 0, mbgid = 0, skgid = 0;
|
||||
int64_t vnhp = 0;
|
||||
int32_t vnhw = 0;
|
||||
std::vector<std::string> trev; // VectorHelper<Mars::String>
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.carr(A("dfts"), defeats);
|
||||
ar.i32(A("rusav"), rusav);
|
||||
ar.i32(A("nenc"), nenc);
|
||||
ar.i32(A("nmb"), nmb);
|
||||
ar.i32(A("nml"), nml);
|
||||
ar.i32(A("nbb"), nbb);
|
||||
ar.i32(A("nbl"), nbl);
|
||||
ar.i32(A("nskb"), nskb);
|
||||
ar.i32(A("nskl"), nskl);
|
||||
ar.carr(A("mts"), mts);
|
||||
ar.i32(A("ntm"), ntm);
|
||||
ar.i32(A("ntb"), ntb);
|
||||
ar.b(A("sken"), sken);
|
||||
ar.i32(A("skdid"), skdid);
|
||||
ar.i32(A("skhid"), skhid);
|
||||
ar.i32(A("skfid"), skfid);
|
||||
ar.i32(A("sktq"), sktq);
|
||||
ar.i32(A("skt"), skt);
|
||||
ar.i32(A("sktc"), sktc);
|
||||
ar.i32(A("mbid"), mbid);
|
||||
ar.i32(A("mbgid"), mbgid);
|
||||
ar.i32(A("skgid"), skgid);
|
||||
ar.i64(A("vnhp"), vnhp);
|
||||
ar.i32(A("vnhw"), vnhw);
|
||||
ar.carr(A("trev"), trev);
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
|
||||
// --- the four named scenario objects (`xsc`, keyed by `xscn`) -------------------
|
||||
struct SVSOTrap { // Game::SVSOTraps::Trap
|
||||
static constexpr const char* kStreamName = "";
|
||||
int32_t sys = 0, pid = 0, trenc = 0, trgenc = 0;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("sys"), sys);
|
||||
ar.i32(A("pid"), pid);
|
||||
ar.i32(A("trenc"), trenc);
|
||||
ar.i32(A("trgenc"), trgenc);
|
||||
}
|
||||
};
|
||||
struct SVSOTraps { // xscn "traps"
|
||||
static constexpr const char* kStreamName = "";
|
||||
std::vector<SVSOTrap> traps;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.carr(A("traps"), traps);
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
struct SVSOCrowDefenders { // xscn "crowdefs"
|
||||
static constexpr const char* kStreamName = "";
|
||||
int32_t sys = 0;
|
||||
// `dsys` is a loop element of `ndsys`, not a plain member: the recovery said
|
||||
// member, but Game::SVSOCrowDefenders::Write emits it inside the ndsys loop.
|
||||
// Both counts are 0 in every save, so only the binary settles this.
|
||||
std::vector<int32_t> dsys, des;
|
||||
float drad = 0;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("sys"), sys);
|
||||
ar.narr(A("ndsys"), dsys, [](Ar& a, int32_t& e) { a.i32(A("dsys"), e); });
|
||||
ar.narr(A("ndes"), des, [](Ar& a, int32_t& e) { a.i32(A("des"), e); });
|
||||
ar.f32(A("drad"), drad);
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
struct SVSOGrandMenaceTrigger { // xscn "gmtrigger"
|
||||
static constexpr const char* kStreamName = "";
|
||||
int32_t gmch = 0;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("gmch"), gmch);
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
struct SVSOIndependentSystems { // xscn "indsys" -- Read and Write are the `ret 4` stub
|
||||
static constexpr const char* kStreamName = "";
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.rest(extra); // the frame is genuinely empty on disk
|
||||
}
|
||||
};
|
||||
|
||||
// One `xsc` body. `which` is set from the preceding `xscn` before io() runs, in
|
||||
// both directions, so a body written back goes out as whatever it came in as.
|
||||
struct ScenarioObject {
|
||||
static constexpr const char* kStreamName = "xsc";
|
||||
enum class Which { Unknown, Traps, CrowDefenders, GrandMenaceTrigger, IndependentSystems };
|
||||
Which which = Which::Unknown;
|
||||
SVSOTraps traps;
|
||||
SVSOCrowDefenders crowDefenders;
|
||||
SVSOGrandMenaceTrigger grandMenace;
|
||||
SVSOIndependentSystems indSys;
|
||||
std::vector<Node> unknown; // a name we do not model: carried verbatim
|
||||
|
||||
void select(const std::string& name) {
|
||||
if (name == "traps") which = Which::Traps;
|
||||
else if (name == "crowdefs") which = Which::CrowDefenders;
|
||||
else if (name == "gmtrigger") which = Which::GrandMenaceTrigger;
|
||||
else if (name == "indsys") which = Which::IndependentSystems;
|
||||
else which = Which::Unknown;
|
||||
}
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
switch (which) {
|
||||
case Which::Traps: traps.io(ar); break;
|
||||
case Which::CrowDefenders: crowDefenders.io(ar); break;
|
||||
case Which::GrandMenaceTrigger: grandMenace.io(ar); break;
|
||||
case Which::IndependentSystems: indSys.io(ar); break;
|
||||
case Which::Unknown: ar.rest(unknown); break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// One `EncObj` body, keyed by the preceding `EncID`. The full jump table is
|
||||
// 1 VonNeumann, 3 Swarm, 4 Derelict, 5 Monitor, 7 SystemKiller, 8 PuppetMaster,
|
||||
// 9 SlaversRefuel, 10 SwarmQueen, 14 Locust, 17 CrowRuins, 20 Refugees,
|
||||
// 21 Ortgay; every other id in 0..23 creates nothing. The four ids no save we
|
||||
// hold exercises (7, 8, 14, 21) are deliberately NOT modelled -- their bodies are
|
||||
// carried as Nodes rather than typed against a shape nothing can check.
|
||||
struct EncounterObject {
|
||||
static constexpr const char* kStreamName = "EncObj";
|
||||
int32_t id = 0;
|
||||
SVSOVonNeumann vonNeumann; // 1
|
||||
SVSOSwarm swarm; // 3
|
||||
SVSODerelict derelict; // 4
|
||||
SVSOMonitor monitor; // 5
|
||||
SVSOSlaversRefuel slavers; // 9
|
||||
SVSOSwarmQueen swarmQueen; // 10
|
||||
SVSOCrowRuins crowRuins; // 17
|
||||
SVSORefugees refugees; // 20
|
||||
std::vector<Node> unknown; // 7, 8, 14, 21 and any id with no factory entry
|
||||
|
||||
void select(int32_t encID) { id = encID; }
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
switch (id) {
|
||||
case 1: vonNeumann.io(ar); break;
|
||||
case 3: swarm.io(ar); break;
|
||||
case 4: derelict.io(ar); break;
|
||||
case 5: monitor.io(ar); break;
|
||||
case 9: slavers.io(ar); break;
|
||||
case 10: swarmQueen.io(ar); break;
|
||||
case 17: crowRuins.io(ar); break;
|
||||
case 20: refugees.io(ar); break;
|
||||
default: ar.rest(unknown); break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct ScriptObjects { // Game::SVSOSots -- the SvSctOb body
|
||||
static constexpr const char* kStreamName = "SvSctOb";
|
||||
struct Extra {
|
||||
std::string name;
|
||||
ScenarioObject obj;
|
||||
};
|
||||
struct Enc {
|
||||
int32_t id = 0;
|
||||
EncounterObject obj;
|
||||
};
|
||||
int32_t scnID = 0;
|
||||
std::optional<Node> scnObj; // StreamableHelper<SVSOScenarioBase>, NULL in our saves
|
||||
std::vector<Extra> extras;
|
||||
std::vector<Enc> encounters;
|
||||
std::vector<Node> extra;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.i32(A("ScnID"), scnID);
|
||||
ar.opt_any(A("ScnObj"), scnObj);
|
||||
ar.narr(A("numx"), extras, [](Ar& a, Extra& e) {
|
||||
a.str(A("xscn"), e.name);
|
||||
e.obj.select(e.name);
|
||||
a.obj(A("xsc"), e.obj);
|
||||
});
|
||||
ar.narr(A("NEncObjs"), encounters, [](Ar& a, Enc& e) {
|
||||
a.i32(A("EncID"), e.id);
|
||||
e.obj.select(e.id);
|
||||
a.obj(A("EncObj"), e.obj);
|
||||
});
|
||||
ar.rest(extra);
|
||||
}
|
||||
};
|
||||
|
||||
struct Sim {
|
||||
static constexpr const char* kStreamName = "Sim";
|
||||
std::string keyPath;
|
||||
|
|
@ -2303,7 +1884,7 @@ struct Sim {
|
|||
SpyManager spymgr;
|
||||
std::vector<FleetEntry> fleets;
|
||||
std::vector<int32_t> acts;
|
||||
std::optional<ScriptObjects> svSctOb;
|
||||
std::optional<Node> svSctOb;
|
||||
std::vector<ZoneDefence> zoneDefence;
|
||||
std::vector<Node> extra;
|
||||
|
||||
|
|
@ -2369,7 +1950,7 @@ struct Sim {
|
|||
ar.obj(A("spymgr"), spymgr);
|
||||
ar.narr(A("NumFlts"), fleets, [](Ar& a, FleetEntry& e) { e.io(a); });
|
||||
ar.narr(A("NumActs"), acts, [](Ar& a, int32_t& e) { a.i32(A("Act"), e); });
|
||||
ar.opt_obj(A("SvSctOb"), svSctOb); // only if the pointer was non-NULL
|
||||
ar.opt_any(A("SvSctOb"), svSctOb); // only if the pointer was non-NULL
|
||||
ar.narr(A("zdsc"), zoneDefence, [](Ar& a, ZoneDefence& e) { e.io(a); });
|
||||
ar.rest(extra);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +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.
|
||||
CHECK(pct >= 97.5);
|
||||
CHECK(pct >= 95.0);
|
||||
}
|
||||
|
||||
// --- round trips ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -476,121 +476,6 @@ static void test_typed_conditionals() {
|
|||
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
||||
}
|
||||
|
||||
// --- 8b. carr<std::string> and the SvSctOb variant dispatch -----------------------------
|
||||
static void test_typed_string_array() {
|
||||
// DOpts / trev are VectorHelper<Mars::String>: a framed count then NULL-named
|
||||
// strings. The element branch has to survive the empty string, which is four
|
||||
// zero bytes and so indistinguishable from the int 0 in isolation.
|
||||
shapes::DesignSection sec;
|
||||
sec.opts = {"OPT_Armour", "", "OPT_Shield"};
|
||||
Writer w;
|
||||
WriteArchive wa(w);
|
||||
wa.obj(A("DSec"), sec);
|
||||
Node root = read_tree(w.bytes());
|
||||
std::vector<Issue> issues;
|
||||
shapes::DesignSection back;
|
||||
ReadArchive ra(root.children, issues, "");
|
||||
ra.obj(A("DSec"), back);
|
||||
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
||||
CHECK_EQ(count(issues, Issue::Warn), size_t(0));
|
||||
CHECK(back.opts.size() == 3);
|
||||
CHECK(back.opts[0] == "OPT_Armour" && back.opts[1].empty() && back.opts[2] == "OPT_Shield");
|
||||
Writer w2;
|
||||
WriteArchive wa2(w2);
|
||||
wa2.obj(A("DSec"), back);
|
||||
CHECK(w2.bytes() == w.bytes());
|
||||
}
|
||||
|
||||
static void test_svsctob_variants() {
|
||||
// `xsc` is keyed by the preceding `xscn` and `EncObj` by the preceding
|
||||
// `EncID`. Nothing on the wire says which body follows, so the round trip is
|
||||
// only correct if the reader applies the same key the writer did.
|
||||
shapes::ScriptObjects so;
|
||||
so.scnID = 0;
|
||||
|
||||
shapes::ScriptObjects::Extra traps;
|
||||
traps.name = "traps";
|
||||
traps.obj.select(traps.name);
|
||||
traps.obj.traps.traps.push_back({272, 0, -1, -1});
|
||||
so.extras.push_back(traps);
|
||||
|
||||
shapes::ScriptObjects::Extra gm;
|
||||
gm.name = "gmtrigger";
|
||||
gm.obj.select(gm.name);
|
||||
gm.obj.grandMenace.gmch = 21;
|
||||
so.extras.push_back(gm);
|
||||
|
||||
shapes::ScriptObjects::Extra ind; // "indsys" serializes nothing at all
|
||||
ind.name = "indsys";
|
||||
ind.obj.select(ind.name);
|
||||
so.extras.push_back(ind);
|
||||
|
||||
shapes::ScriptObjects::Enc swarm; // EncID 3
|
||||
swarm.id = 3;
|
||||
swarm.obj.select(3);
|
||||
swarm.obj.swarm.asg.push_back({1120, 336});
|
||||
swarm.obj.swarm.infest.push_back({336, 0, 1, 2147483647, 0, false});
|
||||
swarm.obj.swarm.deshive = 1072;
|
||||
swarm.obj.swarm.deslarva = 1088;
|
||||
so.encounters.push_back(swarm);
|
||||
|
||||
shapes::ScriptObjects::Enc mon; // EncID 5: Monitor writes Derelict's body first
|
||||
mon.id = 5;
|
||||
mon.obj.select(5);
|
||||
mon.obj.monitor.base.designs.push_back({1312, 30});
|
||||
mon.obj.monitor.base.asg.push_back({1360, 64});
|
||||
mon.obj.monitor.spawns.push_back({"_AsteroidMonitor", 30, 1.0f, 1312});
|
||||
so.encounters.push_back(mon);
|
||||
|
||||
shapes::ScriptObjects::Enc vn; // EncID 1
|
||||
vn.id = 1;
|
||||
vn.obj.select(1);
|
||||
vn.obj.vonNeumann.sken = true;
|
||||
vn.obj.vonNeumann.ntm = 25;
|
||||
vn.obj.vonNeumann.vnhp = 0;
|
||||
so.encounters.push_back(vn);
|
||||
|
||||
shapes::ScriptObjects::Enc unmodelled; // EncID 8 (PuppetMaster): carried, not typed
|
||||
unmodelled.id = 8;
|
||||
unmodelled.obj.select(8);
|
||||
unmodelled.obj.unknown.push_back(Node::int32("Flt", 42));
|
||||
so.encounters.push_back(unmodelled);
|
||||
|
||||
Writer w;
|
||||
WriteArchive wa(w);
|
||||
wa.obj(A("SvSctOb"), so);
|
||||
Node root = read_tree(w.bytes());
|
||||
std::vector<Issue> issues;
|
||||
shapes::ScriptObjects back;
|
||||
ReadArchive ra(root.children, issues, "");
|
||||
ra.obj(A("SvSctOb"), back);
|
||||
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
||||
CHECK_EQ(count(issues, Issue::Warn), size_t(0));
|
||||
CHECK(back.extras.size() == 3 && back.encounters.size() == 4);
|
||||
CHECK(back.extras[0].name == "traps" && back.extras[0].obj.traps.traps.size() == 1);
|
||||
CHECK(back.extras[0].obj.traps.traps[0].sys == 272);
|
||||
CHECK(back.extras[1].obj.grandMenace.gmch == 21);
|
||||
CHECK(back.extras[2].obj.indSys.extra.empty()); // the empty frame really is empty
|
||||
CHECK(back.encounters[0].obj.swarm.deslarva == 1088);
|
||||
CHECK(back.encounters[0].obj.swarm.infest.size() == 1 &&
|
||||
back.encounters[0].obj.swarm.infest[0].mtrn == 2147483647);
|
||||
CHECK(back.encounters[1].obj.monitor.base.designs.size() == 1 &&
|
||||
back.encounters[1].obj.monitor.spawns.size() == 1);
|
||||
CHECK(back.encounters[1].obj.monitor.spawns[0].scnm == "_AsteroidMonitor");
|
||||
CHECK(back.encounters[2].obj.vonNeumann.sken && back.encounters[2].obj.vonNeumann.ntm == 25);
|
||||
CHECK(back.encounters[3].id == 8 && back.encounters[3].obj.unknown.size() == 1);
|
||||
Writer w2;
|
||||
WriteArchive wa2(w2);
|
||||
wa2.obj(A("SvSctOb"), back);
|
||||
CHECK(w2.bytes() == w.bytes());
|
||||
|
||||
// The key really is what selects the body: read the same bytes with the wrong
|
||||
// EncID and the Swarm record no longer parses as a Swarm.
|
||||
shapes::EncounterObject wrong;
|
||||
wrong.select(17); // CrowRuins over Swarm bytes
|
||||
CHECK(wrong.id == 17 && wrong.swarm.asg.empty());
|
||||
}
|
||||
|
||||
// --- 9. gzip container ---------------------------------------------------------------
|
||||
static void test_gzip() {
|
||||
Bytes data;
|
||||
|
|
@ -638,8 +523,6 @@ int main() {
|
|||
test_raw_frame();
|
||||
test_typed_summary();
|
||||
test_typed_conditionals();
|
||||
test_typed_string_array();
|
||||
test_svsctob_variants();
|
||||
test_gzip();
|
||||
test_dump_format();
|
||||
std::printf("test_stream: %s (%d failures)\n", fails ? "FAILED" : "ok", fails);
|
||||
|
|
|
|||
|
|
@ -235,29 +235,6 @@ int main() {
|
|||
check<sh::ShipSectionID>("ShipSectionID", "Game::ShipSectionID");
|
||||
check<sh::DesignSection>("DesignSection", "Game::ShipDesignDef::Section");
|
||||
|
||||
// --- SvSctOb: Game::SVSOSots and the variant bodies it dispatches to -------
|
||||
// The variant SELECTION (xscn name / EncID -> class) is not on the wire and so
|
||||
// is not checkable here; it came from the game's own factories. What this
|
||||
// does check is that each body we bind agrees with that class's serializer.
|
||||
check<sh::ScriptObjects>("ScriptObjects", "Game::SVSOSots");
|
||||
check<sh::SVSOVonNeumann>("SVSOVonNeumann", "Game::SVSOVonNeumann");
|
||||
check<sh::VonNeumannDefeat>("VonNeumannDefeat", "Game::SVSOVonNeumann::DefeatRecord");
|
||||
check<sh::SVSOSwarm>("SVSOSwarm", "Game::SVSOSwarm");
|
||||
check<sh::SwarmInfestation>("SwarmInfestation", "Game::SVSOSwarm::Infestation");
|
||||
check<sh::SVSODerelict>("SVSODerelict", "Game::SVSODerelict");
|
||||
check<sh::SVSOMonitor>("SVSOMonitor", "Game::SVSOMonitor");
|
||||
check<sh::SVSOSlaversRefuel>("SVSOSlaversRefuel", "Game::SVSOSlaversRefuel");
|
||||
check<sh::SVSOSwarmQueen>("SVSOSwarmQueen", "Game::SVSOSwarmQueen");
|
||||
check<sh::SVSOSwarmQueenHive>("SVSOSwarmQueenHive", "Game::SVSOSwarmQueen::HiveInfo");
|
||||
check<sh::SVSOSwarmQueenQueen>("SVSOSwarmQueenQueen", "Game::SVSOSwarmQueen::QueenInfo");
|
||||
check<sh::SVSOCrowRuins>("SVSOCrowRuins", "Game::SVSOCrowRuins");
|
||||
check<sh::SVSORefugees>("SVSORefugees", "Game::SVSORefugees");
|
||||
check<sh::RefugeeStatus>("RefugeeStatus", "Game::SVSORefugees::PlayerStatus");
|
||||
check<sh::SVSOTraps>("SVSOTraps", "Game::SVSOTraps");
|
||||
check<sh::SVSOTrap>("SVSOTrap", "Game::SVSOTraps::Trap");
|
||||
check<sh::SVSOCrowDefenders>("SVSOCrowDefenders", "Game::SVSOCrowDefenders");
|
||||
check<sh::SVSOGrandMenaceTrigger>("SVSOGrandMenaceTrigger", "Game::SVSOGrandMenaceTrigger");
|
||||
|
||||
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