#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")); result.push_back(std::move(out)); } return result; } // The several-group form: one [[recipe.output_group]] entry per possible result, each // carrying its weight and the items it yields together (REQ-MAT-OUTPUT-GROUP). std::vector parseOutputGroups(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); RecipeOutputGroup group; const toml::array& items = utility::requireArray(mt["items"], file, elemPath + ".items"); group.items = parseRecipeOutputs(items, file, elemPath + ".items"); if (group.items.empty()) { throw utility::makeError(file, elemPath + ".items", "produces nothing"); } if (const std::optional p = mt["probability"].value()) { group.probability = *p; } else if (const std::optional p = mt["probability"].value()) { group.probability = static_cast(*p); } result.push_back(std::move(group)); } 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"); } // Either form, never both (REQ-MAT-OUTPUT-GROUP): `outputs` is the single-group // shorthand that all but the reprocessing recipes use, `output_group` the several- // group form. The shorthand carries no weight -- with one group nothing is picked. const bool hasOutputs = mt.contains("outputs"); const bool hasGroups = mt.contains("output_group"); if (hasOutputs && hasGroups) { throw utility::makeError(file, elemPath, "has both 'outputs' and 'output_group'; use one or the other"); } if (hasGroups) { const toml::array& groups = utility::requireArray(mt["output_group"], file, elemPath + ".output_group"); def.outputGroups = parseOutputGroups(groups, file, elemPath + ".output_group"); if (def.outputGroups.empty()) { throw utility::makeError(file, elemPath + ".output_group", "is empty"); } } else { const toml::array& outputs = utility::requireArray(mt["outputs"], file, elemPath + ".outputs"); RecipeOutputGroup group; group.items = parseRecipeOutputs(outputs, file, elemPath + ".outputs"); def.outputGroups.push_back(std::move(group)); } cfg.recipes.push_back(std::move(def)); } return cfg; }