// Catalog on a hand-written miniature data root written to a temp directory. #include #include #include #include #include "game/data/catalog.h" #include "test_main.h" using namespace game::data; namespace fs = std::filesystem; namespace { void put(const fs::path& p, const char* text) { fs::create_directories(p.parent_path()); std::ofstream(p, std::ios::binary) << text; } fs::path make_root() { auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); fs::path root = fs::temp_directory_path() / ("game_data_test_" + std::to_string(static_cast(stamp))); fs::remove_all(root); put(root / "Weapons/bal_gauss.weapon", "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_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"); put(root / "Weapons/_turrets.txt", "small small standard 120 320 1 1 \"s.x\"\nmedium medium standard 500 140 1 1 \"m.x\"\n" "medium medium missile 400 100 1 0.25 \"mm.x\"\nlarge large beam 1500 80 1 1 \"\"\n"); put(root / "Species/Human/sections/DECommand.shipsection", "shipsection { model a.X section_type command section_class destroyer health 1 mass 2 socket_aft CommandNode\n" " option { option IND_PlyAlloy option IND_Nope }\n" " 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_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", "shipsection { model c.X section_class cruiser health 1 mass 2\n" " bank { weapon \"Species/_NPC/weapons/Herald_gun.weapon\" turretclass standard turretsize medium mount { node N1 } }\n" " bank { weapon \"Species/_NPC/weapons/Missing.weapon\" mount { node N2 } } }\n"); put(root / "Species/_NPC/sections/_shipsections.txt", "1 _Herald.shipsection\n"); put(root / "Species/_NPC/weapons/Herald_gun.weapon", "weapon { name @WEAPON_NPC_HERALD weaponclass bullet turretsize medium turretclass standard hidden 1 }\n"); put(root / "TechTree/MasterTechList.tech", "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_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" "WEAPON_BAL_GAUSS,Gauss Cannon,,\n" "WEAPON_MIS,Missile,,\n" "SECTIONNAME_DECommand,Command,,\n" "SECTIONDESC_DECommand,\"The, command\",,\n" "SECTIONNAME_DEFission,Fission,,\n" "TECHNAME_IND_ROOT,Industrial,,\n" "TECHNAME_IND_PlyAlloy,Polysilicate Alloys,,\n" "TECHDESC_IND_PlyAlloy,desc,,\n" "TECHNAME_WEP_GsDrvr,Gauss Driver,,\n" "TECHNAME_DRV_Fissn,Fission,,\n"); return root; } bool has_ref(const std::vector& v, const std::string& from, const std::string& ref) { for (const auto& r : v) if (r.from == from && r.ref == ref) return true; return false; } } // namespace TEST(catalog_loads_mini_root) { fs::path root = make_root(); Catalog cat = load_catalog(root); CHECK_EQ(cat.weapons.size(), std::size_t{4}); CHECK_EQ(cat.sections.size(), std::size_t{3}); CHECK_EQ(cat.tech.nodes.size(), std::size_t{4}); CHECK_EQ(cat.tech.edges.size(), std::size_t{3}); CHECK_EQ(cat.turrets.size(), std::size_t{4}); CHECK_EQ(cat.weapon_ids.size(), std::size_t{3}); CHECK(cat.weapon_ids.is_deleted(36)); CHECK_EQ(cat.section_ids.size(), std::size_t{2}); CHECK_EQ(cat.races.size(), std::size_t{2}); // Human, _NPC (sorted, '_' > letters) CHECK(cat.strings_loaded); CHECK_EQ(cat.strings.size(), std::size_t{10}); // ordering: player weapons first, folded stems CHECK_EQ(cat.weapons[0].stem, "bal_gauss"); CHECK_EQ(cat.weapons[1].stem, "las_odd"); CHECK_EQ(cat.weapons[2].stem, "mis"); CHECK_EQ(cat.weapons[3].stem, "Herald_gun"); CHECK(cat.weapons[3].scope == WeaponScope::NPC); CHECK_EQ(cat.weapons[3].file, "Species/_NPC/weapons/Herald_gun.weapon"); CHECK(!cat.weapons[3].id); CHECK(!cat.weapons[3].display_name); // token missing from strings const WeaponDef* g = cat.weapon("BAL_GAUSS"); CHECK(g != nullptr); CHECK(g->id && *g->id == 8); CHECK(g->display_name && *g->display_name == "Gauss Cannon"); CHECK(cat.weapon_by_id(8) == g); CHECK(cat.weapon_by_file("weapons/BAL_GAUSS.weapon") == g); CHECK(cat.weapon_by_file("Species\\_NPC\\weapons\\herald_gun.weapon") == &cat.weapons[3]); CHECK(cat.weapon("nothing") == nullptr); CHECK(cat.weapon_by_id(40) == nullptr); const ShipSectionDef* cmd = cat.section("human", "decommand"); CHECK(cmd != nullptr); CHECK(cmd->species == Species::Human); CHECK(cmd->id && *cmd->id == 1); CHECK(cmd->display_name && *cmd->display_name == "Command"); CHECK(cmd->description && *cmd->description == "The, command"); CHECK_EQ(cmd->unlocked_by.size(), std::size_t{1}); CHECK_EQ(cmd->unlocked_by[0], "IND_PlyAlloy"); const ShipSectionDef* fis = cat.section("Human", "DEFission"); CHECK(fis && fis->id && *fis->id == 47); // manifest spelled defission.SHIPSECTION CHECK(fis->unlocked_by.empty()); CHECK(!fis->description); CHECK(cat.section_by_id("Human", 47) == fis); CHECK(cat.section_by_id("Human", 98) == nullptr); CHECK(cat.section_by_id("Zuul", 1) == nullptr); const ShipSectionDef* herald = cat.section("_NPC", "_Herald"); CHECK(herald && herald->species == Species::NPC); CHECK_EQ(cat.sections_named("decommand").size(), std::size_t{1}); CHECK(cat.sections_named("nope").empty()); const TechNode* ply = cat.tech_node("ind_plyalloy"); CHECK(ply && ply->display_name && *ply->display_name == "Polysilicate Alloys"); CHECK(ply->description && *ply->description == "desc"); CHECK(!cat.tech_node("IND_ROOT")->description); // load-time problems: the bad allows string only (everything else is well formed) int unparsed = 0; for (const Problem& p : cat.problems) if (p.kind == Problem::Kind::Unparsed) ++unparsed; CHECK_EQ(unparsed, 1); fs::remove_all(root); } TEST(catalog_cross_check_reports_every_dangling_kind) { fs::path root = make_root(); Catalog cat = load_catalog(root); CrossCheck x = cat.cross_check(); CHECK(x.strings_available); CHECK(!x.clean()); CHECK_EQ(x.weapon_requires_dangling.size(), std::size_t{1}); CHECK(has_ref(x.weapon_requires_dangling, "Weapons/las_odd.weapon", "WEP_NOPE")); CHECK_EQ(x.weapon_requires_case_mismatch.size(), std::size_t{1}); CHECK(has_ref(x.weapon_requires_case_mismatch, "Weapons/las_odd.weapon", "wep_gsdrvr")); CHECK_EQ(x.weapons_without_requires.size(), std::size_t{2}); // mis, Herald_gun CHECK_EQ(x.section_requires_dangling.size(), std::size_t{1}); // GRP_Empty CHECK(has_ref(x.section_requires_dangling, "Species/Human/sections/DEFission.shipsection", "GRP_Empty")); CHECK(x.section_requires_case_mismatch.empty()); // grp_torps is a group ref, not a tech CHECK_EQ(x.section_option_dangling.size(), std::size_t{1}); CHECK(has_ref(x.section_option_dangling, "Species/Human/sections/DECommand.shipsection", "IND_Nope")); CHECK_EQ(x.tech_ship_section_dangling.size(), std::size_t{1}); CHECK(has_ref(x.tech_ship_section_dangling, "IND_PlyAlloy", "DENowhere")); CHECK_EQ(x.tech_weapon_file_dangling.size(), std::size_t{1}); CHECK(has_ref(x.tech_weapon_file_dangling, "IND_PlyAlloy", "Weapons/nope.weapon")); CHECK_EQ(x.tech_requires_dangling.size(), std::size_t{2}); // GRP_Nothing, IND_Gone CHECK_EQ(x.tech_allows_dangling.size(), std::size_t{2}); // WEP_Missing, BAD CHECK_EQ(x.tech_allows_unparsed.size(), std::size_t{1}); // BAD has no RP CHECK_EQ(x.tech_roots.size(), std::size_t{3}); // IND_ROOT, WEP_GsDrvr, DRV_Fissn CHECK_EQ(x.bank_weapon_dangling.size(), std::size_t{1}); CHECK(has_ref(x.bank_weapon_dangling, "Species/_NPC/sections/_Herald.shipsection", "Species/_NPC/weapons/Missing.weapon")); CHECK_EQ(x.manifest_ids_without_file.size(), std::size_t{2}); bool ghost = false, dewar = false; for (const auto& m : x.manifest_ids_without_file) { if (m.scope == "Weapons" && m.id == 40 && m.name == "ghost.weapon") ghost = true; if (m.scope == "Human" && m.id == 98 && m.name == "DEWar.shipsection") dewar = true; } CHECK(ghost && dewar); CHECK_EQ(x.files_without_manifest_id.size(), std::size_t{1}); CHECK(has_ref(x.files_without_manifest_id, "Weapons", "Weapons/las_odd.weapon")); // turret fit: las_odd is Large/Beam (row exists, case-folded); Herald_gun medium/standard ok CHECK(x.weapon_turret_pairs_without_row.empty()); CHECK_EQ(x.bank_turret_pairs_without_row.size(), std::size_t{1}); // strafe bank, no strafe row CHECK(has_ref(x.bank_turret_pairs_without_row, "Species/Human/sections/DEFission.shipsection", "small/strafe")); CHECK_EQ(x.unresolved_weapon_names.size(), std::size_t{2}); // LAS_ODD, NPC_HERALD CHECK(has_ref(x.unresolved_weapon_names, "Species/_NPC/weapons/Herald_gun.weapon", "@WEAPON_NPC_HERALD")); CHECK(x.missing_techname.empty()); CHECK_EQ(x.missing_techdesc.size(), std::size_t{3}); CHECK_EQ(x.missing_sectionname.size(), std::size_t{1}); // _herald CHECK_EQ(x.missing_sectiondesc.size(), std::size_t{2}); // defission, _herald CHECK_EQ(x.dangling_count(), std::size_t{1 + 1 + 1 + 1 + 1 + 2 + 2 + 1 + 1 + 2 + 1 + 0 + 1 + 2}); fs::remove_all(root); } TEST(catalog_missing_root_is_all_problems_no_crash) { Catalog cat = load_catalog(fs::temp_directory_path() / "game_data_does_not_exist"); CHECK(cat.weapons.empty()); CHECK(cat.sections.empty()); CHECK(!cat.strings_loaded); CHECK(cat.problems.size() >= 3); // weapons manifest, turrets, tech tree CrossCheck x = cat.cross_check(); CHECK(x.clean()); CHECK(!x.strings_available); }