sots-engine/tests/game_data/test_catalog.cpp

218 lines
11 KiB
C++

// Catalog on a hand-written miniature data root written to a temp directory.
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <fstream>
#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<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"
" 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");
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 DRV_Fissn requires grp_torps requires 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<CrossCheck::Ref>& 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);
}