G: wire schema channel + widen the save codec to 97% typed coverage
The serializer recovery reaches this repo as a generated wire schema
(include/generated/sots_stream_schema.h, 386 classes / 2042 items): for each
class, the ordered sequence of items its Write puts on the stream. Facts only —
no field offsets, no sizeof, no strides. This engine reads and writes the on-disk
format; it does not inherit the original's memory layout.
The table is a specification, not a program: the recovery is a linear pass over
Write, so it cannot see Write's branches (StarShip's BQ2 is gated on hbq but
listed unconditionally) and it flattens container loops. A codec driven off it
would desynchronise. The hand-written io() shapes stay the codec; SchemaProbe
(probe.h) walks them with every branch taken and test_wire_schema LCS-aligns
that against the table — 56 shapes bound, 657 items matched, 0 mismatches.
Four defects the check found, all invisible to a round-trip test:
- SystemParams field 1 is a string, not an int. It is the empty string in
every save, and an empty string is four zero bytes — byte-identical to the
int 0, so it round-tripped by luck. A named planet would have desynced.
- ObservedTech/ObservedWeapon odet is a bool, not an int. Byte-safe only
because a 4-char tag makes a bool item and an int item both 12 bytes.
- SpeciesRatios nv and ShipRecords srbd are counts, not fields.
CoverageArchive separates items a field names from items a Node merely carries,
because a byte-identical round trip is not a coverage claim. Typed coverage of a
real save goes 37.9% -> 97.1% (97.2/97.2/97.6 on the others) with the round trip
still byte-identical, by typing TechTree (both NumTechs sections), Events,
ShipRecs, sprjs, civr, comms, spy2, spymgr, aid, Ojvs, AIEnf, FNG, trdmgr and
the Des section/gun-bank tree. Ratchet at 95%.
trdmgr resolves a recorded trap: ServerTradeManager's Read/Write really are the
inherited no-op, but the call is virtual and ServerTradeManagerImpl has the real
serializer. Same shape resolves IServerSpyManager -> ServerSpyManager.
ctest 34/34, clean_room_check OK, test_save skips cleanly with SOTS_SAVES_DIR unset.
This commit is contained in:
parent
0f7aba1278
commit
5bae56a397
7 changed files with 4351 additions and 32 deletions
78
docs/G-wire-schema.md
Normal file
78
docs/G-wire-schema.md
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# G — the wire schema channel
|
||||||
|
|
||||||
|
`include/generated/sots_stream_schema.h` is the second generated channel into this repo, alongside
|
||||||
|
`include/generated/sots_addresses.h`. Both carry **facts about the original binary and nothing
|
||||||
|
else**, both are produced by a script in the `sots-re` notes repo, and neither is ever hand-edited.
|
||||||
|
|
||||||
|
```
|
||||||
|
sots-re objects/layouts.json (the serializer recovery, memory-layout view)
|
||||||
|
| tools/streams.py -> objects/streams.json (wire view)
|
||||||
|
| tools/gen_stream_schema.py
|
||||||
|
v
|
||||||
|
sots-engine include/generated/sots_stream_schema.h
|
||||||
|
```
|
||||||
|
|
||||||
|
Regenerate after any change on the notes side:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd ~/sots-re
|
||||||
|
uv run python3 tools/streams.py
|
||||||
|
uv run python3 tools/gen_stream_schema.py ../sots-engine/include/generated/sots_stream_schema.h
|
||||||
|
```
|
||||||
|
|
||||||
|
## What crosses, and what deliberately does not
|
||||||
|
|
||||||
|
What crosses is the **wire schema**: for each serializable class, the ordered sequence of items its
|
||||||
|
`Write` puts on the stream — on-disk tag, on-disk primitive, and how the item is framed.
|
||||||
|
|
||||||
|
What does **not** cross is every memory fact in the recovery: field offsets, `sizeof`, gaps,
|
||||||
|
container strides. This engine is our own C++, not a byte-for-byte decompilation. It has to read and
|
||||||
|
write the on-disk *format* faithfully; it must not inherit the original's ABI in its runtime types,
|
||||||
|
and `shapes.h` is free to lay its structs out however C++ likes.
|
||||||
|
|
||||||
|
The shim is the one component that legitimately needs the original's ABI, because it reads the
|
||||||
|
running game's memory. Those offsets have their own home: `offset` entries in `addresses.json`,
|
||||||
|
which arrive through `sots_addresses.h`.
|
||||||
|
|
||||||
|
Note the primitive in the table is the **disk** type. A member the original holds as `int16` or
|
||||||
|
`int8` is written by `Stream::WriteInt` and is four bytes on the wire; nothing narrow reaches the
|
||||||
|
stream.
|
||||||
|
|
||||||
|
## Why it is a check, not a generator
|
||||||
|
|
||||||
|
The table is a **specification, not a program**. The recovery is a linear pass over the game's
|
||||||
|
`Write`, so:
|
||||||
|
|
||||||
|
* **It cannot see branches.** `StarShip` writes `BQ2` only when `hbq` is set; the table lists it
|
||||||
|
unconditionally. The sequence is a *superset* of what any single record contains.
|
||||||
|
* **Container loops are flattened.** A count item is followed by its element items as siblings
|
||||||
|
(`member == false`), not nested inside the container.
|
||||||
|
|
||||||
|
A codec driven straight off the table would desynchronise on the first branch. So the hand-written
|
||||||
|
`io(Ar&)` shapes stay the codec — they express the conditionals and the nesting the binary facts
|
||||||
|
cannot supply — and the table is what proves they agree with the binary.
|
||||||
|
|
||||||
|
## The two extra archives
|
||||||
|
|
||||||
|
`src/mars/stream/probe.h` adds two archives to the three in `archive.h`:
|
||||||
|
|
||||||
|
* **`SchemaProbe`** walks a shape's `io()` with **every branch taken** — the same "all branches" view
|
||||||
|
the recovery has — and records the item sequence. `tests/mars_stream/test_wire_schema.cpp`
|
||||||
|
LCS-aligns that against the generated table. A tag both sides name with different disk primitives
|
||||||
|
is a **hard failure**; a wire-only item is a conditional or coverage debt and is reported only.
|
||||||
|
Run with `SOTS_WIRE_VERBOSE=1` to list every gap.
|
||||||
|
* **`CoverageArchive`** walks a *populated* shape's `io()` and separates items a field **names**
|
||||||
|
(`typed`) from items only a generic `Node` **carried** (`opaque`). This matters because a
|
||||||
|
byte-identical round trip is not a coverage claim: `ar.any` and the `ar.rest` tail round-trip
|
||||||
|
perfectly by copying bytes they do not understand. `test_save` prints the split and the worst
|
||||||
|
offenders, and carries a ratchet so typing a body can never silently regress.
|
||||||
|
|
||||||
|
## Adding a shape
|
||||||
|
|
||||||
|
1. `uv run python3 ~/sots-re/tools/streams.py Game::TheClass` — the item list, in disk order.
|
||||||
|
2. Write the `io()` in `shapes.h`. Watch for two things the table shows but does not spell out: an
|
||||||
|
item marked `[element]` is a **loop body**, so the item before it is a **count**, not a field; and
|
||||||
|
an item the game writes conditionally will be listed anyway.
|
||||||
|
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.
|
||||||
3292
include/generated/sots_stream_schema.h
Normal file
3292
include/generated/sots_stream_schema.h
Normal file
File diff suppressed because it is too large
Load diff
270
src/mars/stream/probe.h
Normal file
270
src/mars/stream/probe.h
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
// mars::stream — SchemaProbe: a fourth archive that records what a shape's
|
||||||
|
// io() puts on the stream, so the shape can be checked against the wire schema
|
||||||
|
// recovered from the game's own serializers (include/generated/sots_stream_schema.h).
|
||||||
|
//
|
||||||
|
// The three working archives read, write and build hints. This one executes
|
||||||
|
// nothing: it walks io() and appends one Item per stream item, in order. Two
|
||||||
|
// choices make its output directly comparable to the generated table:
|
||||||
|
//
|
||||||
|
// * `when(cond, body)` runs the body **unconditionally**. The recovery is a
|
||||||
|
// linear pass over the game's Write and cannot see Write's branches either,
|
||||||
|
// so both sides list every branch. Comparing "all branches taken" against
|
||||||
|
// "all branches taken" is the only alignment that means anything.
|
||||||
|
// * `narr` emits the count item and then the element fields as *siblings*
|
||||||
|
// (member == false), which is exactly how the flattened recovery presents a
|
||||||
|
// container loop.
|
||||||
|
//
|
||||||
|
// `any()` / `raw_frame()` — the escape hatch shapes.h uses for bodies it does
|
||||||
|
// not model — record Opaque. Opaque items are the coverage debt: the shape
|
||||||
|
// round-trips them byte-for-byte by carrying the Node, but it does not
|
||||||
|
// understand them. Counting Opaque against the wire schema is how this
|
||||||
|
// codebase measures how much of the save it actually reads.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "archive.h"
|
||||||
|
|
||||||
|
namespace mars::stream {
|
||||||
|
|
||||||
|
class SchemaProbe {
|
||||||
|
public:
|
||||||
|
static constexpr bool reading = false, writing = false, building = false;
|
||||||
|
|
||||||
|
enum class P : uint8_t { Unknown, I32, I64, F32, Bool, Str, Frame, Raw };
|
||||||
|
enum class S : uint8_t { Scalar, Frame, CArr, NArr, Raw };
|
||||||
|
|
||||||
|
struct Item {
|
||||||
|
std::string tag; // on-disk tag ("." when the game writes a NULL name)
|
||||||
|
P prim = P::Unknown;
|
||||||
|
S shape = S::Scalar;
|
||||||
|
bool member = true; // false: a container element
|
||||||
|
bool optional = false; // the shape consumes it only if the tag matches
|
||||||
|
bool opaque = false; // carried as a Node: round-tripped, not understood
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Item> items;
|
||||||
|
bool has_rest = false; // the shape absorbs a trailing tail into `extra`
|
||||||
|
|
||||||
|
// --- scalars ------------------------------------------------------------
|
||||||
|
void i32(Tag t, int32_t&) { add(t, P::I32, S::Scalar); }
|
||||||
|
void f32(Tag t, float&) { add(t, P::F32, S::Scalar); }
|
||||||
|
void b(Tag t, bool&) { add(t, P::Bool, S::Scalar); }
|
||||||
|
void i64(Tag t, int64_t&) { add(t, P::I64, S::Scalar); }
|
||||||
|
void str(Tag t, std::string&) { add(t, P::Str, S::Scalar); }
|
||||||
|
void vec3(Tag t, Vec3&) { add(t, P::Frame, S::Frame); } // written as a Vector3 frame
|
||||||
|
|
||||||
|
void any(Tag t, Node&) { add(t, P::Unknown, S::Frame).opaque = true; }
|
||||||
|
// raw_frame is a *framed* item (the RNG block) whose body is one opaque
|
||||||
|
// payload, so on the wire it is a frame like any other nested object.
|
||||||
|
void raw_frame(Tag t, Node&) { add(t, P::Frame, S::Frame).opaque = true; }
|
||||||
|
|
||||||
|
// --- optional named items ------------------------------------------------
|
||||||
|
void opt_i32(Tag t, std::optional<int32_t>&) { add(t, P::I32, S::Scalar).optional = true; }
|
||||||
|
void opt_f32(Tag t, std::optional<float>&) { add(t, P::F32, S::Scalar).optional = true; }
|
||||||
|
void opt_b(Tag t, std::optional<bool>&) { add(t, P::Bool, S::Scalar).optional = true; }
|
||||||
|
void opt_any(Tag t, std::optional<Node>&) {
|
||||||
|
Item& i = add(t, P::Unknown, S::Frame);
|
||||||
|
i.optional = true;
|
||||||
|
i.opaque = true;
|
||||||
|
}
|
||||||
|
template <class T>
|
||||||
|
void opt_obj(Tag t, std::optional<T>&) {
|
||||||
|
add(t, P::Frame, S::Frame).optional = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- framed struct / arrays ------------------------------------------------
|
||||||
|
// A nested shape is one item here; the nested class is its own wire entry.
|
||||||
|
template <class T>
|
||||||
|
void obj(Tag t, T&) {
|
||||||
|
add(t, P::Frame, S::Frame);
|
||||||
|
}
|
||||||
|
template <class T>
|
||||||
|
void obj_flex(Tag t, T& v, bool&) {
|
||||||
|
obj(t, v);
|
||||||
|
}
|
||||||
|
template <class T>
|
||||||
|
void carr(Tag t, std::vector<T>&) {
|
||||||
|
add(t, P::Frame, S::CArr);
|
||||||
|
}
|
||||||
|
template <class T>
|
||||||
|
void carr_flex(Tag t, std::vector<T>& v, bool&) {
|
||||||
|
carr(t, v);
|
||||||
|
}
|
||||||
|
template <class T, class F>
|
||||||
|
void narr(Tag t, std::vector<T>&, F elem) {
|
||||||
|
add(t, P::I32, S::NArr);
|
||||||
|
T tmp{};
|
||||||
|
size_t first = items.size();
|
||||||
|
elem(*this, tmp);
|
||||||
|
for (size_t i = first; i < items.size(); ++i) items[i].member = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- control flow: both sides list every branch ------------------------------
|
||||||
|
template <class F>
|
||||||
|
void when(bool, F body) {
|
||||||
|
body(*this);
|
||||||
|
}
|
||||||
|
template <class T, class F>
|
||||||
|
void repeat(const char*, std::vector<T>&, F elem) {
|
||||||
|
T tmp{};
|
||||||
|
size_t first = items.size();
|
||||||
|
elem(*this, tmp);
|
||||||
|
for (size_t i = first; i < items.size(); ++i) items[i].member = false;
|
||||||
|
}
|
||||||
|
void rest(std::vector<Node>&) { has_rest = true; }
|
||||||
|
|
||||||
|
// Run one shape's io() and return its item sequence.
|
||||||
|
template <class T>
|
||||||
|
static SchemaProbe of() {
|
||||||
|
SchemaProbe p;
|
||||||
|
T tmp{};
|
||||||
|
tmp.io(p);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Item& add(Tag t, P prim, S shape) {
|
||||||
|
Item i;
|
||||||
|
i.tag = t.disk; // what actually goes on the wire, not the schema name
|
||||||
|
i.prim = prim;
|
||||||
|
i.shape = shape;
|
||||||
|
items.push_back(std::move(i));
|
||||||
|
return items.back();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CoverageArchive — how much of a real save the shapes actually understand.
|
||||||
|
//
|
||||||
|
// A byte-identical round trip is not a coverage claim. shapes.h reaches it
|
||||||
|
// partly by typing fields and partly by carrying whole bodies as generic Nodes
|
||||||
|
// (`ar.any`, `ar.raw_frame`, and the `ar.rest` tail): a Node round-trips
|
||||||
|
// trivially because it is copied verbatim. This archive runs a *populated*
|
||||||
|
// shape's io() and separates the two — every item a field names is `typed`,
|
||||||
|
// every item that only survives because a Node carried it is `opaque`.
|
||||||
|
//
|
||||||
|
// Unlike SchemaProbe this one honours `when()`, because it walks real data.
|
||||||
|
class CoverageArchive {
|
||||||
|
public:
|
||||||
|
static constexpr bool reading = false, writing = false, building = false;
|
||||||
|
|
||||||
|
size_t typed = 0, opaque = 0;
|
||||||
|
std::map<std::string, size_t> opaque_by_tag; // where the untyped items are
|
||||||
|
|
||||||
|
static size_t count(const Node& n) {
|
||||||
|
size_t c = 1;
|
||||||
|
for (const Node& k : n.children) c += count(k);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void i32(Tag, int32_t&) { ++typed; }
|
||||||
|
void f32(Tag, float&) { ++typed; }
|
||||||
|
void b(Tag, bool&) { ++typed; }
|
||||||
|
void i64(Tag, int64_t&) { ++typed; }
|
||||||
|
void str(Tag, std::string&) { ++typed; }
|
||||||
|
void vec3(Tag, Vec3&) { typed += 4; } // the frame plus three floats
|
||||||
|
|
||||||
|
void any(Tag t, Node& v) { charge(t.name, count(v)); }
|
||||||
|
void raw_frame(Tag t, Node& v) { charge(t.name, count(v)); }
|
||||||
|
|
||||||
|
void opt_i32(Tag, std::optional<int32_t>& v) { typed += v.has_value(); }
|
||||||
|
void opt_f32(Tag, std::optional<float>& v) { typed += v.has_value(); }
|
||||||
|
void opt_b(Tag, std::optional<bool>& v) { typed += v.has_value(); }
|
||||||
|
void opt_any(Tag t, std::optional<Node>& v) {
|
||||||
|
if (v) charge(t.name, count(*v));
|
||||||
|
}
|
||||||
|
template <class T>
|
||||||
|
void opt_obj(Tag t, std::optional<T>& v) {
|
||||||
|
if (v) obj(t, *v);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void obj(Tag t, T& v) {
|
||||||
|
++typed; // the frame item itself
|
||||||
|
const char* save = cur_;
|
||||||
|
cur_ = *t.name ? t.name : (*T::kStreamName ? T::kStreamName : cur_);
|
||||||
|
v.io(*this);
|
||||||
|
cur_ = save;
|
||||||
|
}
|
||||||
|
template <class T>
|
||||||
|
void obj_flex(Tag t, T& v, bool&) {
|
||||||
|
obj(t, v);
|
||||||
|
}
|
||||||
|
template <class T>
|
||||||
|
void carr(Tag t, std::vector<T>& v) {
|
||||||
|
typed += 2; // the frame item and its "." count
|
||||||
|
for (T& e : v) elem_of(t.name, e);
|
||||||
|
}
|
||||||
|
template <class T>
|
||||||
|
void carr_flex(Tag t, std::vector<T>& v, bool&) {
|
||||||
|
carr(t, v);
|
||||||
|
}
|
||||||
|
template <class T, class F>
|
||||||
|
void narr(Tag, std::vector<T>& v, F fn) {
|
||||||
|
++typed; // the count item
|
||||||
|
for (T& e : v) fn(*this, e);
|
||||||
|
}
|
||||||
|
// A tag for the tail is not available here; `rest` is charged to "<rest>".
|
||||||
|
template <class F>
|
||||||
|
void when(bool cond, F body) {
|
||||||
|
if (cond) body(*this);
|
||||||
|
}
|
||||||
|
template <class T, class F>
|
||||||
|
void repeat(const char*, std::vector<T>& v, F fn) {
|
||||||
|
for (T& e : v) fn(*this, e);
|
||||||
|
}
|
||||||
|
void rest(std::vector<Node>& v) {
|
||||||
|
for (const Node& n : v) charge(std::string("<rest:") + cur_ + ">", count(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const char* cur_ = "root";
|
||||||
|
void charge(const std::string& tag, size_t n) {
|
||||||
|
opaque += n;
|
||||||
|
if (n) opaque_by_tag[tag] += n;
|
||||||
|
}
|
||||||
|
void elem_of(const char* tag, Node& n) { charge(tag, count(n)); }
|
||||||
|
void elem_of(const char*, int32_t&) { ++typed; }
|
||||||
|
void elem_of(const char*, float&) { ++typed; }
|
||||||
|
void elem_of(const char*, std::string&) { ++typed; }
|
||||||
|
template <class T>
|
||||||
|
void elem_of(const char* tag, T& v) {
|
||||||
|
++typed; // the element's own frame
|
||||||
|
const char* save = cur_;
|
||||||
|
cur_ = *tag ? tag : cur_;
|
||||||
|
v.io(*this);
|
||||||
|
cur_ = save;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const char* probe_prim_name(SchemaProbe::P p) {
|
||||||
|
switch (p) {
|
||||||
|
case SchemaProbe::P::Unknown: return "?";
|
||||||
|
case SchemaProbe::P::I32: return "i32";
|
||||||
|
case SchemaProbe::P::I64: return "i64";
|
||||||
|
case SchemaProbe::P::F32: return "f32";
|
||||||
|
case SchemaProbe::P::Bool: return "bool";
|
||||||
|
case SchemaProbe::P::Str: return "str";
|
||||||
|
case SchemaProbe::P::Frame: return "frame";
|
||||||
|
case SchemaProbe::P::Raw: return "raw";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const char* probe_shape_name(SchemaProbe::S s) {
|
||||||
|
switch (s) {
|
||||||
|
case SchemaProbe::S::Scalar: return "scalar";
|
||||||
|
case SchemaProbe::S::Frame: return "frame";
|
||||||
|
case SchemaProbe::S::CArr: return "carr";
|
||||||
|
case SchemaProbe::S::NArr: return "narr";
|
||||||
|
case SchemaProbe::S::Raw: return "raw";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mars::stream
|
||||||
|
|
@ -165,13 +165,20 @@ struct Summary {
|
||||||
struct Planet { // SystemParams element, all tags "."
|
struct Planet { // SystemParams element, all tags "."
|
||||||
static constexpr const char* kStreamName = "";
|
static constexpr const char* kStreamName = "";
|
||||||
Vec3 pos;
|
Vec3 pos;
|
||||||
int32_t p1 = 0, p2 = 0, p3 = 0;
|
// p1 is a std::string, not an int: SystemParams::Write calls the string
|
||||||
|
// writer for it (sots-re objects/streams.json, Game::SystemParams item 1).
|
||||||
|
// It is the empty string in every real save, and an empty string is four
|
||||||
|
// zero bytes -- byte-identical to the int 0 the community reader assumed,
|
||||||
|
// which is why reading it as an int has never broken a round trip. A save
|
||||||
|
// carrying a non-empty name here would have desynchronised the parse.
|
||||||
|
std::string p1;
|
||||||
|
int32_t p2 = 0, p3 = 0;
|
||||||
float p4 = 0;
|
float p4 = 0;
|
||||||
std::vector<Node> extra;
|
std::vector<Node> extra;
|
||||||
template <class Ar>
|
template <class Ar>
|
||||||
void io(Ar& ar) {
|
void io(Ar& ar) {
|
||||||
ar.vec3(R("pos"), pos);
|
ar.vec3(R("pos"), pos);
|
||||||
ar.i32(R("p1"), p1);
|
ar.str(R("p1"), p1);
|
||||||
ar.i32(R("p2"), p2);
|
ar.i32(R("p2"), p2);
|
||||||
ar.i32(R("p3"), p3);
|
ar.i32(R("p3"), p3);
|
||||||
ar.f32(R("p4"), p4);
|
ar.f32(R("p4"), p4);
|
||||||
|
|
@ -711,9 +718,16 @@ struct Odes {
|
||||||
ar.rest(extra);
|
ar.rest(extra);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// `odet` is a bool. ObservedWeapon::Write / ObservedTech::Write call the bool
|
||||||
|
// writer, and the campaign's hand-recovered struct table agrees (odet @+0x08,
|
||||||
|
// bool). The community reader models it as an int; that is byte-safe here only
|
||||||
|
// by coincidence -- with a 4-char tag a bool item and an int item both occupy 12
|
||||||
|
// bytes, and the bool's three pad bytes are zeroed, so the two encodings are
|
||||||
|
// identical. A shorter tag would not have been so forgiving.
|
||||||
struct Owep {
|
struct Owep {
|
||||||
static constexpr const char* kStreamName = "";
|
static constexpr const char* kStreamName = "";
|
||||||
int32_t ontF = 0, otnL = 0, odet = 0;
|
int32_t ontF = 0, otnL = 0;
|
||||||
|
bool odet = false;
|
||||||
std::string owep;
|
std::string owep;
|
||||||
int32_t owith = 0;
|
int32_t owith = 0;
|
||||||
std::vector<Node> extra;
|
std::vector<Node> extra;
|
||||||
|
|
@ -721,15 +735,16 @@ struct Owep {
|
||||||
void io(Ar& ar) {
|
void io(Ar& ar) {
|
||||||
ar.i32(R("ontF", "otnF"), ontF);
|
ar.i32(R("ontF", "otnF"), ontF);
|
||||||
ar.i32(R("otnL", "otnL"), otnL);
|
ar.i32(R("otnL", "otnL"), otnL);
|
||||||
ar.i32(R("odet", "odet"), odet);
|
ar.b(R("odet", "odet"), odet);
|
||||||
ar.str(R("owep", "owep"), owep);
|
ar.str(R("owep", "owep"), owep);
|
||||||
ar.i32(R("owith", "owith"), owith);
|
ar.i32(R("owith", "owith"), owith);
|
||||||
ar.rest(extra);
|
ar.rest(extra);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
struct Otch {
|
struct Otch { // see the note on Owep::odet
|
||||||
static constexpr const char* kStreamName = "";
|
static constexpr const char* kStreamName = "";
|
||||||
int32_t ontF = 0, otnL = 0, odet = 0;
|
int32_t ontF = 0, otnL = 0;
|
||||||
|
bool odet = false;
|
||||||
std::string otch;
|
std::string otch;
|
||||||
int32_t owith = 0;
|
int32_t owith = 0;
|
||||||
std::vector<Node> extra;
|
std::vector<Node> extra;
|
||||||
|
|
@ -737,7 +752,7 @@ struct Otch {
|
||||||
void io(Ar& ar) {
|
void io(Ar& ar) {
|
||||||
ar.i32(R("ontF", "otnF"), ontF);
|
ar.i32(R("ontF", "otnF"), ontF);
|
||||||
ar.i32(R("otnL", "otnL"), otnL);
|
ar.i32(R("otnL", "otnL"), otnL);
|
||||||
ar.i32(R("odet", "odet"), odet);
|
ar.b(R("odet", "odet"), odet);
|
||||||
ar.str(R("otch", "otch"), otch);
|
ar.str(R("otch", "otch"), otch);
|
||||||
ar.i32(R("owith", "owith"), owith);
|
ar.i32(R("owith", "owith"), owith);
|
||||||
ar.rest(extra);
|
ar.rest(extra);
|
||||||
|
|
@ -759,19 +774,79 @@ struct Note {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ShipSectionID { // Game::ShipSectionID: two "." ints
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
int32_t a = 0, b = 0;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(R("a"), a);
|
||||||
|
ar.i32(R("b"), b);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct GunWeapon { // DW2: the weapon in a gun bank, named either by id or by name
|
||||||
|
static constexpr const char* kStreamName = "DW2";
|
||||||
|
bool bID = false; // true: the weapon is identified by id, false: by family name
|
||||||
|
int32_t wid = 0;
|
||||||
|
std::string wfn;
|
||||||
|
int32_t did = 0;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.b(A("bID"), bID);
|
||||||
|
ar.when(bID, [&](Ar& a) { a.i32(A("wid"), wid); });
|
||||||
|
ar.when(!bID, [&](Ar& a) { a.str(A("wfn"), wfn); });
|
||||||
|
ar.i32(A("did"), did);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct GunBank { // one DGbnk2 element: an anonymous frame holding one DW2
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
GunWeapon w;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.obj(A("DW2"), w);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct DesignSection { // Game::ShipDesignDef::Section, one DSec of a Des frame
|
||||||
|
static constexpr const char* kStreamName = "DSec";
|
||||||
|
ShipSectionID sec;
|
||||||
|
std::vector<GunBank> gunBanks;
|
||||||
|
std::vector<Node> opts; // DOpts: the recovery says carr<String>
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.obj(A("DSec"), sec);
|
||||||
|
ar.carr(A("DGbnk2"), gunBanks);
|
||||||
|
ar.carr(A("DOpts"), opts);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
struct Design { // on-disk tags are case variants of the reference names (FAIDes, DHide, DWep, DName)
|
struct Design { // on-disk tags are case variants of the reference names (FAIDes, DHide, DWep, DName)
|
||||||
static constexpr const char* kStreamName = "Des";
|
static constexpr const char* kStreamName = "Des";
|
||||||
bool faiDes = false, dHide = false;
|
bool faiDes = false, dHide = false;
|
||||||
int32_t dWep = 0;
|
int32_t dWep = 0;
|
||||||
std::string dName;
|
std::string dName;
|
||||||
std::vector<Node> sections;
|
// The DSec run is uncounted -- it ends when the next tag stops being DSec.
|
||||||
|
// Game::ShipDesign::Write makes no stream call at all, so there is no recovered
|
||||||
|
// serializer for the Des frame itself; the section body is
|
||||||
|
// Game::ShipDesignDef::Section, which there is, and Dtc/Dwgv are from the saves.
|
||||||
|
std::vector<DesignSection> sections;
|
||||||
|
int32_t dtc = 0, dwgv = 0;
|
||||||
|
std::vector<Node> extra;
|
||||||
template <class Ar>
|
template <class Ar>
|
||||||
void io(Ar& ar) {
|
void io(Ar& ar) {
|
||||||
ar.b(R("faiDes", "FAIDes"), faiDes);
|
ar.b(R("faiDes", "FAIDes"), faiDes);
|
||||||
ar.b(R("dHide", "DHide"), dHide);
|
ar.b(R("dHide", "DHide"), dHide);
|
||||||
ar.i32(R("dWep", "DWep"), dWep);
|
ar.i32(R("dWep", "DWep"), dWep);
|
||||||
ar.str(R("dName", "DName"), dName);
|
ar.str(R("dName", "DName"), dName);
|
||||||
ar.rest(sections);
|
ar.repeat("DSec", sections, [](Ar& a, DesignSection& e) { a.obj(A("DSec"), e); });
|
||||||
|
ar.i32(A("Dtc"), dtc);
|
||||||
|
ar.i32(A("Dwgv"), dwgv);
|
||||||
|
ar.rest(extra);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -828,9 +903,334 @@ struct NexpEntry {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- bodies typed from the recovered wire schema ---------------------------------
|
||||||
|
// Each of these was an `ar.any` blob until the serializer recovery named its items.
|
||||||
|
// The class each shape corresponds to is in the comment; tests/mars_stream/test_wire_schema.cpp
|
||||||
|
// checks the field list against include/generated/sots_stream_schema.h.
|
||||||
|
|
||||||
|
struct EventRec { // Game::EventStorage::Event
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
int32_t evEID = 0;
|
||||||
|
std::string evDsc, evMsg, evImg;
|
||||||
|
int32_t evLoc = 0;
|
||||||
|
Vec3 evPos;
|
||||||
|
int32_t evAct = 0, evCID = 0;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("EvEID"), evEID);
|
||||||
|
ar.str(A("EvDsc"), evDsc);
|
||||||
|
ar.str(A("EvMsg"), evMsg);
|
||||||
|
ar.str(A("EvImg"), evImg);
|
||||||
|
ar.i32(A("EvLoc"), evLoc);
|
||||||
|
ar.vec3(A("EvPos"), evPos);
|
||||||
|
ar.i32(A("EvAct"), evAct);
|
||||||
|
ar.i32(A("EvCID"), evCID);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct TurnEvents { // Game::EventStorage::TurnEvents
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
int32_t evTurn = 0;
|
||||||
|
std::vector<EventRec> events;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("EvTurn"), evTurn);
|
||||||
|
ar.carr(A("Events"), events);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct EventStorage { // Game::EventStorage
|
||||||
|
static constexpr const char* kStreamName = "Events";
|
||||||
|
int32_t evNxID = 0;
|
||||||
|
std::vector<TurnEvents> turns;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("EvNxID"), evNxID);
|
||||||
|
ar.carr(A("Events"), turns);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct FleetNameGen { // Game::FleetNameGenerator
|
||||||
|
static constexpr const char* kStreamName = "FNG";
|
||||||
|
int32_t fngNum = 0;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("FNGNum"), fngNum);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct SpeciesRatios { // Game::SpeciesRatios
|
||||||
|
static constexpr const char* kStreamName = "spe";
|
||||||
|
// `nv` is the count, not a field: the recovery marks sp and va2 as loop-body
|
||||||
|
// writes, and the saves agree -- nv==1 frames carry one (sp, va2) pair and
|
||||||
|
// nv==0 frames carry nothing at all.
|
||||||
|
struct Entry {
|
||||||
|
int32_t sp = 0, va2 = 0;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("sp"), sp);
|
||||||
|
ar.i32(A("va2"), va2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::vector<Entry> ratios;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.narr(A("nv"), ratios, [](Ar& a, Entry& e) { e.io(a); });
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct CivilianRatios { // Game::CivilianRatios
|
||||||
|
static constexpr const char* kStreamName = "civr";
|
||||||
|
float smx = 0;
|
||||||
|
SpeciesRatios spe;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.f32(A("smx"), smx);
|
||||||
|
ar.obj(A("spe"), spe);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct TechNode { // one entry of Game::TechTree: a name and its branch names
|
||||||
|
int32_t numBrs = 0;
|
||||||
|
std::string tNm;
|
||||||
|
std::vector<std::string> branches;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.str(A("TNm"), tNm);
|
||||||
|
ar.narr(A("NumBrs"), branches, [](Ar& a, std::string& e) { a.str(A("TNm"), e); });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct TechState { // per-tech state, second NumTechs section of Game::TechTree
|
||||||
|
std::string tNm;
|
||||||
|
int32_t st = 0, tResCost = 0, tResDone = 0, tAcq = 0, tiAcq = 0, tbd = 0;
|
||||||
|
bool tfc = false;
|
||||||
|
int32_t tUnlck = 0;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.str(A("TNm"), tNm);
|
||||||
|
ar.i32(A("St"), st);
|
||||||
|
ar.i32(A("TResCost"), tResCost);
|
||||||
|
ar.i32(A("TResDone"), tResDone);
|
||||||
|
ar.i32(A("TAcq"), tAcq);
|
||||||
|
ar.i32(A("TiAcq"), tiAcq);
|
||||||
|
ar.i32(A("Tbd"), tbd);
|
||||||
|
ar.b(A("Tfc"), tfc);
|
||||||
|
ar.i32(A("TUnlck"), tUnlck);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct TechTree { // Game::TechTree
|
||||||
|
static constexpr const char* kStreamName = "TechTree";
|
||||||
|
// TechTree::Write emits *two* NumTechs-counted sections: the tree topology
|
||||||
|
// (each tech's name and its branch names) and then the per-tech state. The
|
||||||
|
// recovery sees only the first under Game::TechTree; the second section's
|
||||||
|
// field list and types are the ones Game::SpyReportTechTree names -- the spy
|
||||||
|
// report streams the same per-tech record -- which is where Tfc being a bool
|
||||||
|
// rather than an int comes from.
|
||||||
|
std::vector<TechNode> techs;
|
||||||
|
std::vector<TechState> state;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.narr(A("NumTechs"), techs, [](Ar& a, TechNode& e) { e.io(a); });
|
||||||
|
ar.narr(A("NumTechs"), state, [](Ar& a, TechState& e) { e.io(a); });
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct ShipRecord { // one entry of Game::ShipRecords
|
||||||
|
int32_t srb = 0, srl = 0, srk = 0, sri = 0;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("srb"), srb);
|
||||||
|
ar.i32(A("srl"), srl);
|
||||||
|
ar.i32(A("srk"), srk);
|
||||||
|
ar.i32(A("sri"), sri);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct DesignRecord { // second section of Game::ShipRecords -- note: no srk
|
||||||
|
int32_t srd = 0, src = 0, srb = 0, srl = 0, sri = 0;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("srd"), srd);
|
||||||
|
ar.i32(A("src"), src);
|
||||||
|
ar.i32(A("srb"), srb);
|
||||||
|
ar.i32(A("srl"), srl);
|
||||||
|
ar.i32(A("sri"), sri);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct ShipRecords { // Game::ShipRecords
|
||||||
|
static constexpr const char* kStreamName = "ShipRecs";
|
||||||
|
// Two counted sections. `srbd` is the second COUNT, not a field: the recovery
|
||||||
|
// lists srd/src/srb/srl/sri right after it, and the saves confirm srbd records
|
||||||
|
// follow (srbd is 0, 1, 3 or 4 across the players available).
|
||||||
|
std::vector<ShipRecord> recs;
|
||||||
|
std::vector<DesignRecord> designs;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.narr(A("srnc"), recs, [](Ar& a, ShipRecord& e) { e.io(a); });
|
||||||
|
ar.narr(A("srbd"), designs, [](Ar& a, DesignRecord& e) { e.io(a); });
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct AIEncounterFlags { // Game::AIEncounterFlags
|
||||||
|
static constexpr const char* kStreamName = "AIEnf";
|
||||||
|
struct Entry {
|
||||||
|
int32_t fid = 0, ast = 0;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("Fid"), fid);
|
||||||
|
ar.i32(A("Ast"), ast);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::vector<Entry> entries;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.narr(A("Nas"), entries, [](Ar& a, Entry& e) { e.io(a); });
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct PlayerAid { // Game::PlayerAid, one element of the "aid" array
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
int32_t aidto = 0, aidsa = 0, aidst = 0, aidrai = 0, aidrt = 0;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("aidto"), aidto);
|
||||||
|
ar.i32(A("aidsa"), aidsa);
|
||||||
|
ar.i32(A("aidst"), aidst);
|
||||||
|
ar.i32(A("aidrai"), aidrai);
|
||||||
|
ar.i32(A("aidrt"), aidrt);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct CommMessages { // Game::CommMessageContainer
|
||||||
|
static constexpr const char* kStreamName = "comms";
|
||||||
|
struct Entry {
|
||||||
|
int32_t msgt = 0;
|
||||||
|
Node msg; // Game::ICommMessage: polymorphic, the subtype is msgt
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("msgt"), msgt);
|
||||||
|
ar.any(A("msg"), msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::vector<Entry> msgs;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.narr(A("nmsg"), msgs, [](Ar& a, Entry& e) { e.io(a); });
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct SpyReport { // Game::SpyReport: four counted lists, one per report kind
|
||||||
|
static constexpr const char* kStreamName = "spy2";
|
||||||
|
std::vector<Node> defences, trade, events, techTree;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
// Every count is 0 in every save available, so the element shapes named by
|
||||||
|
// the recovery (SpyReportDefences / …Trade / …Events / …TechTree) are
|
||||||
|
// unverified; the counts and the tags of the element frames are not.
|
||||||
|
ar.narr(A("defc2"), defences, [](Ar& a, Node& e) { a.any(A("def"), e); });
|
||||||
|
ar.narr(A("rtc"), trade, [](Ar& a, Node& e) { a.any(A("strd"), e); });
|
||||||
|
ar.narr(A("evc"), events, [](Ar& a, Node& e) { a.any(A("evs"), e); });
|
||||||
|
ar.narr(A("ttc"), techTree, [](Ar& a, Node& e) { a.any(A("tt"), e); });
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct TradeSector { // Game::ServerTradeSector
|
||||||
|
static constexpr const char* kStreamName = "Trade";
|
||||||
|
Vec3 pos, ctr;
|
||||||
|
int32_t gridID = 0, tssec = 0, tsct = 0, tscr = 0, ptssec = 0, ptsct = 0, ptscr = 0;
|
||||||
|
std::vector<Node> fwarn; // ServerTradeSector::FreighterWarning; empty in every save available
|
||||||
|
std::vector<int32_t> systems, fleets;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.vec3(A("Pos"), pos);
|
||||||
|
ar.i32(A("tsgridID"), gridID);
|
||||||
|
ar.vec3(A("tsctr"), ctr);
|
||||||
|
ar.i32(A("tssec"), tssec);
|
||||||
|
ar.i32(A("tsct"), tsct);
|
||||||
|
ar.i32(A("tscr"), tscr);
|
||||||
|
ar.i32(A("ptssec"), ptssec);
|
||||||
|
ar.i32(A("ptsct"), ptsct);
|
||||||
|
ar.i32(A("ptscr"), ptscr);
|
||||||
|
ar.carr(A("fwarn"), fwarn);
|
||||||
|
ar.narr(A("tsnumsys"), systems, [](Ar& a, int32_t& e) { a.i32(A("tssys"), e); });
|
||||||
|
ar.narr(A("tsnumflt"), fleets, [](Ar& a, int32_t& e) { a.i32(A("tsflt"), e); });
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct TradeManager { // Game::ServerTradeManagerImpl
|
||||||
|
// The `trdmgr` member is declared as Game::ServerTradeManager, whose Read and
|
||||||
|
// Write really are the inherited no-op -- the campaign recorded that as an open
|
||||||
|
// trap. The call is virtual: the object is a ServerTradeManagerImpl, and *that*
|
||||||
|
// class has a real serializer, which is where this field list comes from. The
|
||||||
|
// same shape resolves Game::IServerSpyManager -> Game::ServerSpyManager below.
|
||||||
|
static constexpr const char* kStreamName = "trdmgr";
|
||||||
|
struct Entry {
|
||||||
|
int32_t tradeID = 0;
|
||||||
|
TradeSector trade;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("TradeID"), tradeID);
|
||||||
|
ar.obj(A("Trade"), trade);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::vector<Entry> sectors;
|
||||||
|
float sctSize = 0;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.narr(A("NumTradeSectors"), sectors, [](Ar& a, Entry& e) { e.io(a); });
|
||||||
|
ar.f32(A("SctSize"), sctSize);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct SpyManager { // Game::ServerSpyManager
|
||||||
|
static constexpr const char* kStreamName = "spymgr";
|
||||||
|
int32_t xsid = 0;
|
||||||
|
std::vector<Node> spies; // Game::SpyCraft frames; nspy is 0 in every save available
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("xsid"), xsid);
|
||||||
|
ar.narr(A("nspy"), spies, [](Ar& a, Node& e) { a.any(A("spy"), e); });
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct ProjectName { // one entry of Game::SpecialProjectNameGen
|
||||||
|
std::vector<Node> used; // usnc is 0 in every save available
|
||||||
|
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.str(A("Nm"), nm);
|
||||||
|
ar.str(A("Ntg"), ntg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
struct ProjectNames { // Game::SpecialProjectNameGen
|
||||||
|
static constexpr const char* kStreamName = "sprjs";
|
||||||
|
std::vector<ProjectName> names;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.narr(A("NNms2"), names, [](Ar& a, ProjectName& e) { e.io(a); });
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
struct Player {
|
struct Player {
|
||||||
static constexpr const char* kStreamName = "Player";
|
static constexpr const char* kStreamName = "Player";
|
||||||
Node techTree;
|
TechTree techTree;
|
||||||
int32_t homeSys = 0, plyrIdx = 0;
|
int32_t homeSys = 0, plyrIdx = 0;
|
||||||
std::string plryName;
|
std::string plryName;
|
||||||
int32_t species = 0;
|
int32_t species = 0;
|
||||||
|
|
@ -853,18 +1253,20 @@ struct Player {
|
||||||
bool elim = false, npc = false, rebAI = false, reqCL = false;
|
bool elim = false, npc = false, rebAI = false, reqCL = false;
|
||||||
Alliances alliances;
|
Alliances alliances;
|
||||||
int32_t hasVac = 0, hasImm = 0, npTrk = 0, hasDisc = 0, hasDiscSp = 0, hasDiscCl = 0, hasEnc = 0, hasEng = 0;
|
int32_t hasVac = 0, hasImm = 0, npTrk = 0, hasDisc = 0, hasDiscSp = 0, hasDiscCl = 0, hasEnc = 0, hasEng = 0;
|
||||||
Node events, fng;
|
EventStorage events;
|
||||||
|
FleetNameGen fng;
|
||||||
int32_t pvSav = 0;
|
int32_t pvSav = 0;
|
||||||
bool pvMA = false, aiBn = false, cnTrd = false, cnRad = false, hgs = false, hadvs = false, harcc = false,
|
bool pvMA = false, aiBn = false, cnTrd = false, cnRad = false, hgs = false, hadvs = false, harcc = false,
|
||||||
cnVItl = false;
|
cnVItl = false;
|
||||||
float pddm = 0;
|
float pddm = 0;
|
||||||
int32_t bnkWrn = 0, bnkTrn = 0, bnkPr = 0, bnkEl = 0;
|
int32_t bnkWrn = 0, bnkTrn = 0, bnkPr = 0, bnkEl = 0;
|
||||||
Node shipRecs;
|
ShipRecords shipRecs;
|
||||||
int32_t nextPrjID = 0, plcy = 0;
|
int32_t nextPrjID = 0, plcy = 0;
|
||||||
std::string pswd;
|
std::string pswd;
|
||||||
int32_t lret = 0, nmeid = 0;
|
int32_t lret = 0, nmeid = 0;
|
||||||
bool cdp = false;
|
bool cdp = false;
|
||||||
Node spy2, civr;
|
SpyReport spy2;
|
||||||
|
CivilianRatios civr;
|
||||||
int32_t aidf = 0;
|
int32_t aidf = 0;
|
||||||
bool srn = false;
|
bool srn = false;
|
||||||
int32_t srnTo = 0, lboid = 0;
|
int32_t srnTo = 0, lboid = 0;
|
||||||
|
|
@ -881,25 +1283,25 @@ struct Player {
|
||||||
bool hasAIR = false;
|
bool hasAIR = false;
|
||||||
Node air;
|
Node air;
|
||||||
bool cta = false;
|
bool cta = false;
|
||||||
Node aiEnf;
|
AIEncounterFlags aiEnf;
|
||||||
std::vector<SprjEntry> specialProjects;
|
std::vector<SprjEntry> specialProjects;
|
||||||
std::vector<NexpEntry> nexp;
|
std::vector<NexpEntry> nexp;
|
||||||
std::vector<int32_t> weapXcl;
|
std::vector<int32_t> weapXcl;
|
||||||
Node ojvs;
|
std::vector<Node> ojvs; // Game::Objective elements; the count is 0 in every save available
|
||||||
std::vector<DipStat> dipstats;
|
std::vector<DipStat> dipstats;
|
||||||
Node comms;
|
CommMessages comms;
|
||||||
std::vector<Prep> preps;
|
std::vector<Prep> preps;
|
||||||
std::vector<Odes> odes;
|
std::vector<Odes> odes;
|
||||||
std::vector<Owep> owep;
|
std::vector<Owep> owep;
|
||||||
std::vector<Otch> otch;
|
std::vector<Otch> otch;
|
||||||
Node aid;
|
std::vector<PlayerAid> aid;
|
||||||
std::vector<Node> defLayouts, raidTargets;
|
std::vector<Node> defLayouts, raidTargets;
|
||||||
int32_t tnc = 0;
|
int32_t tnc = 0;
|
||||||
std::vector<Node> extra;
|
std::vector<Node> extra;
|
||||||
|
|
||||||
template <class Ar>
|
template <class Ar>
|
||||||
void io(Ar& ar) {
|
void io(Ar& ar) {
|
||||||
ar.any(A("TechTree"), techTree);
|
ar.obj(A("TechTree"), techTree);
|
||||||
ar.i32(A("HomeSys"), homeSys);
|
ar.i32(A("HomeSys"), homeSys);
|
||||||
ar.i32(A("PlyrIdx"), plyrIdx);
|
ar.i32(A("PlyrIdx"), plyrIdx);
|
||||||
ar.str(A("PlryName"), plryName);
|
ar.str(A("PlryName"), plryName);
|
||||||
|
|
@ -951,8 +1353,8 @@ struct Player {
|
||||||
ar.i32(A("HasDiscCl"), hasDiscCl);
|
ar.i32(A("HasDiscCl"), hasDiscCl);
|
||||||
ar.i32(A("HasEnc"), hasEnc);
|
ar.i32(A("HasEnc"), hasEnc);
|
||||||
ar.i32(A("HasEng"), hasEng);
|
ar.i32(A("HasEng"), hasEng);
|
||||||
ar.any(A("Events"), events);
|
ar.obj(A("Events"), events);
|
||||||
ar.any(A("FNG"), fng);
|
ar.obj(A("FNG"), fng);
|
||||||
ar.i32(A("PvSav"), pvSav);
|
ar.i32(A("PvSav"), pvSav);
|
||||||
ar.b(A("PvMA"), pvMA);
|
ar.b(A("PvMA"), pvMA);
|
||||||
ar.b(A("AIBn"), aiBn);
|
ar.b(A("AIBn"), aiBn);
|
||||||
|
|
@ -967,15 +1369,15 @@ struct Player {
|
||||||
ar.i32(A("BnkTrn"), bnkTrn);
|
ar.i32(A("BnkTrn"), bnkTrn);
|
||||||
ar.i32(A("BnkPr"), bnkPr);
|
ar.i32(A("BnkPr"), bnkPr);
|
||||||
ar.i32(A("BnkEl"), bnkEl);
|
ar.i32(A("BnkEl"), bnkEl);
|
||||||
ar.any(A("ShipRecs"), shipRecs);
|
ar.obj(A("ShipRecs"), shipRecs);
|
||||||
ar.i32(A("NextPrjID"), nextPrjID);
|
ar.i32(A("NextPrjID"), nextPrjID);
|
||||||
ar.i32(A("plcy"), plcy);
|
ar.i32(A("plcy"), plcy);
|
||||||
ar.str(A("pswd"), pswd);
|
ar.str(A("pswd"), pswd);
|
||||||
ar.i32(A("lret"), lret);
|
ar.i32(A("lret"), lret);
|
||||||
ar.i32(A("nmeid"), nmeid);
|
ar.i32(A("nmeid"), nmeid);
|
||||||
ar.b(A("cdp"), cdp);
|
ar.b(A("cdp"), cdp);
|
||||||
ar.any(A("spy2"), spy2);
|
ar.obj(A("spy2"), spy2);
|
||||||
ar.any(A("civr"), civr);
|
ar.obj(A("civr"), civr);
|
||||||
ar.i32(A("aidf"), aidf);
|
ar.i32(A("aidf"), aidf);
|
||||||
ar.b(A("Srn"), srn);
|
ar.b(A("Srn"), srn);
|
||||||
ar.i32(A("SrnTo"), srnTo);
|
ar.i32(A("SrnTo"), srnTo);
|
||||||
|
|
@ -993,18 +1395,18 @@ struct Player {
|
||||||
ar.b(A("HasAIR"), hasAIR);
|
ar.b(A("HasAIR"), hasAIR);
|
||||||
ar.when(hasAIR, [&](Ar& a) { a.any(A("AIR"), air); });
|
ar.when(hasAIR, [&](Ar& a) { a.any(A("AIR"), air); });
|
||||||
ar.b(A("cta"), cta);
|
ar.b(A("cta"), cta);
|
||||||
ar.any(A("AIEnf"), aiEnf);
|
ar.obj(A("AIEnf"), aiEnf);
|
||||||
ar.narr(A("NSprj"), specialProjects, [](Ar& a, SprjEntry& e) { e.io(a); });
|
ar.narr(A("NSprj"), specialProjects, [](Ar& a, SprjEntry& e) { e.io(a); });
|
||||||
ar.narr(A("Nexp"), nexp, [](Ar& a, NexpEntry& e) { e.io(a); });
|
ar.narr(A("Nexp"), nexp, [](Ar& a, NexpEntry& e) { e.io(a); });
|
||||||
ar.narr(A("NWeapXcl"), weapXcl, [](Ar& a, int32_t& e) { a.i32(A("WeapXcl"), e); });
|
ar.narr(A("NWeapXcl"), weapXcl, [](Ar& a, int32_t& e) { a.i32(A("WeapXcl"), e); });
|
||||||
ar.any(A("Ojvs"), ojvs);
|
ar.carr(A("Ojvs"), ojvs);
|
||||||
ar.carr(A("dipstats"), dipstats);
|
ar.carr(A("dipstats"), dipstats);
|
||||||
ar.any(A("comms"), comms);
|
ar.obj(A("comms"), comms);
|
||||||
ar.carr(A("preps"), preps);
|
ar.carr(A("preps"), preps);
|
||||||
ar.carr(A("odes"), odes);
|
ar.carr(A("odes"), odes);
|
||||||
ar.carr(A("owep"), owep);
|
ar.carr(A("owep"), owep);
|
||||||
ar.carr(A("otch"), otch);
|
ar.carr(A("otch"), otch);
|
||||||
ar.any(A("aid"), aid);
|
ar.carr(A("aid"), aid);
|
||||||
ar.narr(A("ndeflay"), defLayouts, [](Ar& a, Node& e) { a.any(A("deflay"), e); });
|
ar.narr(A("ndeflay"), defLayouts, [](Ar& a, Node& e) { a.any(A("deflay"), e); });
|
||||||
ar.narr(A("rdtc"), raidTargets, [](Ar& a, Node& e) { a.any(A("rdt"), e); });
|
ar.narr(A("rdtc"), raidTargets, [](Ar& a, Node& e) { a.any(A("rdt"), e); });
|
||||||
ar.i32(A("tnc"), tnc);
|
ar.i32(A("tnc"), tnc);
|
||||||
|
|
@ -1467,7 +1869,7 @@ struct Sim {
|
||||||
int32_t npcm = 0, npco = 0, npci = 0, npcv = 0, npca = 0;
|
int32_t npcm = 0, npco = 0, npci = 0, npcv = 0, npca = 0;
|
||||||
std::optional<int32_t> npc;
|
std::optional<int32_t> npc;
|
||||||
float szadj = 0, rsadj = 0, suadj = 0;
|
float szadj = 0, rsadj = 0, suadj = 0;
|
||||||
Node sprjs;
|
ProjectNames sprjs;
|
||||||
float randEncAdj = 0;
|
float randEncAdj = 0;
|
||||||
int32_t cmbtid = 0;
|
int32_t cmbtid = 0;
|
||||||
TurnStats turnstats;
|
TurnStats turnstats;
|
||||||
|
|
@ -1478,7 +1880,8 @@ struct Sim {
|
||||||
std::vector<Species> species;
|
std::vector<Species> species;
|
||||||
std::vector<SysEntry> systems;
|
std::vector<SysEntry> systems;
|
||||||
NodeGrid ndGr2;
|
NodeGrid ndGr2;
|
||||||
Node trdmgr, spymgr;
|
TradeManager trdmgr;
|
||||||
|
SpyManager spymgr;
|
||||||
std::vector<FleetEntry> fleets;
|
std::vector<FleetEntry> fleets;
|
||||||
std::vector<int32_t> acts;
|
std::vector<int32_t> acts;
|
||||||
std::optional<Node> svSctOb;
|
std::optional<Node> svSctOb;
|
||||||
|
|
@ -1523,7 +1926,7 @@ struct Sim {
|
||||||
ar.f32(A("szadj"), szadj);
|
ar.f32(A("szadj"), szadj);
|
||||||
ar.f32(A("rsadj"), rsadj);
|
ar.f32(A("rsadj"), rsadj);
|
||||||
ar.f32(A("suadj"), suadj);
|
ar.f32(A("suadj"), suadj);
|
||||||
ar.any(A("sprjs"), sprjs);
|
ar.obj(A("sprjs"), sprjs);
|
||||||
ar.f32(A("RandEncAdj"), randEncAdj);
|
ar.f32(A("RandEncAdj"), randEncAdj);
|
||||||
ar.i32(A("cmbtid"), cmbtid);
|
ar.i32(A("cmbtid"), cmbtid);
|
||||||
ar.obj(A("turnstats"), turnstats);
|
ar.obj(A("turnstats"), turnstats);
|
||||||
|
|
@ -1543,8 +1946,8 @@ struct Sim {
|
||||||
ar.repeat("ISsp", species, [](Ar& a, Species& e) { e.io(a); }); // 7 pairs, no count
|
ar.repeat("ISsp", species, [](Ar& a, Species& e) { e.io(a); }); // 7 pairs, no count
|
||||||
ar.narr(A("NumSys"), systems, [](Ar& a, SysEntry& e) { e.io(a); });
|
ar.narr(A("NumSys"), systems, [](Ar& a, SysEntry& e) { e.io(a); });
|
||||||
ar.obj(A("NdGr2"), ndGr2);
|
ar.obj(A("NdGr2"), ndGr2);
|
||||||
ar.any(A("trdmgr"), trdmgr);
|
ar.obj(A("trdmgr"), trdmgr);
|
||||||
ar.any(A("spymgr"), spymgr);
|
ar.obj(A("spymgr"), spymgr);
|
||||||
ar.narr(A("NumFlts"), fleets, [](Ar& a, FleetEntry& e) { e.io(a); });
|
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.narr(A("NumActs"), acts, [](Ar& a, int32_t& e) { a.i32(A("Act"), e); });
|
||||||
ar.opt_any(A("SvSctOb"), svSctOb); // only if the pointer was non-NULL
|
ar.opt_any(A("SvSctOb"), svSctOb); // only if the pointer was non-NULL
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,12 @@ foreach(_t mars_stream_test_rng mars_stream_test_stream mars_stream_test_save)
|
||||||
target_include_directories(${_t} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(${_t} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_compile_options(${_t} PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(${_t} PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
# Conformance of the typed shapes against the wire schema recovered from the game's
|
||||||
|
# serializers (include/generated/sots_stream_schema.h). No game data: the generated
|
||||||
|
# table plus shapes.h, so this one always runs.
|
||||||
|
add_executable(mars_stream_test_wire_schema test_wire_schema.cpp)
|
||||||
|
target_link_libraries(mars_stream_test_wire_schema PRIVATE mars_stream sots_addresses)
|
||||||
|
add_test(NAME mars_stream_wire_schema COMMAND mars_stream_test_wire_schema)
|
||||||
|
target_include_directories(mars_stream_test_wire_schema PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_compile_options(mars_stream_test_wire_schema PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
#include "mars/rng/mt19937.h"
|
#include "mars/rng/mt19937.h"
|
||||||
#include "mars/stream/dump.h"
|
#include "mars/stream/dump.h"
|
||||||
|
#include "mars/stream/probe.h"
|
||||||
#include "mars/stream/save.h"
|
#include "mars/stream/save.h"
|
||||||
|
|
||||||
using namespace mars::stream;
|
using namespace mars::stream;
|
||||||
|
|
@ -104,6 +105,27 @@ static void check_save(const std::string& path, const char* dump_dir) {
|
||||||
CHECK(twists >= 0);
|
CHECK(twists >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- coverage: what the shapes understand vs what a Node merely carries ---------
|
||||||
|
// The round trip below is byte-identical either way, because an opaque Node is
|
||||||
|
// copied verbatim. This is the number that actually moves when a body gets typed.
|
||||||
|
{
|
||||||
|
CoverageArchive cov;
|
||||||
|
doc.game.io(cov);
|
||||||
|
size_t total = cov.typed + cov.opaque;
|
||||||
|
double pct = total ? 100.0 * double(cov.typed) / double(total) : 0.0;
|
||||||
|
std::printf(" coverage: %zu typed, %zu opaque (%.1f%% of %u stream items typed)\n", cov.typed,
|
||||||
|
cov.opaque, pct, doc.stats.items);
|
||||||
|
std::vector<std::pair<size_t, std::string>> worst;
|
||||||
|
for (const auto& kv : cov.opaque_by_tag) worst.emplace_back(kv.second, kv.first);
|
||||||
|
std::sort(worst.rbegin(), worst.rend());
|
||||||
|
std::printf(" still opaque:");
|
||||||
|
for (size_t i = 0; i < worst.size() && i < 8; ++i)
|
||||||
|
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 >= 95.0);
|
||||||
|
}
|
||||||
|
|
||||||
// --- round trips ------------------------------------------------------------------
|
// --- round trips ------------------------------------------------------------------
|
||||||
Bytes tree_bytes = write_tree(doc.tree);
|
Bytes tree_bytes = write_tree(doc.tree);
|
||||||
CHECK(tree_bytes == doc.inflated);
|
CHECK(tree_bytes == doc.inflated);
|
||||||
|
|
|
||||||
245
tests/mars_stream/test_wire_schema.cpp
Normal file
245
tests/mars_stream/test_wire_schema.cpp
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
// Conformance: every typed shape in shapes.h vs the wire schema recovered from
|
||||||
|
// the game's own Mars::IStreamable serializers (include/generated/sots_stream_schema.h).
|
||||||
|
//
|
||||||
|
// Why a check and not a generator. The recovered table is a *specification*, not
|
||||||
|
// a program: the recovery is a linear pass over the game's Write and cannot see
|
||||||
|
// Write's branches, so a conditional field (StarShip's BQ2, gated by hbq) is
|
||||||
|
// listed unconditionally and the sequence is a superset of any single record.
|
||||||
|
// Container loops are flattened the same way. A codec driven straight off the
|
||||||
|
// table would desynchronise on the first branch. The hand-written io() shapes
|
||||||
|
// stay the codec — they can express the conditionals and the nesting that the
|
||||||
|
// binary facts cannot supply — and this test is what proves they agree with the
|
||||||
|
// binary, item for item, in order.
|
||||||
|
//
|
||||||
|
// SchemaProbe runs each io() with every branch taken, which is the same "all
|
||||||
|
// branches" view the recovery has, so the two sequences are comparable. They
|
||||||
|
// are aligned with an LCS and three numbers come out:
|
||||||
|
//
|
||||||
|
// matched the shape and the binary agree on tag and on-disk primitive
|
||||||
|
// MISMATCH same tag, different primitive — a real bug, and a hard failure
|
||||||
|
// wire-only the binary writes an item the shape does not name: either a
|
||||||
|
// conditional the shape models with opt_*/when, or genuine
|
||||||
|
// coverage debt
|
||||||
|
// shape-only the shape names an item the recovery did not resolve
|
||||||
|
//
|
||||||
|
// Opaque items (ar.any / ar.raw_frame — a body carried as a Node) are counted
|
||||||
|
// separately: they round-trip byte-for-byte but are not understood, and the
|
||||||
|
// count is this codebase's honest coverage number.
|
||||||
|
//
|
||||||
|
// No game data, no saves: the whole test is the generated table plus shapes.h.
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "generated/sots_stream_schema.h"
|
||||||
|
#include "mars/stream/probe.h"
|
||||||
|
#include "mars/stream/shapes.h"
|
||||||
|
|
||||||
|
using namespace mars::stream;
|
||||||
|
namespace sh = mars::stream::shapes;
|
||||||
|
|
||||||
|
static int fails = 0;
|
||||||
|
#define CHECK(cond) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) { \
|
||||||
|
std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||||
|
++fails; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
// --- primitive compatibility -------------------------------------------------
|
||||||
|
// The generated Prim is the DISK type: a member the original holds as int16 or
|
||||||
|
// int8 is written by Stream::WriteInt and is I32 on the wire, so there is no
|
||||||
|
// narrow integer to reconcile here.
|
||||||
|
static bool prim_ok(SchemaProbe::P got, sots::wire::Prim want) {
|
||||||
|
using P = SchemaProbe::P;
|
||||||
|
using W = sots::wire::Prim;
|
||||||
|
if (want == W::Unknown) return true; // recovery could not type it
|
||||||
|
switch (got) {
|
||||||
|
case P::Unknown: return true; // opaque on our side: any disk type fits
|
||||||
|
case P::I32: return want == W::I32;
|
||||||
|
case P::I64: return want == W::I64;
|
||||||
|
case P::F32: return want == W::F32;
|
||||||
|
case P::Bool: return want == W::Bool;
|
||||||
|
case P::Str: return want == W::Str;
|
||||||
|
case P::Frame: return want == W::Frame;
|
||||||
|
case P::Raw: return want == W::Raw || want == W::I64;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Result {
|
||||||
|
int matched = 0, mismatch = 0, wire_only = 0, shape_only = 0;
|
||||||
|
int opaque = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// LCS alignment on (tag, compatible primitive). Sequences are short (<= 120).
|
||||||
|
static Result align(const std::vector<SchemaProbe::Item>& a, const sots::wire::Class& c, bool verbose) {
|
||||||
|
const size_t n = a.size(), m = c.count;
|
||||||
|
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(m + 1, 0));
|
||||||
|
auto eq = [&](size_t i, size_t j) {
|
||||||
|
return a[i].tag == c.fields[j].tag && prim_ok(a[i].prim, c.fields[j].prim);
|
||||||
|
};
|
||||||
|
for (size_t i = n; i-- > 0;)
|
||||||
|
for (size_t j = m; j-- > 0;)
|
||||||
|
dp[i][j] = eq(i, j) ? dp[i + 1][j + 1] + 1 : std::max(dp[i + 1][j], dp[i][j + 1]);
|
||||||
|
|
||||||
|
Result r;
|
||||||
|
for (const SchemaProbe::Item& it : a) r.opaque += it.opaque;
|
||||||
|
size_t i = 0, j = 0;
|
||||||
|
while (i < n && j < m) {
|
||||||
|
if (eq(i, j)) {
|
||||||
|
++r.matched;
|
||||||
|
++i;
|
||||||
|
++j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Same tag, incompatible primitive: not a gap, a disagreement.
|
||||||
|
if (a[i].tag == c.fields[j].tag) {
|
||||||
|
++r.mismatch;
|
||||||
|
std::printf(" MISMATCH %-20s shape %-6s vs wire %-6s\n", a[i].tag.c_str(),
|
||||||
|
probe_prim_name(a[i].prim), sots::wire::prim_name(c.fields[j].prim));
|
||||||
|
++i;
|
||||||
|
++j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (dp[i + 1][j] >= dp[i][j + 1]) {
|
||||||
|
++r.shape_only;
|
||||||
|
if (verbose)
|
||||||
|
std::printf(" shape-only %-20s %s\n", a[i].tag.c_str(), probe_prim_name(a[i].prim));
|
||||||
|
++i;
|
||||||
|
} else {
|
||||||
|
++r.wire_only;
|
||||||
|
if (verbose)
|
||||||
|
std::printf(" wire-only %-20s %s %s\n", c.fields[j].tag,
|
||||||
|
sots::wire::prim_name(c.fields[j].prim),
|
||||||
|
sots::wire::shape_name(c.fields[j].shape));
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (; i < n; ++i) {
|
||||||
|
++r.shape_only;
|
||||||
|
if (verbose) std::printf(" shape-only %-20s %s\n", a[i].tag.c_str(), probe_prim_name(a[i].prim));
|
||||||
|
}
|
||||||
|
for (; j < m; ++j) {
|
||||||
|
++r.wire_only;
|
||||||
|
if (verbose)
|
||||||
|
std::printf(" wire-only %-20s %s %s\n", c.fields[j].tag, sots::wire::prim_name(c.fields[j].prim),
|
||||||
|
sots::wire::shape_name(c.fields[j].shape));
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int total_matched = 0, total_mismatch = 0, total_wire_only = 0, total_opaque = 0;
|
||||||
|
static int bound = 0, unresolved_classes = 0;
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
static void check(const char* shape_name, const char* cls) {
|
||||||
|
const sots::wire::Class* c = sots::wire::find(cls);
|
||||||
|
if (!c) {
|
||||||
|
std::printf(" %-18s -> %-34s NOT IN TABLE\n", shape_name, cls);
|
||||||
|
++fails;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SchemaProbe p = SchemaProbe::of<T>();
|
||||||
|
bool verbose = std::getenv("SOTS_WIRE_VERBOSE") != nullptr;
|
||||||
|
Result r = align(p.items, *c, verbose);
|
||||||
|
++bound;
|
||||||
|
total_matched += r.matched;
|
||||||
|
total_mismatch += r.mismatch;
|
||||||
|
total_wire_only += r.wire_only;
|
||||||
|
total_opaque += r.opaque;
|
||||||
|
std::printf(" %-18s -> %-34s [%-8s %2u/%-2u] shape %2zu wire %2u match %2d"
|
||||||
|
" wire-only %2d shape-only %2d opaque %d\n",
|
||||||
|
shape_name, cls, c->grade, c->read_agree, c->read_comparable, p.items.size(), c->count,
|
||||||
|
r.matched, r.wire_only, r.shape_only, r.opaque);
|
||||||
|
// A tag both sides name, typed differently, is a real disagreement between
|
||||||
|
// our codec and the binary. Nothing else here is allowed to fail the build:
|
||||||
|
// wire-only is conditional-field or coverage debt and is reported, not fatal.
|
||||||
|
CHECK(r.mismatch == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::printf("wire schema: %zu classes, generated from the game's serializers\n\n",
|
||||||
|
sots::wire::kClassCount);
|
||||||
|
|
||||||
|
// --- the table's own integrity -------------------------------------------------
|
||||||
|
for (const sots::wire::Class& c : sots::wire::kClasses) {
|
||||||
|
if (c.count == 0) continue;
|
||||||
|
for (uint16_t k = 0; k < c.count; ++k) unresolved_classes += c.fields[k].unresolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("shape -> class (recovery tier, Read/Write cross-check)\n");
|
||||||
|
// Bindings taken from the campaign's own shape->Write table
|
||||||
|
// (sots-re tools/serializers_golden.py DISK_ORDER, resolved through RTTI),
|
||||||
|
// extended with unambiguous name matches.
|
||||||
|
check<sh::Summary>("Summary", "Game::StrategyGameInfo");
|
||||||
|
check<sh::Slot>("Slot", "Game::SlotDef");
|
||||||
|
check<sh::PlayerSettings>("PlayerSettings", "Game::StrategyPlayerGameSettings");
|
||||||
|
check<sh::PlayerColor>("PlayerColor", "Game::PlayerColorID");
|
||||||
|
check<sh::Session>("Session", "Game::StrategySessionParams");
|
||||||
|
check<sh::CreateParams>("CreateParams", "Game::StrategyGameCreateParams");
|
||||||
|
check<sh::Scrp>("Scrp", "Game::StrategyScriptParams");
|
||||||
|
check<sh::MapP>("MapP", "Game::StarMapParams");
|
||||||
|
check<sh::Planet>("Planet", "Game::SystemParams");
|
||||||
|
check<sh::Sim>("Sim", "Game::StrategyServer");
|
||||||
|
check<sh::Sys>("Sys", "Game::ServerSystem");
|
||||||
|
check<sh::Player>("Player", "Game::ServerPlayer");
|
||||||
|
check<sh::Fleet>("Fleet", "Game::StarFleet");
|
||||||
|
check<sh::Ship>("Ship", "Game::StarShip");
|
||||||
|
check<sh::FlightPlan>("FlightPlan", "Game::FlightPlan");
|
||||||
|
check<sh::Waypoint>("Waypoint", "Game::FlightPlan::Waypoint");
|
||||||
|
check<sh::PrisonerHold>("PrisonerHold", "Game::PrisonerHold");
|
||||||
|
check<sh::Otch>("Otch", "Game::ObservedTech");
|
||||||
|
check<sh::Owep>("Owep", "Game::ObservedWeapon");
|
||||||
|
check<sh::Odes>("Odes", "Game::ObservedDesign");
|
||||||
|
check<sh::Rts>("Rts", "Game::StarSystem::OutputRates");
|
||||||
|
check<sh::PlayerView>("PlayerView", "Game::StarSystem::PlayerView");
|
||||||
|
check<sh::IndependenceInfo>("IndependenceInfo", "Game::IndependenceInfo");
|
||||||
|
check<sh::BuildOrder>("BuildOrder", "Game::ShipBuildOrder");
|
||||||
|
check<sh::BuildQueue>("BuildQueue", "Game::BuildQueue");
|
||||||
|
check<sh::Prep>("Prep", "Game::PlayerReport");
|
||||||
|
check<sh::DipStat>("DipStat", "Game::DiplomacyStats");
|
||||||
|
check<sh::Alliances>("Alliances", "Game::PlayerAlliances");
|
||||||
|
check<sh::NodeRoute>("NodeRoute", "Game::NodeRoute");
|
||||||
|
check<sh::SystemEvent>("SystemEvent", "Game::SystemEvent");
|
||||||
|
check<sh::PlayerTurnStats>("PlayerTurnStats", "Game::PlayerTurnStats");
|
||||||
|
check<sh::PlayerTurnHistory>("PlayerTurnHistory", "Game::PlayerTurnHistory");
|
||||||
|
check<sh::TurnStats>("TurnStats", "Game::GameTurnHistory");
|
||||||
|
check<sh::PopG>("PopG", "Game::PopulationGroup");
|
||||||
|
check<sh::Population>("Population", "Game::Population");
|
||||||
|
check<sh::Morale>("Morale", "Game::Morale");
|
||||||
|
check<sh::MoraleEvent>("MoraleEvent", "Game::MoraleEvent");
|
||||||
|
// Game::SimpleNodePath is a different, 2-item type — it is what MapP.nodePaths
|
||||||
|
// holds (VectorHelper<SimpleNodePath>), and it is empty in every real save.
|
||||||
|
check<sh::NodePath>("NodePath", "Game::NodePath");
|
||||||
|
|
||||||
|
// Bodies typed this round from the recovered schema (previously opaque Nodes).
|
||||||
|
check<sh::EventStorage>("EventStorage", "Game::EventStorage");
|
||||||
|
check<sh::TurnEvents>("TurnEvents", "Game::EventStorage::TurnEvents");
|
||||||
|
check<sh::EventRec>("EventRec", "Game::EventStorage::Event");
|
||||||
|
check<sh::FleetNameGen>("FleetNameGen", "Game::FleetNameGenerator");
|
||||||
|
check<sh::SpeciesRatios>("SpeciesRatios", "Game::SpeciesRatios");
|
||||||
|
check<sh::CivilianRatios>("CivilianRatios", "Game::CivilianRatios");
|
||||||
|
check<sh::TechTree>("TechTree", "Game::TechTree");
|
||||||
|
check<sh::ShipRecords>("ShipRecords", "Game::ShipRecords");
|
||||||
|
check<sh::AIEncounterFlags>("AIEncounterFlags", "Game::AIEncounterFlags");
|
||||||
|
check<sh::PlayerAid>("PlayerAid", "Game::PlayerAid");
|
||||||
|
check<sh::CommMessages>("CommMessages", "Game::CommMessageContainer");
|
||||||
|
check<sh::SpyReport>("SpyReport", "Game::SpyReport");
|
||||||
|
check<sh::SpyManager>("SpyManager", "Game::ServerSpyManager");
|
||||||
|
check<sh::ProjectNames>("ProjectNames", "Game::SpecialProjectNameGen");
|
||||||
|
check<sh::TradeManager>("TradeManager", "Game::ServerTradeManagerImpl");
|
||||||
|
check<sh::TradeSector>("TradeSector", "Game::ServerTradeSector");
|
||||||
|
check<sh::ShipSectionID>("ShipSectionID", "Game::ShipSectionID");
|
||||||
|
check<sh::DesignSection>("DesignSection", "Game::ShipDesignDef::Section");
|
||||||
|
|
||||||
|
std::printf(
|
||||||
|
"\ntotals: %d shapes bound, %d items matched, %d MISMATCH, %d wire-only, "
|
||||||
|
"%d opaque item(s) in bound shapes\n",
|
||||||
|
bound, total_matched, total_mismatch, total_wire_only, total_opaque);
|
||||||
|
std::printf("table: %d item(s) the recovery itself could not type\n", unresolved_classes);
|
||||||
|
std::printf("test_wire_schema: %s (%d failures)\n", fails ? "FAILED" : "ok", fails);
|
||||||
|
return fails ? 1 : 0;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue