diff --git a/src/lib/config/CMakeLists.txt b/src/lib/config/CMakeLists.txt index 627c23d..16975ed 100644 --- a/src/lib/config/CMakeLists.txt +++ b/src/lib/config/CMakeLists.txt @@ -23,6 +23,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderWorld.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderBuildings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderRecipes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprintSerializer.cpp diff --git a/src/lib/config/ConfigLoader.cpp b/src/lib/config/ConfigLoader.cpp index a055d65..5b23bf8 100644 --- a/src/lib/config/ConfigLoader.cpp +++ b/src/lib/config/ConfigLoader.cpp @@ -18,38 +18,6 @@ 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 makeError(file, elemPath, "not a table"); - } - toml::table& mt = const_cast(*t); - - RecipeOutput out; - out.item = requireString(mt["item"], file, elemPath + ".item"); - out.amount = static_cast(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; -} - Rotation parseRotationString(const std::string& s) { if (s == "east") { return Rotation::East; } @@ -91,67 +59,6 @@ std::vector parsePlacedModules(const toml::array& arr, // --- Per-file loaders ----------------------------------------------------- -RecipesConfig ConfigLoader::loadRecipes(const std::string& path) -{ - const std::string file = "recipes.toml"; - toml::table tbl = parseFile(path, file); - - RecipesConfig cfg; - const toml::array& arr = 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 makeError(file, elemPath, "not a table"); - } - toml::table& mt = const_cast(*rt); - - RecipeDef def; - def.id = requireString(mt["id"], file, elemPath + ".id"); - def.durationSeconds = requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds"); - - const std::string buildingId = requireString(mt["building"], file, elemPath + ".building"); - const std::optional parsedType = parseBuildingType(buildingId); - if (!parsedType) - { - throw makeError(file, elemPath + ".building", - "unknown building id '" + buildingId + "'"); - } - def.building = *parsedType; - - if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start")) - { - def.unlockedAtStart = 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 = requireArray(mt["inputs"], file, elemPath + ".inputs"); - def.inputs = parseIngredients(inputs, file, elemPath + ".inputs"); - } - - const toml::array& outputs = 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 = requireString(mt["icon"], file, elemPath + ".icon"); - } - - cfg.recipes.push_back(std::move(def)); - } - - return cfg; -} - ShipsConfig ConfigLoader::loadShips(const std::string& path) { const std::string file = "ships.toml"; diff --git a/src/lib/config/ConfigLoaderRecipes.cpp b/src/lib/config/ConfigLoaderRecipes.cpp new file mode 100644 index 0000000..c184aa1 --- /dev/null +++ b/src/lib/config/ConfigLoaderRecipes.cpp @@ -0,0 +1,109 @@ +#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 makeError(file, elemPath, "not a table"); + } + toml::table& mt = const_cast(*t); + + RecipeOutput out; + out.item = requireString(mt["item"], file, elemPath + ".item"); + out.amount = static_cast(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 = parseFile(path, file); + + RecipesConfig cfg; + const toml::array& arr = 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 makeError(file, elemPath, "not a table"); + } + toml::table& mt = const_cast(*rt); + + RecipeDef def; + def.id = requireString(mt["id"], file, elemPath + ".id"); + def.durationSeconds = requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds"); + + const std::string buildingId = requireString(mt["building"], file, elemPath + ".building"); + const std::optional parsedType = parseBuildingType(buildingId); + if (!parsedType) + { + throw makeError(file, elemPath + ".building", + "unknown building id '" + buildingId + "'"); + } + def.building = *parsedType; + + if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start")) + { + def.unlockedAtStart = 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 = requireArray(mt["inputs"], file, elemPath + ".inputs"); + def.inputs = parseIngredients(inputs, file, elemPath + ".inputs"); + } + + const toml::array& outputs = 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 = requireString(mt["icon"], file, elemPath + ".icon"); + } + + cfg.recipes.push_back(std::move(def)); + } + + return cfg; +}