// TechTree: allows edges, groups, links, on a hand-written tree. #include "game/data/techtree.h" #include "test_main.h" using namespace game::data; namespace { const char* kTree = "tech\n{\n" "\tname \t\"IND_ROOT\"\n" "\tfamily\t\"IND\"\n" "\tallows \t\"IND_Waldo \t\tRP:5000\"\n" "\tallows \t\"IND_StlthArm \tRP:0\t\tHuman:0 Zuul:0 Hiver:0 Tarkas:0 Liir:0 Morrigi:100\"\n" "}\n" "tech {\n" "\tname\t\"IND_Waldo\"\n" "\tthreat\t2\n" "\ttype\tP\n" "\tallows\t\"IND_PlyAlloy RP:20000 Human:50 Liir:95\"\n" "\tallows\t\"WEP_Dsrptr RP:10000 Human:20 Zuul:90 Hiver:40 Tarkas:80 Liir:90 Morrigi:80\"\n" "\tstrategy { inc TECHBEN_INDOUTPUT inc TECHBEN_HULLSTR dec TECHBEN_SHIPCONCOST }\n" "\tship { section DEHammerhead section CRHammerhead }\n" "}\n" "tech {\n" "\tname\t\"IND_PlyAlloy\"\n" "\tOPTION_COST\t1.2\n" "\trequires\t\"IND_Waldo\"\n" "}\n" "tech {\n" "\tname\t\"IND_StlthArm\"\n" "\tunlock_explicitly\ttrue\n" "}\n" "tech {\n" "\tname\t\"WEP_Dsrptr\"\n" "\tfamily\t\"TRP\"\n" "\tGROUP\tTorps\n" "\tweapon { filename \"Weapons/trp_disruptor.weapon\" }\n" "}\n" "tech {\n" "\tname\t\"WEP_PhotTrp\"\n" "\tgroup\tTORPS\n" "\trequires\tGRP_Torps\n" "\trequires\tGRP_Mines\n" "}\n"; } // namespace TEST(allows_parsing_full_and_partial) { AllowsEdge e = parse_allows("DRV_Node \t\tRP:0\t\tHuman:100\tZuul:0\t\tHiver:0\t\tTarkas:0\tLiir:0\t\tMorrigi:0", "DRV_ROOT", 7); CHECK_EQ(e.from, "DRV_ROOT"); CHECK_EQ(e.to, "DRV_Node"); CHECK(e.rp && *e.rp == 0); CHECK_EQ(e.percent(Species::Human), 100); CHECK_EQ(e.percent(Species::Zuul), 0); CHECK_EQ(e.percent(Species::Morrigi), 0); CHECK_EQ(e.percent(Species::NPC), 100); // never written; engine default CHECK(e.pct_written[static_cast(Species::Human)]); CHECK(!e.pct_written[static_cast(Species::NPC)]); CHECK(e.unparsed.empty()); CHECK_EQ(e.line, 7); AllowsEdge p = parse_allows("DRV_Fusn RP:85000", "DRV_Fissn"); CHECK(p.rp && *p.rp == 85000); for (int i = 0; i < kSpeciesCount; ++i) { CHECK_EQ(p.pct[static_cast(i)], kDefaultAllowPercent); CHECK(!p.pct_written[static_cast(i)]); } AllowsEdge q = parse_allows("X rp:5 human:10", "Y"); // key case does not matter CHECK(q.rp && *q.rp == 5); CHECK_EQ(q.percent(Species::Human), 10); } TEST(allows_unparsed_tokens_are_reported) { std::vector problems; AllowsEdge e = parse_allows("CHILD Klingon:50 bogus RP:x", "T", 3, &problems, "f.tech"); CHECK_EQ(e.to, "CHILD"); CHECK(!e.rp); CHECK_EQ(e.unparsed.size(), std::size_t{3}); CHECK_EQ(problems.size(), std::size_t{4}); // no RP + 3 tokens CHECK(problems[0].kind == Problem::Kind::Unparsed); CHECK_EQ(problems[0].file, "f.tech"); std::vector none; AllowsEdge empty = parse_allows(" ", "T", 1, &none); CHECK_EQ(empty.to, ""); CHECK_EQ(none.size(), std::size_t{1}); } TEST(tech_tree_nodes_edges_groups) { auto r = parse_tech_tree(kTree, "MasterTechList.tech"); CHECK(r.ok()); CHECK(r.problems.empty()); const TechTree& t = *r.value; CHECK_EQ(t.nodes.size(), std::size_t{6}); CHECK_EQ(t.edges.size(), std::size_t{4}); const TechNode* root = t.find("ind_root"); // case-insensitive CHECK(root != nullptr); CHECK_EQ(root->family, "IND"); CHECK_EQ(root->family_inferred, "IND"); CHECK(root->is_root()); CHECK_EQ(root->allows.size(), std::size_t{2}); const AllowsEdge& stealth = t.edges[root->allows[1]]; CHECK_EQ(stealth.to, "IND_StlthArm"); CHECK_EQ(stealth.percent(Species::Morrigi), 100); CHECK_EQ(stealth.percent(Species::Human), 0); CHECK_EQ(stealth.percent(Species::NPC), 100); const TechNode* waldo = t.find("IND_Waldo"); CHECK(waldo != nullptr); CHECK(waldo->threat && *waldo->threat == 2); CHECK_EQ(waldo->type, "P"); CHECK_EQ(waldo->family, ""); CHECK_EQ(waldo->family_inferred, "IND"); CHECK(!waldo->is_root()); CHECK_EQ(waldo->benefits_inc.size(), std::size_t{2}); CHECK_EQ(waldo->benefits_inc[1], "TECHBEN_HULLSTR"); CHECK_EQ(waldo->benefits_dec.size(), std::size_t{1}); CHECK_EQ(waldo->sections.size(), std::size_t{2}); CHECK_EQ(waldo->sections[0], "DEHammerhead"); const AllowsEdge& ply = t.edges[waldo->allows[0]]; CHECK_EQ(ply.percent(Species::Human), 50); CHECK_EQ(ply.percent(Species::Liir), 95); CHECK_EQ(ply.percent(Species::Zuul), 100); // unlisted -> default const TechNode* alloy = t.find("IND_PlyAlloy"); CHECK(alloy->option_cost && *alloy->option_cost == 1.2); // OPTION_COST key case 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"); CHECK_EQ(dsr->family, "TRP"); CHECK_EQ(dsr->family_inferred, "WEP"); CHECK_EQ(dsr->group, "Torps"); CHECK_EQ(dsr->weapon_files.size(), std::size_t{1}); CHECK_EQ(dsr->weapon_files[0], "Weapons/trp_disruptor.weapon"); CHECK_EQ(t.groups.size(), std::size_t{1}); CHECK_EQ(t.groups[0].first, "TORPS"); CHECK_EQ(t.groups[0].second.size(), std::size_t{2}); const auto* m = t.group_members("GRP_torps"); CHECK(m && m->size() == 2 && (*m)[1] == "WEP_PhotTrp"); CHECK(t.group_members("TORPS") == m); CHECK(t.group_members("GRP_MINES") == nullptr); CHECK(t.requirement_exists("GRP_Torps")); CHECK(!t.requirement_exists("GRP_Mines")); CHECK(t.requirement_exists("ind_waldo")); CHECK(!t.requirement_exists("IND_Nope")); CHECK_EQ(t.edges_to("WEP_Dsrptr").size(), std::size_t{1}); CHECK_EQ(t.edges_from("IND_ROOT").size(), std::size_t{2}); auto roots = t.roots(); CHECK_EQ(roots.size(), std::size_t{2}); // IND_ROOT and WEP_PhotTrp; everything else is allowed by something CHECK_EQ(roots[0]->name, "IND_ROOT"); CHECK_EQ(roots[1]->name, "WEP_PhotTrp"); } TEST(tech_tree_problems) { auto r = parse_tech_tree("tech { name \"A\" allows \"B\" } tech { name \"a\" } tech { threat 1 }", "t.tech"); CHECK(r.ok()); int unparsed = 0, dup = 0, missing = 0; for (const Problem& p : r.problems) { if (p.kind == Problem::Kind::Unparsed) ++unparsed; if (p.kind == Problem::Kind::Duplicate) ++dup; if (p.kind == Problem::Kind::MissingKey) ++missing; } CHECK_EQ(unparsed, 1); // allows "B" without RP: CHECK_EQ(dup, 1); // "a" repeats "A" case-insensitively CHECK_EQ(missing, 1); // nameless tech dropped CHECK_EQ(r.value->nodes.size(), std::size_t{2}); CHECK(!parse_tech_tree("weapon { }").ok()); }