Continues the ConfigLoader.cpp domain split. parseRecipeOutputs is only used by loadRecipes, so it moves along as a domain-local anonymous- namespace helper rather than into TomlHelpers. Pure move; no logic change. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
110 lines
3.7 KiB
C++
110 lines
3.7 KiB
C++
#include "ConfigLoader.h"
|
|
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "toml.hpp"
|
|
|
|
#include "TomlHelpers.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
std::vector<RecipeOutput> parseRecipeOutputs(const toml::array& arr,
|
|
const std::string& file,
|
|
const std::string& path)
|
|
{
|
|
std::vector<RecipeOutput> 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<toml::table&>(*t);
|
|
|
|
RecipeOutput out;
|
|
out.item = requireString(mt["item"], file, elemPath + ".item");
|
|
out.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
|
|
if (const std::optional<double> p = mt["probability"].value<double>())
|
|
{
|
|
out.probability = *p;
|
|
}
|
|
else if (const std::optional<int64_t> p = mt["probability"].value<int64_t>())
|
|
{
|
|
out.probability = static_cast<double>(*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<toml::table&>(*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<BuildingType> 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;
|
|
}
|