#include "VisualsLoader.h" #include #include #include #include #include "toml.hpp" #include "BuildingType.h" namespace { std::runtime_error makeError(const std::string& section, const std::string& why) { return std::runtime_error("visuals.toml: [" + section + "] " + why); } QColor parseColor(const std::string& s, const std::string& ctx) { if (s.empty() || s[0] != '#') { throw std::runtime_error("visuals.toml: invalid color '" + s + "' in " + ctx); } if (s.size() == 9) { bool ok1 = true, ok2 = true, ok3 = true, ok4 = true; int r = QString::fromStdString(s.substr(1, 2)).toInt(&ok1, 16); int g = QString::fromStdString(s.substr(3, 2)).toInt(&ok2, 16); int b = QString::fromStdString(s.substr(5, 2)).toInt(&ok3, 16); int a = QString::fromStdString(s.substr(7, 2)).toInt(&ok4, 16); if (!ok1 || !ok2 || !ok3 || !ok4) { throw std::runtime_error("visuals.toml: malformed color '" + s + "' in " + ctx); } return QColor(r, g, b, a); } QColor c(QString::fromStdString(s)); if (!c.isValid()) { throw std::runtime_error("visuals.toml: invalid color '" + s + "' in " + ctx); } return c; } std::string requireString(toml::table& tbl, const std::string& key, const std::string& ctx) { const std::optional v = tbl[key].value(); if (!v) { throw makeError(ctx, "missing or invalid string '" + key + "'"); } return *v; } int requireInt(toml::table& tbl, const std::string& key, const std::string& ctx) { const std::optional v = tbl[key].value(); if (!v) { throw makeError(ctx, "missing or invalid integer '" + key + "'"); } return static_cast(*v); } toml::table& requireSubtable(toml::table& tbl, const std::string& key, const std::string& ctx) { toml::table* sub = tbl[key].as_table(); if (sub == nullptr) { throw makeError(ctx, "missing section '" + key + "'"); } return *sub; } TileVisuals parseTile(toml::table& tbl, const std::string& ctx) { TileVisuals v; v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill"); return v; } BuildingVisuals parseBuilding(toml::table& tbl, const std::string& ctx) { BuildingVisuals v; v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill"); v.outline = parseColor(requireString(tbl, "outline", ctx), ctx + ".outline"); v.glyph = QString::fromStdString(requireString(tbl, "glyph", ctx)); return v; } ItemVisuals parseItem(toml::table& tbl, const std::string& ctx) { ItemVisuals v; v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill"); v.outline = parseColor(requireString(tbl, "outline", ctx), ctx + ".outline"); return v; } ShipVisuals parseShip(toml::table& tbl, const std::string& ctx) { ShipVisuals v; v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill"); v.outline = parseColor(requireString(tbl, "outline", ctx), ctx + ".outline"); return v; } struct BuildingEntry { const char* key; BuildingType type; }; const BuildingEntry kBuildingEntries[] = { { "hq", BuildingType::Hq }, { "miner", BuildingType::Miner }, { "smelter", BuildingType::Smelter }, { "assembler", BuildingType::Assembler }, { "reprocessing_plant",BuildingType::ReprocessingPlant }, { "shipyard", BuildingType::Shipyard }, { "salvage_bay", BuildingType::SalvageBay }, { "belt", BuildingType::Belt }, { "splitter", BuildingType::Splitter }, { "tunnel_entry", BuildingType::TunnelEntry }, { "tunnel_exit", BuildingType::TunnelExit }, }; } // namespace VisualsConfig VisualsLoader::load(const std::string& path) { toml::table tbl; try { tbl = toml::parse_file(path); } catch (const toml::parse_error& e) { std::ostringstream oss; oss << "visuals.toml: parse error: " << e.description() << " at " << e.source().begin; throw std::runtime_error(oss.str()); } VisualsConfig cfg; // Tiles { toml::table& tiles = requireSubtable(tbl, "tiles", "root"); cfg.asteroid = parseTile(requireSubtable(tiles, "asteroid", "tiles"), "tiles.asteroid"); cfg.space = parseTile(requireSubtable(tiles, "space", "tiles"), "tiles.space"); } // Buildings ([buildings.*] sections) { toml::table& bldgs = requireSubtable(tbl, "buildings", "root"); for (const BuildingEntry& entry : kBuildingEntries) { std::string ctx = std::string("buildings.") + entry.key; cfg.buildings[entry.type] = parseBuilding( requireSubtable(bldgs, entry.key, "buildings"), ctx); } } // Stations ([stations.*] → mapped as PlayerDefenceStation / EnemyDefenceStation) { toml::table& stns = requireSubtable(tbl, "stations", "root"); cfg.buildings[BuildingType::PlayerDefenceStation] = parseBuilding( requireSubtable(stns, "player", "stations"), "stations.player"); cfg.buildings[BuildingType::EnemyDefenceStation] = parseBuilding( requireSubtable(stns, "enemy", "stations"), "stations.enemy"); } // Items (iterate all keys in [items]) { toml::table& items = requireSubtable(tbl, "items", "root"); for (toml::table::iterator it = items.begin(); it != items.end(); ++it) { std::string itemId = std::string(it->first.str()); toml::table* sub = it->second.as_table(); if (sub == nullptr) { throw std::runtime_error("visuals.toml: items." + itemId + " is not a table"); } cfg.items[itemId] = parseItem(*sub, "items." + itemId); } } // Ships { toml::table& ships = requireSubtable(tbl, "ships", "root"); cfg.ships[ShipRole::PlayerCombat] = parseShip( requireSubtable(ships, "player_combat", "ships"), "ships.player_combat"); cfg.ships[ShipRole::Salvage] = parseShip( requireSubtable(ships, "salvage", "ships"), "ships.salvage"); cfg.ships[ShipRole::Repair] = parseShip( requireSubtable(ships, "repair", "ships"), "ships.repair"); cfg.ships[ShipRole::Enemy] = parseShip( requireSubtable(ships, "enemy", "ships"), "ships.enemy"); } // Beams { toml::table& beams = requireSubtable(tbl, "beams", "root"); cfg.beams.color = parseColor(requireString(beams, "color", "beams"), "beams.color"); cfg.beams.widthPx = requireInt(beams, "width_px", "beams"); } // Overlays { toml::table& ov = requireSubtable(tbl, "overlays", "root"); cfg.overlays.ghostValid = parseColor(requireString(ov, "ghost_valid", "overlays"), "overlays.ghost_valid"); cfg.overlays.ghostInvalid = parseColor(requireString(ov, "ghost_invalid", "overlays"), "overlays.ghost_invalid"); cfg.overlays.demolishTint = parseColor(requireString(ov, "demolish_tint", "overlays"), "overlays.demolish_tint"); cfg.overlays.selectionRect = parseColor(requireString(ov, "selection_rect", "overlays"), "overlays.selection_rect"); cfg.overlays.tileHighlight = parseColor(requireString(ov, "tile_highlight", "overlays"), "overlays.tile_highlight"); cfg.overlays.selectedOutline = parseColor(requireString(ov, "selected_outline", "overlays"), "overlays.selected_outline"); } // Toast { toml::table& t = requireSubtable(tbl, "toast", "root"); cfg.toast.bg = parseColor(requireString(t, "bg", "toast"), "toast.bg"); cfg.toast.fg = parseColor(requireString(t, "fg", "toast"), "toast.fg"); cfg.toast.fontSize = requireInt(t, "font_size", "toast"); } return cfg; }