#include "ConfigLoader.h" #include #include #include #include "TomlHelpers.h" namespace { // Validates unlocks.toml against the rest of the config (REQ-LOCK-EXPLICIT, // REQ-LOCK-PREREQ): each granted id resolves to the right kind of definition // (recipe grants must name assembler recipes), each grantable id is granted by // at most one group, group ids are unique, every group grants at least one // item, and every `requires` id names a defined group. void validateUnlocks(const GameConfig& cfg) { const std::string file = "unlocks.toml"; std::unordered_set shipIds; for (const ShipDef& def : cfg.ships.ships) { shipIds.insert(def.id); } std::unordered_set moduleIds; for (const ModuleDef& def : cfg.modules.modules) { moduleIds.insert(def.id); } std::unordered_set buildingIds; for (const BuildingDef& def : cfg.buildings.buildings) { buildingIds.insert(def.id); } std::unordered_set assemblerRecipeIds; for (const RecipeDef& def : cfg.recipes.recipes) { if (def.building == BuildingType::Assembler) { assemblerRecipeIds.insert(def.id); } } std::unordered_set groupIds; std::unordered_set grantedShipIds; std::unordered_set grantedModuleIds; std::unordered_set grantedBuildingIds; std::unordered_set grantedRecipeIds; const auto checkGrants = [&](const std::vector& ids, const std::unordered_set& valid, std::unordered_set& granted, const std::string& kind, const std::string& gPath) { for (const std::string& id : ids) { if (valid.count(id) == 0) { throw utility::makeError(file, gPath, "grants unknown " + kind + " '" + id + "'"); } if (!granted.insert(id).second) { throw utility::makeError(file, gPath, "grants " + kind + " '" + id + "' which is already granted by another unlock group"); } } }; for (const UnlockGroupDef& group : cfg.unlocks.groups) { const std::string gPath = "unlock '" + group.id + "'"; if (!groupIds.insert(group.id).second) { throw utility::makeError(file, gPath, "duplicate unlock group id"); } if (group.ships.empty() && group.modules.empty() && group.buildings.empty() && group.recipes.empty()) { throw utility::makeError(file, gPath, "grants no items (must grant at least one)"); } checkGrants(group.ships, shipIds, grantedShipIds, "ship", gPath); checkGrants(group.modules, moduleIds, grantedModuleIds, "module", gPath); checkGrants(group.buildings, buildingIds, grantedBuildingIds, "building", gPath); checkGrants(group.recipes, assemblerRecipeIds, grantedRecipeIds, "assembler recipe", gPath); } // requires must reference defined group ids (checked once all group ids known). for (const UnlockGroupDef& group : cfg.unlocks.groups) { for (const std::string& req : group.requiredGroupIds) { if (groupIds.count(req) == 0) { throw utility::makeError(file, "unlock '" + group.id + "'.requires", "references unknown unlock group '" + req + "'"); } } } } } // namespace GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir) { GameConfig cfg; cfg.world = loadWorld(configDir + "/world.toml"); cfg.buildings = loadBuildings(configDir + "/buildings.toml"); cfg.recipes = loadRecipes(configDir + "/recipes.toml"); cfg.ships = loadShips(configDir + "/ships.toml"); cfg.stations = loadStations(configDir + "/stations.toml"); cfg.modules = loadModules(configDir + "/modules.toml"); cfg.unlocks = loadUnlocks(configDir + "/unlocks.toml"); validateUnlocks(cfg); cfg.threatCosts = computeThreatCostTable(cfg); return cfg; }