move the shared TOML helpers into the utility namespace

The names are generic (makeError, requireInt, parseFile), and at global scope
with external linkage they would form an overload set with the same-named
anonymous-namespace helpers in VisualsLoader.cpp and BalancingConfig.cpp the
moment either file includes TomlHelpers.h — silently, since the signatures
differ. Namespacing keeps that door shut.

Call sites are qualified explicitly rather than pulled in with a using
directive, matching how utility::getRandomInt and friends are already called.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
This commit is contained in:
2026-08-04 09:27:43 +02:00
parent 21eb6ad096
commit 2c9433cea6
10 changed files with 155 additions and 141 deletions

View File

@@ -46,11 +46,11 @@ void validateUnlocks(const GameConfig& cfg)
{ {
if (valid.count(id) == 0) if (valid.count(id) == 0)
{ {
throw makeError(file, gPath, "grants unknown " + kind + " '" + id + "'"); throw utility::makeError(file, gPath, "grants unknown " + kind + " '" + id + "'");
} }
if (!granted.insert(id).second) if (!granted.insert(id).second)
{ {
throw makeError(file, gPath, throw utility::makeError(file, gPath,
"grants " + kind + " '" + id + "' which is already granted by another unlock group"); "grants " + kind + " '" + id + "' which is already granted by another unlock group");
} }
} }
@@ -61,13 +61,13 @@ void validateUnlocks(const GameConfig& cfg)
const std::string gPath = "unlock '" + group.id + "'"; const std::string gPath = "unlock '" + group.id + "'";
if (!groupIds.insert(group.id).second) if (!groupIds.insert(group.id).second)
{ {
throw makeError(file, gPath, "duplicate unlock group id"); throw utility::makeError(file, gPath, "duplicate unlock group id");
} }
if (group.ships.empty() && group.modules.empty() if (group.ships.empty() && group.modules.empty()
&& group.buildings.empty() && group.recipes.empty()) && group.buildings.empty() && group.recipes.empty())
{ {
throw makeError(file, gPath, "grants no items (must grant at least one)"); throw utility::makeError(file, gPath, "grants no items (must grant at least one)");
} }
checkGrants(group.ships, shipIds, grantedShipIds, "ship", gPath); checkGrants(group.ships, shipIds, grantedShipIds, "ship", gPath);
@@ -83,7 +83,7 @@ void validateUnlocks(const GameConfig& cfg)
{ {
if (groupIds.count(req) == 0) if (groupIds.count(req) == 0)
{ {
throw makeError(file, "unlock '" + group.id + "'.requires", throw utility::makeError(file, "unlock '" + group.id + "'.requires",
"references unknown unlock group '" + req + "'"); "references unknown unlock group '" + req + "'");
} }
} }

View File

@@ -11,10 +11,10 @@
BuildingsConfig ConfigLoader::loadBuildings(const std::string& path) BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
{ {
const std::string file = "buildings.toml"; const std::string file = "buildings.toml";
toml::table tbl = parseFile(path, file); toml::table tbl = utility::parseFile(path, file);
BuildingsConfig cfg; BuildingsConfig cfg;
const toml::array& arr = requireArray(tbl["building"], file, "building"); const toml::array& arr = utility::requireArray(tbl["building"], file, "building");
for (std::size_t i = 0; i < arr.size(); ++i) for (std::size_t i = 0; i < arr.size(); ++i)
{ {
@@ -22,32 +22,32 @@ BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
const toml::table* bt = arr[i].as_table(); const toml::table* bt = arr[i].as_table();
if (bt == nullptr) if (bt == nullptr)
{ {
throw makeError(file, elemPath, "not a table"); throw utility::makeError(file, elemPath, "not a table");
} }
toml::table& mt = const_cast<toml::table&>(*bt); toml::table& mt = const_cast<toml::table&>(*bt);
BuildingDef def; BuildingDef def;
def.id = requireString(mt["id"], file, elemPath + ".id"); def.id = utility::requireString(mt["id"], file, elemPath + ".id");
def.cost = static_cast<int>(requireInt(mt["cost"], file, elemPath + ".cost")); def.cost = static_cast<int>(utility::requireInt(mt["cost"], file, elemPath + ".cost"));
def.playerPlaceable = requireBool(mt["player_placeable"], file, elemPath + ".player_placeable"); def.playerPlaceable = utility::requireBool(mt["player_placeable"], file, elemPath + ".player_placeable");
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds"); def.constructionTimeSeconds = utility::requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask"); def.surfaceMask = utility::requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
if (mt.contains("output_buffer_capacity")) if (mt.contains("output_buffer_capacity"))
{ {
def.outputBufferCapacity = static_cast<int>( def.outputBufferCapacity = static_cast<int>(
requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity")); utility::requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity"));
} }
if (mt.contains("tooltip")) if (mt.contains("tooltip"))
{ {
def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip"); def.tooltip = utility::requireString(mt["tooltip"], file, elemPath + ".tooltip");
} }
const std::optional<BuildingType> parsedType = parseBuildingType(def.id); const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
if (!parsedType) if (!parsedType)
{ {
throw makeError(file, elemPath + ".id", "unknown building id '" + def.id + "'"); throw utility::makeError(file, elemPath + ".id", "unknown building id '" + def.id + "'");
} }
def.type = *parsedType; def.type = *parsedType;

View File

@@ -41,7 +41,7 @@ static const StatEntry kKnownStats[] = {
ModulesConfig ConfigLoader::loadModules(const std::string& path) ModulesConfig ConfigLoader::loadModules(const std::string& path)
{ {
const std::string file = "modules.toml"; const std::string file = "modules.toml";
toml::table tbl = parseFile(path, file); toml::table tbl = utility::parseFile(path, file);
ModulesConfig cfg; ModulesConfig cfg;
@@ -50,7 +50,7 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
return cfg; return cfg;
} }
const toml::array& arr = requireArray(tbl["module"], file, "module"); const toml::array& arr = utility::requireArray(tbl["module"], file, "module");
for (std::size_t i = 0; i < arr.size(); ++i) for (std::size_t i = 0; i < arr.size(); ++i)
{ {
@@ -58,27 +58,27 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
const toml::table* st = arr[i].as_table(); const toml::table* st = arr[i].as_table();
if (st == nullptr) if (st == nullptr)
{ {
throw makeError(file, elemPath, "not a table"); throw utility::makeError(file, elemPath, "not a table");
} }
toml::table& mt = const_cast<toml::table&>(*st); toml::table& mt = const_cast<toml::table&>(*st);
ModuleDef def; ModuleDef def;
def.id = requireString(mt["id"], file, elemPath + ".id"); def.id = utility::requireString(mt["id"], file, elemPath + ".id");
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask"); def.surfaceMask = utility::requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
def.productionTimeSeconds = requireDouble( def.productionTimeSeconds = utility::requireDouble(
mt["production_time_seconds"], file, elemPath + ".production_time_seconds"); mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color"); def.fillColor = utility::requireString(mt["fill_color"], file, elemPath + ".fill_color");
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph"); def.glyph = utility::requireString(mt["glyph"], file, elemPath + ".glyph");
if (mt.contains("tooltip")) if (mt.contains("tooltip"))
{ {
def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip"); def.tooltip = utility::requireString(mt["tooltip"], file, elemPath + ".tooltip");
} }
// Materials // Materials
{ {
const toml::array& materials = requireArray(mt["materials"], file, elemPath + ".materials"); const toml::array& materials = utility::requireArray(mt["materials"], file, elemPath + ".materials");
def.materials = parseIngredients(materials, file, elemPath + ".materials"); def.materials = utility::parseIngredients(materials, file, elemPath + ".materials");
} }
// Stat modifiers from [module.<category>] sub-tables // Stat modifiers from [module.<category>] sub-tables
@@ -88,7 +88,7 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
{ {
continue; continue;
} }
const toml::table& catTable = requireTable(mt[se.category], file, const toml::table& catTable = utility::requireTable(mt[se.category], file,
elemPath + "." + se.category); elemPath + "." + se.category);
toml::table& catMt = const_cast<toml::table&>(catTable); toml::table& catMt = const_cast<toml::table&>(catTable);
@@ -100,7 +100,7 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
ModuleStatModifier mod; ModuleStatModifier mod;
mod.stat = se.stat; mod.stat = se.stat;
mod.modifierType = "additive"; mod.modifierType = "additive";
mod.value = requireDouble(catMt[addedKey], file, mod.value = utility::requireDouble(catMt[addedKey], file,
elemPath + "." + se.category + "." + addedKey); elemPath + "." + se.category + "." + addedKey);
def.statModifiers.push_back(std::move(mod)); def.statModifiers.push_back(std::move(mod));
} }
@@ -110,7 +110,7 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
ModuleStatModifier mod; ModuleStatModifier mod;
mod.stat = se.stat; mod.stat = se.stat;
mod.modifierType = "multiplicative"; mod.modifierType = "multiplicative";
mod.value = requireDouble(catMt[multipliedKey], file, mod.value = utility::requireDouble(catMt[multipliedKey], file,
elemPath + "." + se.category + "." + multipliedKey); elemPath + "." + se.category + "." + multipliedKey);
def.statModifiers.push_back(std::move(mod)); def.statModifiers.push_back(std::move(mod));
} }
@@ -120,17 +120,17 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
if (mt.contains("weapon")) if (mt.contains("weapon"))
{ {
const std::string wPath = elemPath + ".weapon"; const std::string wPath = elemPath + ".weapon";
const toml::table& wTable = requireTable(mt["weapon"], file, wPath); const toml::table& wTable = utility::requireTable(mt["weapon"], file, wPath);
toml::table& wMt = const_cast<toml::table&>(wTable); toml::table& wMt = const_cast<toml::table&>(wTable);
if (wMt.contains("damage") || wMt.contains("attack_range_m") if (wMt.contains("damage") || wMt.contains("attack_range_m")
|| wMt.contains("attack_rate_hz")) || wMt.contains("attack_rate_hz"))
{ {
ModuleWeaponCapability cap; ModuleWeaponCapability cap;
cap.damage = static_cast<float>(requireDouble(wMt["damage"], cap.damage = static_cast<float>(utility::requireDouble(wMt["damage"],
file, wPath + ".damage")); file, wPath + ".damage"));
cap.attackRange_m = static_cast<float>(requireDouble(wMt["attack_range_m"], cap.attackRange_m = static_cast<float>(utility::requireDouble(wMt["attack_range_m"],
file, wPath + ".attack_range_m")); file, wPath + ".attack_range_m"));
cap.attackRate_hz = static_cast<float>(requireDouble(wMt["attack_rate_hz"], cap.attackRate_hz = static_cast<float>(utility::requireDouble(wMt["attack_rate_hz"],
file, wPath + ".attack_rate_hz")); file, wPath + ".attack_rate_hz"));
def.weaponCapability = std::move(cap); def.weaponCapability = std::move(cap);
} }
@@ -140,17 +140,17 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
if (mt.contains("salvage")) if (mt.contains("salvage"))
{ {
const std::string sPath = elemPath + ".salvage"; const std::string sPath = elemPath + ".salvage";
const toml::table& sTable = requireTable(mt["salvage"], file, sPath); const toml::table& sTable = utility::requireTable(mt["salvage"], file, sPath);
toml::table& sMt = const_cast<toml::table&>(sTable); toml::table& sMt = const_cast<toml::table&>(sTable);
if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity") if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity")
|| sMt.contains("collection_rate_hz")) || sMt.contains("collection_rate_hz"))
{ {
ModuleSalvageCapability cap; ModuleSalvageCapability cap;
cap.collectionRange_m = static_cast<float>(requireDouble(sMt["collection_range_m"], cap.collectionRange_m = static_cast<float>(utility::requireDouble(sMt["collection_range_m"],
file, sPath + ".collection_range_m")); file, sPath + ".collection_range_m"));
cap.cargoCapacity = static_cast<float>(requireDouble(sMt["cargo_capacity"], cap.cargoCapacity = static_cast<float>(utility::requireDouble(sMt["cargo_capacity"],
file, sPath + ".cargo_capacity")); file, sPath + ".cargo_capacity"));
cap.collectionRate_hz = static_cast<float>(requireDouble(sMt["collection_rate_hz"], cap.collectionRate_hz = static_cast<float>(utility::requireDouble(sMt["collection_rate_hz"],
file, sPath + ".collection_rate_hz")); file, sPath + ".collection_rate_hz"));
def.salvageCapability = std::move(cap); def.salvageCapability = std::move(cap);
} }
@@ -160,16 +160,16 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
if (mt.contains("repair")) if (mt.contains("repair"))
{ {
const std::string rPath = elemPath + ".repair"; const std::string rPath = elemPath + ".repair";
const toml::table& rTable = requireTable(mt["repair"], file, rPath); const toml::table& rTable = utility::requireTable(mt["repair"], file, rPath);
toml::table& rMt = const_cast<toml::table&>(rTable); toml::table& rMt = const_cast<toml::table&>(rTable);
if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m")) if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m"))
{ {
ModuleRepairCapability cap; ModuleRepairCapability cap;
cap.repairRate_hz = static_cast<float>(requireDouble(rMt["repair_rate_hz"], cap.repairRate_hz = static_cast<float>(utility::requireDouble(rMt["repair_rate_hz"],
file, rPath + ".repair_rate_hz")); file, rPath + ".repair_rate_hz"));
cap.repairAmountHp = static_cast<float>(requireDouble(rMt["repair_amount_hp"], cap.repairAmountHp = static_cast<float>(utility::requireDouble(rMt["repair_amount_hp"],
file, rPath + ".repair_amount_hp")); file, rPath + ".repair_amount_hp"));
cap.repairRange_m = static_cast<float>(requireDouble(rMt["repair_range_m"], cap.repairRange_m = static_cast<float>(utility::requireDouble(rMt["repair_range_m"],
file, rPath + ".repair_range_m")); file, rPath + ".repair_range_m"));
def.repairCapability = std::move(cap); def.repairCapability = std::move(cap);
} }

View File

@@ -25,13 +25,13 @@ std::vector<RecipeOutput> parseRecipeOutputs(const toml::array& arr,
const toml::table* t = arr[i].as_table(); const toml::table* t = arr[i].as_table();
if (t == nullptr) if (t == nullptr)
{ {
throw makeError(file, elemPath, "not a table"); throw utility::makeError(file, elemPath, "not a table");
} }
toml::table& mt = const_cast<toml::table&>(*t); toml::table& mt = const_cast<toml::table&>(*t);
RecipeOutput out; RecipeOutput out;
out.item = requireString(mt["item"], file, elemPath + ".item"); out.item = utility::requireString(mt["item"], file, elemPath + ".item");
out.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount")); out.amount = static_cast<int>(utility::requireInt(mt["amount"], file, elemPath + ".amount"));
if (const std::optional<double> p = mt["probability"].value<double>()) if (const std::optional<double> p = mt["probability"].value<double>())
{ {
out.probability = *p; out.probability = *p;
@@ -50,10 +50,10 @@ std::vector<RecipeOutput> parseRecipeOutputs(const toml::array& arr,
RecipesConfig ConfigLoader::loadRecipes(const std::string& path) RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
{ {
const std::string file = "recipes.toml"; const std::string file = "recipes.toml";
toml::table tbl = parseFile(path, file); toml::table tbl = utility::parseFile(path, file);
RecipesConfig cfg; RecipesConfig cfg;
const toml::array& arr = requireArray(tbl["recipe"], file, "recipe"); const toml::array& arr = utility::requireArray(tbl["recipe"], file, "recipe");
for (std::size_t i = 0; i < arr.size(); ++i) for (std::size_t i = 0; i < arr.size(); ++i)
{ {
@@ -61,37 +61,37 @@ RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
const toml::table* rt = arr[i].as_table(); const toml::table* rt = arr[i].as_table();
if (rt == nullptr) if (rt == nullptr)
{ {
throw makeError(file, elemPath, "not a table"); throw utility::makeError(file, elemPath, "not a table");
} }
toml::table& mt = const_cast<toml::table&>(*rt); toml::table& mt = const_cast<toml::table&>(*rt);
RecipeDef def; RecipeDef def;
def.id = requireString(mt["id"], file, elemPath + ".id"); def.id = utility::requireString(mt["id"], file, elemPath + ".id");
def.durationSeconds = requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds"); def.durationSeconds = utility::requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds");
const std::string buildingId = requireString(mt["building"], file, elemPath + ".building"); const std::string buildingId = utility::requireString(mt["building"], file, elemPath + ".building");
const std::optional<BuildingType> parsedType = parseBuildingType(buildingId); const std::optional<BuildingType> parsedType = parseBuildingType(buildingId);
if (!parsedType) if (!parsedType)
{ {
throw makeError(file, elemPath + ".building", throw utility::makeError(file, elemPath + ".building",
"unknown building id '" + buildingId + "'"); "unknown building id '" + buildingId + "'");
} }
def.building = *parsedType; def.building = *parsedType;
if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start")) if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start"))
{ {
def.unlockedAtStart = requireBool(mt["unlocked_at_start"], file, def.unlockedAtStart = utility::requireBool(mt["unlocked_at_start"], file,
elemPath + ".unlocked_at_start"); elemPath + ".unlocked_at_start");
} }
// inputs may be omitted (e.g. miner recipes). An empty array is fine. // inputs may be omitted (e.g. miner recipes). An empty array is fine.
if (mt.contains("inputs")) if (mt.contains("inputs"))
{ {
const toml::array& inputs = requireArray(mt["inputs"], file, elemPath + ".inputs"); const toml::array& inputs = utility::requireArray(mt["inputs"], file, elemPath + ".inputs");
def.inputs = parseIngredients(inputs, file, elemPath + ".inputs"); def.inputs = utility::parseIngredients(inputs, file, elemPath + ".inputs");
} }
const toml::array& outputs = requireArray(mt["outputs"], file, elemPath + ".outputs"); const toml::array& outputs = utility::requireArray(mt["outputs"], file, elemPath + ".outputs");
def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs"); def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs");
// Optional icon item id (REQ-UI-RECIPE-ICON); defaults to the first output // Optional icon item id (REQ-UI-RECIPE-ICON); defaults to the first output
@@ -99,7 +99,7 @@ RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
// icon is not an error (REQ-UI-ITEM-ICON). // icon is not an error (REQ-UI-ITEM-ICON).
if (mt.contains("icon")) if (mt.contains("icon"))
{ {
def.icon = requireString(mt["icon"], file, elemPath + ".icon"); def.icon = utility::requireString(mt["icon"], file, elemPath + ".icon");
} }
cfg.recipes.push_back(std::move(def)); cfg.recipes.push_back(std::move(def));

View File

@@ -58,10 +58,10 @@ std::vector<PlacedModule> parsePlacedModules(const toml::array& arr,
ShipsConfig ConfigLoader::loadShips(const std::string& path) ShipsConfig ConfigLoader::loadShips(const std::string& path)
{ {
const std::string file = "ships.toml"; const std::string file = "ships.toml";
toml::table tbl = parseFile(path, file); toml::table tbl = utility::parseFile(path, file);
ShipsConfig cfg; ShipsConfig cfg;
const toml::array& arr = requireArray(tbl["ship"], file, "ship"); const toml::array& arr = utility::requireArray(tbl["ship"], file, "ship");
for (std::size_t i = 0; i < arr.size(); ++i) for (std::size_t i = 0; i < arr.size(); ++i)
{ {
@@ -69,58 +69,58 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
const toml::table* st = arr[i].as_table(); const toml::table* st = arr[i].as_table();
if (st == nullptr) if (st == nullptr)
{ {
throw makeError(file, elemPath, "not a table"); throw utility::makeError(file, elemPath, "not a table");
} }
toml::table& mt = const_cast<toml::table&>(*st); toml::table& mt = const_cast<toml::table&>(*st);
ShipDef def; ShipDef def;
def.id = requireString(mt["id"], file, elemPath + ".id"); def.id = utility::requireString(mt["id"], file, elemPath + ".id");
def.layout = requireStringArray(mt["layout"], file, elemPath + ".layout"); def.layout = utility::requireStringArray(mt["layout"], file, elemPath + ".layout");
// Schematic // Schematic
{ {
const std::string bpPath = elemPath + ".schematic"; const std::string bpPath = elemPath + ".schematic";
const toml::table& bpTable = requireTable(mt["schematic"], file, bpPath); const toml::table& bpTable = utility::requireTable(mt["schematic"], file, bpPath);
toml::table& bpMt = const_cast<toml::table&>(bpTable); toml::table& bpMt = const_cast<toml::table&>(bpTable);
const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials"); const toml::array& materials = utility::requireArray(bpMt["materials"], file, bpPath + ".materials");
def.schematic.materials = parseIngredients(materials, file, bpPath + ".materials"); def.schematic.materials = utility::parseIngredients(materials, file, bpPath + ".materials");
def.schematic.productionTimeSeconds = requireDouble( def.schematic.productionTimeSeconds = utility::requireDouble(
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds"); bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
} }
// Health // Health
{ {
const std::string hPath = elemPath + ".health"; const std::string hPath = elemPath + ".health";
const toml::table& hTable = requireTable(mt["health"], file, hPath); const toml::table& hTable = utility::requireTable(mt["health"], file, hPath);
toml::table& hMt = const_cast<toml::table&>(hTable); toml::table& hMt = const_cast<toml::table&>(hTable);
def.health.hp = static_cast<float>(requireDouble(hMt["hp"], file, hPath + ".hp")); def.health.hp = static_cast<float>(utility::requireDouble(hMt["hp"], file, hPath + ".hp"));
} }
// Movement // Movement
{ {
const std::string mPath = elemPath + ".movement"; const std::string mPath = elemPath + ".movement";
const toml::table& mTable = requireTable(mt["movement"], file, mPath); const toml::table& mTable = utility::requireTable(mt["movement"], file, mPath);
toml::table& mMt = const_cast<toml::table&>(mTable); toml::table& mMt = const_cast<toml::table&>(mTable);
def.movement.speed_mps = static_cast<float>(requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps")); def.movement.speed_mps = static_cast<float>(utility::requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps"));
def.movement.mainAcceleration_mpss = static_cast<float>(requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss")); def.movement.mainAcceleration_mpss = static_cast<float>(utility::requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss"));
def.movement.maneuveringAcceleration_mpss = static_cast<float>(requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss")); def.movement.maneuveringAcceleration_mpss = static_cast<float>(utility::requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss"));
def.movement.angularAcceleration_radpss = static_cast<float>(requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss")); def.movement.angularAcceleration_radpss = static_cast<float>(utility::requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss"));
def.movement.maxRotationSpeed_radps = static_cast<float>(requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps")); def.movement.maxRotationSpeed_radps = static_cast<float>(utility::requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps"));
} }
// Sensor // Sensor
{ {
const std::string snsPath = elemPath + ".sensor"; const std::string snsPath = elemPath + ".sensor";
const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath); const toml::table& snsTable = utility::requireTable(mt["sensor"], file, snsPath);
toml::table& snsMt = const_cast<toml::table&>(snsTable); toml::table& snsMt = const_cast<toml::table&>(snsTable);
def.sensor.sensorRange_m = static_cast<float>(requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m")); def.sensor.sensorRange_m = static_cast<float>(utility::requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m"));
} }
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES) // Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
if (mt.contains("default_modules")) if (mt.contains("default_modules"))
{ {
const toml::array& modArr = requireArray(mt["default_modules"], file, const toml::array& modArr = utility::requireArray(mt["default_modules"], file,
elemPath + ".default_modules"); elemPath + ".default_modules");
def.defaultModules = parsePlacedModules(modArr, file, def.defaultModules = parsePlacedModules(modArr, file,
elemPath + ".default_modules"); elemPath + ".default_modules");

View File

@@ -9,38 +9,38 @@
StationsConfig ConfigLoader::loadStations(const std::string& path) StationsConfig ConfigLoader::loadStations(const std::string& path)
{ {
const std::string file = "stations.toml"; const std::string file = "stations.toml";
toml::table tbl = parseFile(path, file); toml::table tbl = utility::parseFile(path, file);
StationsConfig cfg; StationsConfig cfg;
// HQ // HQ
{ {
const std::string p = "hq"; const std::string p = "hq";
cfg.hq.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask"); cfg.hq.surfaceMask = utility::requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
cfg.hq.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula"); cfg.hq.hpFormula = utility::requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
} }
// Player station // Player station
{ {
const std::string p = "player_station"; const std::string p = "player_station";
cfg.playerStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask"); cfg.playerStation.surfaceMask = utility::requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
cfg.playerStation.level = static_cast<int>(requireInt(tbl[p]["level"], file, p + ".level")); cfg.playerStation.level = static_cast<int>(utility::requireInt(tbl[p]["level"], file, p + ".level"));
cfg.playerStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula"); cfg.playerStation.hpFormula = utility::requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
cfg.playerStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula"); cfg.playerStation.damageFormula = utility::requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
cfg.playerStation.rangeFormula = requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula"); cfg.playerStation.rangeFormula = utility::requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula"); cfg.playerStation.fireRateFormula = utility::requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
cfg.playerStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula"); cfg.playerStation.scrapDropFormula = utility::requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
} }
// Enemy station // Enemy station
{ {
const std::string p = "enemy_station"; const std::string p = "enemy_station";
cfg.enemyStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask"); cfg.enemyStation.surfaceMask = utility::requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
cfg.enemyStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula"); cfg.enemyStation.hpFormula = utility::requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
cfg.enemyStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula"); cfg.enemyStation.damageFormula = utility::requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
cfg.enemyStation.rangeFormula = requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula"); cfg.enemyStation.rangeFormula = utility::requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula"); cfg.enemyStation.fireRateFormula = utility::requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
cfg.enemyStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula"); cfg.enemyStation.scrapDropFormula = utility::requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
} }
return cfg; return cfg;

View File

@@ -10,7 +10,7 @@
UnlocksConfig ConfigLoader::loadUnlocks(const std::string& path) UnlocksConfig ConfigLoader::loadUnlocks(const std::string& path)
{ {
const std::string file = "unlocks.toml"; const std::string file = "unlocks.toml";
toml::table tbl = parseFile(path, file); toml::table tbl = utility::parseFile(path, file);
UnlocksConfig cfg; UnlocksConfig cfg;
if (!tbl.contains("unlock")) if (!tbl.contains("unlock"))
@@ -18,7 +18,7 @@ UnlocksConfig ConfigLoader::loadUnlocks(const std::string& path)
return cfg; return cfg;
} }
const toml::array& arr = requireArray(tbl["unlock"], file, "unlock"); const toml::array& arr = utility::requireArray(tbl["unlock"], file, "unlock");
for (std::size_t i = 0; i < arr.size(); ++i) for (std::size_t i = 0; i < arr.size(); ++i)
{ {
@@ -26,33 +26,33 @@ UnlocksConfig ConfigLoader::loadUnlocks(const std::string& path)
const toml::table* ut = arr[i].as_table(); const toml::table* ut = arr[i].as_table();
if (ut == nullptr) if (ut == nullptr)
{ {
throw makeError(file, elemPath, "not a table"); throw utility::makeError(file, elemPath, "not a table");
} }
toml::table& mt = const_cast<toml::table&>(*ut); toml::table& mt = const_cast<toml::table&>(*ut);
UnlockGroupDef def; UnlockGroupDef def;
def.id = requireString(mt["id"], file, elemPath + ".id"); def.id = utility::requireString(mt["id"], file, elemPath + ".id");
def.stationLevel = static_cast<int>( def.stationLevel = static_cast<int>(
requireInt(mt["station_level"], file, elemPath + ".station_level")); utility::requireInt(mt["station_level"], file, elemPath + ".station_level"));
if (mt.contains("requires")) if (mt.contains("requires"))
{ {
def.requiredGroupIds = requireStringArray(mt["requires"], file, elemPath + ".requires"); def.requiredGroupIds = utility::requireStringArray(mt["requires"], file, elemPath + ".requires");
} }
if (mt.contains("ships")) if (mt.contains("ships"))
{ {
def.ships = requireStringArray(mt["ships"], file, elemPath + ".ships"); def.ships = utility::requireStringArray(mt["ships"], file, elemPath + ".ships");
} }
if (mt.contains("modules")) if (mt.contains("modules"))
{ {
def.modules = requireStringArray(mt["modules"], file, elemPath + ".modules"); def.modules = utility::requireStringArray(mt["modules"], file, elemPath + ".modules");
} }
if (mt.contains("buildings")) if (mt.contains("buildings"))
{ {
def.buildings = requireStringArray(mt["buildings"], file, elemPath + ".buildings"); def.buildings = utility::requireStringArray(mt["buildings"], file, elemPath + ".buildings");
} }
if (mt.contains("recipes")) if (mt.contains("recipes"))
{ {
def.recipes = requireStringArray(mt["recipes"], file, elemPath + ".recipes"); def.recipes = utility::requireStringArray(mt["recipes"], file, elemPath + ".recipes");
} }
cfg.groups.push_back(std::move(def)); cfg.groups.push_back(std::move(def));

View File

@@ -10,22 +10,22 @@
WorldConfig ConfigLoader::loadWorld(const std::string& path) WorldConfig ConfigLoader::loadWorld(const std::string& path)
{ {
const std::string file = "world.toml"; const std::string file = "world.toml";
toml::table tbl = parseFile(path, file); toml::table tbl = utility::parseFile(path, file);
WorldConfig cfg; WorldConfig cfg;
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles")); cfg.heightTiles = static_cast<int>(utility::requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage")); cfg.refundPercentage = static_cast<int>(utility::requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
cfg.deconstructionTimeSeconds = requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds"); cfg.deconstructionTimeSeconds = utility::requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds");
cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks")); cfg.startingBuildingBlocks = static_cast<int>(utility::requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
cfg.debrisDespawnSeconds = requireDouble(tbl["world"]["debris_despawn_seconds"], file, "world.debris_despawn_seconds"); cfg.debrisDespawnSeconds = utility::requireDouble(tbl["world"]["debris_despawn_seconds"], file, "world.debris_despawn_seconds");
cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat"); cfg.scrapPerThreat = utility::requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat");
cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m"); cfg.tileSize_m = utility::requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m");
cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m; cfg.beltSpeed_tps = utility::requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
cfg.tunnelMaxDistance_tiles = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles")); cfg.tunnelMaxDistance_tiles = static_cast<int>(utility::requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles"));
cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds"); cfg.departureIntervalSeconds = utility::requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
cfg.orbitFactor = requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor"); cfg.orbitFactor = utility::requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor");
cfg.rallyOrbitRadius_tiles = requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles"); cfg.rallyOrbitRadius_tiles = utility::requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles");
if (const std::optional<std::string> tip = if (const std::optional<std::string> tip =
tbl["world"]["building_blocks_tooltip"].value<std::string>()) tbl["world"]["building_blocks_tooltip"].value<std::string>())
@@ -39,41 +39,41 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.artifactTooltip = *tip; cfg.artifactTooltip = *tip;
} }
cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles")); cfg.regions.asteroidWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles")); cfg.regions.playerBufferWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles")); cfg.regions.contestZoneWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
cfg.regions.enemyBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles")); cfg.regions.enemyBufferWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles"));
cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles")); cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(utility::requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles"));
cfg.expansion.costBuildingBlocksFormula = requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula"); cfg.expansion.costBuildingBlocksFormula = utility::requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula");
cfg.push.pushExpandColumns_tiles = static_cast<int>(requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles")); cfg.push.pushExpandColumns_tiles = static_cast<int>(utility::requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles"));
cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds"); cfg.push.bossAdvanceSeconds = utility::requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula"); cfg.waves.threatRateFormula = utility::requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
cfg.waves.gapMinSeconds = requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds"); cfg.waves.gapMinSeconds = utility::requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
cfg.waves.gapMaxSeconds = requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds"); cfg.waves.gapMaxSeconds = utility::requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
cfg.waves.spawnDurationSeconds = requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds"); cfg.waves.spawnDurationSeconds = utility::requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
cfg.waves.bossCountdownSeconds = requireDouble(tbl["waves"]["boss_countdown_seconds"], file, "waves.boss_countdown_seconds"); cfg.waves.bossCountdownSeconds = utility::requireDouble(tbl["waves"]["boss_countdown_seconds"], file, "waves.boss_countdown_seconds");
cfg.waves.bossThreatDurationSeconds = requireDouble(tbl["waves"]["boss_threat_duration_seconds"], file, "waves.boss_threat_duration_seconds"); cfg.waves.bossThreatDurationSeconds = utility::requireDouble(tbl["waves"]["boss_threat_duration_seconds"], file, "waves.boss_threat_duration_seconds");
cfg.waves.bossQuietBeforeSeconds = requireDouble(tbl["waves"]["boss_quiet_before_seconds"], file, "waves.boss_quiet_before_seconds"); cfg.waves.bossQuietBeforeSeconds = utility::requireDouble(tbl["waves"]["boss_quiet_before_seconds"], file, "waves.boss_quiet_before_seconds");
cfg.waves.bossQuietAfterSeconds = requireDouble(tbl["waves"]["boss_quiet_after_seconds"], file, "waves.boss_quiet_after_seconds"); cfg.waves.bossQuietAfterSeconds = utility::requireDouble(tbl["waves"]["boss_quiet_after_seconds"], file, "waves.boss_quiet_after_seconds");
if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds) if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds)
{ {
throw makeError(file, "waves", "gap_min_seconds > gap_max_seconds"); throw utility::makeError(file, "waves", "gap_min_seconds > gap_max_seconds");
} }
cfg.targeting.targetScoreFormula = requireFormula(tbl["targeting"]["target_score_formula"], file, "targeting.target_score_formula"); cfg.targeting.targetScoreFormula = utility::requireFormula(tbl["targeting"]["target_score_formula"], file, "targeting.target_score_formula");
cfg.targeting.overclaimPenaltyFormula = requireFormula(tbl["targeting"]["overclaim_penalty_formula"], file, "targeting.overclaim_penalty_formula"); cfg.targeting.overclaimPenaltyFormula = utility::requireFormula(tbl["targeting"]["overclaim_penalty_formula"], file, "targeting.overclaim_penalty_formula");
cfg.targeting.hysteresis = requireDouble(tbl["targeting"]["target_hysteresis"], file, "targeting.target_hysteresis"); cfg.targeting.hysteresis = utility::requireDouble(tbl["targeting"]["target_hysteresis"], file, "targeting.target_hysteresis");
cfg.artifacts.artifactChanceFormula = requireFormula(tbl["artifacts"]["artifact_chance_formula"], file, "artifacts.artifact_chance_formula"); cfg.artifacts.artifactChanceFormula = utility::requireFormula(tbl["artifacts"]["artifact_chance_formula"], file, "artifacts.artifact_chance_formula");
cfg.artifacts.artifactWinCount = static_cast<int>(requireInt(tbl["artifacts"]["artifact_win_count"], file, "artifacts.artifact_win_count")); cfg.artifacts.artifactWinCount = static_cast<int>(utility::requireInt(tbl["artifacts"]["artifact_win_count"], file, "artifacts.artifact_win_count"));
cfg.scroll.panSpeedSlow_tps = requireDouble(tbl["scroll"]["pan_speed_slow_tiles_per_second"], file, "scroll.pan_speed_slow_tiles_per_second"); cfg.scroll.panSpeedSlow_tps = utility::requireDouble(tbl["scroll"]["pan_speed_slow_tiles_per_second"], file, "scroll.pan_speed_slow_tiles_per_second");
cfg.scroll.panSpeedFast_tps = requireDouble(tbl["scroll"]["pan_speed_fast_tiles_per_second"], file, "scroll.pan_speed_fast_tiles_per_second"); cfg.scroll.panSpeedFast_tps = utility::requireDouble(tbl["scroll"]["pan_speed_fast_tiles_per_second"], file, "scroll.pan_speed_fast_tiles_per_second");
cfg.scroll.panRampBandWidth_tiles = static_cast<int>(requireInt(tbl["scroll"]["pan_ramp_band_width_tiles"], file, "scroll.pan_ramp_band_width_tiles")); cfg.scroll.panRampBandWidth_tiles = static_cast<int>(utility::requireInt(tbl["scroll"]["pan_ramp_band_width_tiles"], file, "scroll.pan_ramp_band_width_tiles"));
return cfg; return cfg;
} }

View File

@@ -3,6 +3,9 @@
#include <sstream> #include <sstream>
#include <utility> #include <utility>
namespace utility
{
// --- Error helpers -------------------------------------------------------- // --- Error helpers --------------------------------------------------------
std::runtime_error makeError(const std::string& file, std::runtime_error makeError(const std::string& file,
@@ -165,3 +168,5 @@ toml::table parseFile(const std::string& path, const std::string& file)
throw std::runtime_error(oss.str()); throw std::runtime_error(oss.str());
} }
} }
} // namespace utility

View File

@@ -13,6 +13,13 @@
// Shared TOML-parsing helpers used by two or more ConfigLoader per-domain // Shared TOML-parsing helpers used by two or more ConfigLoader per-domain
// loaders. Helpers used by exactly one domain stay local to that domain's // loaders. Helpers used by exactly one domain stay local to that domain's
// .cpp file instead. // .cpp file instead.
//
// Namespaced because the names are generic: VisualsLoader.cpp and
// BalancingConfig.cpp each have their own same-named helpers in anonymous
// namespaces, and unqualified globals here would form an overload set with
// them the moment either file includes this header.
namespace utility
{
// --- Error helpers ---------------------------------------------------------- // --- Error helpers ----------------------------------------------------------
@@ -59,3 +66,5 @@ std::vector<RecipeIngredient> parseIngredients(const toml::array& arr,
const std::string& path); const std::string& path);
toml::table parseFile(const std::string& path, const std::string& file); toml::table parseFile(const std::string& path, const std::string& file);
} // namespace utility