The two derived words the per-player turn record's ship census counts by, and the design serializers that three lanes had been told did not exist. HOW DESIGNS PERSIST. Game::ShipDesign derives from Game::ShipDesignDef and reaches IStreamable through adjustor thunks, so a design is written by TWO serializers: the base emits FAIDes/DHide/DWep/DName and exactly three section frames (command, mission, engine on the wire), the derived one appends Dtc, the Dwgv flag and, only when that flag is set, a weapon-group frame. The earlier "the writer makes no stream call at all" note named an address that is in no vftable at all. Corrected in shapes.h. THREE sections, not five. The "two reserved slots" were Dtc and Dwgv swept into the section list by the reference reader's catch-all tail; the constructor builds a three-element array. design.h's comment is corrected and the fixture loader now accepts 3-5 raw_slots so old fixtures still load; the array keeps five inert entries deliberately, since touching the slot enum reaches rules.cpp and another lane's tests for no behavioural gain. DWep and Dwgv are bools, not ints -- both writers call the bool primitive. With four-character tags a bool item and an int item are the same size on the wire and 0/1 the same bytes, so no save can tell them apart. Byte-neutral: the typed round trip is still byte-identical on all 11 saves at 100% named coverage. HULL SIZE is the section_class of the last resolved section in memory slot order, mapped Destroyer/Cruiser/Dreadnought -> 0/1/2 case-insensitively, with absent or unrecognised meaning 0 rather than an error. The DEFENCE-PLATFORM flag is one bit of a 64-bit role-flag word OR-ed across the design's sections. Neither is on the wire; both are rebuilt from the section catalog. MEASURED, not assumed: the new game_design_census test rebuilds the six census counters per player and compares them against the record the game archived for each save's own frame. 11 saves, 503 designs, 480 leaves, 0 mismatched, 0 ships with an unresolvable design, 0 designs where first- and last-resolved section disagree on hull size. COVERAGE IS THIN AND THE TEST SAYS SO: only 32 of the 480 leaves are nonzero, and three of the six census leaves (both cruiser rows and dreadnought platforms) are never exercised by any save in the corpus -- the test prints the per-leaf nonzero counts and names them unexercised rather than verified. Nothing is wired into the turn record: src/app is another lane's this cycle, so this is evaluated and reported, not written. host ctest 43/43 (was 42/42; +1, skips cleanly without the env). With a data root set, game_data_realdata and mars_text_realdata fail identically on main -- both are the absent Locale/EN/Strings.csv, not this change. clean-room OK. Reference readers fixed openly in the RE repo: save_reader 49/49, design rules 32/32, stock_designs.json regenerated (raw_slots 5->3 and dWep int->bool are the only field changes across all 127 designs).
222 lines
7.7 KiB
C++
222 lines
7.7 KiB
C++
#include "game/data/shipsection.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "game/data/fields.h"
|
|
|
|
namespace game::data {
|
|
|
|
using detail::Fields;
|
|
|
|
std::string_view section_type_name(SectionType t) {
|
|
switch (t) {
|
|
case SectionType::None: return "";
|
|
case SectionType::Command: return "command";
|
|
case SectionType::Mission: return "mission";
|
|
case SectionType::Engine: return "engine";
|
|
case SectionType::Other: return "other";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
std::string_view section_class_name(SectionClass c) {
|
|
switch (c) {
|
|
case SectionClass::None: return "";
|
|
case SectionClass::Destroyer: return "destroyer";
|
|
case SectionClass::Cruiser: return "cruiser";
|
|
case SectionClass::Dreadnought: return "dreadnought";
|
|
case SectionClass::Other: return "other";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
SectionType parse_section_type(std::string_view s) {
|
|
if (s.empty()) return SectionType::None;
|
|
if (iequals(s, "command")) return SectionType::Command;
|
|
if (iequals(s, "mission")) return SectionType::Mission;
|
|
if (iequals(s, "engine")) return SectionType::Engine;
|
|
return SectionType::Other;
|
|
}
|
|
|
|
SectionClass parse_section_class(std::string_view s) {
|
|
if (s.empty()) return SectionClass::None;
|
|
if (iequals(s, "destroyer")) return SectionClass::Destroyer;
|
|
if (iequals(s, "cruiser")) return SectionClass::Cruiser;
|
|
if (iequals(s, "dreadnought")) return SectionClass::Dreadnought;
|
|
return SectionClass::Other;
|
|
}
|
|
|
|
int hull_size(SectionClass c) {
|
|
switch (c) {
|
|
case SectionClass::Cruiser: return 1;
|
|
case SectionClass::Dreadnought: return 2;
|
|
case SectionClass::Destroyer:
|
|
case SectionClass::None:
|
|
case SectionClass::Other: return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// `option` entries: scalar pairs and blocks share the key; the normalised
|
|
// list keeps file order across both forms (ord = position in the block).
|
|
std::vector<OptionGroup> read_options(const Block& section) {
|
|
struct Item { int ord; OptionGroup group; };
|
|
std::vector<Item> items;
|
|
for (const Attr& a : section.attrs) {
|
|
if (!iequals(a.key, "option")) continue;
|
|
OptionGroup g;
|
|
g.members.push_back(a.value);
|
|
g.scalar = true;
|
|
g.line = a.line;
|
|
items.push_back({a.ord, std::move(g)});
|
|
}
|
|
for (const Block& b : section.blocks) {
|
|
if (!iequals(b.name, "option")) continue;
|
|
OptionGroup g;
|
|
g.members = b.strs("option");
|
|
g.scalar = false;
|
|
g.line = b.line;
|
|
items.push_back({b.ord, std::move(g)});
|
|
}
|
|
std::stable_sort(items.begin(), items.end(), [](const Item& x, const Item& y) { return x.ord < y.ord; });
|
|
std::vector<OptionGroup> out;
|
|
out.reserve(items.size());
|
|
for (auto& it : items) out.push_back(std::move(it.group));
|
|
return out;
|
|
}
|
|
|
|
MountDef read_mount(Fields& f) {
|
|
MountDef m;
|
|
m.node = f.required_str("node");
|
|
m.min_azimuth = f.opt_double("min_azimuth");
|
|
m.max_azimuth = f.opt_double("max_azimuth");
|
|
m.min_inclination = f.opt_double("min_inclination");
|
|
m.max_inclination = f.opt_double("max_inclination");
|
|
m.home_azimuth = f.opt_double("home_azimuth");
|
|
m.home_inclination = f.opt_double("home_inclination");
|
|
m.line = f.block().line;
|
|
return m;
|
|
}
|
|
|
|
BankDef read_bank(Fields& f) {
|
|
BankDef b;
|
|
b.turret_class = f.str("turretclass");
|
|
b.turret_size = f.str("turretsize");
|
|
b.weapon = f.str("weapon");
|
|
b.show_turrets = f.opt_bool("showturrets");
|
|
b.invincible = f.opt_bool("invincible");
|
|
b.repeated_turret_spec = f.block().all("turretsize").size() > 1 || f.block().all("turretclass").size() > 1;
|
|
b.line = f.block().line;
|
|
for (const Block* m : f.block().blocks_named("mount")) {
|
|
Fields mf = f.sub(*m);
|
|
b.mounts.push_back(read_mount(mf));
|
|
}
|
|
return b;
|
|
}
|
|
|
|
NetForceLimits read_netforce(Fields& f) {
|
|
NetForceLimits n;
|
|
n.force_forward = f.opt_double("force_forward");
|
|
n.force_right = f.opt_double("force_right");
|
|
n.force_up = f.opt_double("force_up");
|
|
n.torque_yaw = f.opt_double("torque_yaw");
|
|
n.torque_pitch = f.opt_double("torque_pitch");
|
|
n.torque_roll = f.opt_double("torque_roll");
|
|
n.speed = f.opt_double("speed");
|
|
n.rotspeed = f.opt_double("rotspeed");
|
|
return n;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Loaded<ShipSectionDef> load_shipsection(const mars::parse::Document& doc, std::string file) {
|
|
Loaded<ShipSectionDef> out;
|
|
const mars::parse::Node* node = doc.root.first_block("shipsection");
|
|
if (!node) {
|
|
out.problems.push_back({Problem::Kind::MissingBlock, file, 0, "shipsection", "no top-level shipsection{} block"});
|
|
return out;
|
|
}
|
|
ShipSectionDef s;
|
|
s.file = file;
|
|
s.raw = snapshot(*node);
|
|
Fields f(s.raw, file, out.problems);
|
|
|
|
s.model = f.required_str("model");
|
|
s.dam_model = f.str("dam_model");
|
|
s.requires_tech = f.strs("requires");
|
|
s.section_type_text = f.str("section_type");
|
|
s.section_type = parse_section_type(s.section_type_text);
|
|
s.section_class_text = f.str("section_class");
|
|
s.section_class = parse_section_class(s.section_class_text);
|
|
s.design_class = f.str("design_class");
|
|
s.entity_class = f.str("entity_class");
|
|
s.health = f.opt_double("health");
|
|
if (!f.block().find("health")) f.missing("health");
|
|
s.mass = f.opt_double("mass");
|
|
if (!f.block().find("mass")) f.missing("mass");
|
|
s.cost = f.opt_int("cost");
|
|
s.cpoints = f.opt_int("cpoints");
|
|
s.crew = f.opt_int("crew");
|
|
s.command_cost = f.opt_int("command_cost");
|
|
s.maintenance_cost = f.opt_int("maintenance_cost");
|
|
s.command_quota = f.opt_int("command_quota");
|
|
s.socket_fore = f.str("socket_fore");
|
|
s.socket_aft = f.str("socket_aft");
|
|
s.dam_socket_fore = f.str("dam_socket_fore");
|
|
s.dam_socket_aft = f.str("dam_socket_aft");
|
|
s.options = read_options(s.raw);
|
|
if (const Block* od = s.raw.block("optiondef")) {
|
|
OptionGroup g;
|
|
g.members = od->strs("option");
|
|
g.line = od->line;
|
|
s.optiondef = std::move(g);
|
|
}
|
|
for (const Block* b : s.raw.blocks_named("bank")) {
|
|
Fields bf = f.sub(*b);
|
|
s.banks.push_back(read_bank(bf));
|
|
}
|
|
s.ftlspeed = f.opt_double("ftlspeed");
|
|
s.nodespeed = f.opt_double("nodespeed");
|
|
s.range = f.opt_double("range");
|
|
s.scanrange = f.opt_double("scanrange");
|
|
s.tactical_sensor_range = f.opt_double("tacticalsensorrange");
|
|
s.engine_techera = f.str("engine_techera");
|
|
if (const Block* n = s.raw.block("netforcelimits")) {
|
|
Fields nf = f.sub(*n);
|
|
s.netforcelimits = read_netforce(nf);
|
|
}
|
|
for (const Block* t : s.raw.blocks_named("thruster")) {
|
|
ThrusterDef th;
|
|
th.node = t->str("node");
|
|
th.effect = t->str("effect");
|
|
th.idle_effect = t->str("idle_effect");
|
|
s.thrusters.push_back(std::move(th));
|
|
}
|
|
s.exclude = f.strs("exclude"); // one `exclude "STEM"` line per forbidden partner
|
|
s.explicit_command_section = f.str("explicit_command_section");
|
|
s.explicit_engine_section = f.str("explicit_engine_section");
|
|
s.explicit_section = f.opt_bool("explicit_section");
|
|
s.autonomous = f.opt_bool("autonomous");
|
|
s.nodesign = f.opt_bool("nodesign");
|
|
s.defence_platform = f.opt_bool("defence_platform");
|
|
|
|
out.value = std::move(s);
|
|
return out;
|
|
}
|
|
|
|
Loaded<ShipSectionDef> parse_shipsection(std::string_view text, std::string file) {
|
|
auto parsed = mars::parse::parse_blocks(text);
|
|
if (!parsed.ok()) {
|
|
Loaded<ShipSectionDef> out;
|
|
out.problems.push_back({Problem::Kind::Syntax, file, parsed.error().line, "", parsed.error().message});
|
|
return out;
|
|
}
|
|
Loaded<ShipSectionDef> out = load_shipsection(parsed.value(), file);
|
|
for (const auto& d : parsed.value().warnings)
|
|
out.problems.push_back({Problem::Kind::Syntax, file, d.line, "", "recovered: " + d.message});
|
|
return out;
|
|
}
|
|
|
|
} // namespace game::data
|