A: type the AIAgent custom-data blocks; named coverage 98.0% -> 99.9%
Game::StrategyAIAgent::Streamable and the ten shapes under it. The whole writer is unconditional -- the branch the decompiler shows around lnat is an inlined vector destructor whose operator delete is marked noreturn, and both paths converge -- so the recovered sequence and a single record are the same sequence, and all 36 items match with 0 wire-only and 0 shape-only. CD blocks are now selected by the CDT id at the same ordinal, in both directions; the one .TurnCommands_v5 block per save still falls to a Node. Also: Sim's Attrib was not an empty frame, it was an AttribMap holding a count of 0, and typing it closes those two items too. And StreamableEnum<T> writes a frame containing one int, not a bare int, so SysMem/mts/nalat are arrays of one-int frames -- byte-neutral, since all three have count 0 in every save, but the previous typing was wrong. Conformance 74 shapes/769 items -> 86/838, still 0 MISMATCH. Round trip byte-identical on all four saves; ratchet 97.5 -> 99.8. Every container that is empty in all four saves is named as such in the notes; the new unit test populates each one, since nothing else exercises them.
This commit is contained in:
parent
f4f3a7aae7
commit
8e45b43638
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
|
||||
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:
|
||||
|
||||
* **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
|
||||
empty in all available data round-trips perfectly while typed as an int. That is what hid
|
||||
`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
|
||||
// disk spelling differs, and are matched by position. Bodies the format keeps
|
||||
// opaque are held as generic Nodes so a shape can be re-emitted byte-for-byte.
|
||||
// After the SvSctOb round only three regions are still carried that way: the CD
|
||||
// custom-data blocks, the RNG state block (correctly opaque) and Attrib (an empty
|
||||
// frame in every save).
|
||||
// After the AIAgent round only two regions are still carried that way: the RNG
|
||||
// state block (correctly opaque) and the one CD block per save whose CDT id ends
|
||||
// ".TurnCommands_v5", which needs a save with issued orders before it can be typed.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
|
@ -790,6 +790,20 @@ struct ShipSectionID { // Game::ShipSectionID: two "." ints
|
|||
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
|
||||
static constexpr const char* kStreamName = "DW2";
|
||||
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<SVSOSwarmQueenHive> hives;
|
||||
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;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
|
|
@ -2069,7 +2085,7 @@ struct SVSOVonNeumann { // EncID 1
|
|||
static constexpr const char* kStreamName = "";
|
||||
std::vector<VonNeumannDefeat> defeats;
|
||||
int32_t rusav = 0, nenc = 0, nmb = 0, nml = 0, nbb = 0, nbl = 0, nskb = 0, nskl = 0;
|
||||
std::vector<int32_t> mts; // VectorHelper<StreamableEnum<uint>>
|
||||
std::vector<StreamEnum> mts; // VectorHelper<StreamableEnum<uint>>: framed elements
|
||||
int32_t ntm = 0, ntb = 0;
|
||||
bool sken = false;
|
||||
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 {
|
||||
static constexpr const char* kStreamName = "Sim";
|
||||
std::string keyPath;
|
||||
|
|
@ -2275,7 +2566,7 @@ struct Sim {
|
|||
std::vector<int32_t> playerIds, designIds, systemIds, fleetIds, shipIds, tradeIds;
|
||||
int32_t modCount = 0, frame = 0, gameID = 0;
|
||||
std::optional<int32_t> aiDifficultyID;
|
||||
Node attrib;
|
||||
AttribMap attrib; // StreamableHelper<Game::AttribMap>: a "." count, 0 in every save
|
||||
std::optional<int32_t> rand;
|
||||
Node rng; // opaque MT19937 blob: frame with one "." raw item (mt[624] + left, 3 pad bytes)
|
||||
std::string gameName;
|
||||
|
|
@ -2324,7 +2615,7 @@ struct Sim {
|
|||
ar.i32(A("Frame"), frame);
|
||||
ar.i32(A("GameID"), gameID);
|
||||
ar.opt_i32(A("AIDifficultyID"), aiDifficultyID);
|
||||
ar.any(A("Attrib"), attrib);
|
||||
ar.obj(A("Attrib"), attrib);
|
||||
ar.opt_i32(A("Rand"), rand);
|
||||
ar.raw_frame(A("RNG"), rng);
|
||||
ar.str(A("GameName"), gameName);
|
||||
|
|
@ -2393,14 +2684,21 @@ struct SaveGame { // root: Summary, CreateParams, Sim, CDT, then one opaque CD
|
|||
CreateParams createParams;
|
||||
Sim sim;
|
||||
CdTable cdTable;
|
||||
std::vector<Node> customData;
|
||||
std::vector<CustomDataBlock> customData;
|
||||
template <class Ar>
|
||||
void io(Ar& ar) {
|
||||
ar.obj(A("Summary"), summary);
|
||||
ar.obj(A("CreateParams"), createParams);
|
||||
ar.obj(A("Sim"), sim);
|
||||
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("\n");
|
||||
// Ratchet, not a target: typing a body must never silently regress.
|
||||
CHECK(pct >= 97.5);
|
||||
CHECK(pct >= 99.8);
|
||||
}
|
||||
|
||||
// --- round trips ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -591,6 +591,173 @@ static void test_svsctob_variants() {
|
|||
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 ---------------------------------------------------------------
|
||||
static void test_gzip() {
|
||||
Bytes data;
|
||||
|
|
@ -640,6 +807,8 @@ int main() {
|
|||
test_typed_conditionals();
|
||||
test_typed_string_array();
|
||||
test_svsctob_variants();
|
||||
test_aiagent_block();
|
||||
test_customdata_dispatch();
|
||||
test_gzip();
|
||||
test_dump_format();
|
||||
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::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(
|
||||
"\ntotals: %d shapes bound, %d items matched, %d MISMATCH, %d wire-only, "
|
||||
"%d opaque item(s) in bound shapes\n",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue