Implement REQ-BLD-COPY-CONFIG: copy building settings with Shift

Shift+right-click a building copies its settings (miner/assembler recipe,
shipyard schematic + module layout, or splitter output filters) into a
temporary UI cache; Shift+left-click stamps them onto another building of
the same type. The gesture is active only in the default selection mode,
works on operational buildings and construction sites, and the cache
clears when Shift is released (or on Restart).

Paste reuses the existing configuration commands (SetRecipe, SetShipLayout,
SetSplitterFilters, SetSiteSplitterFilters), so it inherits their buffer-
clearing / mid-cycle-cancel semantics and replays for free — no new command
kinds or serializer changes.

Extract the recipe/schematic/layout/filter reader into a shared
readBuildingConfig() (lib/sim), handling buildings and sites alike, and
reuse it from BlueprintPanel to remove the duplicated capture logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNNLeFWhVzvxkK9qVXP2K
This commit is contained in:
2026-07-09 16:21:59 +02:00
parent c144078ea6
commit 92cfcee682
8 changed files with 352 additions and 13 deletions

View File

@@ -0,0 +1,133 @@
#include "catch.hpp"
#include "Building.h"
#include "BuildingConfig.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "ConfigLoader.h"
#include "GameConfig.h"
#include "Rotation.h"
#include "ShipLayout.h"
#include "ShipsConfig.h"
#include "Simulation.h"
#include "SimulationTestAccess.h"
// readBuildingConfig underpins the copy-settings gesture (REQ-BLD-COPY-CONFIG):
// it extracts a building's recipe / schematic / layout / splitter filters so they
// can be stamped onto a same-type building. It reads operational buildings and
// construction sites alike.
namespace
{
GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
const BuildingDef* findDef(const GameConfig& cfg, BuildingType type)
{
for (const BuildingDef& def : cfg.buildings.buildings)
{
if (def.type == type) { return &def; }
}
return nullptr;
}
BuildingId placeOperational(Simulation& sim, const GameConfig& cfg,
BuildingType type, QPoint anchor)
{
const BuildingDef* def = findDef(cfg, type);
REQUIRE(def != nullptr);
return SimulationTestAccess::buildings(sim).placeImmediate(
type, def->surfaceMask, anchor, Rotation::East);
}
const ShipDef* findAvailableSchematic(const GameConfig& cfg)
{
for (const ShipDef& def : cfg.ships.ships)
{
if (def.unlockAtStationLevel == -1) { return &def; }
}
return nullptr;
}
} // namespace
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]")
{
const GameConfig cfg = loadConfig();
Simulation sim(loadConfig(), 7);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());
CHECK(config->type == BuildingType::Miner);
REQUIRE(config->recipeId.has_value());
CHECK(*config->recipeId == "mine_iron_ore");
CHECK_FALSE(config->isSplitter);
CHECK_FALSE(config->shipLayout.has_value());
}
TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
"[copyconfig]")
{
const GameConfig cfg = loadConfig();
Simulation sim(loadConfig(), 7);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Assembler, QPoint(0, 0));
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());
CHECK(config->type == BuildingType::Assembler);
CHECK_FALSE(config->recipeId.has_value()); // nothing to copy
CHECK_FALSE(config->isSplitter);
}
TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
"[copyconfig]")
{
const GameConfig cfg = loadConfig();
Simulation sim(loadConfig(), 7);
const ShipDef* schematic = findAvailableSchematic(cfg);
REQUIRE(schematic != nullptr);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
SimulationTestAccess::buildings(sim).setRecipe(id, schematic->id);
SimulationTestAccess::buildings(sim).setShipLayout(id, ShipLayoutConfig{});
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());
CHECK(config->type == BuildingType::Shipyard);
REQUIRE(config->recipeId.has_value());
CHECK(*config->recipeId == schematic->id);
CHECK(config->shipLayout.has_value());
}
TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
{
const GameConfig cfg = loadConfig();
Simulation sim(loadConfig(), 7);
// A placed miner enters the construction queue as a site (not yet operational).
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.buildings().findBuilding(id) == nullptr);
REQUIRE(sim.buildings().findSite(id) != nullptr);
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());
CHECK(config->type == BuildingType::Miner);
REQUIRE(config->recipeId.has_value());
CHECK(*config->recipeId == "mine_iron_ore");
}
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]")
{
Simulation sim(loadConfig(), 7);
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
}

View File

@@ -9,6 +9,7 @@ add_files(
BeltSystemTest.cpp
SurfaceMaskTest.cpp
BuildingTest.cpp
BuildingConfigTest.cpp
ShipTest.cpp
ScrapTest.cpp
BehaviorSystemTest.cpp