Compare commits
3 Commits
60eaf4dc57
...
1e6d838258
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e6d838258 | |||
| ced4ab5fe3 | |||
| 550f46009f |
@@ -244,3 +244,7 @@ The screen is divided into three vertical sections:
|
|||||||
- REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately.
|
- REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-DELETE: Clicking the "Delete Blueprint" button toggles delete mode on and off. While delete mode is active the button is shown in a visually active/pressed state. Clicking any blueprint button while delete mode is active removes that blueprint from the list and automatically exits delete mode.
|
- REQ-UI-BLUEPRINT-DELETE: Clicking the "Delete Blueprint" button toggles delete mode on and off. While delete mode is active the button is shown in a visually active/pressed state. Clicking any blueprint button while delete mode is active removes that blueprint from the list and automatically exits delete mode.
|
||||||
|
|
||||||
|
- REQ-UI-BLUEPRINT-SAVE: A "Save" button is shown at the bottom of the blueprint panel. Clicking it serializes all current blueprints to a file named `blueprints.toml` located in the same directory as the application executable. The TOML structure matches REQ-UI-BLUEPRINT-STORAGE. If writing fails, a modal error dialog is shown describing the failure.
|
||||||
|
|
||||||
|
- REQ-UI-BLUEPRINT-LOAD: A "Load" button is shown at the bottom of the blueprint panel, to the right of the "Save" button. Clicking it shows a confirmation dialog ("Load blueprints? This will replace all current blueprints.") with Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm reads `blueprints.toml` from the same directory as the application executable, replaces all current blueprints with those from the file (in the order they appear in the file), and exits any active blueprint-related mode (blueprint placement mode, delete mode). If the file does not exist or cannot be parsed, a modal error dialog is shown describing the failure and the current blueprint list is left unchanged.
|
||||||
|
|||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
187
src/test/BlueprintSerializerTest.cpp
Normal file
187
src/test/BlueprintSerializerTest.cpp
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "Blueprint.h"
|
||||||
|
#include "BlueprintSerializer.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
Blueprint makeBlueprintWith(const std::vector<BlueprintBuilding>& buildings,
|
||||||
|
const QString& name = "Test")
|
||||||
|
{
|
||||||
|
Blueprint bp;
|
||||||
|
bp.name = name;
|
||||||
|
bp.buildings = buildings;
|
||||||
|
return bp;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlueprintBuilding makeBuilding(BuildingType type,
|
||||||
|
Rotation rotation,
|
||||||
|
QPoint offset,
|
||||||
|
std::string recipeId = "")
|
||||||
|
{
|
||||||
|
BlueprintBuilding b;
|
||||||
|
b.type = type;
|
||||||
|
b.rotation = rotation;
|
||||||
|
b.offset = offset;
|
||||||
|
b.recipeId = std::move(recipeId);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Case 1 — single blueprint, one building: all fields survive round-trip
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: single blueprint round-trips all fields", "[serializer]")
|
||||||
|
{
|
||||||
|
const BlueprintBuilding building =
|
||||||
|
makeBuilding(BuildingType::Miner, Rotation::East, QPoint(2, -1), "mine_iron_ore");
|
||||||
|
const Blueprint original = makeBlueprintWith({building}, "My Miner");
|
||||||
|
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({original});
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded.size() == 1);
|
||||||
|
REQUIRE(loaded[0].name == "My Miner");
|
||||||
|
REQUIRE(loaded[0].buildings.size() == 1);
|
||||||
|
|
||||||
|
const BlueprintBuilding& b = loaded[0].buildings[0];
|
||||||
|
REQUIRE(b.type == BuildingType::Miner);
|
||||||
|
REQUIRE(b.rotation == Rotation::East);
|
||||||
|
REQUIRE(b.offset == QPoint(2, -1));
|
||||||
|
REQUIRE(b.recipeId == "mine_iron_ore");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Case 2 — multiple blueprints: count and order preserved
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: multiple blueprints preserve count and order", "[serializer]")
|
||||||
|
{
|
||||||
|
const Blueprint first = makeBlueprintWith(
|
||||||
|
{makeBuilding(BuildingType::Belt, Rotation::North, QPoint(0, 0))}, "First");
|
||||||
|
const Blueprint second = makeBlueprintWith(
|
||||||
|
{makeBuilding(BuildingType::Smelter, Rotation::South, QPoint(1, 0))}, "Second");
|
||||||
|
const Blueprint third = makeBlueprintWith(
|
||||||
|
{makeBuilding(BuildingType::Assembler, Rotation::West, QPoint(0, 1))}, "Third");
|
||||||
|
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({first, second, third});
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded.size() == 3);
|
||||||
|
REQUIRE(loaded[0].name == "First");
|
||||||
|
REQUIRE(loaded[1].name == "Second");
|
||||||
|
REQUIRE(loaded[2].name == "Third");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Case 3 — all four Rotation values round-trip
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: all rotations round-trip", "[serializer]")
|
||||||
|
{
|
||||||
|
const std::vector<Rotation> rotations = {
|
||||||
|
Rotation::North, Rotation::East, Rotation::South, Rotation::West
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const Rotation rot : rotations)
|
||||||
|
{
|
||||||
|
const Blueprint bp = makeBlueprintWith(
|
||||||
|
{makeBuilding(BuildingType::Belt, rot, QPoint(0, 0))});
|
||||||
|
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({bp});
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded.size() == 1);
|
||||||
|
REQUIRE(loaded[0].buildings.size() == 1);
|
||||||
|
REQUIRE(loaded[0].buildings[0].rotation == rot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Case 5 — negative and zero offsets survive intact
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: negative and zero offsets round-trip", "[serializer]")
|
||||||
|
{
|
||||||
|
const Blueprint bp = makeBlueprintWith({
|
||||||
|
makeBuilding(BuildingType::Belt, Rotation::North, QPoint(0, 0)),
|
||||||
|
makeBuilding(BuildingType::Belt, Rotation::North, QPoint(-3, -2)),
|
||||||
|
makeBuilding(BuildingType::Belt, Rotation::North, QPoint(5, 4)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({bp});
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded[0].buildings[0].offset == QPoint(0, 0));
|
||||||
|
REQUIRE(loaded[0].buildings[1].offset == QPoint(-3, -2));
|
||||||
|
REQUIRE(loaded[0].buildings[2].offset == QPoint(5, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Case 6 — empty and non-empty recipeId both round-trip correctly
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: empty and non-empty recipeId round-trip", "[serializer]")
|
||||||
|
{
|
||||||
|
const Blueprint bp = makeBlueprintWith({
|
||||||
|
makeBuilding(BuildingType::Miner, Rotation::North, QPoint(0, 0), "mine_iron_ore"),
|
||||||
|
makeBuilding(BuildingType::Assembler, Rotation::North, QPoint(3, 0), ""),
|
||||||
|
});
|
||||||
|
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({bp});
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded[0].buildings[0].recipeId == "mine_iron_ore");
|
||||||
|
REQUIRE(loaded[0].buildings[1].recipeId == "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Case 8 — empty blueprint list round-trips to empty vector
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: empty list round-trips to empty vector", "[serializer]")
|
||||||
|
{
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({});
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Case 9 — malformed TOML throws std::runtime_error
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: malformed TOML throws", "[serializer]")
|
||||||
|
{
|
||||||
|
REQUIRE_THROWS_AS(
|
||||||
|
BlueprintSerializer::deserialize("[[blueprint\nname = 42"),
|
||||||
|
std::runtime_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Case 10 — unknown building type string throws std::runtime_error
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: unknown building type throws", "[serializer]")
|
||||||
|
{
|
||||||
|
const std::string badToml =
|
||||||
|
"[[blueprint]]\n"
|
||||||
|
"name = \"Broken\"\n"
|
||||||
|
"buildings = [{type = \"unicorn\", rotation = \"north\","
|
||||||
|
" offset_x = 0, offset_y = 0, recipe_id = \"\"}]\n";
|
||||||
|
|
||||||
|
REQUIRE_THROWS_AS(
|
||||||
|
BlueprintSerializer::deserialize(badToml),
|
||||||
|
std::runtime_error);
|
||||||
|
}
|
||||||
@@ -16,4 +16,5 @@ add_files(
|
|||||||
CombatSystemTest.cpp
|
CombatSystemTest.cpp
|
||||||
ShipyardTest.cpp
|
ShipyardTest.cpp
|
||||||
BlueprintTest.cpp
|
BlueprintTest.cpp
|
||||||
|
BlueprintSerializerTest.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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