diff --git a/src/lib/config/BlueprintSerializer.cpp b/src/lib/config/BlueprintSerializer.cpp new file mode 100644 index 0000000..5d5f572 --- /dev/null +++ b/src/lib/config/BlueprintSerializer.cpp @@ -0,0 +1,136 @@ +#include "BlueprintSerializer.h" + +#include +#include + +#include "toml.hpp" + +#include "BuildingType.h" +#include "Rotation.h" + +namespace +{ + +std::string rotationToString(Rotation r) +{ + switch (r) + { + case Rotation::North: return "north"; + case Rotation::East: return "east"; + case Rotation::South: return "south"; + case Rotation::West: return "west"; + } + return "north"; +} + +Rotation parseRotation(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; +} + +} // namespace + +namespace BlueprintSerializer +{ + +std::string serialize(const std::vector& blueprints) +{ + toml::array bpArr; + for (const Blueprint& bp : blueprints) + { + toml::array bldArr; + for (const BlueprintBuilding& b : bp.buildings) + { + toml::table bldTbl; + bldTbl.insert("type", buildingTypeId(b.type)); + bldTbl.insert("rotation", rotationToString(b.rotation)); + bldTbl.insert("offset_x", static_cast(b.offset.x())); + bldTbl.insert("offset_y", static_cast(b.offset.y())); + bldTbl.insert("recipe_id", b.recipeId); + bldArr.push_back(std::move(bldTbl)); + } + + toml::table bpTbl; + bpTbl.insert("name", bp.name.toStdString()); + bpTbl.insert("buildings", std::move(bldArr)); + bpArr.push_back(std::move(bpTbl)); + } + + toml::table root; + root.insert("blueprint", std::move(bpArr)); + + std::ostringstream oss; + oss << root; + return oss.str(); +} + +std::vector deserialize(const std::string& tomlContent) +{ + toml::table root; + try + { + root = toml::parse(tomlContent); + } + catch (const toml::parse_error& e) + { + std::ostringstream msg; + msg << "TOML parse error: " << e.description() << " at " << e.source().begin; + throw std::runtime_error(msg.str()); + } + + std::vector result; + + const toml::array* bpArr = root["blueprint"].as_array(); + if (!bpArr) { return result; } + + for (std::size_t i = 0; i < bpArr->size(); ++i) + { + const toml::table* bpTbl = (*bpArr)[i].as_table(); + if (!bpTbl) + { + throw std::runtime_error("blueprint[" + std::to_string(i) + "] is not a table"); + } + + Blueprint bp; + bp.name = QString::fromStdString((*bpTbl)["name"].value_or(std::string{})); + + const toml::array* bldArr = (*bpTbl)["buildings"].as_array(); + if (bldArr) + { + for (std::size_t j = 0; j < bldArr->size(); ++j) + { + const toml::table* bldTbl = (*bldArr)[j].as_table(); + if (!bldTbl) + { + throw std::runtime_error( + "blueprint[" + std::to_string(i) + "].buildings[" + + std::to_string(j) + "] is not a table"); + } + + const std::string typeStr = (*bldTbl)["type"].value_or(std::string{}); + const std::optional parsedType = parseBuildingType(typeStr); + if (!parsedType) + { + throw std::runtime_error("unknown building type: '" + typeStr + "'"); + } + + BlueprintBuilding bb; + bb.type = *parsedType; + bb.rotation = parseRotation((*bldTbl)["rotation"].value_or(std::string{})); + bb.offset.setX(static_cast((*bldTbl)["offset_x"].value_or(int64_t{0}))); + bb.offset.setY(static_cast((*bldTbl)["offset_y"].value_or(int64_t{0}))); + bb.recipeId = (*bldTbl)["recipe_id"].value_or(std::string{}); + bp.buildings.push_back(std::move(bb)); + } + } + + result.push_back(std::move(bp)); + } + + return result; +} + +} // namespace BlueprintSerializer diff --git a/src/lib/config/BlueprintSerializer.h b/src/lib/config/BlueprintSerializer.h new file mode 100644 index 0000000..b2ae470 --- /dev/null +++ b/src/lib/config/BlueprintSerializer.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +#include "Blueprint.h" + +namespace BlueprintSerializer +{ + +std::string serialize(const std::vector& blueprints); +std::vector deserialize(const std::string& tomlContent); + +} // namespace BlueprintSerializer diff --git a/src/lib/config/CMakeLists.txt b/src/lib/config/CMakeLists.txt index 5de8842..389fe83 100644 --- a/src/lib/config/CMakeLists.txt +++ b/src/lib/config/CMakeLists.txt @@ -9,6 +9,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/GameConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h ${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h + ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h PARENT_SCOPE ) @@ -17,6 +18,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/Formula.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.cpp PARENT_SCOPE ) diff --git a/src/ui/BlueprintPanel.cpp b/src/ui/BlueprintPanel.cpp index c6f760b..8e2dba3 100644 --- a/src/ui/BlueprintPanel.cpp +++ b/src/ui/BlueprintPanel.cpp @@ -3,11 +3,17 @@ #include #include +#include +#include +#include #include +#include #include #include #include +#include "BlueprintSerializer.h" + #include "Building.h" #include "BuildingSystem.h" #include "Simulation.h" @@ -49,6 +55,18 @@ BlueprintPanel::BlueprintPanel(Simulation* sim, const GameConfig* config, QWidge connect(m_createBtn, &QPushButton::clicked, this, &BlueprintPanel::onCreateClicked); connect(m_deleteBtn, &QPushButton::clicked, this, &BlueprintPanel::onDeleteClicked); + + QHBoxLayout* ioLayout = new QHBoxLayout(); + m_saveBtn = new QPushButton("Save", this); + m_loadBtn = new QPushButton("Load", this); + m_saveBtn->setFixedHeight(36); + m_loadBtn->setFixedHeight(36); + ioLayout->addWidget(m_saveBtn); + ioLayout->addWidget(m_loadBtn); + layout->addLayout(ioLayout); + + connect(m_saveBtn, &QPushButton::clicked, this, &BlueprintPanel::onSaveClicked); + connect(m_loadBtn, &QPushButton::clicked, this, &BlueprintPanel::onLoadClicked); } void BlueprintPanel::onSelectionChanged(const std::vector& ids) @@ -242,6 +260,72 @@ void BlueprintPanel::rebuildButtons() refreshButtonStates(); } +void BlueprintPanel::onSaveClicked() +{ + const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml"; + try + { + const std::string content = BlueprintSerializer::serialize(m_blueprints); + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QMessageBox::critical(this, "Save Failed", + QString("Could not open file for writing:\n%1").arg(path)); + return; + } + file.write(QByteArray::fromStdString(content)); + } + catch (const std::exception& e) + { + QMessageBox::critical(this, "Save Failed", + QString("Failed to save blueprints:\n%1").arg(e.what())); + } +} + +void BlueprintPanel::onLoadClicked() +{ + QMessageBox box(this); + box.setWindowTitle("Load Blueprints"); + box.setText("Load blueprints? This will replace all current blueprints."); + QPushButton* confirmBtn = box.addButton("Confirm", QMessageBox::AcceptRole); + box.addButton("Cancel", QMessageBox::RejectRole); + box.exec(); + if (box.clickedButton() != confirmBtn) { return; } + + const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml"; + try + { + QFile file(path); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QMessageBox::critical(this, "Load Failed", + QString("Could not open file:\n%1").arg(path)); + return; + } + const std::string content = file.readAll().toStdString(); + std::vector loaded = BlueprintSerializer::deserialize(content); + + if (m_activeIndex >= 0) + { + emit exitBlueprintModeRequested(); + m_activeIndex = -1; + } + if (m_deleteMode) + { + m_deleteMode = false; + m_deleteBtn->setChecked(false); + } + + m_blueprints = std::move(loaded); + rebuildButtons(); + } + catch (const std::exception& e) + { + QMessageBox::critical(this, "Load Failed", + QString("Failed to load blueprints:\n%1").arg(e.what())); + } +} + void BlueprintPanel::refreshButtonStates() { const bool anyPlaceable = [&]() { diff --git a/src/ui/BlueprintPanel.h b/src/ui/BlueprintPanel.h index a936411..263328f 100644 --- a/src/ui/BlueprintPanel.h +++ b/src/ui/BlueprintPanel.h @@ -34,6 +34,8 @@ private slots: void onCreateClicked(); void onDeleteClicked(); void onBlueprintButtonClicked(int index); + void onSaveClicked(); + void onLoadClicked(); private: Blueprint createBlueprintFromSelection() const; @@ -51,6 +53,8 @@ private: std::vector m_blueprintButtons; QPushButton* m_createBtn; QPushButton* m_deleteBtn; + QPushButton* m_saveBtn; + QPushButton* m_loadBtn; QWidget* m_buttonsContainer; QVBoxLayout* m_buttonsLayout; };