// ShipSectionDef loader on hand-written samples; option normalisation. #include "game/data/shipsection.h" #include "test_main.h" using namespace game::data; namespace { const char* kEngine = "shipsection\n{\n" "\tmodel Species/Test/art/sections/Engine.X\n" "\tsocket_fore EngineNode\n" "\tSection_Type Engine\n" "\tsection_class Destroyer\n" "\trequires DRV_Fissn\n" "\trequires DRV_Node\n" "\tengine_techera fission\n" "\thealth 450\n\tmass 2500\n\tcost 5000\n\tcpoints 1380\n\tcrew 3\n" "\tnodespeed 4\n\tftlspeed .2\n\trange 9\n" "\toption IND_First\n" // scalar group before the blocks "\toption { option IND_PlyAlloy option IND_MagLat option IND_QrkRes }\n" "\toption { option IND_RefCoat option IND_ImpRfCt }\n" "\toption DRV_RecFiss\n" // scalar group after them "\toptiondef { option SLD_MkOne option SLD_MkTwo }\n" "\texclude \"DEFission\"\n\texclude \"DEPulseFission\"\n" "\tnetforcelimits { force_forward 23000 force_right 23000 force_up 23000\n" "\t torque_yaw 0 torque_pitch 0 torque_roll 6.4e+7 speed 40 rotspeed -10 }\n" "\tthruster { node EngineThruster01 effect effects/a.effect idle_effect effects/b.effect }\n" "\tthruster { node EngineThruster02 effect effects/a.effect }\n" "\tbank\n\t{\n" "\t\tturretclass standard\n\t\tturretsize medium\n\t\tturretsize small\n" // repeated: last wins "\t\tmount { node LightGunNode15 min_azimuth -140 max_azimuth 140 min_inclination -12 max_inclination 90 }\n" "\t\tmount { node LightGunNode16 min_azimuth -140 max_azimuth 140 home_azimuth 0 }\n" "\t}\n" "\tbank { weapon \"Species/_NPC/weapons/fixed.weapon\" turretclass standard turretsize small showturrets false\n" "\t mount { node N1 min_azimuth 0 max_azimuth 0 } }\n" "\tautonomous 1\n" "\tnodesign true\n" "}\n"; } // namespace TEST(section_typed_fields) { auto r = parse_shipsection(kEngine, "Species/Test/sections/DEEngine.shipsection"); CHECK(r.ok()); CHECK(r.problems.empty()); const ShipSectionDef& s = *r.value; CHECK_EQ(s.model, "Species/Test/art/sections/Engine.X"); CHECK_EQ(s.socket_fore, "EngineNode"); CHECK_EQ(s.socket_aft, ""); CHECK(s.has_sockets()); CHECK_EQ(s.section_type_text, "Engine"); CHECK(s.section_type == SectionType::Engine); CHECK_EQ(s.section_class_text, "Destroyer"); CHECK(s.section_class == SectionClass::Destroyer); 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); CHECK(s.cost && *s.cost == 5000); CHECK(s.cpoints && *s.cpoints == 1380); CHECK(s.crew && *s.crew == 3); CHECK(s.nodespeed && *s.nodespeed == 4); CHECK(s.ftlspeed && *s.ftlspeed == 0.2); CHECK(s.range && *s.range == 9); CHECK(s.autonomous && *s.autonomous); CHECK(s.nodesign && *s.nodesign); CHECK(!s.explicit_section); CHECK_EQ(s.exclude.size(), std::size_t{2}); CHECK_EQ(s.exclude[1], "DEPulseFission"); } TEST(section_option_normalisation_keeps_file_order_across_forms) { auto r = parse_shipsection(kEngine); CHECK(r.ok()); const ShipSectionDef& s = *r.value; CHECK_EQ(s.options.size(), std::size_t{4}); CHECK(s.options[0].scalar); CHECK_EQ(s.options[0].members.size(), std::size_t{1}); CHECK_EQ(s.options[0].members[0], "IND_First"); CHECK(!s.options[1].scalar); CHECK_EQ(s.options[1].members.size(), std::size_t{3}); CHECK_EQ(s.options[1].members[2], "IND_QrkRes"); CHECK(!s.options[2].scalar); CHECK_EQ(s.options[2].members.size(), std::size_t{2}); CHECK(s.options[3].scalar); CHECK_EQ(s.options[3].members[0], "DRV_RecFiss"); CHECK(s.options[0].line < s.options[1].line && s.options[2].line < s.options[3].line); CHECK(s.optiondef.has_value()); CHECK_EQ(s.optiondef->members.size(), std::size_t{2}); CHECK_EQ(s.optiondef->members[1], "SLD_MkTwo"); } TEST(section_banks_and_mounts) { auto r = parse_shipsection(kEngine); CHECK(r.ok()); const ShipSectionDef& s = *r.value; CHECK_EQ(s.banks.size(), std::size_t{2}); const BankDef& b0 = s.banks[0]; CHECK_EQ(b0.turret_class, "standard"); CHECK_EQ(b0.turret_size, "small"); // last of the two turretsize lines CHECK(b0.repeated_turret_spec); CHECK_EQ(b0.weapon, ""); CHECK(!b0.show_turrets); CHECK_EQ(b0.mounts.size(), std::size_t{2}); CHECK_EQ(b0.mounts[0].node, "LightGunNode15"); CHECK(b0.mounts[0].min_azimuth && *b0.mounts[0].min_azimuth == -140); CHECK(b0.mounts[0].max_inclination && *b0.mounts[0].max_inclination == 90); CHECK(!b0.mounts[1].min_inclination); CHECK(b0.mounts[1].home_azimuth && *b0.mounts[1].home_azimuth == 0); const BankDef& b1 = s.banks[1]; CHECK_EQ(b1.weapon, "Species/_NPC/weapons/fixed.weapon"); CHECK(!b1.repeated_turret_spec); CHECK(b1.show_turrets && !*b1.show_turrets); CHECK_EQ(b1.mounts.size(), std::size_t{1}); } TEST(section_netforcelimits_and_thrusters) { auto r = parse_shipsection(kEngine); CHECK(r.ok()); const ShipSectionDef& s = *r.value; CHECK(s.netforcelimits.has_value()); CHECK(s.netforcelimits->force_forward && *s.netforcelimits->force_forward == 23000); CHECK(s.netforcelimits->torque_roll && *s.netforcelimits->torque_roll == 6.4e7); CHECK(s.netforcelimits->rotspeed && *s.netforcelimits->rotspeed == -10); CHECK_EQ(s.thrusters.size(), std::size_t{2}); CHECK_EQ(s.thrusters[0].idle_effect, "effects/b.effect"); CHECK_EQ(s.thrusters[1].idle_effect, ""); } TEST(section_enum_parsing_is_case_insensitive) { CHECK(parse_section_type("command") == SectionType::Command); CHECK(parse_section_type("Command") == SectionType::Command); CHECK(parse_section_type("MISSION") == SectionType::Mission); CHECK(parse_section_type("") == SectionType::None); CHECK(parse_section_type("rider") == SectionType::Other); CHECK(parse_section_class("Dreadnought") == SectionClass::Dreadnought); CHECK(parse_section_class("cruiser") == SectionClass::Cruiser); CHECK(parse_section_class("station") == SectionClass::Other); CHECK_EQ(std::string(section_type_name(SectionType::Engine)), "engine"); } TEST(section_bad_values_are_problems_raw_keeps_text) { auto r = parse_shipsection( "shipsection { model m.X health 10 mass 20 crew false netforcelimits { force_right o speed 1 } }", "s.shipsection"); CHECK(r.ok()); int bad = 0; for (const Problem& p : r.problems) if (p.kind == Problem::Kind::BadValue) ++bad; CHECK_EQ(bad, 2); CHECK(!r.value->crew); CHECK(r.value->netforcelimits && !r.value->netforcelimits->force_right); CHECK(r.value->netforcelimits->speed && *r.value->netforcelimits->speed == 1); CHECK_EQ(r.value->raw.str("crew"), "false"); } TEST(section_missing_required_keys_reported) { auto r = parse_shipsection("shipsection { section_type mission }"); CHECK(r.ok()); int missing = 0; for (const Problem& p : r.problems) if (p.kind == Problem::Kind::MissingKey) ++missing; CHECK_EQ(missing, 3); // model, health, mass CHECK(!r.value->has_sockets()); CHECK(r.value->options.empty()); CHECK(!r.value->optiondef); CHECK(!r.value->netforcelimits); } TEST(section_unclosed_file_is_recovered) { auto r = parse_shipsection("shipsection\n{\n model m.X\n health 1\n mass 2\n bank { turretclass standard turretsize small }\n"); CHECK(r.ok()); CHECK_EQ(r.problems.size(), std::size_t{1}); CHECK(r.problems[0].kind == Problem::Kind::Syntax); CHECK_EQ(r.value->banks.size(), std::size_t{1}); } TEST(section_long_tail_reachable_through_raw) { auto r = parse_shipsection("shipsection { model m.X health 1 mass 2 refinery true mining_rate 5 death_effect a death_effect b }"); CHECK(r.ok()); const Block& raw = r.value->raw; CHECK(raw.find("refinery") && raw.find("refinery")->as_bool().value_or(false)); CHECK(raw.find("MINING_RATE") && *raw.find("MINING_RATE")->as_int() == 5); CHECK_EQ(raw.strs("death_effect").size(), std::size_t{2}); CHECK(!raw.has("police")); }