fix bug where splitter filters were not taken over to blueprint

This commit is contained in:
2026-06-23 21:08:46 +02:00
parent d271d65678
commit 577927ef70
6 changed files with 123 additions and 2 deletions

View File

@@ -51,6 +51,24 @@ std::string serialize(const std::vector<Blueprint>& blueprints)
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);
if (!b.splitterFilterA.empty())
{
toml::array filterArr;
for (const ItemType& item : b.splitterFilterA)
{
filterArr.push_back(item.id);
}
bldTbl.insert("filter_a", std::move(filterArr));
}
if (!b.splitterFilterB.empty())
{
toml::array filterArr;
for (const ItemType& item : b.splitterFilterB)
{
filterArr.push_back(item.id);
}
bldTbl.insert("filter_b", std::move(filterArr));
}
if (b.shipLayout.has_value())
{
toml::array modArr;
@@ -138,6 +156,28 @@ std::vector<Blueprint> deserialize(const std::string& tomlContent)
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{});
const toml::array* filterAArr = (*bldTbl)["filter_a"].as_array();
if (filterAArr)
{
for (std::size_t k = 0; k < filterAArr->size(); ++k)
{
const std::optional<std::string> itemId =
(*filterAArr)[k].value<std::string>();
if (itemId) { bb.splitterFilterA.push_back(ItemType{*itemId}); }
}
}
const toml::array* filterBArr = (*bldTbl)["filter_b"].as_array();
if (filterBArr)
{
for (std::size_t k = 0; k < filterBArr->size(); ++k)
{
const std::optional<std::string> itemId =
(*filterBArr)[k].value<std::string>();
if (itemId) { bb.splitterFilterB.push_back(ItemType{*itemId}); }
}
}
const toml::array* modArr = (*bldTbl)["modules"].as_array();
if (modArr)
{

View File

@@ -8,6 +8,7 @@
#include <QString>
#include "BuildingType.h"
#include "ItemType.h"
#include "Rotation.h"
#include "ShipLayout.h"
@@ -18,6 +19,11 @@ struct BlueprintBuilding
QPoint offset; // tile offset from bounding-box center (floor for even sizes)
std::string recipeId; // empty = none selected
std::optional<ShipLayoutConfig> shipLayout;
// Splitter output filters captured at blueprint creation (REQ-UI-BLUEPRINT-STORAGE).
// Empty = accept all. Re-applied to the placed splitter site (REQ-UI-BLUEPRINT-PLACE).
std::vector<ItemType> splitterFilterA;
std::vector<ItemType> splitterFilterB;
};
struct Blueprint

View File

@@ -10,6 +10,7 @@
#include "Blueprint.h"
#include "BlueprintSerializer.h"
#include "BuildingType.h"
#include "ItemType.h"
#include "Rotation.h"
#include "ShipLayout.h"
@@ -263,3 +264,42 @@ TEST_CASE("BlueprintSerializer: shipyard with empty modules list round-trips", "
REQUIRE(loaded[0].buildings[0].shipLayout.has_value());
REQUIRE(loaded[0].buildings[0].shipLayout->placedModules.empty());
}
// ---------------------------------------------------------------------------
// Splitter output filters round-trip through TOML serialization
// (REQ-UI-BLUEPRINT-STORAGE).
// ---------------------------------------------------------------------------
TEST_CASE("BlueprintSerializer: splitter output filters round-trip", "[serializer]")
{
BlueprintBuilding splitter =
makeBuilding(BuildingType::Splitter, Rotation::East, QPoint(0, 0));
splitter.splitterFilterA = { ItemType{"iron_ore"}, ItemType{"copper_ore"} };
splitter.splitterFilterB = { ItemType{"scrap"} };
const Blueprint original = makeBlueprintWith({ splitter }, "Sorter");
const std::string toml = BlueprintSerializer::serialize({ original });
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
REQUIRE(loaded.size() == 1);
REQUIRE(loaded[0].buildings.size() == 1);
const BlueprintBuilding& b = loaded[0].buildings[0];
REQUIRE(b.splitterFilterA
== std::vector<ItemType>{ ItemType{"iron_ore"}, ItemType{"copper_ore"} });
REQUIRE(b.splitterFilterB == std::vector<ItemType>{ ItemType{"scrap"} });
}
TEST_CASE("BlueprintSerializer: splitter with no filters round-trips empty", "[serializer]")
{
const Blueprint original = makeBlueprintWith(
{ makeBuilding(BuildingType::Splitter, Rotation::East, QPoint(0, 0)) }, "Open");
const std::string toml = BlueprintSerializer::serialize({ original });
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
REQUIRE(loaded.size() == 1);
REQUIRE(loaded[0].buildings.size() == 1);
REQUIRE(loaded[0].buildings[0].splitterFilterA.empty());
REQUIRE(loaded[0].buildings[0].splitterFilterB.empty());
}

View File

@@ -201,6 +201,16 @@ Blueprint BlueprintPanel::createBlueprintFromSelection() const
bb.offset = e.building->anchor - center;
bb.recipeId = e.building->recipeId;
bb.shipLayout = e.building->shipLayout;
if (e.building->type == BuildingType::Splitter)
{
const std::optional<BeltSystem::SplitterInfo> info =
m_sim->belts().getSplitterInfo(e.building->anchor);
if (info.has_value())
{
bb.splitterFilterA = info->filterA;
bb.splitterFilterB = info->filterB;
}
}
bp.buildings.push_back(bb);
}
return bp;

View File

@@ -58,6 +58,19 @@
namespace
{
// Keep only the filter entries whose item type is currently unlocked
// (REQ-LOCK-UI-BLUEPRINT). An empty result means "accept all".
std::vector<ItemType> filterUnlockedItems(const std::vector<ItemType>& filter,
const Simulation& sim)
{
std::vector<ItemType> result;
for (const ItemType& item : filter)
{
if (sim.isItemUnlocked(item.id)) { result.push_back(item); }
}
return result;
}
Rotation rotateClockwise(Rotation r)
{
switch (r)
@@ -532,6 +545,18 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
{
m_sim->buildings().setShipLayout(id, *bb.shipLayout);
}
if (bb.type == BuildingType::Splitter
&& (!bb.splitterFilterA.empty() || !bb.splitterFilterB.empty()))
{
// The splitter is still a construction site, so the filters carry
// over when it finishes building (REQ-UI-BLUEPRINT-PLACE). Locked
// item types are dropped per REQ-LOCK-UI-BLUEPRINT.
m_sim->buildings().setSiteSplitterFilters(
id,
filterUnlockedItems(bb.splitterFilterA, *m_sim),
filterUnlockedItems(bb.splitterFilterB, *m_sim));
}
}
}