From c8ff7da3457622ee7fd42b670085fd14806172e4 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Tue, 4 Aug 2026 18:05:27 +0200 Subject: [PATCH] extract load methods into their own files --- src/lib/config/CMakeLists.txt | 7 + src/lib/config/ConfigLoader.cpp | 606 ----------------------- src/lib/config/ConfigLoaderBuildings.cpp | 58 +++ src/lib/config/ConfigLoaderModules.cpp | 182 +++++++ src/lib/config/ConfigLoaderRecipes.cpp | 109 ++++ src/lib/config/ConfigLoaderShips.cpp | 133 +++++ src/lib/config/ConfigLoaderStations.cpp | 47 ++ src/lib/config/ConfigLoaderUnlocks.cpp | 62 +++ src/lib/config/ConfigLoaderWorld.cpp | 79 +++ 9 files changed, 677 insertions(+), 606 deletions(-) create mode 100644 src/lib/config/ConfigLoaderBuildings.cpp create mode 100644 src/lib/config/ConfigLoaderModules.cpp create mode 100644 src/lib/config/ConfigLoaderRecipes.cpp create mode 100644 src/lib/config/ConfigLoaderShips.cpp create mode 100644 src/lib/config/ConfigLoaderStations.cpp create mode 100644 src/lib/config/ConfigLoaderUnlocks.cpp create mode 100644 src/lib/config/ConfigLoaderWorld.cpp diff --git a/src/lib/config/CMakeLists.txt b/src/lib/config/CMakeLists.txt index 4d00a6d..b46b91d 100644 --- a/src/lib/config/CMakeLists.txt +++ b/src/lib/config/CMakeLists.txt @@ -21,6 +21,13 @@ SET(SRCS ${SRCS} ${CMAKE_CURRENT_SOURCE_DIR}/Formula.cpp ${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}/ConfigLoaderShips.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderStations.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderModules.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderUnlocks.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 5fe5f82..522921f 100644 --- a/src/lib/config/ConfigLoader.cpp +++ b/src/lib/config/ConfigLoader.cpp @@ -1,620 +1,14 @@ #include "ConfigLoader.h" -#include -#include #include #include -#include #include -#include - -#include "toml.hpp" - -#include "Rotation.h" -#include "ShipLayout.h" #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; -} - -Rotation parseRotationString(const std::string& s) -{ - if (s == "east") { return Rotation::East; } - if (s == "south") { return Rotation::South; } - if (s == "west") { return Rotation::West; } - return Rotation::North; -} - -std::vector parsePlacedModules(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) { continue; } - toml::table& mt = const_cast(*t); - - const std::optional type = mt["type"].value(); - const std::optional x = mt["x"].value(); - const std::optional y = mt["y"].value(); - const std::optional rot = mt["rotation"].value(); - if (!type || !x || !y || !rot) { continue; } - - PlacedModule pm; - pm.moduleId = *type; - pm.position = QPoint(static_cast(*x), static_cast(*y)); - pm.rotation = parseRotationString(*rot); - result.push_back(std::move(pm)); - } - return result; -} - -} // 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(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles")); - cfg.refundPercentage = static_cast(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage")); - cfg.deconstructionTimeSeconds = requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds"); - cfg.startingBuildingBlocks = static_cast(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks")); - cfg.debrisDespawnSeconds = requireDouble(tbl["world"]["debris_despawn_seconds"], file, "world.debris_despawn_seconds"); - cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat"); - cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m"); - cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m; - cfg.tunnelMaxDistance_tiles = static_cast(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles")); - cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds"); - cfg.orbitFactor = requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor"); - cfg.rallyOrbitRadius_tiles = requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles"); - - if (const std::optional tip = - tbl["world"]["building_blocks_tooltip"].value()) - { - cfg.buildingBlocksTooltip = *tip; - } - - if (const std::optional tip = - tbl["world"]["artifact_tooltip"].value()) - { - cfg.artifactTooltip = *tip; - } - - cfg.regions.asteroidWidth_tiles = static_cast(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles")); - cfg.regions.playerBufferWidth_tiles = static_cast(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles")); - cfg.regions.contestZoneWidth_tiles = static_cast(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles")); - cfg.regions.enemyBufferWidth_tiles = static_cast(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles")); - - cfg.expansion.columnsPerExpansion_tiles = static_cast(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles")); - cfg.expansion.costBuildingBlocksFormula = requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula"); - - cfg.push.pushExpandColumns_tiles = static_cast(requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles")); - cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds"); - - cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_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"); - cfg.waves.bossCountdownSeconds = requireDouble(tbl["waves"]["boss_countdown_seconds"], file, "waves.boss_countdown_seconds"); - cfg.waves.bossThreatDurationSeconds = requireDouble(tbl["waves"]["boss_threat_duration_seconds"], file, "waves.boss_threat_duration_seconds"); - cfg.waves.bossQuietBeforeSeconds = requireDouble(tbl["waves"]["boss_quiet_before_seconds"], file, "waves.boss_quiet_before_seconds"); - cfg.waves.bossQuietAfterSeconds = requireDouble(tbl["waves"]["boss_quiet_after_seconds"], file, "waves.boss_quiet_after_seconds"); - - if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds) - { - throw makeError(file, "waves", "gap_min_seconds > gap_max_seconds"); - } - - cfg.targeting.targetScoreFormula = requireFormula(tbl["targeting"]["target_score_formula"], file, "targeting.target_score_formula"); - cfg.targeting.overclaimPenaltyFormula = requireFormula(tbl["targeting"]["overclaim_penalty_formula"], file, "targeting.overclaim_penalty_formula"); - cfg.targeting.hysteresis = requireDouble(tbl["targeting"]["target_hysteresis"], file, "targeting.target_hysteresis"); - - cfg.artifacts.artifactChanceFormula = requireFormula(tbl["artifacts"]["artifact_chance_formula"], file, "artifacts.artifact_chance_formula"); - cfg.artifacts.artifactWinCount = static_cast(requireInt(tbl["artifacts"]["artifact_win_count"], file, "artifacts.artifact_win_count")); - - cfg.scroll.panSpeedSlow_tps = requireDouble(tbl["scroll"]["pan_speed_slow_tiles_per_second"], file, "scroll.pan_speed_slow_tiles_per_second"); - cfg.scroll.panSpeedFast_tps = requireDouble(tbl["scroll"]["pan_speed_fast_tiles_per_second"], file, "scroll.pan_speed_fast_tiles_per_second"); - cfg.scroll.panRampBandWidth_tiles = static_cast(requireInt(tbl["scroll"]["pan_ramp_band_width_tiles"], file, "scroll.pan_ramp_band_width_tiles")); - - 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(*bt); - - BuildingDef def; - def.id = requireString(mt["id"], file, elemPath + ".id"); - def.cost = static_cast(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"); - - if (mt.contains("output_buffer_capacity")) - { - def.outputBufferCapacity = static_cast( - requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity")); - } - - if (mt.contains("tooltip")) - { - def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip"); - } - - const std::optional 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(*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"; - 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(*st); - - ShipDef def; - def.id = requireString(mt["id"], file, elemPath + ".id"); - def.layout = requireStringArray(mt["layout"], file, elemPath + ".layout"); - - // Schematic - { - const std::string bpPath = elemPath + ".schematic"; - const toml::table& bpTable = requireTable(mt["schematic"], file, bpPath); - toml::table& bpMt = const_cast(bpTable); - - const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials"); - def.schematic.materials = parseIngredients(materials, file, bpPath + ".materials"); - def.schematic.productionTimeSeconds = requireDouble( - bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds"); - } - - // Health - { - const std::string hPath = elemPath + ".health"; - const toml::table& hTable = requireTable(mt["health"], file, hPath); - toml::table& hMt = const_cast(hTable); - def.health.hp = static_cast(requireDouble(hMt["hp"], file, hPath + ".hp")); - } - - // Movement - { - const std::string mPath = elemPath + ".movement"; - const toml::table& mTable = requireTable(mt["movement"], file, mPath); - toml::table& mMt = const_cast(mTable); - def.movement.speed_mps = static_cast(requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps")); - def.movement.mainAcceleration_mpss = static_cast(requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss")); - def.movement.maneuveringAcceleration_mpss = static_cast(requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss")); - def.movement.angularAcceleration_radpss = static_cast(requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss")); - def.movement.maxRotationSpeed_radps = static_cast(requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps")); - } - - // Sensor - { - const std::string snsPath = elemPath + ".sensor"; - const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath); - toml::table& snsMt = const_cast(snsTable); - def.sensor.sensorRange_m = static_cast(requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m")); - } - - // Optional: default_modules (REQ-WAV-DEFAULT-MODULES) - if (mt.contains("default_modules")) - { - const toml::array& modArr = requireArray(mt["default_modules"], file, - elemPath + ".default_modules"); - def.defaultModules = parsePlacedModules(modArr, file, - elemPath + ".default_modules"); - } - - 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(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_m_formula"], file, p + ".range_m_formula"); - cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_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_m_formula"], file, p + ".range_m_formula"); - cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula"); - cfg.enemyStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula"); - } - - return cfg; -} - -// Known category→stat mappings for module stat modifier discovery. -// addedKeySuffix: unit suffix appended before "_formula" for additive modifier keys only. -// Multiplicative modifier keys are always dimensionless and carry no suffix. -struct StatEntry -{ - const char* category; - const char* stat; - const char* addedKeySuffix; -}; - -static const StatEntry kKnownStats[] = { - {"health", "hp", ""}, - {"movement", "speed", "_mps"}, - {"movement", "main_acceleration", "_mpss"}, - {"movement", "maneuvering_acceleration", "_mpss"}, - {"sensor", "sensor_range", "_m"}, - {"weapon", "damage", ""}, - {"weapon", "attack_range", "_m"}, - {"weapon", "attack_rate", "_hz"}, - {"salvage", "collection_range", "_m"}, - {"salvage", "collection_rate", "_hz"}, - {"cargo", "cargo_capacity", ""}, - {"repair", "repair_rate", "_hz"}, - {"repair", "repair_range", "_m"}, -}; - -ModulesConfig ConfigLoader::loadModules(const std::string& path) -{ - const std::string file = "modules.toml"; - toml::table tbl = parseFile(path, file); - - ModulesConfig cfg; - - if (!tbl.contains("module")) - { - return cfg; - } - - const toml::array& arr = requireArray(tbl["module"], file, "module"); - - for (std::size_t i = 0; i < arr.size(); ++i) - { - const std::string elemPath = "module[" + 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(*st); - - ModuleDef def; - def.id = requireString(mt["id"], file, elemPath + ".id"); - def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask"); - def.productionTimeSeconds = requireDouble( - mt["production_time_seconds"], file, elemPath + ".production_time_seconds"); - def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color"); - def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph"); - - if (mt.contains("tooltip")) - { - def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip"); - } - - // Materials - { - const toml::array& materials = requireArray(mt["materials"], file, elemPath + ".materials"); - def.materials = parseIngredients(materials, file, elemPath + ".materials"); - } - - // Stat modifiers from [module.] sub-tables - for (const StatEntry& se : kKnownStats) - { - if (!mt.contains(se.category)) - { - continue; - } - const toml::table& catTable = requireTable(mt[se.category], file, - elemPath + "." + se.category); - toml::table& catMt = const_cast(catTable); - - const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix; - const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix; - - if (catMt.contains(addedKey)) - { - ModuleStatModifier mod; - mod.stat = se.stat; - mod.modifierType = "additive"; - mod.value = requireDouble(catMt[addedKey], file, - elemPath + "." + se.category + "." + addedKey); - def.statModifiers.push_back(std::move(mod)); - } - - if (catMt.contains(multipliedKey)) - { - ModuleStatModifier mod; - mod.stat = se.stat; - mod.modifierType = "multiplicative"; - mod.value = requireDouble(catMt[multipliedKey], file, - elemPath + "." + se.category + "." + multipliedKey); - def.statModifiers.push_back(std::move(mod)); - } - } - - // Weapon capability section: [module.weapon] with base stat formulas - if (mt.contains("weapon")) - { - const std::string wPath = elemPath + ".weapon"; - const toml::table& wTable = requireTable(mt["weapon"], file, wPath); - toml::table& wMt = const_cast(wTable); - if (wMt.contains("damage") || wMt.contains("attack_range_m") - || wMt.contains("attack_rate_hz")) - { - ModuleWeaponCapability cap; - cap.damage = static_cast(requireDouble(wMt["damage"], - file, wPath + ".damage")); - cap.attackRange_m = static_cast(requireDouble(wMt["attack_range_m"], - file, wPath + ".attack_range_m")); - cap.attackRate_hz = static_cast(requireDouble(wMt["attack_rate_hz"], - file, wPath + ".attack_rate_hz")); - def.weaponCapability = std::move(cap); - } - } - - // Salvage capability section: [module.salvage] with base stat formulas - if (mt.contains("salvage")) - { - const std::string sPath = elemPath + ".salvage"; - const toml::table& sTable = requireTable(mt["salvage"], file, sPath); - toml::table& sMt = const_cast(sTable); - if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity") - || sMt.contains("collection_rate_hz")) - { - ModuleSalvageCapability cap; - cap.collectionRange_m = static_cast(requireDouble(sMt["collection_range_m"], - file, sPath + ".collection_range_m")); - cap.cargoCapacity = static_cast(requireDouble(sMt["cargo_capacity"], - file, sPath + ".cargo_capacity")); - cap.collectionRate_hz = static_cast(requireDouble(sMt["collection_rate_hz"], - file, sPath + ".collection_rate_hz")); - def.salvageCapability = std::move(cap); - } - } - - // Repair capability section: [module.repair] with base stat formulas - if (mt.contains("repair")) - { - const std::string rPath = elemPath + ".repair"; - const toml::table& rTable = requireTable(mt["repair"], file, rPath); - toml::table& rMt = const_cast(rTable); - if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m")) - { - ModuleRepairCapability cap; - cap.repairRate_hz = static_cast(requireDouble(rMt["repair_rate_hz"], - file, rPath + ".repair_rate_hz")); - cap.repairAmountHp = static_cast(requireDouble(rMt["repair_amount_hp"], - file, rPath + ".repair_amount_hp")); - cap.repairRange_m = static_cast(requireDouble(rMt["repair_range_m"], - file, rPath + ".repair_range_m")); - def.repairCapability = std::move(cap); - } - } - - cfg.modules.push_back(std::move(def)); - } - - return cfg; -} - -UnlocksConfig ConfigLoader::loadUnlocks(const std::string& path) -{ - const std::string file = "unlocks.toml"; - toml::table tbl = parseFile(path, file); - - UnlocksConfig cfg; - if (!tbl.contains("unlock")) - { - return cfg; - } - - const toml::array& arr = requireArray(tbl["unlock"], file, "unlock"); - - for (std::size_t i = 0; i < arr.size(); ++i) - { - const std::string elemPath = "unlock[" + std::to_string(i) + "]"; - const toml::table* ut = arr[i].as_table(); - if (ut == nullptr) - { - throw makeError(file, elemPath, "not a table"); - } - toml::table& mt = const_cast(*ut); - - UnlockGroupDef def; - def.id = requireString(mt["id"], file, elemPath + ".id"); - def.stationLevel = static_cast( - requireInt(mt["station_level"], file, elemPath + ".station_level")); - if (mt.contains("requires")) - { - def.requiredGroupIds = requireStringArray(mt["requires"], file, elemPath + ".requires"); - } - if (mt.contains("ships")) - { - def.ships = requireStringArray(mt["ships"], file, elemPath + ".ships"); - } - if (mt.contains("modules")) - { - def.modules = requireStringArray(mt["modules"], file, elemPath + ".modules"); - } - if (mt.contains("buildings")) - { - def.buildings = requireStringArray(mt["buildings"], file, elemPath + ".buildings"); - } - if (mt.contains("recipes")) - { - def.recipes = requireStringArray(mt["recipes"], file, elemPath + ".recipes"); - } - - cfg.groups.push_back(std::move(def)); - } - - return cfg; -} - -namespace -{ - // Validates unlocks.toml against the rest of the config (REQ-LOCK-EXPLICIT, // REQ-LOCK-PREREQ): each granted id resolves to the right kind of definition // (recipe grants must name assembler recipes), each grantable id is granted by diff --git a/src/lib/config/ConfigLoaderBuildings.cpp b/src/lib/config/ConfigLoaderBuildings.cpp new file mode 100644 index 0000000..1c17a23 --- /dev/null +++ b/src/lib/config/ConfigLoaderBuildings.cpp @@ -0,0 +1,58 @@ +#include "ConfigLoader.h" + +#include +#include +#include + +#include "toml.hpp" + +#include "TomlHelpers.h" + +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(*bt); + + BuildingDef def; + def.id = requireString(mt["id"], file, elemPath + ".id"); + def.cost = static_cast(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"); + + if (mt.contains("output_buffer_capacity")) + { + def.outputBufferCapacity = static_cast( + requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity")); + } + + if (mt.contains("tooltip")) + { + def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip"); + } + + const std::optional 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; +} diff --git a/src/lib/config/ConfigLoaderModules.cpp b/src/lib/config/ConfigLoaderModules.cpp new file mode 100644 index 0000000..f93caa3 --- /dev/null +++ b/src/lib/config/ConfigLoaderModules.cpp @@ -0,0 +1,182 @@ +#include "ConfigLoader.h" + +#include +#include + +#include "toml.hpp" + +#include "TomlHelpers.h" + +namespace +{ + +// Known category→stat mappings for module stat modifier discovery. +// addedKeySuffix: unit suffix appended before "_formula" for additive modifier keys only. +// Multiplicative modifier keys are always dimensionless and carry no suffix. +struct StatEntry +{ + const char* category; + const char* stat; + const char* addedKeySuffix; +}; + +static const StatEntry kKnownStats[] = { + {"health", "hp", ""}, + {"movement", "speed", "_mps"}, + {"movement", "main_acceleration", "_mpss"}, + {"movement", "maneuvering_acceleration", "_mpss"}, + {"sensor", "sensor_range", "_m"}, + {"weapon", "damage", ""}, + {"weapon", "attack_range", "_m"}, + {"weapon", "attack_rate", "_hz"}, + {"salvage", "collection_range", "_m"}, + {"salvage", "collection_rate", "_hz"}, + {"cargo", "cargo_capacity", ""}, + {"repair", "repair_rate", "_hz"}, + {"repair", "repair_range", "_m"}, +}; + +} // namespace + +ModulesConfig ConfigLoader::loadModules(const std::string& path) +{ + const std::string file = "modules.toml"; + toml::table tbl = parseFile(path, file); + + ModulesConfig cfg; + + if (!tbl.contains("module")) + { + return cfg; + } + + const toml::array& arr = requireArray(tbl["module"], file, "module"); + + for (std::size_t i = 0; i < arr.size(); ++i) + { + const std::string elemPath = "module[" + 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(*st); + + ModuleDef def; + def.id = requireString(mt["id"], file, elemPath + ".id"); + def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask"); + def.productionTimeSeconds = requireDouble( + mt["production_time_seconds"], file, elemPath + ".production_time_seconds"); + def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color"); + def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph"); + + if (mt.contains("tooltip")) + { + def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip"); + } + + // Materials + { + const toml::array& materials = requireArray(mt["materials"], file, elemPath + ".materials"); + def.materials = parseIngredients(materials, file, elemPath + ".materials"); + } + + // Stat modifiers from [module.] sub-tables + for (const StatEntry& se : kKnownStats) + { + if (!mt.contains(se.category)) + { + continue; + } + const toml::table& catTable = requireTable(mt[se.category], file, + elemPath + "." + se.category); + toml::table& catMt = const_cast(catTable); + + const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix; + const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix; + + if (catMt.contains(addedKey)) + { + ModuleStatModifier mod; + mod.stat = se.stat; + mod.modifierType = "additive"; + mod.value = requireDouble(catMt[addedKey], file, + elemPath + "." + se.category + "." + addedKey); + def.statModifiers.push_back(std::move(mod)); + } + + if (catMt.contains(multipliedKey)) + { + ModuleStatModifier mod; + mod.stat = se.stat; + mod.modifierType = "multiplicative"; + mod.value = requireDouble(catMt[multipliedKey], file, + elemPath + "." + se.category + "." + multipliedKey); + def.statModifiers.push_back(std::move(mod)); + } + } + + // Weapon capability section: [module.weapon] with base stat formulas + if (mt.contains("weapon")) + { + const std::string wPath = elemPath + ".weapon"; + const toml::table& wTable = requireTable(mt["weapon"], file, wPath); + toml::table& wMt = const_cast(wTable); + if (wMt.contains("damage") || wMt.contains("attack_range_m") + || wMt.contains("attack_rate_hz")) + { + ModuleWeaponCapability cap; + cap.damage = static_cast(requireDouble(wMt["damage"], + file, wPath + ".damage")); + cap.attackRange_m = static_cast(requireDouble(wMt["attack_range_m"], + file, wPath + ".attack_range_m")); + cap.attackRate_hz = static_cast(requireDouble(wMt["attack_rate_hz"], + file, wPath + ".attack_rate_hz")); + def.weaponCapability = std::move(cap); + } + } + + // Salvage capability section: [module.salvage] with base stat formulas + if (mt.contains("salvage")) + { + const std::string sPath = elemPath + ".salvage"; + const toml::table& sTable = requireTable(mt["salvage"], file, sPath); + toml::table& sMt = const_cast(sTable); + if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity") + || sMt.contains("collection_rate_hz")) + { + ModuleSalvageCapability cap; + cap.collectionRange_m = static_cast(requireDouble(sMt["collection_range_m"], + file, sPath + ".collection_range_m")); + cap.cargoCapacity = static_cast(requireDouble(sMt["cargo_capacity"], + file, sPath + ".cargo_capacity")); + cap.collectionRate_hz = static_cast(requireDouble(sMt["collection_rate_hz"], + file, sPath + ".collection_rate_hz")); + def.salvageCapability = std::move(cap); + } + } + + // Repair capability section: [module.repair] with base stat formulas + if (mt.contains("repair")) + { + const std::string rPath = elemPath + ".repair"; + const toml::table& rTable = requireTable(mt["repair"], file, rPath); + toml::table& rMt = const_cast(rTable); + if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m")) + { + ModuleRepairCapability cap; + cap.repairRate_hz = static_cast(requireDouble(rMt["repair_rate_hz"], + file, rPath + ".repair_rate_hz")); + cap.repairAmountHp = static_cast(requireDouble(rMt["repair_amount_hp"], + file, rPath + ".repair_amount_hp")); + cap.repairRange_m = static_cast(requireDouble(rMt["repair_range_m"], + file, rPath + ".repair_range_m")); + def.repairCapability = std::move(cap); + } + } + + cfg.modules.push_back(std::move(def)); + } + + return cfg; +} 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; +} diff --git a/src/lib/config/ConfigLoaderShips.cpp b/src/lib/config/ConfigLoaderShips.cpp new file mode 100644 index 0000000..41557b9 --- /dev/null +++ b/src/lib/config/ConfigLoaderShips.cpp @@ -0,0 +1,133 @@ +#include "ConfigLoader.h" + +#include +#include +#include +#include +#include + +#include + +#include "toml.hpp" + +#include "Rotation.h" +#include "ShipLayout.h" +#include "TomlHelpers.h" + +namespace +{ + +Rotation parseRotationString(const std::string& s) +{ + if (s == "east") { return Rotation::East; } + if (s == "south") { return Rotation::South; } + if (s == "west") { return Rotation::West; } + return Rotation::North; +} + +std::vector parsePlacedModules(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) { continue; } + toml::table& mt = const_cast(*t); + + const std::optional type = mt["type"].value(); + const std::optional x = mt["x"].value(); + const std::optional y = mt["y"].value(); + const std::optional rot = mt["rotation"].value(); + if (!type || !x || !y || !rot) { continue; } + + PlacedModule pm; + pm.moduleId = *type; + pm.position = QPoint(static_cast(*x), static_cast(*y)); + pm.rotation = parseRotationString(*rot); + result.push_back(std::move(pm)); + } + return result; +} + +} // namespace + +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(*st); + + ShipDef def; + def.id = requireString(mt["id"], file, elemPath + ".id"); + def.layout = requireStringArray(mt["layout"], file, elemPath + ".layout"); + + // Schematic + { + const std::string bpPath = elemPath + ".schematic"; + const toml::table& bpTable = requireTable(mt["schematic"], file, bpPath); + toml::table& bpMt = const_cast(bpTable); + + const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials"); + def.schematic.materials = parseIngredients(materials, file, bpPath + ".materials"); + def.schematic.productionTimeSeconds = requireDouble( + bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds"); + } + + // Health + { + const std::string hPath = elemPath + ".health"; + const toml::table& hTable = requireTable(mt["health"], file, hPath); + toml::table& hMt = const_cast(hTable); + def.health.hp = static_cast(requireDouble(hMt["hp"], file, hPath + ".hp")); + } + + // Movement + { + const std::string mPath = elemPath + ".movement"; + const toml::table& mTable = requireTable(mt["movement"], file, mPath); + toml::table& mMt = const_cast(mTable); + def.movement.speed_mps = static_cast(requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps")); + def.movement.mainAcceleration_mpss = static_cast(requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss")); + def.movement.maneuveringAcceleration_mpss = static_cast(requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss")); + def.movement.angularAcceleration_radpss = static_cast(requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss")); + def.movement.maxRotationSpeed_radps = static_cast(requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps")); + } + + // Sensor + { + const std::string snsPath = elemPath + ".sensor"; + const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath); + toml::table& snsMt = const_cast(snsTable); + def.sensor.sensorRange_m = static_cast(requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m")); + } + + // Optional: default_modules (REQ-WAV-DEFAULT-MODULES) + if (mt.contains("default_modules")) + { + const toml::array& modArr = requireArray(mt["default_modules"], file, + elemPath + ".default_modules"); + def.defaultModules = parsePlacedModules(modArr, file, + elemPath + ".default_modules"); + } + + cfg.ships.push_back(std::move(def)); + } + + return cfg; +} diff --git a/src/lib/config/ConfigLoaderStations.cpp b/src/lib/config/ConfigLoaderStations.cpp new file mode 100644 index 0000000..fcf4e6e --- /dev/null +++ b/src/lib/config/ConfigLoaderStations.cpp @@ -0,0 +1,47 @@ +#include "ConfigLoader.h" + +#include + +#include "toml.hpp" + +#include "TomlHelpers.h" + +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(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_m_formula"], file, p + ".range_m_formula"); + cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_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_m_formula"], file, p + ".range_m_formula"); + cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula"); + cfg.enemyStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula"); + } + + return cfg; +} diff --git a/src/lib/config/ConfigLoaderUnlocks.cpp b/src/lib/config/ConfigLoaderUnlocks.cpp new file mode 100644 index 0000000..eb6442e --- /dev/null +++ b/src/lib/config/ConfigLoaderUnlocks.cpp @@ -0,0 +1,62 @@ +#include "ConfigLoader.h" + +#include +#include + +#include "toml.hpp" + +#include "TomlHelpers.h" + +UnlocksConfig ConfigLoader::loadUnlocks(const std::string& path) +{ + const std::string file = "unlocks.toml"; + toml::table tbl = parseFile(path, file); + + UnlocksConfig cfg; + if (!tbl.contains("unlock")) + { + return cfg; + } + + const toml::array& arr = requireArray(tbl["unlock"], file, "unlock"); + + for (std::size_t i = 0; i < arr.size(); ++i) + { + const std::string elemPath = "unlock[" + std::to_string(i) + "]"; + const toml::table* ut = arr[i].as_table(); + if (ut == nullptr) + { + throw makeError(file, elemPath, "not a table"); + } + toml::table& mt = const_cast(*ut); + + UnlockGroupDef def; + def.id = requireString(mt["id"], file, elemPath + ".id"); + def.stationLevel = static_cast( + requireInt(mt["station_level"], file, elemPath + ".station_level")); + if (mt.contains("requires")) + { + def.requiredGroupIds = requireStringArray(mt["requires"], file, elemPath + ".requires"); + } + if (mt.contains("ships")) + { + def.ships = requireStringArray(mt["ships"], file, elemPath + ".ships"); + } + if (mt.contains("modules")) + { + def.modules = requireStringArray(mt["modules"], file, elemPath + ".modules"); + } + if (mt.contains("buildings")) + { + def.buildings = requireStringArray(mt["buildings"], file, elemPath + ".buildings"); + } + if (mt.contains("recipes")) + { + def.recipes = requireStringArray(mt["recipes"], file, elemPath + ".recipes"); + } + + cfg.groups.push_back(std::move(def)); + } + + return cfg; +} diff --git a/src/lib/config/ConfigLoaderWorld.cpp b/src/lib/config/ConfigLoaderWorld.cpp new file mode 100644 index 0000000..a1a2fd3 --- /dev/null +++ b/src/lib/config/ConfigLoaderWorld.cpp @@ -0,0 +1,79 @@ +#include "ConfigLoader.h" + +#include +#include + +#include "toml.hpp" + +#include "TomlHelpers.h" + +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(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles")); + cfg.refundPercentage = static_cast(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage")); + cfg.deconstructionTimeSeconds = requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds"); + cfg.startingBuildingBlocks = static_cast(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks")); + cfg.debrisDespawnSeconds = requireDouble(tbl["world"]["debris_despawn_seconds"], file, "world.debris_despawn_seconds"); + cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat"); + cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m"); + cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m; + cfg.tunnelMaxDistance_tiles = static_cast(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles")); + cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds"); + cfg.orbitFactor = requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor"); + cfg.rallyOrbitRadius_tiles = requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles"); + + if (const std::optional tip = + tbl["world"]["building_blocks_tooltip"].value()) + { + cfg.buildingBlocksTooltip = *tip; + } + + if (const std::optional tip = + tbl["world"]["artifact_tooltip"].value()) + { + cfg.artifactTooltip = *tip; + } + + cfg.regions.asteroidWidth_tiles = static_cast(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles")); + cfg.regions.playerBufferWidth_tiles = static_cast(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles")); + cfg.regions.contestZoneWidth_tiles = static_cast(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles")); + cfg.regions.enemyBufferWidth_tiles = static_cast(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles")); + + cfg.expansion.columnsPerExpansion_tiles = static_cast(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles")); + cfg.expansion.costBuildingBlocksFormula = requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula"); + + cfg.push.pushExpandColumns_tiles = static_cast(requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles")); + cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds"); + + cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_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"); + cfg.waves.bossCountdownSeconds = requireDouble(tbl["waves"]["boss_countdown_seconds"], file, "waves.boss_countdown_seconds"); + cfg.waves.bossThreatDurationSeconds = requireDouble(tbl["waves"]["boss_threat_duration_seconds"], file, "waves.boss_threat_duration_seconds"); + cfg.waves.bossQuietBeforeSeconds = requireDouble(tbl["waves"]["boss_quiet_before_seconds"], file, "waves.boss_quiet_before_seconds"); + cfg.waves.bossQuietAfterSeconds = requireDouble(tbl["waves"]["boss_quiet_after_seconds"], file, "waves.boss_quiet_after_seconds"); + + if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds) + { + throw makeError(file, "waves", "gap_min_seconds > gap_max_seconds"); + } + + cfg.targeting.targetScoreFormula = requireFormula(tbl["targeting"]["target_score_formula"], file, "targeting.target_score_formula"); + cfg.targeting.overclaimPenaltyFormula = requireFormula(tbl["targeting"]["overclaim_penalty_formula"], file, "targeting.overclaim_penalty_formula"); + cfg.targeting.hysteresis = requireDouble(tbl["targeting"]["target_hysteresis"], file, "targeting.target_hysteresis"); + + cfg.artifacts.artifactChanceFormula = requireFormula(tbl["artifacts"]["artifact_chance_formula"], file, "artifacts.artifact_chance_formula"); + cfg.artifacts.artifactWinCount = static_cast(requireInt(tbl["artifacts"]["artifact_win_count"], file, "artifacts.artifact_win_count")); + + cfg.scroll.panSpeedSlow_tps = requireDouble(tbl["scroll"]["pan_speed_slow_tiles_per_second"], file, "scroll.pan_speed_slow_tiles_per_second"); + cfg.scroll.panSpeedFast_tps = requireDouble(tbl["scroll"]["pan_speed_fast_tiles_per_second"], file, "scroll.pan_speed_fast_tiles_per_second"); + cfg.scroll.panRampBandWidth_tiles = static_cast(requireInt(tbl["scroll"]["pan_ramp_band_width_tiles"], file, "scroll.pan_ramp_band_width_tiles")); + + return cfg; +}