#include "ConfigLoader.h" #include #include #include #include #include #include "toml.hpp" #include "TomlHelpers.h" namespace { std::vector parseRecipeOutputs(const toml::array& arr, const std::string& file, const std::string& path) { std::vector result; result.reserve(arr.size()); for (std::size_t i = 0; i < arr.size(); ++i) { const std::string elemPath = path + "[" + std::to_string(i) + "]"; const toml::table* t = arr[i].as_table(); if (t == nullptr) { throw utility::makeError(file, elemPath, "not a table"); } toml::table& mt = const_cast(*t); RecipeOutput out; out.item = utility::requireString(mt["item"], file, elemPath + ".item"); out.amount = static_cast(utility::requireInt(mt["amount"], file, elemPath + ".amount")); if (const std::optional p = mt["probability"].value()) { out.probability = *p; } else if (const std::optional p = mt["probability"].value()) { out.probability = static_cast(*p); } result.push_back(std::move(out)); } return result; } } // namespace RecipesConfig ConfigLoader::loadRecipes(const std::string& path) { const std::string file = "recipes.toml"; toml::table tbl = utility::parseFile(path, file); RecipesConfig cfg; const toml::array& arr = utility::requireArray(tbl["recipe"], file, "recipe"); for (std::size_t i = 0; i < arr.size(); ++i) { const std::string elemPath = "recipe[" + std::to_string(i) + "]"; const toml::table* rt = arr[i].as_table(); if (rt == nullptr) { throw utility::makeError(file, elemPath, "not a table"); } toml::table& mt = const_cast(*rt); RecipeDef def; def.id = utility::requireString(mt["id"], file, elemPath + ".id"); def.durationSeconds = utility::requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds"); const std::string buildingId = utility::requireString(mt["building"], file, elemPath + ".building"); const std::optional parsedType = parseBuildingType(buildingId); if (!parsedType) { throw utility::makeError(file, elemPath + ".building", "unknown building id '" + buildingId + "'"); } def.building = *parsedType; if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start")) { def.unlockedAtStart = utility::requireBool(mt["unlocked_at_start"], file, elemPath + ".unlocked_at_start"); } // inputs may be omitted (e.g. miner recipes). An empty array is fine. if (mt.contains("inputs")) { const toml::array& inputs = utility::requireArray(mt["inputs"], file, elemPath + ".inputs"); def.inputs = utility::parseIngredients(inputs, file, elemPath + ".inputs"); } const toml::array& outputs = utility::requireArray(mt["outputs"], file, elemPath + ".outputs"); def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs"); // Optional icon item id (REQ-UI-RECIPE-ICON); defaults to the first output // in the UI when unset. Not validated against known items here — a missing // icon is not an error (REQ-UI-ITEM-ICON). if (mt.contains("icon")) { def.icon = utility::requireString(mt["icon"], file, elemPath + ".icon"); } cfg.recipes.push_back(std::move(def)); } return cfg; }