implement save and load blueprints
This commit is contained in:
136
src/lib/config/BlueprintSerializer.cpp
Normal file
136
src/lib/config/BlueprintSerializer.cpp
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#include "BlueprintSerializer.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#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<Blueprint>& 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<int64_t>(b.offset.x()));
|
||||||
|
bldTbl.insert("offset_y", static_cast<int64_t>(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<Blueprint> 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<Blueprint> 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<BuildingType> 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<int>((*bldTbl)["offset_x"].value_or(int64_t{0})));
|
||||||
|
bb.offset.setY(static_cast<int>((*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
|
||||||
14
src/lib/config/BlueprintSerializer.h
Normal file
14
src/lib/config/BlueprintSerializer.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Blueprint.h"
|
||||||
|
|
||||||
|
namespace BlueprintSerializer
|
||||||
|
{
|
||||||
|
|
||||||
|
std::string serialize(const std::vector<Blueprint>& blueprints);
|
||||||
|
std::vector<Blueprint> deserialize(const std::string& tomlContent);
|
||||||
|
|
||||||
|
} // namespace BlueprintSerializer
|
||||||
@@ -9,6 +9,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameConfig.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/Formula.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/Formula.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,17 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QHBoxLayout>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BlueprintSerializer.h"
|
||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "Simulation.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_createBtn, &QPushButton::clicked, this, &BlueprintPanel::onCreateClicked);
|
||||||
connect(m_deleteBtn, &QPushButton::clicked, this, &BlueprintPanel::onDeleteClicked);
|
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<EntityId>& ids)
|
void BlueprintPanel::onSelectionChanged(const std::vector<EntityId>& ids)
|
||||||
@@ -242,6 +260,72 @@ void BlueprintPanel::rebuildButtons()
|
|||||||
refreshButtonStates();
|
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<Blueprint> 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()
|
void BlueprintPanel::refreshButtonStates()
|
||||||
{
|
{
|
||||||
const bool anyPlaceable = [&]() {
|
const bool anyPlaceable = [&]() {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ private slots:
|
|||||||
void onCreateClicked();
|
void onCreateClicked();
|
||||||
void onDeleteClicked();
|
void onDeleteClicked();
|
||||||
void onBlueprintButtonClicked(int index);
|
void onBlueprintButtonClicked(int index);
|
||||||
|
void onSaveClicked();
|
||||||
|
void onLoadClicked();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Blueprint createBlueprintFromSelection() const;
|
Blueprint createBlueprintFromSelection() const;
|
||||||
@@ -51,6 +53,8 @@ private:
|
|||||||
std::vector<QPushButton*> m_blueprintButtons;
|
std::vector<QPushButton*> m_blueprintButtons;
|
||||||
QPushButton* m_createBtn;
|
QPushButton* m_createBtn;
|
||||||
QPushButton* m_deleteBtn;
|
QPushButton* m_deleteBtn;
|
||||||
|
QPushButton* m_saveBtn;
|
||||||
|
QPushButton* m_loadBtn;
|
||||||
QWidget* m_buttonsContainer;
|
QWidget* m_buttonsContainer;
|
||||||
QVBoxLayout* m_buttonsLayout;
|
QVBoxLayout* m_buttonsLayout;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user