sots-engine/tests/game_data/test_shipsection.cpp

187 lines
8.1 KiB
C++

// 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"));
}