From 9622fa434575a90457d60074311d56139652576e Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Mon, 20 Jul 2026 20:27:20 +0200 Subject: [PATCH] Use std::optional instead of sentinel values for absent data --- src/balancing/ArenaSimulation.cpp | 3 +- src/balancing/ArenaSimulation.h | 6 +- src/balancing/BalancingWindow.cpp | 17 ++-- src/balancing/BalancingWindow.h | 3 +- src/lib/ecs/component/DeliverScrapBehavior.h | 6 +- src/lib/ecs/system/SalvagerSystem.cpp | 6 +- src/lib/ecs/system/ShipSystem.cpp | 3 +- .../ecs/system/ai/DeliverScrapEvaluator.cpp | 2 +- .../ecs/system/ai/DeliverScrapExecutor.cpp | 4 +- src/lib/sim/BuildingSystem.cpp | 4 +- src/lib/sim/BuildingSystem.h | 12 +-- src/lib/sim/Command.h | 18 ++--- src/lib/sim/CommandSerializer.cpp | 30 ++++--- src/lib/sim/Simulation.cpp | 24 +++--- src/lib/sim/Simulation.h | 7 +- src/test/BehaviorSystemTest.cpp | 4 +- src/test/BlueprintTest.cpp | 32 ++++---- src/test/BuildingConfigTest.cpp | 2 +- src/test/BuildingTest.cpp | 78 +++++++++---------- src/test/CommandTest.cpp | 6 +- src/test/ShipTest.cpp | 2 +- src/test/SimulationTestAccess.h | 6 +- src/ui/BlueprintPanel.cpp | 17 ++-- src/ui/BlueprintPanel.h | 3 +- src/ui/GameWorldView.cpp | 52 ++++++------- src/ui/GameWorldView.h | 6 +- src/ui/SelectedBuildingPanel.cpp | 23 +++--- src/ui/SelectedBuildingPanel.h | 2 +- src/ui/ShipLayoutDialog.cpp | 41 +++++----- src/ui/ShipLayoutDialog.h | 9 ++- src/ui/ShipLayoutPreview.cpp | 6 +- src/ui/ShipLayoutPreview.h | 3 +- 32 files changed, 228 insertions(+), 209 deletions(-) diff --git a/src/balancing/ArenaSimulation.cpp b/src/balancing/ArenaSimulation.cpp index 18b61e5..7e63772 100644 --- a/src/balancing/ArenaSimulation.cpp +++ b/src/balancing/ArenaSimulation.cpp @@ -44,7 +44,6 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig, , m_team1HqEntity(entt::null) , m_team2HqEntity(entt::null) , m_finished(false) - , m_winnerTeam(-1) , m_stopRequested(false) { m_buildingSystem = std::make_unique( @@ -473,7 +472,7 @@ bool ArenaSimulation::isFinished() const return m_finished; } -int ArenaSimulation::getWinnerTeam() const +std::optional ArenaSimulation::getWinnerTeam() const { return m_winnerTeam; } diff --git a/src/balancing/ArenaSimulation.h b/src/balancing/ArenaSimulation.h index f7ad106..7936279 100644 --- a/src/balancing/ArenaSimulation.h +++ b/src/balancing/ArenaSimulation.h @@ -58,7 +58,7 @@ struct ArenaStatus TeamStatus teams[2]; bool finished = false; - int winnerTeam = -1; // 0 or 1 when finished; -1 while running + std::optional winnerTeam; // 0 or 1 when finished; nullopt while running // Game time the fight has lasted (simulated ticks * fixed tick duration). // Meaningful once finished; the battle duration shown for completed runs. double durationSeconds = 0.0; @@ -80,7 +80,7 @@ public: ArenaStatus getStatus() const; bool isFinished() const; - int getWinnerTeam() const; + std::optional getWinnerTeam() const; Tick getCurrentTick() const; const ArenaConfig& getArenaConfig() const; @@ -122,7 +122,7 @@ private: entt::entity m_team2HqEntity; bool m_finished; - int m_winnerTeam; + std::optional m_winnerTeam; std::atomic m_stopRequested; // Static accumulated threat per team, computed once from the configured roster. diff --git a/src/balancing/BalancingWindow.cpp b/src/balancing/BalancingWindow.cpp index aa0ecae..81488bd 100644 --- a/src/balancing/BalancingWindow.cpp +++ b/src/balancing/BalancingWindow.cpp @@ -76,7 +76,6 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig, , m_balancingConfigPath(balancingConfigPath) , m_nextSeed(0) , m_inspectWindow(nullptr) - , m_inspectedArenaIndex(-1) { m_visuals = VisualsLoader::load(m_configDir + "/visuals.toml"); setWindowTitle(tr("DotaFactory — Balancing Tool")); @@ -184,10 +183,10 @@ void BalancingWindow::pollStatuses() } } - if (m_inspectedSim && m_inspectedArenaIndex >= 0) + if (m_inspectedSim && m_inspectedArenaIndex.has_value()) { const ArenaStatus status = m_inspectedSim->getStatus(); - m_arenas[static_cast(m_inspectedArenaIndex)].widget->updateStatus(status); + m_arenas[static_cast(*m_inspectedArenaIndex)].widget->updateStatus(status); } updateButtons(); @@ -255,13 +254,13 @@ void BalancingWindow::inspectArena(int index) delete m_inspectWindow; m_inspectWindow = nullptr; - if (m_inspectedSim && m_inspectedArenaIndex >= 0 + if (m_inspectedSim && m_inspectedArenaIndex.has_value() && !m_inspectedSim->isFinished()) { - m_arenas[static_cast(m_inspectedArenaIndex)].widget->resetToGrey(); + m_arenas[static_cast(*m_inspectedArenaIndex)].widget->resetToGrey(); } m_inspectedSim.reset(); - m_inspectedArenaIndex = -1; + m_inspectedArenaIndex = std::nullopt; } ArenaEntry& entry = m_arenas[static_cast(index)]; @@ -297,16 +296,16 @@ void BalancingWindow::closeInspectWindow() m_inspectWindow->deleteLater(); m_inspectWindow = nullptr; - if (m_inspectedArenaIndex >= 0 && m_inspectedSim) + if (m_inspectedArenaIndex.has_value() && m_inspectedSim) { if (!m_inspectedSim->isFinished()) { - m_arenas[static_cast(m_inspectedArenaIndex)].widget->resetToGrey(); + m_arenas[static_cast(*m_inspectedArenaIndex)].widget->resetToGrey(); } } m_inspectedSim.reset(); - m_inspectedArenaIndex = -1; + m_inspectedArenaIndex = std::nullopt; setMainControlsEnabled(true); updateButtons(); } diff --git a/src/balancing/BalancingWindow.h b/src/balancing/BalancingWindow.h index e82aaea..9c58a2b 100644 --- a/src/balancing/BalancingWindow.h +++ b/src/balancing/BalancingWindow.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -78,6 +79,6 @@ private: QTimer* m_pollTimer; InspectWindow* m_inspectWindow; - int m_inspectedArenaIndex; + std::optional m_inspectedArenaIndex; // nullopt = no arena inspected std::unique_ptr m_inspectedSim; }; diff --git a/src/lib/ecs/component/DeliverScrapBehavior.h b/src/lib/ecs/component/DeliverScrapBehavior.h index 5e7015c..46d11e0 100644 --- a/src/lib/ecs/component/DeliverScrapBehavior.h +++ b/src/lib/ecs/component/DeliverScrapBehavior.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "BuildingId.h" // Deliver-scrap behavior (one half of the old SalvageBehaviorComponent). Scored @@ -7,6 +9,6 @@ // SalvagerSystem performs the actual delivery. struct DeliverScrapBehavior { - BuildingId deliveryBay = kInvalidBuildingId; - float score = 0.0f; + std::optional deliveryBay; // nullopt until a bay is assigned + float score = 0.0f; }; diff --git a/src/lib/ecs/system/SalvagerSystem.cpp b/src/lib/ecs/system/SalvagerSystem.cpp index c5af52f..5652713 100644 --- a/src/lib/ecs/system/SalvagerSystem.cpp +++ b/src/lib/ecs/system/SalvagerSystem.cpp @@ -88,8 +88,8 @@ void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem& m_admin.forEach( [&](entt::entity ship, const DeliverScrapBehavior& deliver, const PositionComponent& pos) { - if (deliver.deliveryBay == kInvalidBuildingId) { return; } - const Building* bay = buildings.findBuilding(deliver.deliveryBay); + if (!deliver.deliveryBay.has_value()) { return; } + const Building* bay = buildings.findBuilding(*deliver.deliveryBay); if (!bay) { return; } const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f, @@ -100,7 +100,7 @@ void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem& if (!m_admin.hasAll(ship)) { return; } CargoComponent& cargo = m_admin.get(ship); if (cargo.current <= 0) { return; } - if (buildings.deliverScrapToSalvageBay(deliver.deliveryBay)) + if (buildings.deliverScrapToSalvageBay(*deliver.deliveryBay)) { --cargo.current; } diff --git a/src/lib/ecs/system/ShipSystem.cpp b/src/lib/ecs/system/ShipSystem.cpp index 9cf0ccf..d2f6417 100644 --- a/src/lib/ecs/system/ShipSystem.cpp +++ b/src/lib/ecs/system/ShipSystem.cpp @@ -398,8 +398,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, maxCollRange * static_cast(m_config.world.orbitFactor); m_admin.addComponent(entity, salvage); - DeliverScrapBehavior deliver; - deliver.deliveryBay = kInvalidBuildingId; + DeliverScrapBehavior deliver; // deliveryBay starts unassigned (nullopt) m_admin.addComponent(entity, deliver); } diff --git a/src/lib/ecs/system/ai/DeliverScrapEvaluator.cpp b/src/lib/ecs/system/ai/DeliverScrapEvaluator.cpp index ca1595f..e354c88 100644 --- a/src/lib/ecs/system/ai/DeliverScrapEvaluator.cpp +++ b/src/lib/ecs/system/ai/DeliverScrapEvaluator.cpp @@ -31,7 +31,7 @@ void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const BuildingSystem& b } // Assign nearest SalvageBay if not yet assigned. - if (deliver.deliveryBay == kInvalidBuildingId) + if (!deliver.deliveryBay.has_value()) { const Building* bay = buildings.findNearestBuilding(pos.value, BuildingType::SalvageBay); diff --git a/src/lib/ecs/system/ai/DeliverScrapExecutor.cpp b/src/lib/ecs/system/ai/DeliverScrapExecutor.cpp index 9358dcb..b92073b 100644 --- a/src/lib/ecs/system/ai/DeliverScrapExecutor.cpp +++ b/src/lib/ecs/system/ai/DeliverScrapExecutor.cpp @@ -24,9 +24,9 @@ void DeliverScrapExecutor::execute(EntityAdmin& admin, const BuildingSystem& bui if (selected.winner != BehaviorKind::DeliverScrap) { return; } QVector2D dest = pos.value; - if (deliver.deliveryBay != kInvalidBuildingId) + if (deliver.deliveryBay.has_value()) { - const Building* bay = buildings.findBuilding(deliver.deliveryBay); + const Building* bay = buildings.findBuilding(*deliver.deliveryBay); if (bay) { dest = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f, diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 94d5f2e..2744d04 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -346,7 +346,7 @@ std::vector BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe // Placement // --------------------------------------------------------------------------- -BuildingId BuildingSystem::place(BuildingType type, QPoint anchor, +std::optional BuildingSystem::place(BuildingType type, QPoint anchor, Rotation rotation, Tick currentTick) { const BuildingDef* def = findBuildingDef(type); @@ -356,7 +356,7 @@ BuildingId BuildingSystem::place(BuildingType type, QPoint anchor, // Reject placements that fall outside the world (REQ-BLD-PLACE-VALID). if (!bodyCellsWithinWorldBounds(mask.bodyCells, anchor)) { - return kInvalidBuildingId; + return std::nullopt; } const BuildingId id = m_allocateBuildingId(); diff --git a/src/lib/sim/BuildingSystem.h b/src/lib/sim/BuildingSystem.h index e0f6f17..6e272aa 100644 --- a/src/lib/sim/BuildingSystem.h +++ b/src/lib/sim/BuildingSystem.h @@ -43,13 +43,13 @@ public: std::mt19937& rng); // -- Placement / demolish ------------------------------------------------ - // Returns the new entity id, or kInvalidBuildingId if the placement falls - // outside the world bounds (vertical extent and asteroid left edge). Belt - // and Splitter register with BeltSystem directly; other types enter the - // construction queue. Terrain type (A vs S) is NOT checked here so that - // tests can stage arbitrary layouts; the player-facing entry point + // Returns the new entity id, or nullopt if the placement falls outside the + // world bounds (vertical extent and asteroid left edge). Belt and Splitter + // register with BeltSystem directly; other types enter the construction + // queue. Terrain type (A vs S) is NOT checked here so that tests can stage + // arbitrary layouts; the player-facing entry point // (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid. - BuildingId place(BuildingType type, QPoint anchor, Rotation rotation, + std::optional place(BuildingType type, QPoint anchor, Rotation rotation, Tick currentTick); // Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and diff --git a/src/lib/sim/Command.h b/src/lib/sim/Command.h index 5551bb5..d76c63e 100644 --- a/src/lib/sim/Command.h +++ b/src/lib/sim/Command.h @@ -75,28 +75,28 @@ struct PlaceBuildingCommand : Command struct DemolishCommand : Command { DemolishCommand() : Command(CommandKind::Demolish) {} - BuildingId id = kInvalidBuildingId; + std::optional id; }; struct RotateInPlaceCommand : Command { RotateInPlaceCommand() : Command(CommandKind::RotateInPlace) {} - BuildingId id = kInvalidBuildingId; - Rotation newRotation = Rotation::East; + std::optional id; + Rotation newRotation = Rotation::East; }; struct SetRecipeCommand : Command { SetRecipeCommand() : Command(CommandKind::SetRecipe) {} - BuildingId id = kInvalidBuildingId; - std::string recipeId; + std::optional id; + std::string recipeId; }; struct SetShipLayoutCommand : Command { SetShipLayoutCommand() : Command(CommandKind::SetShipLayout) {} - BuildingId id = kInvalidBuildingId; - ShipLayoutConfig layout; + std::optional id; + ShipLayoutConfig layout; }; // Splitter filters for a queued / under-construction Splitter site (configured by @@ -104,8 +104,8 @@ struct SetShipLayoutCommand : Command struct SetSiteSplitterFiltersCommand : Command { SetSiteSplitterFiltersCommand() : Command(CommandKind::SetSiteSplitterFilters) {} - BuildingId id = kInvalidBuildingId; - std::vector filterA; + std::optional id; + std::vector filterA; std::vector filterB; }; diff --git a/src/lib/sim/CommandSerializer.cpp b/src/lib/sim/CommandSerializer.cpp index 1a0d80b..b2a4667 100644 --- a/src/lib/sim/CommandSerializer.cpp +++ b/src/lib/sim/CommandSerializer.cpp @@ -129,24 +129,24 @@ std::string serializeCommand(const Command& command) break; } case CommandKind::Demolish: - out << "demolish " << static_cast(command).id; + out << "demolish " << static_cast(command).id.value(); break; case CommandKind::RotateInPlace: { const RotateInPlaceCommand& c = static_cast(command); - out << "rotate " << c.id << ' ' << rotationToChar(c.newRotation); + out << "rotate " << c.id.value() << ' ' << rotationToChar(c.newRotation); break; } case CommandKind::SetRecipe: { const SetRecipeCommand& c = static_cast(command); - out << "setrecipe " << c.id << ' ' << c.recipeId; + out << "setrecipe " << c.id.value() << ' ' << c.recipeId; break; } case CommandKind::SetShipLayout: { const SetShipLayoutCommand& c = static_cast(command); - out << "setlayout " << c.id << ' '; + out << "setlayout " << c.id.value() << ' '; appendLayout(out, c.layout); break; } @@ -154,7 +154,7 @@ std::string serializeCommand(const Command& command) { const SetSiteSplitterFiltersCommand& c = static_cast(command); - out << "sitefilters " << c.id << ' '; + out << "sitefilters " << c.id.value() << ' '; appendFilters(out, c.filterA, c.filterB); break; } @@ -242,27 +242,35 @@ std::shared_ptr parseCommand(const std::string& tokens) if (verb == "demolish") { std::shared_ptr c = std::make_shared(); - if (!(in >> c->id)) { return nullptr; } + BuildingId id = 0; + if (!(in >> id)) { return nullptr; } + c->id = id; return c; } if (verb == "rotate") { std::shared_ptr c = std::make_shared(); std::string rotToken; - if (!(in >> c->id >> rotToken)) { return nullptr; } + BuildingId id = 0; + if (!(in >> id >> rotToken)) { return nullptr; } + c->id = id; c->newRotation = rotationFromString(rotToken); return c; } if (verb == "setrecipe") { std::shared_ptr c = std::make_shared(); - if (!(in >> c->id >> c->recipeId)) { return nullptr; } + BuildingId id = 0; + if (!(in >> id >> c->recipeId)) { return nullptr; } + c->id = id; return c; } if (verb == "setlayout") { std::shared_ptr c = std::make_shared(); - if (!(in >> c->id)) { return nullptr; } + BuildingId id = 0; + if (!(in >> id)) { return nullptr; } + c->id = id; c->layout = parseLayout(in, ok); if (!ok) { return nullptr; } return c; @@ -271,7 +279,9 @@ std::shared_ptr parseCommand(const std::string& tokens) { std::shared_ptr c = std::make_shared(); - if (!(in >> c->id)) { return nullptr; } + BuildingId id = 0; + if (!(in >> id)) { return nullptr; } + c->id = id; parseFilters(in, c->filterA, c->filterB, ok); if (!ok) { return nullptr; } return c; diff --git a/src/lib/sim/Simulation.cpp b/src/lib/sim/Simulation.cpp index 69be951..2f9e94a 100644 --- a/src/lib/sim/Simulation.cpp +++ b/src/lib/sim/Simulation.cpp @@ -40,7 +40,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed) , m_nextBuildingId(1) , m_buildingBlocksStock(m_config.world.startingBuildingBlocks) , m_gameOver(false) - , m_hqBuildingId(kInvalidBuildingId) , m_hqProxyEntity(entt::null) , m_playerStation1Entity(entt::null) , m_playerStation2Entity(entt::null) @@ -137,7 +136,7 @@ void Simulation::reset(unsigned int seed) m_gameOver = false; m_isWon = false; m_artifactCount = 0; - m_hqBuildingId = kInvalidBuildingId; + m_hqBuildingId = std::nullopt; m_hqProxyEntity = entt::null; m_playerStation1Entity = entt::null; m_playerStation2Entity = entt::null; @@ -217,11 +216,12 @@ void Simulation::apply(const Command& command) case CommandKind::PlaceBuilding: { const PlaceBuildingCommand& c = static_cast(command); - const BuildingId id = tryPlaceBuilding(c.type, c.anchor, c.rotation); - if (id == kInvalidBuildingId) + const std::optional placed = tryPlaceBuilding(c.type, c.anchor, c.rotation); + if (!placed.has_value()) { break; } + const BuildingId id = *placed; if (c.recipeId.has_value()) { m_buildingSystem->setRecipe(id, *c.recipeId); @@ -237,31 +237,31 @@ void Simulation::apply(const Command& command) break; } case CommandKind::Demolish: - demolish(static_cast(command).id); + demolish(*static_cast(command).id); break; case CommandKind::RotateInPlace: { const RotateInPlaceCommand& c = static_cast(command); - m_buildingSystem->rotateInPlace(c.id, c.newRotation); + m_buildingSystem->rotateInPlace(*c.id, c.newRotation); break; } case CommandKind::SetRecipe: { const SetRecipeCommand& c = static_cast(command); - m_buildingSystem->setRecipe(c.id, c.recipeId); + m_buildingSystem->setRecipe(*c.id, c.recipeId); break; } case CommandKind::SetShipLayout: { const SetShipLayoutCommand& c = static_cast(command); - m_buildingSystem->setShipLayout(c.id, c.layout); + m_buildingSystem->setShipLayout(*c.id, c.layout); break; } case CommandKind::SetSiteSplitterFilters: { const SetSiteSplitterFiltersCommand& c = static_cast(command); - m_buildingSystem->setSiteSplitterFilters(c.id, c.filterA, c.filterB); + m_buildingSystem->setSiteSplitterFilters(*c.id, c.filterA, c.filterB); break; } case CommandKind::SetSplitterFilters: @@ -1193,11 +1193,11 @@ bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const return it->second.unlocked; } -BuildingId Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation) +std::optional Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation) { if (!m_buildingSystem->isPlacementValid(type, anchor, rotation)) { - return kInvalidBuildingId; + return std::nullopt; } int cost = 0; @@ -1211,7 +1211,7 @@ BuildingId Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotati } if (m_buildingBlocksStock < cost) { - return kInvalidBuildingId; + return std::nullopt; } m_buildingBlocksStock -= cost; return m_buildingSystem->place(type, anchor, rotation, m_currentTick); diff --git a/src/lib/sim/Simulation.h b/src/lib/sim/Simulation.h index 40a224e..1e055b2 100644 --- a/src/lib/sim/Simulation.h +++ b/src/lib/sim/Simulation.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -131,8 +132,8 @@ private: // Reached during play exclusively via apply(); never called by UI/app code. // Checks affordability, deducts building blocks, and places the building. - // Returns the new entity id, or kInvalidBuildingId if blocks are insufficient. - BuildingId tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation); + // Returns the new entity id, or nullopt if blocks are insufficient. + std::optional tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation); // Demolishes the building with the given id and refunds building blocks. void demolish(BuildingId id); @@ -182,7 +183,7 @@ private: int m_artifactCount = 0; // Pre-placed structure IDs. - BuildingId m_hqBuildingId; // Building id (for belt integration) + std::optional m_hqBuildingId; // Building id (for belt integration) entt::entity m_hqProxyEntity; // ECS entity (HP, targeting) entt::entity m_playerStation1Entity; entt::entity m_playerStation2Entity; diff --git a/src/test/BehaviorSystemTest.cpp b/src/test/BehaviorSystemTest.cpp index 2be6191..28bee2b 100644 --- a/src/test/BehaviorSystemTest.cpp +++ b/src/test/BehaviorSystemTest.cpp @@ -954,7 +954,7 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b Fixture f; const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay, - QPoint(-4, 0), Rotation::East, 0); + QPoint(-4, 0), Rotation::East, 0).value(); Tick t = 0; for (int i = 0; i < 500; ++i) { @@ -989,7 +989,7 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo", Fixture f; const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay, - QPoint(-4, 0), Rotation::East, 0); + QPoint(-4, 0), Rotation::East, 0).value(); Tick t = 0; for (int i = 0; i < 500; ++i) { diff --git a/src/test/BlueprintTest.cpp b/src/test/BlueprintTest.cpp index 3eb1b71..4c2d9d0 100644 --- a/src/test/BlueprintTest.cpp +++ b/src/test/BlueprintTest.cpp @@ -527,9 +527,9 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor", const QPoint offsetB( 1, 0); const BuildingId idA = SimulationTestAccess::place(sim, - BuildingType::Belt, cursor + offsetA, Rotation::East); + BuildingType::Belt, cursor + offsetA, Rotation::East).value(); const BuildingId idB = SimulationTestAccess::place(sim, - BuildingType::Belt, cursor + offsetB, Rotation::East); + BuildingType::Belt, cursor + offsetB, Rotation::East).value(); REQUIRE(idA != kInvalidBuildingId); REQUIRE(idB != kInvalidBuildingId); @@ -583,11 +583,11 @@ TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId a } const int blocksBeforeAttempt = sim.getBuildingBlocksStock(); - const BuildingId id = SimulationTestAccess::place(sim, + const std::optional id = SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(col - 2, 0), Rotation::East); // Placement must fail and leave the stock unchanged. - REQUIRE(id == kInvalidBuildingId); + REQUIRE_FALSE(id.has_value()); REQUIRE(sim.getBuildingBlocksStock() == blocksBeforeAttempt); } @@ -599,10 +599,10 @@ TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and ch // A miner is all-asteroid; placing it in space (x >= 0) violates the terrain // rule, so it must be rejected without consuming building blocks. - const BuildingId id = + const std::optional id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(0, 0), Rotation::East); - REQUIRE(id == kInvalidBuildingId); + REQUIRE_FALSE(id.has_value()); REQUIRE(sim.getBuildingBlocksStock() == startBlocks); REQUIRE(sim.getBuildings().getAllSites().empty()); } @@ -623,7 +623,7 @@ TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies // Miner mask ["AA","A>"] East at (-3,0) → all-asteroid body at // (-3,0),(-2,0),(-3,1); a valid spot. const BuildingId id = - SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-3, 0), Rotation::East); + SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-3, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost); @@ -666,7 +666,7 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", " Simulation sim(loadConfig()); // Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid. - const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East); + const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); @@ -681,7 +681,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction { Simulation sim(loadConfig()); - const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East); + const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); SimulationTestAccess::buildings(sim).setRecipe(id, "mine_copper_ore"); @@ -710,7 +710,7 @@ TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]") // Freshly placed → a ConstructionSite (not ticked to completion). A 1x1 belt keeps // the body-cell bounding-box centered on the anchor, so a single site → zero offset. const BuildingId id = - SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East); + SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); REQUIRE(sim.getBuildings().findSite(id) != nullptr); REQUIRE(sim.getBuildings().findBuilding(id) == nullptr); @@ -727,7 +727,7 @@ TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blue Simulation sim(loadConfig()); const BuildingId id = - SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East); + SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); @@ -744,7 +744,7 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site // Building A: place, configure, and tick to completion so it is operational. const BuildingId idA = - SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East); + SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); REQUIRE(idA != kInvalidBuildingId); SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore"); for (int i = 0; i <= static_cast(secondsToTicks(10.0)); ++i) { sim.tick(); } @@ -752,7 +752,7 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site // Building B: place and configure, but leave as a construction site. const BuildingId idB = - SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East); + SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value(); REQUIRE(idB != kInvalidBuildingId); SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore"); REQUIRE(sim.getBuildings().findSite(idB) != nullptr); @@ -777,7 +777,7 @@ TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {})); const BuildingId id = - SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East); + SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); REQUIRE(sim.getBuildings().findSite(id) != nullptr); REQUIRE(selectionHasPlaceableBuilding(sim, { id })); @@ -847,7 +847,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout // Shipyard surface_mask ["AAAS>","AAAS "] with Rotation::East: // A-tiles at (-3,0),(-2,0),(-1,0),(-3,1),(-2,1),(-1,1) — all x < 0, valid asteroid tiles. // S-tile at (0,0) and (0,1) — x >= 0, valid space tiles. - const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East); + const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); ShipLayoutConfig layout; @@ -871,7 +871,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc { Simulation sim(loadConfig()); - const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East); + const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); ShipLayoutConfig layout; diff --git a/src/test/BuildingConfigTest.cpp b/src/test/BuildingConfigTest.cpp index 29951af..d21986f 100644 --- a/src/test/BuildingConfigTest.cpp +++ b/src/test/BuildingConfigTest.cpp @@ -112,7 +112,7 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]") // 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); + SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); REQUIRE(id != kInvalidBuildingId); REQUIRE(sim.getBuildings().findBuilding(id) == nullptr); REQUIRE(sim.getBuildings().findSite(id) != nullptr); diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index e77f123..0a53cba 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -119,7 +119,7 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); REQUIRE(id != kInvalidBuildingId); // Miner mask ["AA","A>"] with East rotation → body at (0,0),(1,0),(0,1). @@ -138,8 +138,8 @@ TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[ // Miner mask ["AA","A>"] East → body at (0,0),(1,0),(0,1); at y=-1 the top // row sits above the world. - const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0); - REQUIRE(id == kInvalidBuildingId); + const std::optional id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0); + REQUIRE_FALSE(id.has_value()); REQUIRE(f.bs.getAllSites().empty()); REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0))); } @@ -151,9 +151,9 @@ TEST_CASE("BuildingSystem: place rejects a building below the world (y >= height // Anchored on the last in-bounds row, the miner's lower body row reaches // y == heightTiles, which is outside the world. - const BuildingId id = f.bs.place(BuildingType::Miner, + const std::optional id = f.bs.place(BuildingType::Miner, QPoint(0, heightTiles - 1), Rotation::East, 0); - REQUIRE(id == kInvalidBuildingId); + REQUIRE_FALSE(id.has_value()); REQUIRE(f.bs.getAllSites().empty()); } @@ -162,9 +162,9 @@ TEST_CASE("BuildingSystem: place rejects a building left of the asteroid edge", PlacementFixture f; const int leftEdgeX = -f.cfg.world.regions.asteroidWidth_tiles; - const BuildingId id = f.bs.place(BuildingType::Miner, + const std::optional id = f.bs.place(BuildingType::Miner, QPoint(leftEdgeX - 1, 0), Rotation::East, 0); - REQUIRE(id == kInvalidBuildingId); + REQUIRE_FALSE(id.has_value()); REQUIRE(f.bs.getAllSites().empty()); } @@ -176,7 +176,7 @@ TEST_CASE("BuildingSystem: place accepts a building flush against the world's le // Miner body min relative x is 0, so its leftmost cell sits exactly on the edge. const BuildingId id = f.bs.place(BuildingType::Miner, - QPoint(leftEdgeX, 0), Rotation::East, 0); + QPoint(leftEdgeX, 0), Rotation::East, 0).value(); REQUIRE(id != kInvalidBuildingId); REQUIRE(f.bs.isTileOccupied(QPoint(leftEdgeX, 0))); } @@ -187,7 +187,7 @@ TEST_CASE("BuildingSystem: place imposes no right-side bound (space extends righ PlacementFixture f; const BuildingId id = f.bs.place(BuildingType::Miner, - QPoint(1000, 0), Rotation::East, 0); + QPoint(1000, 0), Rotation::East, 0).value(); REQUIRE(id != kInvalidBuildingId); } @@ -256,7 +256,7 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); REQUIRE(bs.getAllSites().size() == 1); REQUIRE(bs.getAllBuildings().empty()); @@ -277,7 +277,7 @@ TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building] [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); // Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300. // We need to process tick 300 itself, so run 301 ticks (ticks 0..300). @@ -351,7 +351,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[ [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); // Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300. // We need to process tick 300 itself, so run 301 ticks (ticks 0..300). @@ -377,7 +377,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil rng); bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); - const BuildingId id2 = bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0); + const BuildingId id2 = bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0).value(); // Process through tick 300 to complete first miner's construction. Tick tick = 0; @@ -406,7 +406,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); bs.setRecipe(id, "mine_iron_ore"); Tick tick = 0; @@ -439,7 +439,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]") [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); bs.setRecipe(id, "mine_iron_ore"); Tick tick = 0; @@ -479,8 +479,8 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", [](const std::string&) -> bool { return true; }, rng); - const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); - const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0); + const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0).value(); (void)smelterId; Tick tick = 0; @@ -519,7 +519,7 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); bs.setRecipe(id, "mine_iron_ore"); Tick tick = 0; @@ -563,7 +563,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing // Smelter mask ["AA ","AA>"] → body (0,0),(1,0),(0,1),(1,1). // Output port (2,1) East. Input port example: (2,0) West. - const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0); + const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); // Smelters have no recipe selection (REQ-BLD-SMELTER); they auto-accept any // ore/scrap that is an input to a smelter recipe. @@ -603,7 +603,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf [](const std::string&) -> bool { return true; }, rng); - const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0); + const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runTicks(bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); @@ -645,7 +645,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th rng); const BuildingId id = bs.place(BuildingType::ReprocessingPlant, - QPoint(0, 0), Rotation::East, 0); + QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runTicks(bs, belts, static_cast(secondsToTicks(25.0)) + 1, tick); @@ -686,7 +686,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection", [](const std::string&) -> bool { return true; }, rng); - const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0); + const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runTicks(bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); @@ -732,7 +732,7 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete [](const std::string&) -> bool { return true; }, rng); - const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0); + const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runTicks(bs, belts, static_cast(secondsToTicks(15.0)) + 1, tick); @@ -786,7 +786,7 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); bs.setRecipe(id, "mine_iron_ore"); // Belt at the miner's output port tile (1,1) flowing East. @@ -826,12 +826,12 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p rng); // Miner at (0,0): body (0,0),(1,0),(0,1); output port tile (1,1) flowing East. - const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); bs.setRecipe(minerId, "mine_iron_ore"); // Smelter anchored at (1,1): body (1,1),(2,1),(1,2),(2,2). Its body cell (1,1) is // the miner's output-port tile, and its west input edge there faces East, so the // two ports meet — no belt placed anywhere. - const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(1, 1), Rotation::East, 0); + const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(1, 1), Rotation::East, 0).value(); Tick tick = 0; // Smelter build (15s) + margin for coupling and a smelt cycle. @@ -866,11 +866,11 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu rng); // Producing miner at (0,0), output port (1,1) East. - const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); bs.setRecipe(minerId, "mine_iron_ore"); // A second, idle miner anchored at (1,1) occupies the output-port tile but takes // no inputs, so it cannot accept the iron_ore. - const BuildingId sinkId = bs.place(BuildingType::Miner, QPoint(1, 1), Rotation::East, 0); + const BuildingId sinkId = bs.place(BuildingType::Miner, QPoint(1, 1), Rotation::East, 0).value(); Tick tick = 0; // Both miners build sequentially (10s each), then the producer runs and jams. @@ -904,7 +904,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production" [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); bs.setRecipe(id, "mine_iron_ore"); Tick tick = 0; @@ -948,7 +948,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max rng); const BuildingId id = bs.place(BuildingType::ReprocessingPlant, - QPoint(0, 0), Rotation::East, 0); + QPoint(0, 0), Rotation::East, 0).value(); // Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the // single reprocessing recipe is applied automatically on completion. @@ -980,7 +980,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta rng); const BuildingId id = bs.place(BuildingType::ReprocessingPlant, - QPoint(0, 0), Rotation::East, 0); + QPoint(0, 0), Rotation::East, 0).value(); // Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the // single reprocessing recipe is applied automatically on completion. @@ -1056,7 +1056,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); const std::optional result = bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::North); @@ -1079,7 +1079,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runTicks(bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); @@ -1154,7 +1154,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t // Smelter is a fully filled 2×2 footprint — rotating the ghost produces the // same four body tiles, so findRotateInPlaceTarget must still return the id. - const BuildingId id = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); const std::optional result = bs.findRotateInPlaceTarget(BuildingType::Smelter, QPoint(0, 0), Rotation::North); @@ -1181,7 +1181,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a constru [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); REQUIRE(bs.findSite(id)->rotation == Rotation::East); bs.rotateInPlace(id, Rotation::North); @@ -1204,7 +1204,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); const Tick completesAt = bs.findSite(id)->completesAt; REQUIRE(completesAt > 0); @@ -1228,7 +1228,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runTicks(bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); @@ -1259,7 +1259,7 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste [](const std::string&) -> bool { return true; }, rng); - const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0); + const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; runTicks(bs, belts, static_cast(secondsToTicks(1.0)) + 1, tick); @@ -1276,7 +1276,7 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca PlacementFixture f; const QPoint tile(5, 5); - const BuildingId id = f.bs.place(BuildingType::Splitter, tile, Rotation::East, 0); + const BuildingId id = f.bs.place(BuildingType::Splitter, tile, Rotation::East, 0).value(); REQUIRE(id != kInvalidBuildingId); REQUIRE(f.bs.findSite(id) != nullptr); diff --git a/src/test/CommandTest.cpp b/src/test/CommandTest.cpp index 8a43cd1..a1c44f7 100644 --- a/src/test/CommandTest.cpp +++ b/src/test/CommandTest.cpp @@ -52,7 +52,7 @@ TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe" viaCommand.apply(command); const BuildingId id = - SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East); + SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); SimulationTestAccess::buildings(viaDirect).setRecipe(id, "mine_iron_ore"); REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum()); @@ -64,9 +64,9 @@ TEST_CASE("apply(DemolishCommand) matches direct demolish", "[command]") Simulation viaDirect(loadConfig(), 99); const BuildingId idA = - SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East); + SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value(); const BuildingId idB = - SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East); + SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value(); REQUIRE(idA == idB); DemolishCommand command; diff --git a/src/test/ShipTest.cpp b/src/test/ShipTest.cpp index 9da3e60..0a91c17 100644 --- a/src/test/ShipTest.cpp +++ b/src/test/ShipTest.cpp @@ -224,7 +224,7 @@ TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]") // Cargo capacity is now a ship-level pool (REQ-MOD-CARGO-CAPACITY). REQUIRE(admin.get(e).maxCapacity == 10); REQUIRE(admin.get(e).current == 0); - REQUIRE(admin.get(e).deliveryBay == kInvalidBuildingId); + REQUIRE_FALSE(admin.get(e).deliveryBay.has_value()); REQUIRE_FALSE(admin.get(e).scrapTarget.has_value()); REQUIRE(admin.get(e).maxCollectionRange_tiles == Approx(50.0f)); } diff --git a/src/test/SimulationTestAccess.h b/src/test/SimulationTestAccess.h index df3eda0..2a7b913 100644 --- a/src/test/SimulationTestAccess.h +++ b/src/test/SimulationTestAccess.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include "BuildingId.h" @@ -25,8 +27,8 @@ struct SimulationTestAccess static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); } static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); } - static BuildingId place(Simulation& sim, BuildingType type, QPoint anchor, - Rotation rotation) + static std::optional place(Simulation& sim, BuildingType type, + QPoint anchor, Rotation rotation) { return sim.tryPlaceBuilding(type, anchor, rotation); } diff --git a/src/ui/BlueprintPanel.cpp b/src/ui/BlueprintPanel.cpp index ea2753a..8dcbdc3 100644 --- a/src/ui/BlueprintPanel.cpp +++ b/src/ui/BlueprintPanel.cpp @@ -28,7 +28,6 @@ BlueprintPanel::BlueprintPanel(Simulation* sim, const GameConfig* config, QWidge , m_sim(sim) , m_config(config) , m_currentBlocks(0) - , m_activeIndex(-1) { QVBoxLayout* layout = new QVBoxLayout(this); layout->setContentsMargins(4, 4, 4, 4); @@ -80,11 +79,11 @@ void BlueprintPanel::handleEvent(std::shared_ptr= 0 && m_activeIndex < static_cast(m_blueprintButtons.size())) + if (m_activeIndex.has_value() && *m_activeIndex < static_cast(m_blueprintButtons.size())) { - m_blueprintButtons[static_cast(m_activeIndex)]->setChecked(false); + m_blueprintButtons[static_cast(*m_activeIndex)]->setChecked(false); } - m_activeIndex = -1; + m_activeIndex = std::nullopt; refreshButtonStates(); } @@ -109,13 +108,13 @@ void BlueprintPanel::onDeleteBlueprintClicked(int index) { if (m_activeIndex == index) { - m_activeIndex = -1; + m_activeIndex = std::nullopt; EventManager::getInstance()->sendEventImmediately( std::make_shared()); } - else if (m_activeIndex > index) + else if (m_activeIndex.has_value() && *m_activeIndex > index) { - m_activeIndex--; + --*m_activeIndex; } m_blueprints.erase(m_blueprints.begin() + index); rebuildButtons(); @@ -133,9 +132,9 @@ void BlueprintPanel::onBlueprintButtonClicked(int index) return; } - if (m_activeIndex >= 0 && m_activeIndex < static_cast(m_blueprintButtons.size())) + if (m_activeIndex.has_value() && *m_activeIndex < static_cast(m_blueprintButtons.size())) { - m_blueprintButtons[static_cast(m_activeIndex)]->setChecked(false); + m_blueprintButtons[static_cast(*m_activeIndex)]->setChecked(false); } m_activeIndex = index; diff --git a/src/ui/BlueprintPanel.h b/src/ui/BlueprintPanel.h index bbb03b9..513d0ef 100644 --- a/src/ui/BlueprintPanel.h +++ b/src/ui/BlueprintPanel.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -56,7 +57,7 @@ private: const GameConfig* m_config; std::vector m_selectedBuildingIds; int m_currentBlocks; - int m_activeIndex; + std::optional m_activeIndex; // nullopt = no blueprint selected std::vector m_blueprints; std::vector m_blueprintButtons; QPushButton* m_createBtn; diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 796e24e..4ab98bb 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -142,7 +142,6 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config, , m_ghostValid(false) , m_dragging(false) , m_demolishMode(false) - , m_demolishHoverBuildingId(kInvalidBuildingId) , m_debugDraw(false) , m_rng(std::random_device{}()) , m_boxSelecting(false) @@ -621,34 +620,34 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor, return true; } -BuildingId GameWorldView::buildingAtTile(QPoint tile) const +std::optional GameWorldView::buildingAtTile(QPoint tile) const { for (const Building& b : m_sim->getBuildings().getAllBuildings()) { for (const QPoint& cell : b.bodyCells) { - if (cell == tile) - { - return b.id; + if (cell == tile) + { + return b.id; } } } - return kInvalidBuildingId; + return std::nullopt; } -BuildingId GameWorldView::siteAtTile(QPoint tile) const +std::optional GameWorldView::siteAtTile(QPoint tile) const { for (const ConstructionSite& s : m_sim->getBuildings().getAllSites()) { for (const QPoint& cell : s.bodyCells) { - if (cell == tile) + if (cell == tile) { - return s.id; + return s.id; } } } - return kInvalidBuildingId; + return std::nullopt; } @@ -1617,9 +1616,9 @@ void GameWorldView::drawOverlays(QPainter& painter) } } } - else if (m_demolishMode && m_demolishHoverBuildingId != kInvalidBuildingId) + else if (m_demolishMode && m_demolishHoverBuildingId.has_value()) { - const Building* b = m_sim->getBuildings().findBuilding(m_demolishHoverBuildingId); + const Building* b = m_sim->getBuildings().findBuilding(*m_demolishHoverBuildingId); if (b) { for (const QPoint& cell : b->bodyCells) @@ -1989,9 +1988,9 @@ void GameWorldView::mousePressEvent(QMouseEvent* event) // Shift + right-click copies a building's settings, but only in the // default selection mode (REQ-BLD-COPY-CONFIG). const QPoint tile = widgetToTile(event->pos()); - BuildingId id = buildingAtTile(tile); - if (id == kInvalidBuildingId) { id = siteAtTile(tile); } - if (id != kInvalidBuildingId) { copyConfigFrom(id); } + std::optional id = buildingAtTile(tile); + if (!id.has_value()) { id = siteAtTile(tile); } + if (id.has_value()) { copyConfigFrom(*id); } } } return; @@ -2032,11 +2031,11 @@ void GameWorldView::mousePressEvent(QMouseEvent* event) // selection. Only active in the default selection mode. if ((event->modifiers() & Qt::ShiftModifier) && m_copiedConfig.has_value()) { - BuildingId id = buildingAtTile(tile); - if (id == kInvalidBuildingId) { id = siteAtTile(tile); } - if (id != kInvalidBuildingId) + std::optional id = buildingAtTile(tile); + if (!id.has_value()) { id = siteAtTile(tile); } + if (id.has_value()) { - pasteConfigTo(id); + pasteConfigTo(*id); return; } } @@ -2064,13 +2063,14 @@ void GameWorldView::mousePressEvent(QMouseEvent* event) std::make_shared(std::nullopt)); } - BuildingId id = buildingAtTile(tile); - if (id == kInvalidBuildingId) + std::optional hit = buildingAtTile(tile); + if (!hit.has_value()) { - id = siteAtTile(tile); + hit = siteAtTile(tile); } - if (id != kInvalidBuildingId) + if (hit.has_value()) { + const BuildingId id = *hit; // A building/construction site outranks scrap (REQ-UI-SCRAP-CLICK-SELECT). clearScrapSelection(); if (event->modifiers() & Qt::ControlModifier) @@ -2198,7 +2198,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event) command->id = id; enqueueCommand(command); } - m_demolishHoverBuildingId = kInvalidBuildingId; + m_demolishHoverBuildingId = std::nullopt; return; } @@ -2283,7 +2283,7 @@ void GameWorldView::toggleDemolishMode() if (m_demolishMode) { m_demolishMode = false; - m_demolishHoverBuildingId = kInvalidBuildingId; + m_demolishHoverBuildingId = std::nullopt; } else { @@ -2479,7 +2479,7 @@ void GameWorldView::resetForNewGame() m_ghostRotation = Rotation::East; m_ghostValid = false; m_demolishMode = false; - m_demolishHoverBuildingId = kInvalidBuildingId; + m_demolishHoverBuildingId = std::nullopt; EventManager::getInstance()->sendEventImmediately( std::make_shared(false)); m_selectedBuildingIds.clear(); diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index fc2e396..661d9da 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -168,8 +168,8 @@ private: bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const; const BuildingDef* findBuildingDef(BuildingType type) const; - BuildingId buildingAtTile(QPoint tile) const; - BuildingId siteAtTile(QPoint tile) const; + std::optional buildingAtTile(QPoint tile) const; + std::optional siteAtTile(QPoint tile) const; // Ids of all buildings and construction sites whose footprint intersects // the tile box spanned by the two (unordered) corner tiles. std::vector buildingsInBox(QPoint cornerA, QPoint cornerB) const; @@ -270,7 +270,7 @@ private: static constexpr qint64 kCopyFlashDurationMs = 300; bool m_demolishMode; - BuildingId m_demolishHoverBuildingId; + std::optional m_demolishHoverBuildingId; bool m_debugDraw; std::vector m_selectedBuildingIds; diff --git a/src/ui/SelectedBuildingPanel.cpp b/src/ui/SelectedBuildingPanel.cpp index 97ae58a..f4c8693 100644 --- a/src/ui/SelectedBuildingPanel.cpp +++ b/src/ui/SelectedBuildingPanel.cpp @@ -131,7 +131,6 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim, : QWidget(parent) , m_sim(sim) , m_config(config) - , m_singleBuildingId(kInvalidBuildingId) , m_splitterTile(0, 0) { m_layout = new QVBoxLayout(this); @@ -170,10 +169,10 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim, connect(m_clearBeltBtn, &QPushButton::clicked, this, &SelectedBuildingPanel::onClearBelt); connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() { - if (m_singleBuildingId != kInvalidBuildingId) + if (m_singleBuildingId.has_value()) { EventManager::getInstance()->sendEventImmediately( - std::make_shared(m_singleBuildingId)); + std::make_shared(*m_singleBuildingId)); } }); connect(m_filterAList, &QListWidget::itemChanged, @@ -257,7 +256,7 @@ void SelectedBuildingPanel::hideAllWidgets() void SelectedBuildingPanel::clearContent() { - m_singleBuildingId = kInvalidBuildingId; + m_singleBuildingId = std::nullopt; hideAllWidgets(); } @@ -672,8 +671,8 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason) return; } - if (m_singleBuildingId == kInvalidBuildingId) { return; } - const Building* b = m_sim->getBuildings().findBuilding(m_singleBuildingId); + if (!m_singleBuildingId.has_value()) { return; } + const Building* b = m_sim->getBuildings().findBuilding(*m_singleBuildingId); if (b) { if (m_titleLabel->text().startsWith(tr("(Building) "))) @@ -686,7 +685,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason) } return; } - const ConstructionSite* s = m_sim->getBuildings().findSite(m_singleBuildingId); + const ConstructionSite* s = m_sim->getBuildings().findSite(*m_singleBuildingId); if (s) { // A periodic tick only advances construction progress, so update just the @@ -708,7 +707,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason) void SelectedBuildingPanel::buildMulti(const std::vector& ids) { - m_singleBuildingId = kInvalidBuildingId; + m_singleBuildingId = std::nullopt; m_recipeSelectButton->hide(); m_clearBeltBtn->hide(); m_filterALabel->hide(); @@ -764,7 +763,7 @@ void SelectedBuildingPanel::buildMulti(const std::vector& ids) void SelectedBuildingPanel::onSelectRecipeClicked() { - if (m_singleBuildingId == kInvalidBuildingId) + if (!m_singleBuildingId.has_value()) { return; } @@ -775,7 +774,7 @@ void SelectedBuildingPanel::onSelectRecipeClicked() // refreshBuffers() path picks up the new schematic (and shows the layout // preview + Configure Layout button) once the command has been applied. EventManager::getInstance()->sendEventImmediately( - std::make_shared(m_singleBuildingId)); + std::make_shared(*m_singleBuildingId)); rebuild(); } @@ -825,7 +824,7 @@ void SelectedBuildingPanel::buildSplitterFilters( void SelectedBuildingPanel::onSplitterFilterChanged() { - if (m_singleBuildingId == kInvalidBuildingId) + if (!m_singleBuildingId.has_value()) { return; } @@ -848,7 +847,7 @@ void SelectedBuildingPanel::onSplitterFilterChanged() { std::shared_ptr command = std::make_shared(); - command->id = m_singleBuildingId; + command->id = *m_singleBuildingId; command->filterA = collectFilter(m_filterAList); command->filterB = collectFilter(m_filterBList); EventManager::getInstance()->sendEventImmediately( diff --git a/src/ui/SelectedBuildingPanel.h b/src/ui/SelectedBuildingPanel.h index 87f035e..2717c0a 100644 --- a/src/ui/SelectedBuildingPanel.h +++ b/src/ui/SelectedBuildingPanel.h @@ -109,7 +109,7 @@ private: ShipLayoutPreview* m_layoutPreview; QPushButton* m_configureLayoutBtn; - BuildingId m_singleBuildingId; + std::optional m_singleBuildingId; bool m_singleIsSite = false; // selected single entity is a construction site QPoint m_splitterTile; std::string m_currentRecipeId; diff --git a/src/ui/ShipLayoutDialog.cpp b/src/ui/ShipLayoutDialog.cpp index e015d4c..abddf82 100644 --- a/src/ui/ShipLayoutDialog.cpp +++ b/src/ui/ShipLayoutDialog.cpp @@ -83,7 +83,7 @@ public: setFixedSize(cols * kCellSize + 1, rows * kCellSize + 1); } - void setGhostData(int moduleIndex, Rotation rotation) + void setGhostData(std::optional moduleIndex, Rotation rotation) { m_ghostModuleIdx = moduleIndex; m_ghostRotation = rotation; @@ -111,9 +111,9 @@ protected: { painter.fillRect(cellRect, QColor(30, 30, 30)); } - else if (cell.moduleIndex >= 0) + else if (cell.moduleIndex.has_value()) { - const PlacedModule& pm = (*m_placed)[cell.moduleIndex]; + const PlacedModule& pm = (*m_placed)[*cell.moduleIndex]; const ModuleDef* def = findModule(pm.moduleId); QColor color(Qt::gray); QString glyph; @@ -140,9 +140,9 @@ protected: } // Draw ghost - if (m_ghostModuleIdx >= 0 && m_hoverCell.x() >= 0 && m_config) + if (m_ghostModuleIdx.has_value() && m_hoverCell.x() >= 0 && m_config) { - const ModuleDef& def = m_config->modules.modules[m_ghostModuleIdx]; + const ModuleDef& def = m_config->modules.modules[*m_ghostModuleIdx]; const std::vector mask = rotateMask(def.surfaceMask, m_ghostRotation); QColor ghostColor(QString::fromStdString(def.fillColor)); ghostColor.setAlpha(100); @@ -238,7 +238,7 @@ private: int m_cols = 0; const std::vector* m_placed = nullptr; const GameConfig* m_config = nullptr; - int m_ghostModuleIdx = -2; + std::optional m_ghostModuleIdx; Rotation m_ghostRotation = Rotation::East; QPoint m_hoverCell = QPoint(-1, -1); }; @@ -374,7 +374,6 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, , m_rows(0) , m_cols(0) , m_placedModules(currentLayout.placedModules) - , m_activeModuleIndex(-2) , m_currentRotation(Rotation::East) , m_removeButton(nullptr) , m_gridWidget(nullptr) @@ -406,7 +405,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, } // Initialize grid. - m_grid.assign(m_rows, std::vector(m_cols, {false, -1})); + m_grid.assign(m_rows, std::vector(m_cols, {false, std::nullopt})); for (int r = 0; r < m_rows; ++r) { for (int c = 0; c < static_cast(m_shipLayout[r].size()); ++c) @@ -491,9 +490,9 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, } buttonGrid->addWidget(m_removeButton, row, 0, 1, kCols); connect(m_removeButton, &QPushButton::clicked, this, [this]() { - if (m_activeModuleIndex == -1) + if (m_removeMode) { - m_activeModuleIndex = -2; + m_removeMode = false; m_removeButton->setChecked(false); } else @@ -502,7 +501,8 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, { if (btn) { btn->setChecked(false); } } - m_activeModuleIndex = -1; + m_activeModuleIndex = std::nullopt; + m_removeMode = true; m_removeButton->setChecked(true); } updateGridWidget(); @@ -540,20 +540,20 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, // Grid click handler. connect(this, &ShipLayoutDialog::gridCellClicked, this, [this](QPoint cell) { - if (m_activeModuleIndex == -2) + if (!m_removeMode && !m_activeModuleIndex.has_value()) { return; } - if (m_activeModuleIndex == -1) + if (m_removeMode) { // Remove mode: find and remove module at cell. if (cell.y() >= 0 && cell.y() < m_rows && cell.x() >= 0 && cell.x() < m_cols) { - const int idx = m_grid[cell.y()][cell.x()].moduleIndex; - if (idx >= 0) + const std::optional idx = m_grid[cell.y()][cell.x()].moduleIndex; + if (idx.has_value()) { - m_placedModules.erase(m_placedModules.begin() + idx); + m_placedModules.erase(m_placedModules.begin() + *idx); rebuildOccupancy(); updateGridWidget(); updateStats(); @@ -563,7 +563,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, } // Place module. - const ModuleDef& def = m_config->modules.modules[m_activeModuleIndex]; + const ModuleDef& def = m_config->modules.modules[*m_activeModuleIndex]; if (canPlaceModule(def, cell, m_currentRotation)) { PlacedModule pm; @@ -622,7 +622,7 @@ void ShipLayoutDialog::onModuleButtonClicked(int index) if (m_activeModuleIndex == index) { if (m_moduleButtons[index]) { m_moduleButtons[index]->setChecked(false); } - m_activeModuleIndex = -2; + m_activeModuleIndex = std::nullopt; } else { @@ -631,6 +631,7 @@ void ShipLayoutDialog::onModuleButtonClicked(int index) if (m_moduleButtons[i]) { m_moduleButtons[i]->setChecked(i == index); } } m_removeButton->setChecked(false); + m_removeMode = false; m_activeModuleIndex = index; } updateGridWidget(); @@ -656,7 +657,7 @@ void ShipLayoutDialog::rebuildOccupancy() { for (int c = 0; c < m_cols; ++c) { - m_grid[r][c].moduleIndex = -1; + m_grid[r][c].moduleIndex = std::nullopt; } } @@ -726,7 +727,7 @@ bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position, { return false; } - if (m_grid[gr][gc].moduleIndex >= 0) + if (m_grid[gr][gc].moduleIndex.has_value()) { return false; } diff --git a/src/ui/ShipLayoutDialog.h b/src/ui/ShipLayoutDialog.h index 5e9bd02..cdb3579 100644 --- a/src/ui/ShipLayoutDialog.h +++ b/src/ui/ShipLayoutDialog.h @@ -47,7 +47,7 @@ public: struct CellInfo { bool buildable; - int moduleIndex; // -1 if empty + std::optional moduleIndex; // nullopt if empty }; private: @@ -69,7 +69,12 @@ private: std::vector m_placedModules; std::vector> m_grid; - int m_activeModuleIndex; // -1 = remove mode, -2 = no selection + // The module to place, as an index into config modules; nullopt when no + // module is selected for placement. m_removeMode is a separate mode in which + // clicking a cell removes the module there (mutually exclusive with a + // selected module). + std::optional m_activeModuleIndex; + bool m_removeMode = false; Rotation m_currentRotation; std::vector m_moduleButtons; diff --git a/src/ui/ShipLayoutPreview.cpp b/src/ui/ShipLayoutPreview.cpp index 0754157..3c01e5a 100644 --- a/src/ui/ShipLayoutPreview.cpp +++ b/src/ui/ShipLayoutPreview.cpp @@ -129,7 +129,7 @@ void ShipLayoutPreview::setShipAndLayout(const std::vector& shipLay } } - m_grid.assign(m_rows, std::vector(m_cols, {false, -1})); + m_grid.assign(m_rows, std::vector(m_cols, {false, std::nullopt})); for (int r = 0; r < m_rows; ++r) { for (int c = 0; c < static_cast(shipLayout[r].size()); ++c) @@ -205,9 +205,9 @@ void ShipLayoutPreview::paintEvent(QPaintEvent* /*event*/) { painter.fillRect(cellRect, Qt::black); } - else if (cell.moduleIndex >= 0) + else if (cell.moduleIndex.has_value()) { - const PlacedModule& pm = m_placedModules[cell.moduleIndex]; + const PlacedModule& pm = m_placedModules[*cell.moduleIndex]; const ModuleDef* def = findModuleDef(*m_modules, pm.moduleId); QColor color(Qt::gray); if (def) diff --git a/src/ui/ShipLayoutPreview.h b/src/ui/ShipLayoutPreview.h index 558fc31..92ca1c9 100644 --- a/src/ui/ShipLayoutPreview.h +++ b/src/ui/ShipLayoutPreview.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -31,7 +32,7 @@ private: struct CellInfo { bool buildable; - int moduleIndex; // -1 if empty + std::optional moduleIndex; // nullopt if empty }; std::vector> m_grid;