game/data+design: rename member 'requires' -> requires_tech (C++20 keyword, -Werror under design tests)

This commit is contained in:
alex 2026-09-07 22:12:26 -04:00
parent ba9ec951d2
commit 587e4ab9fb
18 changed files with 56 additions and 56 deletions

View file

@ -297,10 +297,10 @@ CrossCheck Catalog::cross_check() const {
return t;
};
// weapon requires -> tech
// weapon requires_tech -> tech
for (const WeaponDef& w : weapons) {
if (w.requires.empty()) x.weapons_without_requires.push_back(w.file);
for (const std::string& r : w.requires) {
if (w.requires_tech.empty()) x.weapons_without_requires.push_back(w.file);
for (const std::string& r : w.requires_tech) {
const TechNode* exact = nullptr;
const TechNode* t = tech_ref(r, &exact);
if (!t)
@ -314,9 +314,9 @@ CrossCheck Catalog::cross_check() const {
x.unresolved_weapon_names.push_back({w.file, w.name, 0});
}
// sections: requires, options, bank weapons, turret pairs
// sections: requires_tech, options, bank weapons, turret pairs
for (const ShipSectionDef& s : sections) {
for (const std::string& r : s.requires) {
for (const std::string& r : s.requires_tech) {
if (is_group_ref(r)) {
if (!tech.requirement_exists(r)) x.section_requires_dangling.push_back({s.file, r, 0});
continue;
@ -342,7 +342,7 @@ CrossCheck Catalog::cross_check() const {
}
}
// tech: ship sections, weapon files, requires, allows
// tech: ship sections, weapon files, requires_tech, allows
std::set<std::string> weapon_files;
for (const WeaponDef& w : weapons) weapon_files.insert(fold(w.file));
for (const TechNode& t : tech.nodes) {
@ -353,7 +353,7 @@ CrossCheck Catalog::cross_check() const {
std::replace(k.begin(), k.end(), '\\', '/');
if (!weapon_files.count(k)) x.tech_weapon_file_dangling.push_back({t.name, wf, t.line});
}
for (const std::string& r : t.requires)
for (const std::string& r : t.requires_tech)
if (!tech.requirement_exists(r)) x.tech_requires_dangling.push_back({t.name, r, t.line});
for (std::size_t ei : t.allows) {
const AllowsEdge& e = tech.edges[ei];

View file

@ -26,7 +26,7 @@ Species parse_species(std::string_view name); // case-insensitive; "NPC
struct Problem {
enum class Kind {
MissingBlock, // file has no top-level block of the expected name
MissingKey, // a key the schema requires is absent
MissingKey, // a key the schema requires_tech is absent
BadValue, // a value does not have the expected shape (kept in raw)
Unparsed, // a structured value (allows string, turret row) could not be parsed
Duplicate, // an id / name occurs twice

View file

@ -134,7 +134,7 @@ Loaded<ShipSectionDef> load_shipsection(const mars::parse::Document& doc, std::s
s.model = f.required_str("model");
s.dam_model = f.str("dam_model");
s.requires = f.strs("requires");
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");

View file

@ -1,7 +1,7 @@
// game::data -- ShipSectionDef: one `Species/<Race>/sections/*.shipsection`.
//
// shipsection {
// model PATH requires TECH... section_type command|mission|engine
// model PATH requires_tech TECH... section_type command|mission|engine
// section_class destroyer|cruiser|dreadnought
// health mass cost cpoints crew socket_fore/socket_aft NODE
// option TECH -- one-member option group
@ -83,7 +83,7 @@ struct ShipSectionDef {
// body
std::string model, dam_model;
std::vector<std::string> requires; // AND; may name GRP_<group>
std::vector<std::string> requires_tech; // AND; may name GRP_<group>
std::string section_type_text;
SectionType section_type = SectionType::None;
std::string section_class_text;

View file

@ -150,7 +150,7 @@ Loaded<TechTree> load_tech_tree(const mars::parse::Document& doc, std::string fi
t.group = f.str("group");
t.option_cost = f.opt_double("option_cost");
t.unlock_explicitly = f.opt_bool("unlock_explicitly");
t.requires = f.strs("requires");
t.requires_tech = f.strs("requires");
for (const Block* s : t.raw.blocks_named("strategy")) {
for (const std::string& v : s->strs("inc")) t.benefits_inc.push_back(v);
for (const std::string& v : s->strs("dec")) t.benefits_dec.push_back(v);

View file

@ -2,7 +2,7 @@
//
// tech {
// name "IND_Waldo" family "IND" type P threat N group TORPS option_cost 1.2
// requires "TECH" | "GRP_<group>" -- prerequisite beyond the allows edge
// requires_tech "TECH" | "GRP_<group>" -- prerequisite beyond the allows edge
// allows "CHILD RP:cost Human:% Zuul:% Hiver:% Tarkas:% Liir:% Morrigi:%"
// strategy { inc TECHBEN_X dec TECHBEN_Y }
// ship { section STEM ... } -- "new section" notice (not a build gate)
@ -59,7 +59,7 @@ struct TechNode {
std::string group; // as written; group membership is matched upper-cased
std::optional<double> option_cost;
std::optional<bool> unlock_explicitly;
std::vector<std::string> requires;
std::vector<std::string> requires_tech;
std::vector<std::string> benefits_inc, benefits_dec; // TECHBEN_* tokens, kept as data
std::vector<std::string> sections; // ship{section} stems
std::vector<std::string> weapon_files; // weapon{filename}
@ -87,7 +87,7 @@ public:
std::vector<const AllowsEdge*> edges_from(std::string_view name) const;
std::vector<const TechNode*> roots() const; // nodes no edge points at
// A `requires` token is satisfiable if it names a tech or a non-empty group.
// A `requires_tech` token is satisfiable if it names a tech or a non-empty group.
bool requirement_exists(std::string_view token) const;
void rebuild_index();

View file

@ -56,7 +56,7 @@ Loaded<WeaponDef> load_weapon(const mars::parse::Document& doc, std::string file
w.weapon_class = f.required_str("weaponclass");
w.weapon_family = f.str("weaponfamily");
w.weapon_damage_type = f.str("weapondamagetype");
w.requires = f.strs("requires");
w.requires_tech = f.strs("requires");
w.compatible_section = f.strs("compatible_section");
w.exclusive_species = f.str("exclusive_species");
w.cost = f.opt_int("cost");

View file

@ -2,7 +2,7 @@
//
// weapon {
// name @WEAPON_X weaponclass bullet weaponfamily gauss
// requires TECH [requires TECH2] cost N
// requires_tech TECH [requires_tech TECH2] cost N
// turretsize small turretclass standard
// burst_volleys N recharge_time T range R range_planet RP
// fc_* true/false rating_* N
@ -72,7 +72,7 @@ struct WeaponDef {
std::string weapon_class; // drives engine behaviour (bullet, beam, missile, ...)
std::string weapon_family; // AI grouping (gauss, laser, torpedo, ...)
std::string weapon_damage_type;
std::vector<std::string> requires; // AND of techs; may be empty
std::vector<std::string> requires_tech; // AND of techs; may be empty
std::vector<std::string> compatible_section; // rider weapons: section stems they carry
std::string exclusive_species;
std::optional<std::int64_t> cost;

View file

@ -48,7 +48,7 @@ std::vector<const data::OptionGroup*> option_groups(const ShipSectionDef& s) {
std::vector<std::string> applied_techs(const ShipSectionDef& s, const std::vector<std::string>& chosen) {
std::vector<std::string> out;
if (auto hct = hull_class_tech(s)) out.push_back(*hct);
out.insert(out.end(), s.requires.begin(), s.requires.end());
out.insert(out.end(), s.requires_tech.begin(), s.requires_tech.end());
for (const data::OptionGroup* g : option_groups(s))
for (const std::string& member : g->members)
for (const std::string& c : chosen)
@ -64,7 +64,7 @@ std::vector<std::string> options_from_applied(const ShipSectionDef& s, const std
auto hct = hull_class_tech(s);
for (const std::string& t : applied) {
if (hct && iequals(t, *hct)) continue;
if (contains_fold(s.requires, t)) continue;
if (contains_fold(s.requires_tech, t)) continue;
out.push_back(t);
}
return out;
@ -248,7 +248,7 @@ std::vector<Violation> Ruleset::validate(const Design& d) const {
if (want->empty()) continue;
if (!part->def || !iequals(part->def->stem, *want))
add(v, "A8", Level::Error,
m.stem + " requires " + slot_str(part->slot) + " section " + *want + ", got " +
m.stem + " requires_tech " + slot_str(part->slot) + " section " + *want + ", got " +
(part->def ? part->def->stem : std::string("none")),
part->slot);
}
@ -365,8 +365,8 @@ std::vector<Violation> Ruleset::validate(const Design& d) const {
}
}
if (known && bank.weapon.empty())
for (const std::string& t : w->requires)
if (!known->satisfies(cat.tech, t)) add(v, "C2", Level::Error, w->stem + " requires " + t, p->slot, bi);
for (const std::string& t : w->requires_tech)
if (!known->satisfies(cat.tech, t)) add(v, "C2", Level::Error, w->stem + " requires_tech " + t, p->slot, bi);
}
}
@ -394,18 +394,18 @@ std::vector<Violation> Ruleset::validate(const Design& d) const {
// -- tech gating ----------------------------------------------------------
// Engine-generated hidden designs (the per-race "Default Assault Shuttle")
// exist in every save regardless of research: the Tarkas _AssaultShuttle
// requires DRN_AdvFrm and the design is there on turn 1. Gating on hidden
// requires_tech DRN_AdvFrm and the design is there on turn 1. Gating on hidden
// designs is therefore reported at warn level.
const Level gate = hidden ? Level::Warn : Level::Error;
for (const Filled* p : present) {
const ShipSectionDef& s = *p->def;
for (const std::string& t : s.requires) {
for (const std::string& t : s.requires_tech) {
if (!cat.tech.find(t) && !data::is_group_ref(t))
add(v, "C1", Level::Warn, s.stem + " requires unknown tech " + t, p->slot);
add(v, "C1", Level::Warn, s.stem + " requires_tech unknown tech " + t, p->slot);
else if (known && !known->satisfies(cat.tech, t))
add(v, "C1", gate, s.stem + " requires " + t, p->slot);
add(v, "C1", gate, s.stem + " requires_tech " + t, p->slot);
if (unobtainable(t, race))
add(v, "C5", Level::Error, s.stem + " requires " + t + ", which " + std::string(rk) + " can never research", p->slot);
add(v, "C5", Level::Error, s.stem + " requires_tech " + t + ", which " + std::string(rk) + " can never research", p->slot);
}
if (auto hct = hull_class_tech(s); hct && known && !known->satisfies(cat.tech, *hct))
add(v, "C4", gate, s.stem + " is a " + fold(s.section_class_text) + " section; " + *hct + " not researched", p->slot);

View file

@ -11,12 +11,12 @@
// B* banks: one weapon per bank{}, none empty, fixed NPC banks bound to
// their file, fit = class-accept table + a _turrets.txt row, weapon
// scope / exclusive_species / rider carriers / hidden weapons.
// C* tech gating: section and weapon `requires` (GRP_ = any of the group),
// C* tech gating: section and weapon `requires_tech` (GRP_ = any of the group),
// chosen options, the implicit hull-class tech (IND_CruisCon /
// IND_DreadCon, stations exempt), race availability. Hidden default
// designs are gated at warn level (code rule the data does not show).
// D* options: at most one per group, members only; applied_techs =
// [hull-class tech] + requires + chosen options in group order (D2).
// [hull-class tech] + requires_tech + chosen options in group order (D2).
//
// Nothing here throws; every finding is a Violation.
#pragma once
@ -44,12 +44,12 @@ inline bool is_station(const data::ShipSectionDef& s) { return data::iequals(s.d
std::vector<const data::OptionGroup*> option_groups(const data::ShipSectionDef& s);
// D2: the techs a section instance is built with -- exactly the save's DOpts
// list: hull-class tech, `requires` in file order, then the chosen option of
// list: hull-class tech, `requires_tech` in file order, then the chosen option of
// each group in group order (spelled as chosen).
std::vector<std::string> applied_techs(const data::ShipSectionDef& s, const std::vector<std::string>& chosen_options);
// Inverse of applied_techs for a save record: drops the hull-class tech and
// the `requires` from a DOpts list, leaving the chosen options.
// the `requires_tech` from a DOpts list, leaving the chosen options.
std::vector<std::string> options_from_applied(const data::ShipSectionDef& s, const std::vector<std::string>& applied);
// B4: which weapon turret classes a bank turret class takes besides an exact

View file

@ -191,7 +191,7 @@ J weapon_json(const WeaponDef& w) {
.set("weaponclass", jstr(w.weapon_class))
.set("weaponfamily", jstr(w.weapon_family))
.set("weapondamagetype", jstr(w.weapon_damage_type))
.set("requires", jstrs(w.requires))
.set("requires", jstrs(w.requires_tech))
.set("compatible_section", jstrs(w.compatible_section))
.set("exclusive_species", jstr(w.exclusive_species))
.set("cost", jopt(w.cost))
@ -270,7 +270,7 @@ J section_json(const ShipSectionDef& s) {
JObj t;
t.set("model", jstr(s.model))
.set("dam_model", jstr(s.dam_model))
.set("requires", jstrs(s.requires))
.set("requires", jstrs(s.requires_tech))
.set("section_type", jstr(s.section_type_text))
.set("section_type_enum", jstr(section_type_name(s.section_type)))
.set("section_class", jstr(s.section_class_text))
@ -385,7 +385,7 @@ J tech_json(const TechTree& t) {
.set("group", n.group.empty() ? jnull() : jstr(n.group))
.set("option_cost", jopt(n.option_cost))
.set("unlock_explicitly", jopt(n.unlock_explicitly))
.set("requires", jstrs(n.requires))
.set("requires", jstrs(n.requires_tech))
.set("benefits_inc", jstrs(n.benefits_inc))
.set("benefits_dec", jstrs(n.benefits_dec))
.set("sections", jstrs(n.sections))

View file

@ -57,17 +57,17 @@ int main() {
const WeaponDef* g = cat.weapon("bal_gauss");
EXPECT(g && g->id && *g->id == 8);
EXPECT(g && g->cost && *g->cost == 50 && g->turret_size == "small" && g->turret_class == "standard");
EXPECT(g && g->requires.size() == 1 && g->requires[0] == "WEP_GsDrvr");
EXPECT(g && g->requires_tech.size() == 1 && g->requires_tech[0] == "WEP_GsDrvr");
EXPECT(g && g->bolt && g->bolt->rangetable.maximum.range && *g->bolt->rangetable.maximum.range == 455);
EXPECT(g && g->bolt && g->bolt->planet.pop && *g->bolt->planet.pop == 3500);
EXPECT(g && g->fc.requires_los && *g->fc.requires_los && g->fc.manual_target && !*g->fc.manual_target);
EXPECT(g && g->display_name && *g->display_name == "Gauss Cannon");
EXPECT(cat.weapon_by_id(8) == g);
EXPECT(cat.weapon("mis") && cat.weapon("mis")->requires.empty());
EXPECT(cat.weapon("mis") && cat.weapon("mis")->requires_tech.empty());
const ShipSectionDef* fis = cat.section("Human", "DEFission");
EXPECT(fis && fis->id && *fis->id == 47);
EXPECT(fis && fis->requires.size() == 2 && fis->requires[1] == "DRV_Node");
EXPECT(fis && fis->requires_tech.size() == 2 && fis->requires_tech[1] == "DRV_Node");
EXPECT(fis && fis->section_type == SectionType::Engine && fis->section_class == SectionClass::Destroyer);
EXPECT(fis && fis->options.size() == 3 && fis->options[0].members.size() == 5 && fis->options[2].scalar &&
fis->options[2].members[0] == "DRV_RecFiss");
@ -131,7 +131,7 @@ int main() {
// ---- cross-check --------------------------------------------------
CrossCheck x = cat.cross_check();
std::printf("cross-check: dangling %zu (manifest gaps %zu, unresolved names %zu); case mismatches %zu/%zu; "
"no-requires %zu; missing sectiondesc %zu; roots %zu\n",
"no-requires_tech %zu; missing sectiondesc %zu; roots %zu\n",
x.dangling_count(), x.manifest_ids_without_file.size(), x.unresolved_weapon_names.size(),
x.weapon_requires_case_mismatch.size(), x.section_requires_case_mismatch.size(),
x.weapons_without_requires.size(), x.missing_sectiondesc.size(), x.tech_roots.size());

View file

@ -22,10 +22,10 @@ fs::path make_root() {
fs::path root = fs::temp_directory_path() / ("game_data_test_" + std::to_string(static_cast<long long>(stamp)));
fs::remove_all(root);
put(root / "Weapons/bal_gauss.weapon",
"weapon { name @WEAPON_BAL_GAUSS weaponclass bullet requires WEP_GsDrvr cost 50 turretsize small turretclass standard\n"
"weapon { name @WEAPON_BAL_GAUSS weaponclass bullet requires_tech WEP_GsDrvr cost 50 turretsize small turretclass standard\n"
" bolt { rangetable { max_range 455 } dam_pop 3500 } }\n");
put(root / "Weapons/las_odd.weapon",
"weapon { name @WEAPON_LAS_ODD weaponclass beam requires WEP_NOPE requires wep_gsdrvr turretsize Large turretclass Beam }\n");
"weapon { name @WEAPON_LAS_ODD weaponclass beam requires_tech WEP_NOPE requires_tech wep_gsdrvr turretsize Large turretclass Beam }\n");
put(root / "Weapons/mis.weapon",
"weapon { name @WEAPON_MIS weaponclass missile turretsize medium turretclass missile }\n");
put(root / "Weapons/_weapons.txt", "// ids\n8 bal_gauss.weapon\n21 mis.weapon\n// DELETED - 36\n40 ghost.weapon\n");
@ -38,7 +38,7 @@ fs::path make_root() {
" bank { turretclass standard turretsize small mount { node N1 } } }\n");
put(root / "Species/Human/sections/DEFission.shipsection",
"shipsection { model b.X section_type engine section_class destroyer health 1 mass 2 socket_fore EngineNode\n"
" requires DRV_Fissn requires grp_torps requires GRP_Empty\n"
" requires_tech DRV_Fissn requires_tech grp_torps requires_tech GRP_Empty\n"
" bank { turretclass strafe turretsize small mount { node N1 } } }\n");
put(root / "Species/Human/sections/_shipsections.txt", "1 DECommand.shipsection\n47 defission.SHIPSECTION\n98 DEWar.shipsection\n");
put(root / "Species/_NPC/sections/_Herald.shipsection",
@ -52,7 +52,7 @@ fs::path make_root() {
"tech { name \"IND_ROOT\" family \"IND\" allows \"IND_PlyAlloy RP:5000\" allows \"WEP_Missing RP:1\" allows \"BAD\" }\n"
"tech { name \"IND_PlyAlloy\" ship { section DECommand section DENowhere } weapon { filename \"Weapons/bal_gauss.weapon\" }\n"
" weapon { filename \"Weapons/nope.weapon\" } }\n"
"tech { name \"WEP_GsDrvr\" group TORPS requires \"GRP_TORPS\" requires \"GRP_Nothing\" requires \"IND_Gone\" }\n"
"tech { name \"WEP_GsDrvr\" group TORPS requires_tech \"GRP_TORPS\" requires_tech \"GRP_Nothing\" requires_tech \"IND_Gone\" }\n"
"tech { name \"DRV_Fissn\" }\n");
put(root / "Locale/EN/Strings.csv",
"#Key+A955,String,Size,Notes\n"

View file

@ -53,8 +53,8 @@ TEST(section_typed_fields) {
CHECK(s.section_type == SectionType::Engine);
CHECK_EQ(s.section_class_text, "Destroyer");
CHECK(s.section_class == SectionClass::Destroyer);
CHECK_EQ(s.requires.size(), std::size_t{2});
CHECK_EQ(s.requires[1], "DRV_Node");
CHECK_EQ(s.requires_tech.size(), std::size_t{2});
CHECK_EQ(s.requires_tech[1], "DRV_Node");
CHECK_EQ(s.engine_techera, "fission");
CHECK(s.health && *s.health == 450);
CHECK(s.mass && *s.mass == 2500);

View file

@ -125,7 +125,7 @@ TEST(tech_tree_nodes_edges_groups) {
const TechNode* alloy = t.find("IND_PlyAlloy");
CHECK(alloy->option_cost && *alloy->option_cost == 1.2); // OPTION_COST key case
CHECK_EQ(alloy->requires.size(), std::size_t{1});
CHECK_EQ(alloy->requires_tech.size(), std::size_t{1});
CHECK(t.find("IND_StlthArm")->unlock_explicitly.value_or(false));
const TechNode* dsr = t.find("WEP_Dsrptr");

View file

@ -56,9 +56,9 @@ TEST(weapon_typed_fields) {
CHECK_EQ(w.name, "@WEAPON_TEST_GUN");
CHECK_EQ(w.weapon_class, "bullet");
CHECK_EQ(w.weapon_family, "gauss");
CHECK_EQ(w.requires.size(), std::size_t{2});
CHECK_EQ(w.requires[0], "WEP_A");
CHECK_EQ(w.requires[1], "WEP_B");
CHECK_EQ(w.requires_tech.size(), std::size_t{2});
CHECK_EQ(w.requires_tech[0], "WEP_A");
CHECK_EQ(w.requires_tech[1], "WEP_B");
CHECK(w.cost && *w.cost == 50);
CHECK_EQ(w.turret_size, "small");
CHECK_EQ(w.turret_class, "Standard"); // kept as written

View file

@ -43,7 +43,7 @@ std::string de_armor(const char* extra = "") {
std::string de_fission(const char* drive) {
return "shipsection { model c.X Section_Type Engine section_class Destroyer socket_fore EngineNode\n"
" requires DRV_Fissn requires " + std::string(drive) +
" requires_tech DRV_Fissn requires_tech " + std::string(drive) +
"\n health 450 mass 2500 cost 5000 cpoints 1380 engine_techera fission ftlspeed .2 nodespeed 4 range 9\n" +
std::string(kArmorOptions) + " option DRV_RecFiss\n"
" netforcelimits { force_forward 23000 force_right 23000 force_up 23000 torque_yaw 5 torque_pitch 5 torque_roll 5 speed 40 rotspeed 10 }\n" +
@ -59,7 +59,7 @@ std::string de_platform() {
const char* kRider = "shipsection { model r.X section_class destroyer design_class rider entity_class rider health 50 mass 100 cost 100 }\n";
const char* kRiderGated =
"shipsection { model r.X section_class destroyer design_class rider entity_class rider requires DRN_AdvFrm health 50 mass 100 cost 100 }\n";
"shipsection { model r.X section_class destroyer design_class rider entity_class rider requires_tech DRN_AdvFrm health 50 mass 100 cost 100 }\n";
void write_human(const fs::path& r) {
fs::path s = r / "Species/Human/sections";
@ -75,10 +75,10 @@ void write_human(const fs::path& r) {
bank("strafe", "small", 4) + bank("standard", "large", 1) + "}\n");
put(s / "DETorpedo.shipsection",
"shipsection { model e.X section_type mission section_class destroyer socket_fore F socket_aft A health 1 mass 1 cost 1\n"
" requires GRP_Torps\n" + bank("torpedo", "large", 1) + bank("missile", "medium", 1) + "}\n");
" requires_tech GRP_Torps\n" + bank("torpedo", "large", 1) + bank("missile", "medium", 1) + "}\n");
put(s / "DEAssaultShuttle.shipsection",
"shipsection { model e.X section_type mission section_class destroyer socket_fore F socket_aft A health 1 mass 1 cost 1\n"
" requires DRN_AdvFrm\n" + bank("assaultshuttlerider", "large", 1) + bank("tarkahunter", "large", 1) + "}\n");
" requires_tech DRN_AdvFrm\n" + bank("assaultshuttlerider", "large", 1) + bank("tarkahunter", "large", 1) + "}\n");
put(s / "DEPlanetMissile.shipsection",
"shipsection { model e.X section_type mission section_class destroyer socket_fore F socket_aft A health 1 mass 1 cost 1\n" +
bank("planetmissile", "large", 1) + "}\n");
@ -90,12 +90,12 @@ void write_human(const fs::path& r) {
bank("standard", "large", 2) + "}\n");
put(s / "CRFission.shipsection",
"shipsection { model f.X section_type engine section_class cruiser socket_fore EngineNode health 1 mass 30 cost 300 crew 15\n"
" requires DRV_Fissn requires DRV_Node engine_techera fission ftlspeed .2 nodespeed 4\n" +
" requires_tech DRV_Fissn requires_tech DRV_Node engine_techera fission ftlspeed .2 nodespeed 4\n" +
bank("standard", "medium", 1) + "}\n");
// the station triple (design_class station: no hull-class tech; partners pinned)
put(s / "DNStation.shipsection",
"shipsection { model g.X section_type mission section_class dreadnought socket_fore Fore socket_aft Aft\n"
" requires IND_OrbCom health 9000 mass 100000 cost 700000 crew 100 entity_class \"Station\" design_class station command_quota 46\n"
" requires_tech IND_OrbCom health 9000 mass 100000 cost 700000 crew 100 entity_class \"Station\" design_class station command_quota 46\n"
" explicit_command_section \"DNStation_Fore\" explicit_engine_section \"DNStation_Aft\"\n" +
bank("standard", "large", 2) + "}\n");
put(s / "DNStation_Fore.shipsection",

View file

@ -42,7 +42,7 @@ TEST(C1_group_requirements) {
TEST(C1_unknown_tech_is_a_warning) {
Design d = armor();
d.engine() = use("DEOddTech", {"bal_gauss"});
// DEOddTech requires DRV_Fissn (known) and XXX_Nowhere (not in the tree):
// DEOddTech requires_tech DRV_Fissn (known) and XXX_Nowhere (not in the tree):
// a token the tree cannot satisfy is reported, never an error.
std::vector<Violation> v = mini_rules().validate(d);
CHECK_EQ(join(rules_at(v, Level::Error)), "");