502 lines
20 KiB
C++
502 lines
20 KiB
C++
#include "ConfigLoader.h"
|
|
|
|
#include <cstdint>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "toml.hpp"
|
|
|
|
namespace
|
|
{
|
|
|
|
// --- Error helpers --------------------------------------------------------
|
|
|
|
std::runtime_error makeError(const std::string& file,
|
|
const std::string& path,
|
|
const std::string& why)
|
|
{
|
|
return std::runtime_error("Config: " + file + ": '" + path + "' " + why);
|
|
}
|
|
|
|
// --- Typed accessors (throw on missing or wrong type) ---------------------
|
|
|
|
int64_t requireInt(const toml::node_view<toml::node>& node,
|
|
const std::string& file,
|
|
const std::string& path)
|
|
{
|
|
const std::optional<int64_t> value = node.value<int64_t>();
|
|
if (!value)
|
|
{
|
|
throw makeError(file, path, "missing or not an integer");
|
|
}
|
|
return *value;
|
|
}
|
|
|
|
double requireDouble(const toml::node_view<toml::node>& node,
|
|
const std::string& file,
|
|
const std::string& path)
|
|
{
|
|
if (const std::optional<double> v = node.value<double>())
|
|
{
|
|
return *v;
|
|
}
|
|
if (const std::optional<int64_t> v = node.value<int64_t>())
|
|
{
|
|
return static_cast<double>(*v);
|
|
}
|
|
throw makeError(file, path, "missing or not a number");
|
|
}
|
|
|
|
std::string requireString(const toml::node_view<toml::node>& node,
|
|
const std::string& file,
|
|
const std::string& path)
|
|
{
|
|
const std::optional<std::string> value = node.value<std::string>();
|
|
if (!value)
|
|
{
|
|
throw makeError(file, path, "missing or not a string");
|
|
}
|
|
return *value;
|
|
}
|
|
|
|
bool requireBool(const toml::node_view<toml::node>& node,
|
|
const std::string& file,
|
|
const std::string& path)
|
|
{
|
|
const std::optional<bool> value = node.value<bool>();
|
|
if (!value)
|
|
{
|
|
throw makeError(file, path, "missing or not a boolean");
|
|
}
|
|
return *value;
|
|
}
|
|
|
|
const toml::array& requireArray(const toml::node_view<toml::node>& node,
|
|
const std::string& file,
|
|
const std::string& path)
|
|
{
|
|
const toml::array* arr = node.as_array();
|
|
if (arr == nullptr)
|
|
{
|
|
throw makeError(file, path, "missing or not an array");
|
|
}
|
|
return *arr;
|
|
}
|
|
|
|
const toml::table& requireTable(const toml::node_view<toml::node>& node,
|
|
const std::string& file,
|
|
const std::string& path)
|
|
{
|
|
const toml::table* tbl = node.as_table();
|
|
if (tbl == nullptr)
|
|
{
|
|
throw makeError(file, path, "missing or not a table");
|
|
}
|
|
return *tbl;
|
|
}
|
|
|
|
Formula requireFormula(const toml::node_view<toml::node>& node,
|
|
const std::string& file,
|
|
const std::string& path)
|
|
{
|
|
const std::string source = requireString(node, file, path);
|
|
try
|
|
{
|
|
return Formula::compile(source);
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
throw makeError(file, path, std::string("formula error: ") + e.what());
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> requireStringArray(const toml::node_view<toml::node>& node,
|
|
const std::string& file,
|
|
const std::string& path)
|
|
{
|
|
const toml::array& arr = requireArray(node, file, path);
|
|
std::vector<std::string> 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 std::optional<std::string> s = arr[i].value<std::string>();
|
|
if (!s)
|
|
{
|
|
throw makeError(file, elemPath, "not a string");
|
|
}
|
|
result.push_back(*s);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<RecipeIngredient> parseIngredients(const toml::array& arr,
|
|
const std::string& file,
|
|
const std::string& path)
|
|
{
|
|
std::vector<RecipeIngredient> 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");
|
|
}
|
|
|
|
// We need a mutable node_view to reuse our helpers, which is fine
|
|
// because the helpers never mutate.
|
|
toml::table& mt = const_cast<toml::table&>(*t);
|
|
|
|
RecipeIngredient ing;
|
|
ing.item = requireString(mt["item"], file, elemPath + ".item");
|
|
ing.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
|
|
result.push_back(std::move(ing));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
toml::table parseFile(const std::string& path, const std::string& file)
|
|
{
|
|
try
|
|
{
|
|
return toml::parse_file(path);
|
|
}
|
|
catch (const toml::parse_error& e)
|
|
{
|
|
std::ostringstream oss;
|
|
oss << "Config: " << file << ": TOML parse error: " << e.description()
|
|
<< " at " << e.source().begin;
|
|
throw std::runtime_error(oss.str());
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
// --- Per-file loaders -----------------------------------------------------
|
|
|
|
WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|
{
|
|
const std::string file = "world.toml";
|
|
toml::table tbl = parseFile(path, file);
|
|
|
|
WorldConfig cfg;
|
|
|
|
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
|
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
|
cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
|
|
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds");
|
|
cfg.beltSpeedTilesPerSecond = requireDouble(tbl["world"]["belt_speed_tiles_per_second"], file, "world.belt_speed_tiles_per_second");
|
|
cfg.tunnelMaxDistance = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance"], file, "world.tunnel_max_distance"));
|
|
|
|
cfg.regions.asteroidWidth = static_cast<int>(requireInt(tbl["regions"]["asteroid_width"], file, "regions.asteroid_width"));
|
|
cfg.regions.playerBufferWidth = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width"], file, "regions.player_buffer_width"));
|
|
cfg.regions.contestZoneWidth = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width"], file, "regions.contest_zone_width"));
|
|
cfg.regions.enemyBufferWidth = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width"], file, "regions.enemy_buffer_width"));
|
|
|
|
cfg.expansion.columnsPerExpansion = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion"], file, "expansion.columns_per_expansion"));
|
|
cfg.expansion.costBuildingBlocks = static_cast<int>(requireInt(tbl["expansion"]["cost_building_blocks"], file, "expansion.cost_building_blocks"));
|
|
|
|
cfg.push.pushExpandColumns = static_cast<int>(requireInt(tbl["push"]["push_expand_columns"], file, "push.push_expand_columns"));
|
|
cfg.push.scalingFactor = requireDouble(tbl["push"]["scaling_factor"], file, "push.scaling_factor");
|
|
|
|
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
|
|
cfg.waves.shipLevelFormula = requireFormula(tbl["waves"]["ship_level_formula"], file, "waves.ship_level_formula");
|
|
cfg.waves.gapMinSeconds = 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.spawnDurationSeconds = requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
|
|
|
|
if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds)
|
|
{
|
|
throw makeError(file, "waves", "gap_min_seconds > gap_max_seconds");
|
|
}
|
|
|
|
return cfg;
|
|
}
|
|
|
|
BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
|
|
{
|
|
const std::string file = "buildings.toml";
|
|
toml::table tbl = parseFile(path, file);
|
|
|
|
BuildingsConfig cfg;
|
|
const toml::array& arr = requireArray(tbl["building"], file, "building");
|
|
|
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
{
|
|
const std::string elemPath = "building[" + std::to_string(i) + "]";
|
|
const toml::table* bt = arr[i].as_table();
|
|
if (bt == nullptr)
|
|
{
|
|
throw makeError(file, elemPath, "not a table");
|
|
}
|
|
toml::table& mt = const_cast<toml::table&>(*bt);
|
|
|
|
BuildingDef def;
|
|
def.id = requireString(mt["id"], file, elemPath + ".id");
|
|
def.cost = static_cast<int>(requireInt(mt["cost"], file, elemPath + ".cost"));
|
|
def.playerPlaceable = requireBool(mt["player_placeable"], file, elemPath + ".player_placeable");
|
|
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
|
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
|
|
|
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
|
if (!parsedType)
|
|
{
|
|
throw makeError(file, elemPath + ".id", "unknown building id '" + def.id + "'");
|
|
}
|
|
def.type = *parsedType;
|
|
|
|
cfg.buildings.push_back(std::move(def));
|
|
}
|
|
|
|
return cfg;
|
|
}
|
|
|
|
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;
|
|
|
|
// 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");
|
|
|
|
cfg.recipes.push_back(std::move(def));
|
|
}
|
|
|
|
return cfg;
|
|
}
|
|
|
|
ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|
{
|
|
const std::string file = "ships.toml";
|
|
toml::table tbl = parseFile(path, file);
|
|
|
|
ShipsConfig cfg;
|
|
const toml::array& arr = requireArray(tbl["ship"], file, "ship");
|
|
|
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
{
|
|
const std::string elemPath = "ship[" + std::to_string(i) + "]";
|
|
const toml::table* st = arr[i].as_table();
|
|
if (st == nullptr)
|
|
{
|
|
throw makeError(file, elemPath, "not a table");
|
|
}
|
|
toml::table& mt = const_cast<toml::table&>(*st);
|
|
|
|
ShipDef def;
|
|
def.id = requireString(mt["id"], file, elemPath + ".id");
|
|
def.availableFromStart = requireBool(mt["available_from_start"], file, elemPath + ".available_from_start");
|
|
|
|
// Schematic
|
|
{
|
|
const std::string bpPath = elemPath + ".schematic";
|
|
const toml::table& bpTable = requireTable(mt["schematic"], file, bpPath);
|
|
toml::table& bpMt = const_cast<toml::table&>(bpTable);
|
|
|
|
const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials");
|
|
def.schematic.materials = parseIngredients(materials, file, bpPath + ".materials");
|
|
def.schematic.playerProductionLevel = static_cast<int>(requireInt(
|
|
bpMt["player_production_level"], file, bpPath + ".player_production_level"));
|
|
def.schematic.productionTimeSeconds = requireDouble(
|
|
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
|
}
|
|
|
|
// Threat
|
|
{
|
|
const std::string tPath = elemPath + ".threat";
|
|
const toml::table& tTable = requireTable(mt["threat"], file, tPath);
|
|
toml::table& tMt = const_cast<toml::table&>(tTable);
|
|
def.threat.costFormula = requireFormula(tMt["cost_formula"], file, tPath + ".cost_formula");
|
|
}
|
|
|
|
// Health
|
|
{
|
|
const std::string hPath = elemPath + ".health";
|
|
const toml::table& hTable = requireTable(mt["health"], file, hPath);
|
|
toml::table& hMt = const_cast<toml::table&>(hTable);
|
|
def.health.hpFormula = requireFormula(hMt["hp_formula"], file, hPath + ".hp_formula");
|
|
}
|
|
|
|
// Movement
|
|
{
|
|
const std::string mPath = elemPath + ".movement";
|
|
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
|
|
toml::table& mMt = const_cast<toml::table&>(mTable);
|
|
def.movement.speedFormula = requireFormula(mMt["speed_formula"], file, mPath + ".speed_formula");
|
|
}
|
|
|
|
// Loot
|
|
{
|
|
const std::string lPath = elemPath + ".loot";
|
|
const toml::table& lTable = requireTable(mt["loot"], file, lPath);
|
|
toml::table& lMt = const_cast<toml::table&>(lTable);
|
|
def.loot.scrapDrop = static_cast<int>(requireInt(lMt["scrap_drop"], file, lPath + ".scrap_drop"));
|
|
}
|
|
|
|
// Optional: combat
|
|
if (mt.contains("combat"))
|
|
{
|
|
const std::string cPath = elemPath + ".combat";
|
|
const toml::table& cTable = requireTable(mt["combat"], file, cPath);
|
|
toml::table& cMt = const_cast<toml::table&>(cTable);
|
|
ShipCombat combat {
|
|
requireFormula(cMt["damage_formula"], file, cPath + ".damage_formula"),
|
|
requireFormula(cMt["attack_range_formula"], file, cPath + ".attack_range_formula"),
|
|
requireFormula(cMt["attack_rate_formula"], file, cPath + ".attack_rate_formula"),
|
|
};
|
|
def.combat = std::move(combat);
|
|
}
|
|
|
|
// Optional: salvage
|
|
if (mt.contains("salvage"))
|
|
{
|
|
const std::string sPath = elemPath + ".salvage";
|
|
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
|
|
toml::table& sMt = const_cast<toml::table&>(sTable);
|
|
ShipSalvage salvage;
|
|
salvage.collectionRange = requireDouble(sMt["collection_range"], file, sPath + ".collection_range");
|
|
salvage.cargoCapacity = static_cast<int>(requireInt(sMt["cargo_capacity"], file, sPath + ".cargo_capacity"));
|
|
def.salvage = salvage;
|
|
}
|
|
|
|
// Optional: repair
|
|
if (mt.contains("repair"))
|
|
{
|
|
const std::string rPath = elemPath + ".repair";
|
|
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
|
|
toml::table& rMt = const_cast<toml::table&>(rTable);
|
|
ShipRepair repair {
|
|
requireFormula(rMt["repair_rate_formula"], file, rPath + ".repair_rate_formula"),
|
|
requireFormula(rMt["repair_range_formula"], file, rPath + ".repair_range_formula"),
|
|
};
|
|
def.repair = std::move(repair);
|
|
}
|
|
|
|
cfg.ships.push_back(std::move(def));
|
|
}
|
|
|
|
return cfg;
|
|
}
|
|
|
|
StationsConfig ConfigLoader::loadStations(const std::string& path)
|
|
{
|
|
const std::string file = "stations.toml";
|
|
toml::table tbl = parseFile(path, file);
|
|
|
|
StationsConfig cfg;
|
|
|
|
// HQ
|
|
{
|
|
const std::string p = "hq";
|
|
cfg.hq.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
|
cfg.hq.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
|
}
|
|
|
|
// Player station
|
|
{
|
|
const std::string p = "player_station";
|
|
cfg.playerStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
|
cfg.playerStation.level = static_cast<int>(requireInt(tbl[p]["level"], file, p + ".level"));
|
|
cfg.playerStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
|
cfg.playerStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
|
cfg.playerStation.rangeFormula = requireFormula(tbl[p]["range_formula"], file, p + ".range_formula");
|
|
cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_formula"], file, p + ".fire_rate_formula");
|
|
cfg.playerStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
|
}
|
|
|
|
// Enemy station
|
|
{
|
|
const std::string p = "enemy_station";
|
|
cfg.enemyStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
|
cfg.enemyStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
|
cfg.enemyStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
|
cfg.enemyStation.rangeFormula = requireFormula(tbl[p]["range_formula"], file, p + ".range_formula");
|
|
cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_formula"], file, p + ".fire_rate_formula");
|
|
cfg.enemyStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
|
}
|
|
|
|
return cfg;
|
|
}
|
|
|
|
GameConfig ConfigLoader::loadFromDirectory(const std::string& configDir)
|
|
{
|
|
GameConfig cfg;
|
|
cfg.world = loadWorld(configDir + "/world.toml");
|
|
cfg.buildings = loadBuildings(configDir + "/buildings.toml");
|
|
cfg.recipes = loadRecipes(configDir + "/recipes.toml");
|
|
cfg.ships = loadShips(configDir + "/ships.toml");
|
|
cfg.stations = loadStations(configDir + "/stations.toml");
|
|
return cfg;
|
|
}
|