merge lane A: StrategyAIAgent blocks typed; named coverage 98.0 -> 99.9%, opaque items 748 -> 37
This commit is contained in:
commit
1df6977808
5 changed files with 511 additions and 10 deletions
|
|
@ -88,6 +88,12 @@ both directions — the reader `select()`s from what it just read, the writer fr
|
||||||
to write — so a body goes back out as whatever it came in as, and a key with no shape falls to a
|
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.
|
generic `Node` and still round-trips.
|
||||||
|
|
||||||
|
The `CD` custom-data blocks are the same problem one level up, and worse: the key is not even
|
||||||
|
adjacent. `CDT` lists the block ids and the `CD` frames follow in that order, so a block's body is
|
||||||
|
selected by the id at the **same ordinal** in a different frame. `SaveGame::io()` walks the two in
|
||||||
|
step and `select()`s from the id's suffix, in both directions; a suffix with no shape (today,
|
||||||
|
`.TurnCommands_v5`) falls to a generic `Node`.
|
||||||
|
|
||||||
Two more things the table's `prim` column will not save you from:
|
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 `bool` and an `int` item are the same size only when the tag length makes the padding agree.**
|
||||||
|
|
@ -96,3 +102,9 @@ Two more things the table's `prim` column will not save you from:
|
||||||
* **An empty string is four zero bytes**, byte-identical to the int 0, so a string field that is
|
* **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
|
empty in all available data round-trips perfectly while typed as an int. That is what hid
|
||||||
`Game::SystemParams`'s name field.
|
`Game::SystemParams`'s name field.
|
||||||
|
* **A container whose count is 0 in every save hides its element type completely.** The table names
|
||||||
|
the element class, but the framing is a property of the *helper* that writes it, not of the
|
||||||
|
element: `VectorHelper<Mars::StreamableEnum<T>>` writes each element as a **frame** holding one
|
||||||
|
int, not as a bare int, because `StreamableEnum`'s own writer is a nested object. `SysMem`, `mts`
|
||||||
|
and `nalat` were all typed as int arrays on the strength of the decorated name and all three were
|
||||||
|
wrong in the same way — invisibly, because none of them has ever had an element.
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
// by the game with a NULL name ("." on disk) or carry a reference name whose
|
// 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
|
// 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.
|
// 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
|
// After the AIAgent round only two regions are still carried that way: the RNG
|
||||||
// custom-data blocks, the RNG state block (correctly opaque) and Attrib (an empty
|
// state block (correctly opaque) and the one CD block per save whose CDT id ends
|
||||||
// frame in every save).
|
// ".TurnCommands_v5", which needs a save with issued orders before it can be typed.
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
@ -790,6 +790,20 @@ struct ShipSectionID { // Game::ShipSectionID: two "." ints
|
||||||
ar.rest(extra);
|
ar.rest(extra);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Mars::StreamableEnum<T>: a framed wrapper whose body is a single "." int. It
|
||||||
|
// is how an enum-valued key or element reaches the stream, so it is a frame and
|
||||||
|
// not a bare int -- a distinction our saves cannot show, because every container
|
||||||
|
// that holds one has count 0.
|
||||||
|
struct StreamEnum {
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
int32_t value = 0;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(R("value"), value);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
struct GunWeapon { // DW2: the weapon in a gun bank, named either by id or by name
|
struct GunWeapon { // DW2: the weapon in a gun bank, named either by id or by name
|
||||||
static constexpr const char* kStreamName = "DW2";
|
static constexpr const char* kStreamName = "DW2";
|
||||||
bool bID = false; // true: the weapon is identified by id, false: by family name
|
bool bID = false; // true: the weapon is identified by id, false: by family name
|
||||||
|
|
@ -1990,7 +2004,9 @@ struct SVSOSwarmQueen { // EncID 10
|
||||||
std::vector<int32_t> designIds;
|
std::vector<int32_t> designIds;
|
||||||
std::vector<SVSOSwarmQueenHive> hives;
|
std::vector<SVSOSwarmQueenHive> hives;
|
||||||
std::vector<SVSOSwarmQueenQueen> queens;
|
std::vector<SVSOSwarmQueenQueen> queens;
|
||||||
std::vector<int32_t> sysMem; // VectorHelper<StreamableEnum<uint>>
|
// VectorHelper<StreamableEnum<uint>>: each element is a "." FRAME holding one
|
||||||
|
// "." int, not a bare int. Count 0 in every save, so nothing exercises it.
|
||||||
|
std::vector<StreamEnum> sysMem;
|
||||||
std::vector<Node> extra;
|
std::vector<Node> extra;
|
||||||
template <class Ar>
|
template <class Ar>
|
||||||
void io(Ar& ar) {
|
void io(Ar& ar) {
|
||||||
|
|
@ -2069,7 +2085,7 @@ struct SVSOVonNeumann { // EncID 1
|
||||||
static constexpr const char* kStreamName = "";
|
static constexpr const char* kStreamName = "";
|
||||||
std::vector<VonNeumannDefeat> defeats;
|
std::vector<VonNeumannDefeat> defeats;
|
||||||
int32_t rusav = 0, nenc = 0, nmb = 0, nml = 0, nbb = 0, nbl = 0, nskb = 0, nskl = 0;
|
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>>
|
std::vector<StreamEnum> mts; // VectorHelper<StreamableEnum<uint>>: framed elements
|
||||||
int32_t ntm = 0, ntb = 0;
|
int32_t ntm = 0, ntb = 0;
|
||||||
bool sken = false;
|
bool sken = false;
|
||||||
int32_t skdid = 0, skhid = 0, skfid = 0, sktq = 0, skt = 0, sktc = 0;
|
int32_t skdid = 0, skhid = 0, skfid = 0, sktq = 0, skt = 0, sktc = 0;
|
||||||
|
|
@ -2268,6 +2284,281 @@ struct ScriptObjects { // Game::SVSOSots -- the SvSctOb body
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- the strategy AI agent's cache, one CD block per AI player ------------------
|
||||||
|
//
|
||||||
|
// `CDT` lists the custom-data ids in order and one `CD` frame follows per id. An
|
||||||
|
// id ending in ".AIAgent" selects Game::StrategyAIAgent::Streamable, whose whole
|
||||||
|
// Write is unconditional -- every item below is present in every block. The
|
||||||
|
// shapes here follow that writer item for item; the notes repo carries the
|
||||||
|
// derivation (findings/objects/aiagent-block.md).
|
||||||
|
|
||||||
|
// Game::AttribMap: a string->string map, written as a "." count followed by two
|
||||||
|
// "." strings per entry. Empty in every save we hold, so the pair layout is a
|
||||||
|
// hypothesis from the writer, not something the data exercises.
|
||||||
|
struct AttribMap {
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
struct Entry {
|
||||||
|
std::string key, value;
|
||||||
|
};
|
||||||
|
std::vector<Entry> entries;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.narr(R("n"), entries, [](Ar& a, Entry& e) {
|
||||||
|
a.str(R("key"), e.key);
|
||||||
|
a.str(R("value"), e.value);
|
||||||
|
});
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Game::AIWeightMap<K>: a "." count then, per entry, a "." key frame and a "."
|
||||||
|
// float weight. Four instantiations exist and all four share this wire shape;
|
||||||
|
// only the key body differs (StreamEnum, or ShipSectionID's two ints).
|
||||||
|
template <class Key>
|
||||||
|
struct AIWeightMap {
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
struct Entry {
|
||||||
|
Key key;
|
||||||
|
float weight = 0;
|
||||||
|
};
|
||||||
|
std::vector<Entry> entries;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.narr(R("n"), entries, [](Ar& a, Entry& e) {
|
||||||
|
a.obj(R("key"), e.key);
|
||||||
|
a.f32(R("w"), e.weight);
|
||||||
|
});
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
using AIWeightMapEnum = AIWeightMap<StreamEnum>;
|
||||||
|
using AIWeightMapSection = AIWeightMap<ShipSectionID>;
|
||||||
|
|
||||||
|
struct AISituation { // Game::AISituation
|
||||||
|
static constexpr const char* kStreamName = "AISit";
|
||||||
|
AIWeightMapSection sections;
|
||||||
|
AIWeightMapEnum weaponFamilies;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.obj(A("AISitSecs"), sections);
|
||||||
|
ar.obj(A("AISitWeapFams"), weaponFamilies);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Game::AISystem writes one AISituation and nothing else -- and the situation it
|
||||||
|
// writes is default-constructed at the call, not read from the object, so every
|
||||||
|
// AISys body on disk is an empty pair of weight maps regardless of game state.
|
||||||
|
struct AISystem {
|
||||||
|
static constexpr const char* kStreamName = "AISys";
|
||||||
|
AISituation situation;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.obj(A("AISysSit"), situation);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DesignNameGen { // Game::StrategyAIAgent::DesignNameGen
|
||||||
|
static constexpr const char* kStreamName = "AIDNG";
|
||||||
|
struct Name {
|
||||||
|
ShipSectionID section;
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
std::vector<Name> names; // dnnc counts the (dnid, dnnm) pairs
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.narr(A("dnnc"), names, [](Ar& a, Name& e) {
|
||||||
|
a.obj(A("dnid"), e.section);
|
||||||
|
a.str(A("dnnm"), e.name);
|
||||||
|
});
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AIAutoPeaceRun { // Game::AIAutoPeaceRun
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
int32_t sid = 0, tn0 = 0, tn1 = 0;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("sid"), sid);
|
||||||
|
ar.i32(A("tn0"), tn0);
|
||||||
|
ar.i32(A("tn1"), tn1);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CombatPlayerStats { // Game::CombatPlayerStats
|
||||||
|
static constexpr const char* kStreamName = "CRPlSv2";
|
||||||
|
float rpBon = 0;
|
||||||
|
int32_t rpBonT = 0, savBonus = 0;
|
||||||
|
bool maintHF = false;
|
||||||
|
// Game::TacReport has a computed count and three trailing scalar runs that no
|
||||||
|
// save we hold exercises; its body stays carried rather than typed on a guess.
|
||||||
|
std::vector<Node> tacReports;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.f32(A("RPBon"), rpBon);
|
||||||
|
ar.i32(A("RPBonT"), rpBonT);
|
||||||
|
ar.i32(A("SavBonus"), savBonus);
|
||||||
|
ar.b(A("MaintHF"), maintHF);
|
||||||
|
ar.carr(A("TacReports"), tacReports);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AICombatReport { // Game::AICombatReport
|
||||||
|
static constexpr const char* kStreamName = "CmbR";
|
||||||
|
int32_t crTrn = 0;
|
||||||
|
bool crPce = false;
|
||||||
|
StreamEnum crSys;
|
||||||
|
CombatPlayerStats crPlSv2;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("CRTrn"), crTrn);
|
||||||
|
ar.b(A("CRPce"), crPce);
|
||||||
|
ar.obj(A("CRSys"), crSys);
|
||||||
|
ar.obj(A("CRPlSv2"), crPlSv2);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Game::AIPlayerRequestStamp is a POD reached only through a specialised helper,
|
||||||
|
// so it has no RTTI class and no entry in the wire table; the two named ints come
|
||||||
|
// straight from the helper's own writer.
|
||||||
|
struct AIRequestStamp {
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
int32_t pid = 0, trn = 0;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.i32(A("pid"), pid);
|
||||||
|
ar.i32(A("trn"), trn);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StrategyAIAgent { // Game::StrategyAIAgent::Streamable
|
||||||
|
static constexpr const char* kStreamName = "CD";
|
||||||
|
struct DesignShare { // one entry of the map `dsh` counts
|
||||||
|
int32_t pid = 0;
|
||||||
|
std::vector<int32_t> turns;
|
||||||
|
};
|
||||||
|
struct AISysEntry {
|
||||||
|
int32_t id = 0;
|
||||||
|
AISystem sys;
|
||||||
|
};
|
||||||
|
struct ColonyLoss {
|
||||||
|
int32_t turn = 0, sysID = 0, plID = 0;
|
||||||
|
};
|
||||||
|
struct Provocation {
|
||||||
|
int32_t id = 0;
|
||||||
|
float value = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
AttribMap attrib;
|
||||||
|
AIWeightMapEnum turnPriorities;
|
||||||
|
AISituation situation;
|
||||||
|
AIWeightMapEnum playerHate;
|
||||||
|
std::vector<AIRequestStamp> requestStamps;
|
||||||
|
// `dsh` is the _Mysize of the map the (pid, trns) pairs are walked out of, so
|
||||||
|
// it is the element count and not a scalar field.
|
||||||
|
std::vector<DesignShare> designShares;
|
||||||
|
std::vector<IntPair> borderStability; // NBStab x (BStabPl, BStabTn)
|
||||||
|
std::vector<IntPair> menaceBlacklist; // NMBlst x (MBlstSy, MBlstTn)
|
||||||
|
DesignNameGen designNames;
|
||||||
|
int32_t aiHivJ = 0, sdFlT = 0;
|
||||||
|
std::vector<StreamEnum> newAlliances; // nalat
|
||||||
|
int32_t lnat = 0, lat = 0;
|
||||||
|
std::vector<AISysEntry> systems; // AINumSys x (AISysID, AISys)
|
||||||
|
std::vector<AICombatReport> combatReports;
|
||||||
|
std::vector<ColonyLoss> colonyLosses;
|
||||||
|
std::vector<Provocation> provocations;
|
||||||
|
std::vector<int32_t> techScores;
|
||||||
|
int32_t fct = 0;
|
||||||
|
std::vector<AIAutoPeaceRun> autoPeaceRuns;
|
||||||
|
std::vector<Node> extra;
|
||||||
|
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.obj(A("AIAttr"), attrib);
|
||||||
|
ar.obj(A("AITurnPris"), turnPriorities);
|
||||||
|
ar.obj(A("AISit"), situation);
|
||||||
|
ar.obj(A("AIPlyHat"), playerHate);
|
||||||
|
ar.carr(A("prs2"), requestStamps);
|
||||||
|
ar.narr(A("dsh"), designShares, [](Ar& a, DesignShare& e) {
|
||||||
|
a.i32(A("pid"), e.pid);
|
||||||
|
a.carr(A("trns"), e.turns);
|
||||||
|
});
|
||||||
|
ar.narr(A("NBStab"), borderStability, [](Ar& a, IntPair& e) {
|
||||||
|
a.i32(A("BStabPl"), e.a);
|
||||||
|
a.i32(A("BStabTn"), e.b);
|
||||||
|
});
|
||||||
|
ar.narr(A("NMBlst"), menaceBlacklist, [](Ar& a, IntPair& e) {
|
||||||
|
a.i32(A("MBlstSy"), e.a);
|
||||||
|
a.i32(A("MBlstTn"), e.b);
|
||||||
|
});
|
||||||
|
ar.obj(A("AIDNG"), designNames);
|
||||||
|
ar.i32(A("AIHivJ"), aiHivJ);
|
||||||
|
ar.i32(A("SDFlT"), sdFlT);
|
||||||
|
ar.carr(A("nalat"), newAlliances);
|
||||||
|
ar.i32(A("lnat"), lnat);
|
||||||
|
ar.i32(A("lat"), lat);
|
||||||
|
ar.narr(A("AINumSys"), systems, [](Ar& a, AISysEntry& e) {
|
||||||
|
a.i32(A("AISysID"), e.id);
|
||||||
|
a.obj(A("AISys"), e.sys);
|
||||||
|
});
|
||||||
|
ar.narr(A("NCmbR"), combatReports, [](Ar& a, AICombatReport& e) { a.obj(A("CmbR"), e); });
|
||||||
|
ar.narr(A("NumCL"), colonyLosses, [](Ar& a, ColonyLoss& e) {
|
||||||
|
a.i32(A("CLTn"), e.turn);
|
||||||
|
a.i32(A("CLSyID"), e.sysID);
|
||||||
|
a.i32(A("CLPlID"), e.plID);
|
||||||
|
});
|
||||||
|
ar.narr(A("NPrv"), provocations, [](Ar& a, Provocation& e) {
|
||||||
|
a.i32(A("NPrvId"), e.id);
|
||||||
|
a.f32(A("NPrvVa"), e.value);
|
||||||
|
});
|
||||||
|
ar.narr(A("NTecS"), techScores, [](Ar& a, int32_t& e) { a.i32(A("TecS"), e); });
|
||||||
|
ar.i32(A("fct"), fct);
|
||||||
|
ar.carr(A("apr"), autoPeaceRuns);
|
||||||
|
ar.rest(extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// One `CD` frame, keyed by the id at the same ordinal in `CDT`. The other id
|
||||||
|
// suffix in our saves, ".TurnCommands_v5", stays carried: its writer's recovered
|
||||||
|
// sequence cannot be aligned to a no-orders save even as a subsequence, so it
|
||||||
|
// needs a save with issued orders before anything can be typed.
|
||||||
|
struct CustomDataBlock {
|
||||||
|
static constexpr const char* kStreamName = "CD";
|
||||||
|
enum class Which { Unknown, AIAgent };
|
||||||
|
Which which = Which::Unknown;
|
||||||
|
StrategyAIAgent aiAgent;
|
||||||
|
std::vector<Node> unknown;
|
||||||
|
|
||||||
|
void select(const std::string& id) {
|
||||||
|
static const std::string suffix = ".AIAgent";
|
||||||
|
which = (id.size() >= suffix.size() && id.compare(id.size() - suffix.size(), suffix.size(), suffix) == 0)
|
||||||
|
? Which::AIAgent
|
||||||
|
: Which::Unknown;
|
||||||
|
}
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
switch (which) {
|
||||||
|
case Which::AIAgent: aiAgent.io(ar); break;
|
||||||
|
case Which::Unknown: ar.rest(unknown); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
struct Sim {
|
struct Sim {
|
||||||
static constexpr const char* kStreamName = "Sim";
|
static constexpr const char* kStreamName = "Sim";
|
||||||
std::string keyPath;
|
std::string keyPath;
|
||||||
|
|
@ -2275,7 +2566,7 @@ struct Sim {
|
||||||
std::vector<int32_t> playerIds, designIds, systemIds, fleetIds, shipIds, tradeIds;
|
std::vector<int32_t> playerIds, designIds, systemIds, fleetIds, shipIds, tradeIds;
|
||||||
int32_t modCount = 0, frame = 0, gameID = 0;
|
int32_t modCount = 0, frame = 0, gameID = 0;
|
||||||
std::optional<int32_t> aiDifficultyID;
|
std::optional<int32_t> aiDifficultyID;
|
||||||
Node attrib;
|
AttribMap attrib; // StreamableHelper<Game::AttribMap>: a "." count, 0 in every save
|
||||||
std::optional<int32_t> rand;
|
std::optional<int32_t> rand;
|
||||||
Node rng; // opaque MT19937 blob: frame with one "." raw item (mt[624] + left, 3 pad bytes)
|
Node rng; // opaque MT19937 blob: frame with one "." raw item (mt[624] + left, 3 pad bytes)
|
||||||
std::string gameName;
|
std::string gameName;
|
||||||
|
|
@ -2324,7 +2615,7 @@ struct Sim {
|
||||||
ar.i32(A("Frame"), frame);
|
ar.i32(A("Frame"), frame);
|
||||||
ar.i32(A("GameID"), gameID);
|
ar.i32(A("GameID"), gameID);
|
||||||
ar.opt_i32(A("AIDifficultyID"), aiDifficultyID);
|
ar.opt_i32(A("AIDifficultyID"), aiDifficultyID);
|
||||||
ar.any(A("Attrib"), attrib);
|
ar.obj(A("Attrib"), attrib);
|
||||||
ar.opt_i32(A("Rand"), rand);
|
ar.opt_i32(A("Rand"), rand);
|
||||||
ar.raw_frame(A("RNG"), rng);
|
ar.raw_frame(A("RNG"), rng);
|
||||||
ar.str(A("GameName"), gameName);
|
ar.str(A("GameName"), gameName);
|
||||||
|
|
@ -2393,14 +2684,21 @@ struct SaveGame { // root: Summary, CreateParams, Sim, CDT, then one opaque CD
|
||||||
CreateParams createParams;
|
CreateParams createParams;
|
||||||
Sim sim;
|
Sim sim;
|
||||||
CdTable cdTable;
|
CdTable cdTable;
|
||||||
std::vector<Node> customData;
|
std::vector<CustomDataBlock> customData;
|
||||||
template <class Ar>
|
template <class Ar>
|
||||||
void io(Ar& ar) {
|
void io(Ar& ar) {
|
||||||
ar.obj(A("Summary"), summary);
|
ar.obj(A("Summary"), summary);
|
||||||
ar.obj(A("CreateParams"), createParams);
|
ar.obj(A("CreateParams"), createParams);
|
||||||
ar.obj(A("Sim"), sim);
|
ar.obj(A("Sim"), sim);
|
||||||
ar.obj(A("CDT"), cdTable);
|
ar.obj(A("CDT"), cdTable);
|
||||||
ar.repeat("CD", customData, [](Ar& a, Node& e) { a.any(A("CD"), e); });
|
// The CD frames carry no key of their own: the id at the same ordinal in
|
||||||
|
// CDT selects the body, in both directions.
|
||||||
|
size_t k = 0;
|
||||||
|
ar.repeat("CD", customData, [&](Ar& a, CustomDataBlock& e) {
|
||||||
|
e.select(k < cdTable.ids.size() ? cdTable.ids[k] : std::string());
|
||||||
|
++k;
|
||||||
|
a.obj(A("CD"), e);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(" %s=%zu", worst[i].second.c_str(), worst[i].first);
|
||||||
std::printf("\n");
|
std::printf("\n");
|
||||||
// Ratchet, not a target: typing a body must never silently regress.
|
// Ratchet, not a target: typing a body must never silently regress.
|
||||||
CHECK(pct >= 97.5);
|
CHECK(pct >= 99.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- round trips ------------------------------------------------------------------
|
// --- round trips ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -591,6 +591,173 @@ static void test_svsctob_variants() {
|
||||||
CHECK(wrong.id == 17 && wrong.swarm.asg.empty());
|
CHECK(wrong.id == 17 && wrong.swarm.asg.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 8c. the AIAgent custom-data block -------------------------------------------------
|
||||||
|
// Every container in the block has count 0 in all four saves we hold, so the real
|
||||||
|
// data exercises the frames and none of the element layouts. This test populates
|
||||||
|
// each of them, which is the only check those layouts get.
|
||||||
|
static void test_aiagent_block() {
|
||||||
|
shapes::StrategyAIAgent ai;
|
||||||
|
|
||||||
|
// Game::AttribMap: a "." count then two "." strings per entry. The second pair
|
||||||
|
// is empty on both sides -- an empty string is four zero bytes, the same as the
|
||||||
|
// int 0, which is exactly how a mistyped string field stays invisible.
|
||||||
|
ai.attrib.entries = {{"aggression", "high"}, {"", ""}};
|
||||||
|
|
||||||
|
// AIWeightMap keys are FRAMES (Mars::StreamableEnum, or ShipSectionID's two
|
||||||
|
// ints), never bare ints, and each key is followed by a "." float.
|
||||||
|
ai.turnPriorities.entries.push_back({{7, {}}, 0.5f});
|
||||||
|
ai.situation.sections.entries.push_back({{2, 44, {}}, 1.5f});
|
||||||
|
ai.situation.weaponFamilies.entries.push_back({{3, {}}, -2.0f});
|
||||||
|
ai.playerHate.entries.push_back({{16, {}}, 0.25f});
|
||||||
|
|
||||||
|
ai.requestStamps.push_back({16, 4, {}});
|
||||||
|
// `dsh` counts (pid, trns) pairs walked out of a map; trns is a VectorHelper<int>.
|
||||||
|
ai.designShares.push_back({16, {1, 2, 3}});
|
||||||
|
ai.designShares.push_back({32, {}});
|
||||||
|
ai.borderStability.push_back({16, 5});
|
||||||
|
ai.menaceBlacklist.push_back({272, 9});
|
||||||
|
ai.designNames.names.push_back({{5, 37, {}}, "Ravenous"});
|
||||||
|
ai.designNames.names.push_back({{5, 63, {}}, ""}); // an empty design name
|
||||||
|
ai.aiHivJ = 3;
|
||||||
|
ai.sdFlT = 7;
|
||||||
|
ai.newAlliances.push_back({11, {}}); // framed element, not a bare int
|
||||||
|
ai.newAlliances.push_back({12, {}});
|
||||||
|
ai.lnat = 4;
|
||||||
|
ai.lat = 5;
|
||||||
|
shapes::StrategyAIAgent::AISysEntry sys;
|
||||||
|
sys.id = 48;
|
||||||
|
ai.systems.push_back(sys);
|
||||||
|
shapes::AICombatReport rep;
|
||||||
|
rep.crTrn = 2;
|
||||||
|
rep.crPce = true;
|
||||||
|
rep.crSys.value = 272;
|
||||||
|
rep.crPlSv2.rpBon = 1.25f;
|
||||||
|
rep.crPlSv2.rpBonT = 3;
|
||||||
|
rep.crPlSv2.savBonus = 4;
|
||||||
|
rep.crPlSv2.maintHF = true;
|
||||||
|
rep.crPlSv2.tacReports.push_back(Node::int32("TRid", 9)); // carried, not typed
|
||||||
|
ai.combatReports.push_back(rep);
|
||||||
|
ai.colonyLosses.push_back({2, 272, 16});
|
||||||
|
ai.provocations.push_back({16, 450.0f});
|
||||||
|
ai.techScores = {2, 4, 6};
|
||||||
|
ai.fct = 1;
|
||||||
|
ai.autoPeaceRuns.push_back({16, 2, 5, {}});
|
||||||
|
|
||||||
|
Writer w;
|
||||||
|
WriteArchive wa(w);
|
||||||
|
wa.obj(A("CD"), ai);
|
||||||
|
Node root = read_tree(w.bytes());
|
||||||
|
std::vector<Issue> issues;
|
||||||
|
shapes::StrategyAIAgent back;
|
||||||
|
ReadArchive ra(root.children, issues, "");
|
||||||
|
ra.obj(A("CD"), back);
|
||||||
|
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
||||||
|
CHECK_EQ(count(issues, Issue::Warn), size_t(0));
|
||||||
|
|
||||||
|
CHECK(back.attrib.entries.size() == 2);
|
||||||
|
CHECK(back.attrib.entries[0].key == "aggression" && back.attrib.entries[0].value == "high");
|
||||||
|
CHECK(back.attrib.entries[1].key.empty() && back.attrib.entries[1].value.empty());
|
||||||
|
CHECK(back.turnPriorities.entries.size() == 1 && back.turnPriorities.entries[0].key.value == 7);
|
||||||
|
CHECK(back.turnPriorities.entries[0].weight == 0.5f);
|
||||||
|
CHECK(back.situation.sections.entries.size() == 1 && back.situation.sections.entries[0].key.a == 2 &&
|
||||||
|
back.situation.sections.entries[0].key.b == 44);
|
||||||
|
CHECK(back.situation.weaponFamilies.entries[0].weight == -2.0f);
|
||||||
|
CHECK(back.requestStamps.size() == 1 && back.requestStamps[0].pid == 16 && back.requestStamps[0].trn == 4);
|
||||||
|
CHECK(back.designShares.size() == 2 && back.designShares[0].turns.size() == 3 &&
|
||||||
|
back.designShares[0].turns[2] == 3 && back.designShares[1].turns.empty());
|
||||||
|
CHECK(back.borderStability.size() == 1 && back.borderStability[0].b == 5);
|
||||||
|
CHECK(back.menaceBlacklist.size() == 1 && back.menaceBlacklist[0].a == 272);
|
||||||
|
CHECK(back.designNames.names.size() == 2 && back.designNames.names[0].name == "Ravenous" &&
|
||||||
|
back.designNames.names[0].section.b == 37 && back.designNames.names[1].name.empty());
|
||||||
|
CHECK(back.newAlliances.size() == 2 && back.newAlliances[1].value == 12);
|
||||||
|
CHECK(back.lnat == 4 && back.lat == 5);
|
||||||
|
CHECK(back.systems.size() == 1 && back.systems[0].id == 48);
|
||||||
|
CHECK(back.combatReports.size() == 1 && back.combatReports[0].crPce &&
|
||||||
|
back.combatReports[0].crSys.value == 272 && back.combatReports[0].crPlSv2.maintHF &&
|
||||||
|
back.combatReports[0].crPlSv2.tacReports.size() == 1);
|
||||||
|
CHECK(back.colonyLosses.size() == 1 && back.colonyLosses[0].sysID == 272);
|
||||||
|
CHECK(back.provocations.size() == 1 && back.provocations[0].value == 450.0f);
|
||||||
|
CHECK(back.techScores.size() == 3 && back.techScores[1] == 4);
|
||||||
|
CHECK(back.autoPeaceRuns.size() == 1 && back.autoPeaceRuns[0].tn1 == 5);
|
||||||
|
|
||||||
|
Writer w2;
|
||||||
|
WriteArchive wa2(w2);
|
||||||
|
wa2.obj(A("CD"), back);
|
||||||
|
CHECK(w2.bytes() == w.bytes());
|
||||||
|
|
||||||
|
// A StreamableEnum element is a frame, not a scalar: `nalat`'s two elements must
|
||||||
|
// come back as complex nodes, which is what a bare-int typing would get wrong.
|
||||||
|
const Node* cd = &root.children[0];
|
||||||
|
const Node* nalat = nullptr;
|
||||||
|
for (const Node& n : cd->children)
|
||||||
|
if (n.name == "nalat") nalat = &n;
|
||||||
|
CHECK(nalat != nullptr);
|
||||||
|
if (nalat) {
|
||||||
|
CHECK(nalat->children.size() == 3); // the "." count plus two elements
|
||||||
|
CHECK(!nalat->children[0].is_complex());
|
||||||
|
CHECK(nalat->children[1].is_complex() && nalat->children[2].is_complex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CDT plus the CD frames, exactly as SaveGame drives them.
|
||||||
|
struct CdOnly {
|
||||||
|
static constexpr const char* kStreamName = "";
|
||||||
|
shapes::CdTable cdTable;
|
||||||
|
std::vector<shapes::CustomDataBlock> customData;
|
||||||
|
template <class Ar>
|
||||||
|
void io(Ar& ar) {
|
||||||
|
ar.obj(A("CDT"), cdTable);
|
||||||
|
size_t k = 0;
|
||||||
|
ar.repeat("CD", customData, [&](Ar& a, shapes::CustomDataBlock& e) {
|
||||||
|
e.select(k < cdTable.ids.size() ? cdTable.ids[k] : std::string());
|
||||||
|
++k;
|
||||||
|
a.obj(A("CD"), e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The CD frames carry no key: the id at the same ordinal in CDT selects the body.
|
||||||
|
static void test_customdata_dispatch() {
|
||||||
|
CdOnly doc;
|
||||||
|
doc.cdTable.ids = {"Player.00000016.TurnCommands_v5", "Player.00000032.AIAgent"};
|
||||||
|
shapes::CustomDataBlock tc; // not modelled: carried verbatim
|
||||||
|
tc.select(doc.cdTable.ids[0]);
|
||||||
|
tc.unknown.push_back(Node::int32(".", 42));
|
||||||
|
doc.customData.push_back(tc);
|
||||||
|
shapes::CustomDataBlock agent;
|
||||||
|
agent.select(doc.cdTable.ids[1]);
|
||||||
|
agent.aiAgent.fct = 9;
|
||||||
|
agent.aiAgent.techScores = {2, 4};
|
||||||
|
doc.customData.push_back(agent);
|
||||||
|
|
||||||
|
Writer w;
|
||||||
|
WriteArchive wa(w);
|
||||||
|
doc.io(wa);
|
||||||
|
Node root = read_tree(w.bytes());
|
||||||
|
std::vector<Issue> issues;
|
||||||
|
CdOnly back;
|
||||||
|
ReadArchive ra(root.children, issues, "");
|
||||||
|
back.io(ra);
|
||||||
|
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
||||||
|
CHECK(back.customData.size() == 2);
|
||||||
|
CHECK(back.customData[0].which == shapes::CustomDataBlock::Which::Unknown);
|
||||||
|
CHECK(back.customData[0].unknown.size() == 1);
|
||||||
|
CHECK(back.customData[1].which == shapes::CustomDataBlock::Which::AIAgent);
|
||||||
|
CHECK(back.customData[1].aiAgent.fct == 9 && back.customData[1].aiAgent.techScores.size() == 2);
|
||||||
|
Writer w2;
|
||||||
|
WriteArchive wa2(w2);
|
||||||
|
back.io(wa2);
|
||||||
|
CHECK(w2.bytes() == w.bytes());
|
||||||
|
|
||||||
|
// The same bytes with the ids swapped select the other way round: nothing in the
|
||||||
|
// CD frame itself says which body it holds.
|
||||||
|
shapes::CustomDataBlock probe;
|
||||||
|
probe.select("Player.00000032.AIAgentX");
|
||||||
|
CHECK(probe.which == shapes::CustomDataBlock::Which::Unknown);
|
||||||
|
probe.select(".AIAgent");
|
||||||
|
CHECK(probe.which == shapes::CustomDataBlock::Which::AIAgent);
|
||||||
|
}
|
||||||
|
|
||||||
// --- 9. gzip container ---------------------------------------------------------------
|
// --- 9. gzip container ---------------------------------------------------------------
|
||||||
static void test_gzip() {
|
static void test_gzip() {
|
||||||
Bytes data;
|
Bytes data;
|
||||||
|
|
@ -640,6 +807,8 @@ int main() {
|
||||||
test_typed_conditionals();
|
test_typed_conditionals();
|
||||||
test_typed_string_array();
|
test_typed_string_array();
|
||||||
test_svsctob_variants();
|
test_svsctob_variants();
|
||||||
|
test_aiagent_block();
|
||||||
|
test_customdata_dispatch();
|
||||||
test_gzip();
|
test_gzip();
|
||||||
test_dump_format();
|
test_dump_format();
|
||||||
std::printf("test_stream: %s (%d failures)\n", fails ? "FAILED" : "ok", fails);
|
std::printf("test_stream: %s (%d failures)\n", fails ? "FAILED" : "ok", fails);
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,28 @@ int main() {
|
||||||
check<sh::SVSOCrowDefenders>("SVSOCrowDefenders", "Game::SVSOCrowDefenders");
|
check<sh::SVSOCrowDefenders>("SVSOCrowDefenders", "Game::SVSOCrowDefenders");
|
||||||
check<sh::SVSOGrandMenaceTrigger>("SVSOGrandMenaceTrigger", "Game::SVSOGrandMenaceTrigger");
|
check<sh::SVSOGrandMenaceTrigger>("SVSOGrandMenaceTrigger", "Game::SVSOGrandMenaceTrigger");
|
||||||
|
|
||||||
|
// --- the AIAgent custom-data block ------------------------------------------
|
||||||
|
// The block SELECTION (a CDT id ending ".AIAgent") is not a wire item, so it is
|
||||||
|
// not checkable here; what is checked is that each body agrees with its class's
|
||||||
|
// serializer. Game::AIPlayerRequestStamp has no entry: it is a POD reached only
|
||||||
|
// through a specialised helper, so it has no RTTI class for the recovery to find.
|
||||||
|
check<sh::StrategyAIAgent>("StrategyAIAgent", "Game::StrategyAIAgent::Streamable");
|
||||||
|
check<sh::AttribMap>("AttribMap", "Game::AttribMap");
|
||||||
|
check<sh::AISituation>("AISituation", "Game::AISituation");
|
||||||
|
check<sh::AISystem>("AISystem", "Game::AISystem");
|
||||||
|
check<sh::DesignNameGen>("DesignNameGen", "Game::StrategyAIAgent::DesignNameGen");
|
||||||
|
check<sh::AICombatReport>("AICombatReport", "Game::AICombatReport");
|
||||||
|
check<sh::CombatPlayerStats>("CombatPlayerStats", "Game::CombatPlayerStats");
|
||||||
|
check<sh::AIAutoPeaceRun>("AIAutoPeaceRun", "Game::AIAutoPeaceRun");
|
||||||
|
// One shape covers all four Game::AIWeightMap<K> instantiations: they differ
|
||||||
|
// only in the key body, which is its own frame on the wire.
|
||||||
|
check<sh::AIWeightMapSection>("AIWeightMap<Sec>", "Game::Game::VShipSectionID::?$AIWeightMap");
|
||||||
|
check<sh::AIWeightMapEnum>("AIWeightMap<Pur>",
|
||||||
|
"Game::Mars::Game::W4AIPurposeID::U?$StreamableEnum::?$AIWeightMap");
|
||||||
|
check<sh::AIWeightMapEnum>("AIWeightMap<Wep>",
|
||||||
|
"Game::Mars::Game::W4WeaponFamilyID::U?$StreamableEnum::?$AIWeightMap");
|
||||||
|
check<sh::AIWeightMapEnum>("AIWeightMap<uint>", "Game::Mars::I::U?$StreamableEnum::?$AIWeightMap");
|
||||||
|
|
||||||
std::printf(
|
std::printf(
|
||||||
"\ntotals: %d shapes bound, %d items matched, %d MISMATCH, %d wire-only, "
|
"\ntotals: %d shapes bound, %d items matched, %d MISMATCH, %d wire-only, "
|
||||||
"%d opaque item(s) in bound shapes\n",
|
"%d opaque item(s) in bound shapes\n",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue