From 577927ef70645f0a33c6d0b89cf3cee29b99cc5f Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Tue, 23 Jun 2026 21:08:46 +0200 Subject: [PATCH] fix bug where splitter filters were not taken over to blueprint --- docs/requirements.md | 4 +-- src/lib/config/BlueprintSerializer.cpp | 40 ++++++++++++++++++++++++++ src/lib/core/Blueprint.h | 6 ++++ src/test/BlueprintSerializerTest.cpp | 40 ++++++++++++++++++++++++++ src/ui/BlueprintPanel.cpp | 10 +++++++ src/ui/GameWorldView.cpp | 25 ++++++++++++++++ 6 files changed, 123 insertions(+), 2 deletions(-) diff --git a/docs/requirements.md b/docs/requirements.md index 30880f3..a56537d 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -455,13 +455,13 @@ The screen is divided into two columns: a main column (75% width) containing the - REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list. -- REQ-UI-BLUEPRINT-STORAGE: A blueprint stores its name and, for each building in the selection, the building type, its rotation, its tile offset (integer dx, dy) from the center of the bounding box of all selected buildings' footprints, and — where applicable — the selected recipe ID (miners and assemblers) or schematic ID (shipyards) at the time of capture. If no recipe or schematic was selected at capture time, none is stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building). +- REQ-UI-BLUEPRINT-STORAGE: A blueprint stores its name and, for each building in the selection, the building type, its rotation, its tile offset (integer dx, dy) from the center of the bounding box of all selected buildings' footprints, and — where applicable — the selected recipe ID (miners and assemblers) or schematic ID (shipyards), and for splitters the two output filters (each a list of item types; an empty list means accept-all), at the time of capture. If no recipe or schematic was selected at capture time, none is stored; for a splitter with no filters set, no filter lists are stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building, with the splitter filters as `filter_a`/`filter_b` arrays of item-type ids). - REQ-UI-BLUEPRINT-BUTTON: Each blueprint entry consists of a blueprint button and a dedicated delete icon ("×") placed to the right of the button. The blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. The delete icon is always enabled regardless of whether the player can afford the blueprint. - REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). Pressing Q/E rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint. -- 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. Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. 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. If splitter output filters are stored, they are applied to the construction site immediately and carry over when it finishes building (REQ-BLD-SITE-CONFIG). Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. 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 icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited. diff --git a/src/lib/config/BlueprintSerializer.cpp b/src/lib/config/BlueprintSerializer.cpp index 381ed8e..7ddcf0a 100644 --- a/src/lib/config/BlueprintSerializer.cpp +++ b/src/lib/config/BlueprintSerializer.cpp @@ -51,6 +51,24 @@ std::string serialize(const std::vector& blueprints) bldTbl.insert("offset_x", static_cast(b.offset.x())); bldTbl.insert("offset_y", static_cast(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 deserialize(const std::string& tomlContent) 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{}); + + const toml::array* filterAArr = (*bldTbl)["filter_a"].as_array(); + if (filterAArr) + { + for (std::size_t k = 0; k < filterAArr->size(); ++k) + { + const std::optional itemId = + (*filterAArr)[k].value(); + 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 itemId = + (*filterBArr)[k].value(); + if (itemId) { bb.splitterFilterB.push_back(ItemType{*itemId}); } + } + } + const toml::array* modArr = (*bldTbl)["modules"].as_array(); if (modArr) { diff --git a/src/lib/core/Blueprint.h b/src/lib/core/Blueprint.h index c1a7642..9fc3133 100644 --- a/src/lib/core/Blueprint.h +++ b/src/lib/core/Blueprint.h @@ -8,6 +8,7 @@ #include #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 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 splitterFilterA; + std::vector splitterFilterB; }; struct Blueprint diff --git a/src/test/BlueprintSerializerTest.cpp b/src/test/BlueprintSerializerTest.cpp index 145ede3..d932bc5 100644 --- a/src/test/BlueprintSerializerTest.cpp +++ b/src/test/BlueprintSerializerTest.cpp @@ -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 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{"iron_ore"}, ItemType{"copper_ore"} }); + REQUIRE(b.splitterFilterB == std::vector{ 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 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()); +} diff --git a/src/ui/BlueprintPanel.cpp b/src/ui/BlueprintPanel.cpp index 63397db..1b5f2bd 100644 --- a/src/ui/BlueprintPanel.cpp +++ b/src/ui/BlueprintPanel.cpp @@ -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 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; diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index dbe928a..8c9dddf 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -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 filterUnlockedItems(const std::vector& filter, + const Simulation& sim) +{ + std::vector 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)); + } } }