232 lines
7.9 KiB
C++
232 lines
7.9 KiB
C++
#include "VisualsLoader.h"
|
|
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
#include <QColor>
|
|
|
|
#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<std::string> v = tbl[key].value<std::string>();
|
|
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<int64_t> v = tbl[key].value<int64_t>();
|
|
if (!v)
|
|
{
|
|
throw makeError(ctx, "missing or invalid integer '" + key + "'");
|
|
}
|
|
return static_cast<int>(*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 },
|
|
};
|
|
|
|
} // 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;
|
|
}
|