Use std::optional instead of sentinel values for absent data

Replace sentinel values that signalled "no value" with std::optional
across simulation, UI, and balancing code:

- BuildingId references: DeliverScrapBehavior::deliveryBay,
  Simulation::m_hqBuildingId, the return types of BuildingSystem::place
  and Simulation::tryPlaceBuilding (previously kInvalidBuildingId on
  failure), GameWorldView building/site-at-tile lookups and demolish
  hover, SelectedBuildingPanel::m_singleBuildingId.
- Command id fields: the five id-carrying commands now hold
  optional<BuildingId>; CommandSerializer and Simulation::apply updated.
  The serialized replay format is unchanged.
- Index sentinels: BlueprintPanel::m_activeIndex,
  BalancingWindow::m_inspectedArenaIndex, ArenaSimulation winnerTeam,
  and the moduleIndex grid cells in ShipLayoutDialog/ShipLayoutPreview.
  ShipLayoutDialog::m_activeModuleIndex was a tri-state, so it is
  modelled as bool m_removeMode + optional<int>.

Building/ConstructionSite id defaults keep kInvalidBuildingId (identity,
not an absent reference); entt::null and config -1 domain values are
left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
2026-07-20 07:36:34 +02:00
parent b708e1b29d
commit 388d354e51
32 changed files with 228 additions and 209 deletions

View File

@@ -44,7 +44,6 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
, m_team1HqEntity(entt::null) , m_team1HqEntity(entt::null)
, m_team2HqEntity(entt::null) , m_team2HqEntity(entt::null)
, m_finished(false) , m_finished(false)
, m_winnerTeam(-1)
, m_stopRequested(false) , m_stopRequested(false)
{ {
m_buildingSystem = std::make_unique<BuildingSystem>( m_buildingSystem = std::make_unique<BuildingSystem>(
@@ -473,7 +472,7 @@ bool ArenaSimulation::isFinished() const
return m_finished; return m_finished;
} }
int ArenaSimulation::getWinnerTeam() const std::optional<int> ArenaSimulation::getWinnerTeam() const
{ {
return m_winnerTeam; return m_winnerTeam;
} }

View File

@@ -58,7 +58,7 @@ struct ArenaStatus
TeamStatus teams[2]; TeamStatus teams[2];
bool finished = false; bool finished = false;
int winnerTeam = -1; // 0 or 1 when finished; -1 while running std::optional<int> winnerTeam; // 0 or 1 when finished; nullopt while running
// Game time the fight has lasted (simulated ticks * fixed tick duration). // Game time the fight has lasted (simulated ticks * fixed tick duration).
// Meaningful once finished; the battle duration shown for completed runs. // Meaningful once finished; the battle duration shown for completed runs.
double durationSeconds = 0.0; double durationSeconds = 0.0;
@@ -80,7 +80,7 @@ public:
ArenaStatus getStatus() const; ArenaStatus getStatus() const;
bool isFinished() const; bool isFinished() const;
int getWinnerTeam() const; std::optional<int> getWinnerTeam() const;
Tick getCurrentTick() const; Tick getCurrentTick() const;
const ArenaConfig& getArenaConfig() const; const ArenaConfig& getArenaConfig() const;
@@ -122,7 +122,7 @@ private:
entt::entity m_team2HqEntity; entt::entity m_team2HqEntity;
bool m_finished; bool m_finished;
int m_winnerTeam; std::optional<int> m_winnerTeam;
std::atomic<bool> m_stopRequested; std::atomic<bool> m_stopRequested;
// Static accumulated threat per team, computed once from the configured roster. // Static accumulated threat per team, computed once from the configured roster.

View File

@@ -76,7 +76,6 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
, m_balancingConfigPath(balancingConfigPath) , m_balancingConfigPath(balancingConfigPath)
, m_nextSeed(0) , m_nextSeed(0)
, m_inspectWindow(nullptr) , m_inspectWindow(nullptr)
, m_inspectedArenaIndex(-1)
{ {
m_visuals = VisualsLoader::load(m_configDir + "/visuals.toml"); m_visuals = VisualsLoader::load(m_configDir + "/visuals.toml");
setWindowTitle(tr("DotaFactory — Balancing Tool")); 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(); const ArenaStatus status = m_inspectedSim->getStatus();
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->updateStatus(status); m_arenas[static_cast<std::size_t>(*m_inspectedArenaIndex)].widget->updateStatus(status);
} }
updateButtons(); updateButtons();
@@ -255,13 +254,13 @@ void BalancingWindow::inspectArena(int index)
delete m_inspectWindow; delete m_inspectWindow;
m_inspectWindow = nullptr; m_inspectWindow = nullptr;
if (m_inspectedSim && m_inspectedArenaIndex >= 0 if (m_inspectedSim && m_inspectedArenaIndex.has_value()
&& !m_inspectedSim->isFinished()) && !m_inspectedSim->isFinished())
{ {
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->resetToGrey(); m_arenas[static_cast<std::size_t>(*m_inspectedArenaIndex)].widget->resetToGrey();
} }
m_inspectedSim.reset(); m_inspectedSim.reset();
m_inspectedArenaIndex = -1; m_inspectedArenaIndex = std::nullopt;
} }
ArenaEntry& entry = m_arenas[static_cast<std::size_t>(index)]; ArenaEntry& entry = m_arenas[static_cast<std::size_t>(index)];
@@ -297,16 +296,16 @@ void BalancingWindow::closeInspectWindow()
m_inspectWindow->deleteLater(); m_inspectWindow->deleteLater();
m_inspectWindow = nullptr; m_inspectWindow = nullptr;
if (m_inspectedArenaIndex >= 0 && m_inspectedSim) if (m_inspectedArenaIndex.has_value() && m_inspectedSim)
{ {
if (!m_inspectedSim->isFinished()) if (!m_inspectedSim->isFinished())
{ {
m_arenas[static_cast<std::size_t>(m_inspectedArenaIndex)].widget->resetToGrey(); m_arenas[static_cast<std::size_t>(*m_inspectedArenaIndex)].widget->resetToGrey();
} }
} }
m_inspectedSim.reset(); m_inspectedSim.reset();
m_inspectedArenaIndex = -1; m_inspectedArenaIndex = std::nullopt;
setMainControlsEnabled(true); setMainControlsEnabled(true);
updateButtons(); updateButtons();
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <optional>
#include <string> #include <string>
#include <thread> #include <thread>
#include <vector> #include <vector>
@@ -78,6 +79,6 @@ private:
QTimer* m_pollTimer; QTimer* m_pollTimer;
InspectWindow* m_inspectWindow; InspectWindow* m_inspectWindow;
int m_inspectedArenaIndex; std::optional<int> m_inspectedArenaIndex; // nullopt = no arena inspected
std::unique_ptr<ArenaSimulation> m_inspectedSim; std::unique_ptr<ArenaSimulation> m_inspectedSim;
}; };

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <optional>
#include "BuildingId.h" #include "BuildingId.h"
// Deliver-scrap behavior (one half of the old SalvageBehaviorComponent). Scored // Deliver-scrap behavior (one half of the old SalvageBehaviorComponent). Scored
@@ -7,6 +9,6 @@
// SalvagerSystem performs the actual delivery. // SalvagerSystem performs the actual delivery.
struct DeliverScrapBehavior struct DeliverScrapBehavior
{ {
BuildingId deliveryBay = kInvalidBuildingId; std::optional<BuildingId> deliveryBay; // nullopt until a bay is assigned
float score = 0.0f; float score = 0.0f;
}; };

View File

@@ -88,8 +88,8 @@ void SalvagerSystem::tick(Tick currentTick, ScrapSystem& scraps, BuildingSystem&
m_admin.forEach<DeliverScrapBehavior, PositionComponent>( m_admin.forEach<DeliverScrapBehavior, PositionComponent>(
[&](entt::entity ship, const DeliverScrapBehavior& deliver, const PositionComponent& pos) [&](entt::entity ship, const DeliverScrapBehavior& deliver, const PositionComponent& pos)
{ {
if (deliver.deliveryBay == kInvalidBuildingId) { return; } if (!deliver.deliveryBay.has_value()) { return; }
const Building* bay = buildings.findBuilding(deliver.deliveryBay); const Building* bay = buildings.findBuilding(*deliver.deliveryBay);
if (!bay) { return; } if (!bay) { return; }
const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f, 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<CargoComponent>(ship)) { return; } if (!m_admin.hasAll<CargoComponent>(ship)) { return; }
CargoComponent& cargo = m_admin.get<CargoComponent>(ship); CargoComponent& cargo = m_admin.get<CargoComponent>(ship);
if (cargo.current <= 0) { return; } if (cargo.current <= 0) { return; }
if (buildings.deliverScrapToSalvageBay(deliver.deliveryBay)) if (buildings.deliverScrapToSalvageBay(*deliver.deliveryBay))
{ {
--cargo.current; --cargo.current;
} }

View File

@@ -398,8 +398,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId,
maxCollRange * static_cast<float>(m_config.world.orbitFactor); maxCollRange * static_cast<float>(m_config.world.orbitFactor);
m_admin.addComponent<SalvageScrapBehavior>(entity, salvage); m_admin.addComponent<SalvageScrapBehavior>(entity, salvage);
DeliverScrapBehavior deliver; DeliverScrapBehavior deliver; // deliveryBay starts unassigned (nullopt)
deliver.deliveryBay = kInvalidBuildingId;
m_admin.addComponent<DeliverScrapBehavior>(entity, deliver); m_admin.addComponent<DeliverScrapBehavior>(entity, deliver);
} }

View File

@@ -31,7 +31,7 @@ void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const BuildingSystem& b
} }
// Assign nearest SalvageBay if not yet assigned. // Assign nearest SalvageBay if not yet assigned.
if (deliver.deliveryBay == kInvalidBuildingId) if (!deliver.deliveryBay.has_value())
{ {
const Building* bay = const Building* bay =
buildings.findNearestBuilding(pos.value, BuildingType::SalvageBay); buildings.findNearestBuilding(pos.value, BuildingType::SalvageBay);

View File

@@ -24,9 +24,9 @@ void DeliverScrapExecutor::execute(EntityAdmin& admin, const BuildingSystem& bui
if (selected.winner != BehaviorKind::DeliverScrap) { return; } if (selected.winner != BehaviorKind::DeliverScrap) { return; }
QVector2D dest = pos.value; 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) if (bay)
{ {
dest = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f, dest = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,

View File

@@ -346,7 +346,7 @@ std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe
// Placement // Placement
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
BuildingId BuildingSystem::place(BuildingType type, QPoint anchor, std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor,
Rotation rotation, Tick currentTick) Rotation rotation, Tick currentTick)
{ {
const BuildingDef* def = findBuildingDef(type); 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). // Reject placements that fall outside the world (REQ-BLD-PLACE-VALID).
if (!bodyCellsWithinWorldBounds(mask.bodyCells, anchor)) if (!bodyCellsWithinWorldBounds(mask.bodyCells, anchor))
{ {
return kInvalidBuildingId; return std::nullopt;
} }
const BuildingId id = m_allocateBuildingId(); const BuildingId id = m_allocateBuildingId();

View File

@@ -43,13 +43,13 @@ public:
std::mt19937& rng); std::mt19937& rng);
// -- Placement / demolish ------------------------------------------------ // -- Placement / demolish ------------------------------------------------
// Returns the new entity id, or kInvalidBuildingId if the placement falls // Returns the new entity id, or nullopt if the placement falls outside the
// outside the world bounds (vertical extent and asteroid left edge). Belt // world bounds (vertical extent and asteroid left edge). Belt and Splitter
// and Splitter register with BeltSystem directly; other types enter the // register with BeltSystem directly; other types enter the construction
// construction queue. Terrain type (A vs S) is NOT checked here so that // queue. Terrain type (A vs S) is NOT checked here so that tests can stage
// tests can stage arbitrary layouts; the player-facing entry point // arbitrary layouts; the player-facing entry point
// (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid. // (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid.
BuildingId place(BuildingType type, QPoint anchor, Rotation rotation, std::optional<BuildingId> place(BuildingType type, QPoint anchor, Rotation rotation,
Tick currentTick); Tick currentTick);
// Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and // Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and

View File

@@ -75,28 +75,28 @@ struct PlaceBuildingCommand : Command
struct DemolishCommand : Command struct DemolishCommand : Command
{ {
DemolishCommand() : Command(CommandKind::Demolish) {} DemolishCommand() : Command(CommandKind::Demolish) {}
BuildingId id = kInvalidBuildingId; std::optional<BuildingId> id;
}; };
struct RotateInPlaceCommand : Command struct RotateInPlaceCommand : Command
{ {
RotateInPlaceCommand() : Command(CommandKind::RotateInPlace) {} RotateInPlaceCommand() : Command(CommandKind::RotateInPlace) {}
BuildingId id = kInvalidBuildingId; std::optional<BuildingId> id;
Rotation newRotation = Rotation::East; Rotation newRotation = Rotation::East;
}; };
struct SetRecipeCommand : Command struct SetRecipeCommand : Command
{ {
SetRecipeCommand() : Command(CommandKind::SetRecipe) {} SetRecipeCommand() : Command(CommandKind::SetRecipe) {}
BuildingId id = kInvalidBuildingId; std::optional<BuildingId> id;
std::string recipeId; std::string recipeId;
}; };
struct SetShipLayoutCommand : Command struct SetShipLayoutCommand : Command
{ {
SetShipLayoutCommand() : Command(CommandKind::SetShipLayout) {} SetShipLayoutCommand() : Command(CommandKind::SetShipLayout) {}
BuildingId id = kInvalidBuildingId; std::optional<BuildingId> id;
ShipLayoutConfig layout; ShipLayoutConfig layout;
}; };
// Splitter filters for a queued / under-construction Splitter site (configured by // Splitter filters for a queued / under-construction Splitter site (configured by
@@ -104,8 +104,8 @@ struct SetShipLayoutCommand : Command
struct SetSiteSplitterFiltersCommand : Command struct SetSiteSplitterFiltersCommand : Command
{ {
SetSiteSplitterFiltersCommand() : Command(CommandKind::SetSiteSplitterFilters) {} SetSiteSplitterFiltersCommand() : Command(CommandKind::SetSiteSplitterFilters) {}
BuildingId id = kInvalidBuildingId; std::optional<BuildingId> id;
std::vector<ItemType> filterA; std::vector<ItemType> filterA;
std::vector<ItemType> filterB; std::vector<ItemType> filterB;
}; };

View File

@@ -129,24 +129,24 @@ std::string serializeCommand(const Command& command)
break; break;
} }
case CommandKind::Demolish: case CommandKind::Demolish:
out << "demolish " << static_cast<const DemolishCommand&>(command).id; out << "demolish " << static_cast<const DemolishCommand&>(command).id.value();
break; break;
case CommandKind::RotateInPlace: case CommandKind::RotateInPlace:
{ {
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command); const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
out << "rotate " << c.id << ' ' << rotationToChar(c.newRotation); out << "rotate " << c.id.value() << ' ' << rotationToChar(c.newRotation);
break; break;
} }
case CommandKind::SetRecipe: case CommandKind::SetRecipe:
{ {
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command); const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
out << "setrecipe " << c.id << ' ' << c.recipeId; out << "setrecipe " << c.id.value() << ' ' << c.recipeId;
break; break;
} }
case CommandKind::SetShipLayout: case CommandKind::SetShipLayout:
{ {
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command); const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
out << "setlayout " << c.id << ' '; out << "setlayout " << c.id.value() << ' ';
appendLayout(out, c.layout); appendLayout(out, c.layout);
break; break;
} }
@@ -154,7 +154,7 @@ std::string serializeCommand(const Command& command)
{ {
const SetSiteSplitterFiltersCommand& c = const SetSiteSplitterFiltersCommand& c =
static_cast<const SetSiteSplitterFiltersCommand&>(command); static_cast<const SetSiteSplitterFiltersCommand&>(command);
out << "sitefilters " << c.id << ' '; out << "sitefilters " << c.id.value() << ' ';
appendFilters(out, c.filterA, c.filterB); appendFilters(out, c.filterA, c.filterB);
break; break;
} }
@@ -242,27 +242,35 @@ std::shared_ptr<Command> parseCommand(const std::string& tokens)
if (verb == "demolish") if (verb == "demolish")
{ {
std::shared_ptr<DemolishCommand> c = std::make_shared<DemolishCommand>(); std::shared_ptr<DemolishCommand> c = std::make_shared<DemolishCommand>();
if (!(in >> c->id)) { return nullptr; } BuildingId id = 0;
if (!(in >> id)) { return nullptr; }
c->id = id;
return c; return c;
} }
if (verb == "rotate") if (verb == "rotate")
{ {
std::shared_ptr<RotateInPlaceCommand> c = std::make_shared<RotateInPlaceCommand>(); std::shared_ptr<RotateInPlaceCommand> c = std::make_shared<RotateInPlaceCommand>();
std::string rotToken; 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); c->newRotation = rotationFromString(rotToken);
return c; return c;
} }
if (verb == "setrecipe") if (verb == "setrecipe")
{ {
std::shared_ptr<SetRecipeCommand> c = std::make_shared<SetRecipeCommand>(); std::shared_ptr<SetRecipeCommand> c = std::make_shared<SetRecipeCommand>();
if (!(in >> c->id >> c->recipeId)) { return nullptr; } BuildingId id = 0;
if (!(in >> id >> c->recipeId)) { return nullptr; }
c->id = id;
return c; return c;
} }
if (verb == "setlayout") if (verb == "setlayout")
{ {
std::shared_ptr<SetShipLayoutCommand> c = std::make_shared<SetShipLayoutCommand>(); std::shared_ptr<SetShipLayoutCommand> c = std::make_shared<SetShipLayoutCommand>();
if (!(in >> c->id)) { return nullptr; } BuildingId id = 0;
if (!(in >> id)) { return nullptr; }
c->id = id;
c->layout = parseLayout(in, ok); c->layout = parseLayout(in, ok);
if (!ok) { return nullptr; } if (!ok) { return nullptr; }
return c; return c;
@@ -271,7 +279,9 @@ std::shared_ptr<Command> parseCommand(const std::string& tokens)
{ {
std::shared_ptr<SetSiteSplitterFiltersCommand> c = std::shared_ptr<SetSiteSplitterFiltersCommand> c =
std::make_shared<SetSiteSplitterFiltersCommand>(); std::make_shared<SetSiteSplitterFiltersCommand>();
if (!(in >> c->id)) { return nullptr; } BuildingId id = 0;
if (!(in >> id)) { return nullptr; }
c->id = id;
parseFilters(in, c->filterA, c->filterB, ok); parseFilters(in, c->filterA, c->filterB, ok);
if (!ok) { return nullptr; } if (!ok) { return nullptr; }
return c; return c;

View File

@@ -40,7 +40,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
, m_nextBuildingId(1) , m_nextBuildingId(1)
, m_buildingBlocksStock(m_config.world.startingBuildingBlocks) , m_buildingBlocksStock(m_config.world.startingBuildingBlocks)
, m_gameOver(false) , m_gameOver(false)
, m_hqBuildingId(kInvalidBuildingId)
, m_hqProxyEntity(entt::null) , m_hqProxyEntity(entt::null)
, m_playerStation1Entity(entt::null) , m_playerStation1Entity(entt::null)
, m_playerStation2Entity(entt::null) , m_playerStation2Entity(entt::null)
@@ -137,7 +136,7 @@ void Simulation::reset(unsigned int seed)
m_gameOver = false; m_gameOver = false;
m_isWon = false; m_isWon = false;
m_artifactCount = 0; m_artifactCount = 0;
m_hqBuildingId = kInvalidBuildingId; m_hqBuildingId = std::nullopt;
m_hqProxyEntity = entt::null; m_hqProxyEntity = entt::null;
m_playerStation1Entity = entt::null; m_playerStation1Entity = entt::null;
m_playerStation2Entity = entt::null; m_playerStation2Entity = entt::null;
@@ -217,11 +216,12 @@ void Simulation::apply(const Command& command)
case CommandKind::PlaceBuilding: case CommandKind::PlaceBuilding:
{ {
const PlaceBuildingCommand& c = static_cast<const PlaceBuildingCommand&>(command); const PlaceBuildingCommand& c = static_cast<const PlaceBuildingCommand&>(command);
const BuildingId id = tryPlaceBuilding(c.type, c.anchor, c.rotation); const std::optional<BuildingId> placed = tryPlaceBuilding(c.type, c.anchor, c.rotation);
if (id == kInvalidBuildingId) if (!placed.has_value())
{ {
break; break;
} }
const BuildingId id = *placed;
if (c.recipeId.has_value()) if (c.recipeId.has_value())
{ {
m_buildingSystem->setRecipe(id, *c.recipeId); m_buildingSystem->setRecipe(id, *c.recipeId);
@@ -237,31 +237,31 @@ void Simulation::apply(const Command& command)
break; break;
} }
case CommandKind::Demolish: case CommandKind::Demolish:
demolish(static_cast<const DemolishCommand&>(command).id); demolish(*static_cast<const DemolishCommand&>(command).id);
break; break;
case CommandKind::RotateInPlace: case CommandKind::RotateInPlace:
{ {
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command); const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
m_buildingSystem->rotateInPlace(c.id, c.newRotation); m_buildingSystem->rotateInPlace(*c.id, c.newRotation);
break; break;
} }
case CommandKind::SetRecipe: case CommandKind::SetRecipe:
{ {
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command); const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
m_buildingSystem->setRecipe(c.id, c.recipeId); m_buildingSystem->setRecipe(*c.id, c.recipeId);
break; break;
} }
case CommandKind::SetShipLayout: case CommandKind::SetShipLayout:
{ {
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command); const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
m_buildingSystem->setShipLayout(c.id, c.layout); m_buildingSystem->setShipLayout(*c.id, c.layout);
break; break;
} }
case CommandKind::SetSiteSplitterFilters: case CommandKind::SetSiteSplitterFilters:
{ {
const SetSiteSplitterFiltersCommand& c = const SetSiteSplitterFiltersCommand& c =
static_cast<const SetSiteSplitterFiltersCommand&>(command); static_cast<const SetSiteSplitterFiltersCommand&>(command);
m_buildingSystem->setSiteSplitterFilters(c.id, c.filterA, c.filterB); m_buildingSystem->setSiteSplitterFilters(*c.id, c.filterA, c.filterB);
break; break;
} }
case CommandKind::SetSplitterFilters: case CommandKind::SetSplitterFilters:
@@ -1193,11 +1193,11 @@ bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
return it->second.unlocked; return it->second.unlocked;
} }
BuildingId Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation) std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
{ {
if (!m_buildingSystem->isPlacementValid(type, anchor, rotation)) if (!m_buildingSystem->isPlacementValid(type, anchor, rotation))
{ {
return kInvalidBuildingId; return std::nullopt;
} }
int cost = 0; int cost = 0;
@@ -1211,7 +1211,7 @@ BuildingId Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotati
} }
if (m_buildingBlocksStock < cost) if (m_buildingBlocksStock < cost)
{ {
return kInvalidBuildingId; return std::nullopt;
} }
m_buildingBlocksStock -= cost; m_buildingBlocksStock -= cost;
return m_buildingSystem->place(type, anchor, rotation, m_currentTick); return m_buildingSystem->place(type, anchor, rotation, m_currentTick);

View File

@@ -2,6 +2,7 @@
#include <map> #include <map>
#include <memory> #include <memory>
#include <optional>
#include <random> #include <random>
#include <set> #include <set>
#include <string> #include <string>
@@ -131,8 +132,8 @@ private:
// Reached during play exclusively via apply(); never called by UI/app code. // Reached during play exclusively via apply(); never called by UI/app code.
// Checks affordability, deducts building blocks, and places the building. // Checks affordability, deducts building blocks, and places the building.
// Returns the new entity id, or kInvalidBuildingId if blocks are insufficient. // Returns the new entity id, or nullopt if blocks are insufficient.
BuildingId tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation); std::optional<BuildingId> tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
// Demolishes the building with the given id and refunds building blocks. // Demolishes the building with the given id and refunds building blocks.
void demolish(BuildingId id); void demolish(BuildingId id);
@@ -182,7 +183,7 @@ private:
int m_artifactCount = 0; int m_artifactCount = 0;
// Pre-placed structure IDs. // Pre-placed structure IDs.
BuildingId m_hqBuildingId; // Building id (for belt integration) std::optional<BuildingId> m_hqBuildingId; // Building id (for belt integration)
entt::entity m_hqProxyEntity; // ECS entity (HP, targeting) entt::entity m_hqProxyEntity; // ECS entity (HP, targeting)
entt::entity m_playerStation1Entity; entt::entity m_playerStation1Entity;
entt::entity m_playerStation2Entity; entt::entity m_playerStation2Entity;

View File

@@ -954,7 +954,7 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
Fixture f; Fixture f;
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay, const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
QPoint(-4, 0), Rotation::East, 0); QPoint(-4, 0), Rotation::East, 0).value();
Tick t = 0; Tick t = 0;
for (int i = 0; i < 500; ++i) 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; Fixture f;
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay, const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
QPoint(-4, 0), Rotation::East, 0); QPoint(-4, 0), Rotation::East, 0).value();
Tick t = 0; Tick t = 0;
for (int i = 0; i < 500; ++i) for (int i = 0; i < 500; ++i)
{ {

View File

@@ -527,9 +527,9 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
const QPoint offsetB( 1, 0); const QPoint offsetB( 1, 0);
const BuildingId idA = SimulationTestAccess::place(sim, 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, const BuildingId idB = SimulationTestAccess::place(sim,
BuildingType::Belt, cursor + offsetB, Rotation::East); BuildingType::Belt, cursor + offsetB, Rotation::East).value();
REQUIRE(idA != kInvalidBuildingId); REQUIRE(idA != kInvalidBuildingId);
REQUIRE(idB != kInvalidBuildingId); REQUIRE(idB != kInvalidBuildingId);
@@ -583,11 +583,11 @@ TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId a
} }
const int blocksBeforeAttempt = sim.getBuildingBlocksStock(); const int blocksBeforeAttempt = sim.getBuildingBlocksStock();
const BuildingId id = SimulationTestAccess::place(sim, const std::optional<BuildingId> id = SimulationTestAccess::place(sim,
BuildingType::Miner, QPoint(col - 2, 0), Rotation::East); BuildingType::Miner, QPoint(col - 2, 0), Rotation::East);
// Placement must fail and leave the stock unchanged. // Placement must fail and leave the stock unchanged.
REQUIRE(id == kInvalidBuildingId); REQUIRE_FALSE(id.has_value());
REQUIRE(sim.getBuildingBlocksStock() == blocksBeforeAttempt); 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 // A miner is all-asteroid; placing it in space (x >= 0) violates the terrain
// rule, so it must be rejected without consuming building blocks. // rule, so it must be rejected without consuming building blocks.
const BuildingId id = const std::optional<BuildingId> id =
SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(0, 0), Rotation::East); SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(0, 0), Rotation::East);
REQUIRE(id == kInvalidBuildingId); REQUIRE_FALSE(id.has_value());
REQUIRE(sim.getBuildingBlocksStock() == startBlocks); REQUIRE(sim.getBuildingBlocksStock() == startBlocks);
REQUIRE(sim.getBuildings().getAllSites().empty()); 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 // Miner mask ["AA","A>"] East at (-3,0) → all-asteroid body at
// (-3,0),(-2,0),(-3,1); a valid spot. // (-3,0),(-2,0),(-3,1); a valid spot.
const BuildingId id = 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(id != kInvalidBuildingId);
REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost); REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost);
@@ -666,7 +666,7 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
Simulation sim(loadConfig()); Simulation sim(loadConfig());
// Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid. // 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); REQUIRE(id != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); 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()); 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); REQUIRE(id != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_copper_ore"); 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 // 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. // the body-cell bounding-box centered on the anchor, so a single site → zero offset.
const BuildingId id = 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(id != kInvalidBuildingId);
REQUIRE(sim.getBuildings().findSite(id) != nullptr); REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(sim.getBuildings().findBuilding(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()); Simulation sim(loadConfig());
const BuildingId id = 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(id != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); 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. // Building A: place, configure, and tick to completion so it is operational.
const BuildingId idA = 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); REQUIRE(idA != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore"); SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore");
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); } for (int i = 0; i <= static_cast<int>(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. // Building B: place and configure, but leave as a construction site.
const BuildingId idB = 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); REQUIRE(idB != kInvalidBuildingId);
SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore"); SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore");
REQUIRE(sim.getBuildings().findSite(idB) != nullptr); REQUIRE(sim.getBuildings().findSite(idB) != nullptr);
@@ -777,7 +777,7 @@ TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction
REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {})); REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {}));
const BuildingId id = 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(id != kInvalidBuildingId);
REQUIRE(sim.getBuildings().findSite(id) != nullptr); REQUIRE(sim.getBuildings().findSite(id) != nullptr);
REQUIRE(selectionHasPlaceableBuilding(sim, { id })); 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: // 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. // 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. // 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); REQUIRE(id != kInvalidBuildingId);
ShipLayoutConfig layout; ShipLayoutConfig layout;
@@ -871,7 +871,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
{ {
Simulation sim(loadConfig()); 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); REQUIRE(id != kInvalidBuildingId);
ShipLayoutConfig layout; ShipLayoutConfig layout;

View File

@@ -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). // A placed miner enters the construction queue as a site (not yet operational).
const BuildingId id = 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(id != kInvalidBuildingId);
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr); REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
REQUIRE(sim.getBuildings().findSite(id) != nullptr); REQUIRE(sim.getBuildings().findSite(id) != nullptr);

View File

@@ -119,7 +119,7 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng); 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); REQUIRE(id != kInvalidBuildingId);
// Miner mask ["AA","A>"] with East rotation → body at (0,0),(1,0),(0,1). // 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 // Miner mask ["AA","A>"] East → body at (0,0),(1,0),(0,1); at y=-1 the top
// row sits above the world. // row sits above the world.
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0); const std::optional<BuildingId> id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0);
REQUIRE(id == kInvalidBuildingId); REQUIRE_FALSE(id.has_value());
REQUIRE(f.bs.getAllSites().empty()); REQUIRE(f.bs.getAllSites().empty());
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0))); 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 // Anchored on the last in-bounds row, the miner's lower body row reaches
// y == heightTiles, which is outside the world. // y == heightTiles, which is outside the world.
const BuildingId id = f.bs.place(BuildingType::Miner, const std::optional<BuildingId> id = f.bs.place(BuildingType::Miner,
QPoint(0, heightTiles - 1), Rotation::East, 0); QPoint(0, heightTiles - 1), Rotation::East, 0);
REQUIRE(id == kInvalidBuildingId); REQUIRE_FALSE(id.has_value());
REQUIRE(f.bs.getAllSites().empty()); REQUIRE(f.bs.getAllSites().empty());
} }
@@ -162,9 +162,9 @@ TEST_CASE("BuildingSystem: place rejects a building left of the asteroid edge",
PlacementFixture f; PlacementFixture f;
const int leftEdgeX = -f.cfg.world.regions.asteroidWidth_tiles; const int leftEdgeX = -f.cfg.world.regions.asteroidWidth_tiles;
const BuildingId id = f.bs.place(BuildingType::Miner, const std::optional<BuildingId> id = f.bs.place(BuildingType::Miner,
QPoint(leftEdgeX - 1, 0), Rotation::East, 0); QPoint(leftEdgeX - 1, 0), Rotation::East, 0);
REQUIRE(id == kInvalidBuildingId); REQUIRE_FALSE(id.has_value());
REQUIRE(f.bs.getAllSites().empty()); 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. // Miner body min relative x is 0, so its leftmost cell sits exactly on the edge.
const BuildingId id = f.bs.place(BuildingType::Miner, 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(id != kInvalidBuildingId);
REQUIRE(f.bs.isTileOccupied(QPoint(leftEdgeX, 0))); 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; PlacementFixture f;
const BuildingId id = f.bs.place(BuildingType::Miner, const BuildingId id = f.bs.place(BuildingType::Miner,
QPoint(1000, 0), Rotation::East, 0); QPoint(1000, 0), Rotation::East, 0).value();
REQUIRE(id != kInvalidBuildingId); REQUIRE(id != kInvalidBuildingId);
} }
@@ -256,7 +256,7 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng); 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.getAllSites().size() == 1);
REQUIRE(bs.getAllBuildings().empty()); REQUIRE(bs.getAllBuildings().empty());
@@ -277,7 +277,7 @@ TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building]
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng); 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. // Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300.
// We need to process tick 300 itself, so run 301 ticks (ticks 0..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; }, [](const std::string&) -> bool { return true; },
rng); 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. // Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300.
// We need to process tick 300 itself, so run 301 ticks (ticks 0..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); rng);
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); 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. // Process through tick 300 to complete first miner's construction.
Tick tick = 0; Tick tick = 0;
@@ -406,7 +406,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng); 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"); bs.setRecipe(id, "mine_iron_ore");
Tick tick = 0; Tick tick = 0;
@@ -439,7 +439,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng); 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"); bs.setRecipe(id, "mine_iron_ore");
Tick tick = 0; Tick tick = 0;
@@ -479,8 +479,8 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng); rng);
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();
const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0); const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0).value();
(void)smelterId; (void)smelterId;
Tick tick = 0; Tick tick = 0;
@@ -519,7 +519,7 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng); 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"); bs.setRecipe(id, "mine_iron_ore");
Tick tick = 0; 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). // Smelter mask ["AA ","AA>"] → body (0,0),(1,0),(0,1),(1,1).
// Output port (2,1) East. Input port example: (2,0) West. // 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 // Smelters have no recipe selection (REQ-BLD-SMELTER); they auto-accept any
// ore/scrap that is an input to a smelter recipe. // 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; }, [](const std::string&) -> bool { return true; },
rng); 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; Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
@@ -645,7 +645,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th
rng); rng);
const BuildingId id = bs.place(BuildingType::ReprocessingPlant, const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
QPoint(0, 0), Rotation::East, 0); QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0; Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(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; }, [](const std::string&) -> bool { return true; },
rng); 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; Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(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; }, [](const std::string&) -> bool { return true; },
rng); 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; Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(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; }, [](const std::string&) -> bool { return true; },
rng); 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"); bs.setRecipe(id, "mine_iron_ore");
// Belt at the miner's output port tile (1,1) flowing East. // 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); rng);
// Miner at (0,0): body (0,0),(1,0),(0,1); output port tile (1,1) flowing East. // 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"); 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 // 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 // the miner's output-port tile, and its west input edge there faces East, so the
// two ports meet — no belt placed anywhere. // 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; Tick tick = 0;
// Smelter build (15s) + margin for coupling and a smelt cycle. // 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); rng);
// Producing miner at (0,0), output port (1,1) East. // 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"); bs.setRecipe(minerId, "mine_iron_ore");
// A second, idle miner anchored at (1,1) occupies the output-port tile but takes // A second, idle miner anchored at (1,1) occupies the output-port tile but takes
// no inputs, so it cannot accept the iron_ore. // 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; Tick tick = 0;
// Both miners build sequentially (10s each), then the producer runs and jams. // 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; }, [](const std::string&) -> bool { return true; },
rng); 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"); bs.setRecipe(id, "mine_iron_ore");
Tick tick = 0; Tick tick = 0;
@@ -948,7 +948,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
rng); rng);
const BuildingId id = bs.place(BuildingType::ReprocessingPlant, 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 // Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the
// single reprocessing recipe is applied automatically on completion. // single reprocessing recipe is applied automatically on completion.
@@ -980,7 +980,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
rng); rng);
const BuildingId id = bs.place(BuildingType::ReprocessingPlant, 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 // Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the
// single reprocessing recipe is applied automatically on completion. // 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; }, [](const std::string&) -> bool { return true; },
rng); 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<BuildingId> result = const std::optional<BuildingId> result =
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::North); 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; }, [](const std::string&) -> bool { return true; },
rng); 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; Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(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 // Smelter is a fully filled 2×2 footprint — rotating the ghost produces the
// same four body tiles, so findRotateInPlaceTarget must still return the id. // 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<BuildingId> result = const std::optional<BuildingId> result =
bs.findRotateInPlaceTarget(BuildingType::Smelter, QPoint(0, 0), Rotation::North); 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; }, [](const std::string&) -> bool { return true; },
rng); 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); REQUIRE(bs.findSite(id)->rotation == Rotation::East);
bs.rotateInPlace(id, Rotation::North); bs.rotateInPlace(id, Rotation::North);
@@ -1204,7 +1204,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng); 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; const Tick completesAt = bs.findSite(id)->completesAt;
REQUIRE(completesAt > 0); REQUIRE(completesAt > 0);
@@ -1228,7 +1228,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct
[](const std::string&) -> bool { return true; }, [](const std::string&) -> bool { return true; },
rng); 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; Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(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; }, [](const std::string&) -> bool { return true; },
rng); 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; Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
@@ -1276,7 +1276,7 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
PlacementFixture f; PlacementFixture f;
const QPoint tile(5, 5); 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(id != kInvalidBuildingId);
REQUIRE(f.bs.findSite(id) != nullptr); REQUIRE(f.bs.findSite(id) != nullptr);

View File

@@ -52,7 +52,7 @@ TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe"
viaCommand.apply(command); viaCommand.apply(command);
const BuildingId id = 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"); SimulationTestAccess::buildings(viaDirect).setRecipe(id, "mine_iron_ore");
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum()); REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
@@ -64,9 +64,9 @@ TEST_CASE("apply(DemolishCommand) matches direct demolish", "[command]")
Simulation viaDirect(loadConfig(), 99); Simulation viaDirect(loadConfig(), 99);
const BuildingId idA = 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 = 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); REQUIRE(idA == idB);
DemolishCommand command; DemolishCommand command;

View File

@@ -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). // Cargo capacity is now a ship-level pool (REQ-MOD-CARGO-CAPACITY).
REQUIRE(admin.get<CargoComponent>(e).maxCapacity == 10); REQUIRE(admin.get<CargoComponent>(e).maxCapacity == 10);
REQUIRE(admin.get<CargoComponent>(e).current == 0); REQUIRE(admin.get<CargoComponent>(e).current == 0);
REQUIRE(admin.get<DeliverScrapBehavior>(e).deliveryBay == kInvalidBuildingId); REQUIRE_FALSE(admin.get<DeliverScrapBehavior>(e).deliveryBay.has_value());
REQUIRE_FALSE(admin.get<SalvageScrapBehavior>(e).scrapTarget.has_value()); REQUIRE_FALSE(admin.get<SalvageScrapBehavior>(e).scrapTarget.has_value());
REQUIRE(admin.get<SalvageScrapBehavior>(e).maxCollectionRange_tiles == Approx(50.0f)); REQUIRE(admin.get<SalvageScrapBehavior>(e).maxCollectionRange_tiles == Approx(50.0f));
} }

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <optional>
#include <QPoint> #include <QPoint>
#include "BuildingId.h" #include "BuildingId.h"
@@ -25,8 +27,8 @@ struct SimulationTestAccess
static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); } static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); }
static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); } static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); }
static BuildingId place(Simulation& sim, BuildingType type, QPoint anchor, static std::optional<BuildingId> place(Simulation& sim, BuildingType type,
Rotation rotation) QPoint anchor, Rotation rotation)
{ {
return sim.tryPlaceBuilding(type, anchor, rotation); return sim.tryPlaceBuilding(type, anchor, rotation);
} }

View File

@@ -28,7 +28,6 @@ BlueprintPanel::BlueprintPanel(Simulation* sim, const GameConfig* config, QWidge
, m_sim(sim) , m_sim(sim)
, m_config(config) , m_config(config)
, m_currentBlocks(0) , m_currentBlocks(0)
, m_activeIndex(-1)
{ {
QVBoxLayout* layout = new QVBoxLayout(this); QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(4, 4, 4, 4); layout->setContentsMargins(4, 4, 4, 4);
@@ -80,11 +79,11 @@ void BlueprintPanel::handleEvent(std::shared_ptr<const BuildingBlocksChangedEven
void BlueprintPanel::clearActiveBlueprintButton() void BlueprintPanel::clearActiveBlueprintButton()
{ {
if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_blueprintButtons.size())) if (m_activeIndex.has_value() && *m_activeIndex < static_cast<int>(m_blueprintButtons.size()))
{ {
m_blueprintButtons[static_cast<std::size_t>(m_activeIndex)]->setChecked(false); m_blueprintButtons[static_cast<std::size_t>(*m_activeIndex)]->setChecked(false);
} }
m_activeIndex = -1; m_activeIndex = std::nullopt;
refreshButtonStates(); refreshButtonStates();
} }
@@ -109,13 +108,13 @@ void BlueprintPanel::onDeleteBlueprintClicked(int index)
{ {
if (m_activeIndex == index) if (m_activeIndex == index)
{ {
m_activeIndex = -1; m_activeIndex = std::nullopt;
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<ExitBlueprintModeRequestedEvent>()); std::make_shared<ExitBlueprintModeRequestedEvent>());
} }
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); m_blueprints.erase(m_blueprints.begin() + index);
rebuildButtons(); rebuildButtons();
@@ -133,9 +132,9 @@ void BlueprintPanel::onBlueprintButtonClicked(int index)
return; return;
} }
if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_blueprintButtons.size())) if (m_activeIndex.has_value() && *m_activeIndex < static_cast<int>(m_blueprintButtons.size()))
{ {
m_blueprintButtons[static_cast<std::size_t>(m_activeIndex)]->setChecked(false); m_blueprintButtons[static_cast<std::size_t>(*m_activeIndex)]->setChecked(false);
} }
m_activeIndex = index; m_activeIndex = index;

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <optional>
#include <vector> #include <vector>
#include <QWidget> #include <QWidget>
@@ -56,7 +57,7 @@ private:
const GameConfig* m_config; const GameConfig* m_config;
std::vector<BuildingId> m_selectedBuildingIds; std::vector<BuildingId> m_selectedBuildingIds;
int m_currentBlocks; int m_currentBlocks;
int m_activeIndex; std::optional<int> m_activeIndex; // nullopt = no blueprint selected
std::vector<Blueprint> m_blueprints; std::vector<Blueprint> m_blueprints;
std::vector<QPushButton*> m_blueprintButtons; std::vector<QPushButton*> m_blueprintButtons;
QPushButton* m_createBtn; QPushButton* m_createBtn;

View File

@@ -142,7 +142,6 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
, m_ghostValid(false) , m_ghostValid(false)
, m_dragging(false) , m_dragging(false)
, m_demolishMode(false) , m_demolishMode(false)
, m_demolishHoverBuildingId(kInvalidBuildingId)
, m_debugDraw(false) , m_debugDraw(false)
, m_rng(std::random_device{}()) , m_rng(std::random_device{}())
, m_boxSelecting(false) , m_boxSelecting(false)
@@ -621,34 +620,34 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
return true; return true;
} }
BuildingId GameWorldView::buildingAtTile(QPoint tile) const std::optional<BuildingId> GameWorldView::buildingAtTile(QPoint tile) const
{ {
for (const Building& b : m_sim->getBuildings().getAllBuildings()) for (const Building& b : m_sim->getBuildings().getAllBuildings())
{ {
for (const QPoint& cell : b.bodyCells) for (const QPoint& cell : b.bodyCells)
{ {
if (cell == tile) if (cell == tile)
{ {
return b.id; return b.id;
} }
} }
} }
return kInvalidBuildingId; return std::nullopt;
} }
BuildingId GameWorldView::siteAtTile(QPoint tile) const std::optional<BuildingId> GameWorldView::siteAtTile(QPoint tile) const
{ {
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites()) for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
{ {
for (const QPoint& cell : s.bodyCells) for (const QPoint& cell : s.bodyCells)
{ {
if (cell == tile) if (cell == tile)
{ {
return s.id; return s.id;
} }
} }
} }
return kInvalidBuildingId; return std::nullopt;
} }
@@ -1606,9 +1605,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) if (b)
{ {
for (const QPoint& cell : b->bodyCells) for (const QPoint& cell : b->bodyCells)
@@ -1961,9 +1960,9 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
// Shift + right-click copies a building's settings, but only in the // Shift + right-click copies a building's settings, but only in the
// default selection mode (REQ-BLD-COPY-CONFIG). // default selection mode (REQ-BLD-COPY-CONFIG).
const QPoint tile = widgetToTile(event->pos()); const QPoint tile = widgetToTile(event->pos());
BuildingId id = buildingAtTile(tile); std::optional<BuildingId> id = buildingAtTile(tile);
if (id == kInvalidBuildingId) { id = siteAtTile(tile); } if (!id.has_value()) { id = siteAtTile(tile); }
if (id != kInvalidBuildingId) { copyConfigFrom(id); } if (id.has_value()) { copyConfigFrom(*id); }
} }
} }
return; return;
@@ -2004,11 +2003,11 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
// selection. Only active in the default selection mode. // selection. Only active in the default selection mode.
if ((event->modifiers() & Qt::ShiftModifier) && m_copiedConfig.has_value()) if ((event->modifiers() & Qt::ShiftModifier) && m_copiedConfig.has_value())
{ {
BuildingId id = buildingAtTile(tile); std::optional<BuildingId> id = buildingAtTile(tile);
if (id == kInvalidBuildingId) { id = siteAtTile(tile); } if (!id.has_value()) { id = siteAtTile(tile); }
if (id != kInvalidBuildingId) if (id.has_value())
{ {
pasteConfigTo(id); pasteConfigTo(*id);
return; return;
} }
} }
@@ -2036,13 +2035,14 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
std::make_shared<EntitySelectedEvent>(std::nullopt)); std::make_shared<EntitySelectedEvent>(std::nullopt));
} }
BuildingId id = buildingAtTile(tile); std::optional<BuildingId> hit = buildingAtTile(tile);
if (id == kInvalidBuildingId) 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). // A building/construction site outranks scrap (REQ-UI-SCRAP-CLICK-SELECT).
clearScrapSelection(); clearScrapSelection();
if (event->modifiers() & Qt::ControlModifier) if (event->modifiers() & Qt::ControlModifier)
@@ -2170,7 +2170,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
command->id = id; command->id = id;
enqueueCommand(command); enqueueCommand(command);
} }
m_demolishHoverBuildingId = kInvalidBuildingId; m_demolishHoverBuildingId = std::nullopt;
return; return;
} }
@@ -2255,7 +2255,7 @@ void GameWorldView::toggleDemolishMode()
if (m_demolishMode) if (m_demolishMode)
{ {
m_demolishMode = false; m_demolishMode = false;
m_demolishHoverBuildingId = kInvalidBuildingId; m_demolishHoverBuildingId = std::nullopt;
} }
else else
{ {
@@ -2451,7 +2451,7 @@ void GameWorldView::resetForNewGame()
m_ghostRotation = Rotation::East; m_ghostRotation = Rotation::East;
m_ghostValid = false; m_ghostValid = false;
m_demolishMode = false; m_demolishMode = false;
m_demolishHoverBuildingId = kInvalidBuildingId; m_demolishHoverBuildingId = std::nullopt;
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeChangedEvent>(false)); std::make_shared<DemolishModeChangedEvent>(false));
m_selectedBuildingIds.clear(); m_selectedBuildingIds.clear();

View File

@@ -168,8 +168,8 @@ private:
bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const; bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const;
const BuildingDef* findBuildingDef(BuildingType type) const; const BuildingDef* findBuildingDef(BuildingType type) const;
BuildingId buildingAtTile(QPoint tile) const; std::optional<BuildingId> buildingAtTile(QPoint tile) const;
BuildingId siteAtTile(QPoint tile) const; std::optional<BuildingId> siteAtTile(QPoint tile) const;
// Ids of all buildings and construction sites whose footprint intersects // Ids of all buildings and construction sites whose footprint intersects
// the tile box spanned by the two (unordered) corner tiles. // the tile box spanned by the two (unordered) corner tiles.
std::vector<BuildingId> buildingsInBox(QPoint cornerA, QPoint cornerB) const; std::vector<BuildingId> buildingsInBox(QPoint cornerA, QPoint cornerB) const;
@@ -268,7 +268,7 @@ private:
static constexpr qint64 kCopyFlashDurationMs = 300; static constexpr qint64 kCopyFlashDurationMs = 300;
bool m_demolishMode; bool m_demolishMode;
BuildingId m_demolishHoverBuildingId; std::optional<BuildingId> m_demolishHoverBuildingId;
bool m_debugDraw; bool m_debugDraw;
std::vector<BuildingId> m_selectedBuildingIds; std::vector<BuildingId> m_selectedBuildingIds;

View File

@@ -131,7 +131,6 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
: QWidget(parent) : QWidget(parent)
, m_sim(sim) , m_sim(sim)
, m_config(config) , m_config(config)
, m_singleBuildingId(kInvalidBuildingId)
, m_splitterTile(0, 0) , m_splitterTile(0, 0)
{ {
m_layout = new QVBoxLayout(this); m_layout = new QVBoxLayout(this);
@@ -170,10 +169,10 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
connect(m_clearBeltBtn, &QPushButton::clicked, connect(m_clearBeltBtn, &QPushButton::clicked,
this, &SelectedBuildingPanel::onClearBelt); this, &SelectedBuildingPanel::onClearBelt);
connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() { connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() {
if (m_singleBuildingId != kInvalidBuildingId) if (m_singleBuildingId.has_value())
{ {
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<LayoutDialogRequestedEvent>(m_singleBuildingId)); std::make_shared<LayoutDialogRequestedEvent>(*m_singleBuildingId));
} }
}); });
connect(m_filterAList, &QListWidget::itemChanged, connect(m_filterAList, &QListWidget::itemChanged,
@@ -257,7 +256,7 @@ void SelectedBuildingPanel::hideAllWidgets()
void SelectedBuildingPanel::clearContent() void SelectedBuildingPanel::clearContent()
{ {
m_singleBuildingId = kInvalidBuildingId; m_singleBuildingId = std::nullopt;
hideAllWidgets(); hideAllWidgets();
} }
@@ -672,8 +671,8 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
return; return;
} }
if (m_singleBuildingId == kInvalidBuildingId) { return; } if (!m_singleBuildingId.has_value()) { return; }
const Building* b = m_sim->getBuildings().findBuilding(m_singleBuildingId); const Building* b = m_sim->getBuildings().findBuilding(*m_singleBuildingId);
if (b) if (b)
{ {
if (m_titleLabel->text().startsWith(tr("(Building) "))) if (m_titleLabel->text().startsWith(tr("(Building) ")))
@@ -686,7 +685,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
} }
return; return;
} }
const ConstructionSite* s = m_sim->getBuildings().findSite(m_singleBuildingId); const ConstructionSite* s = m_sim->getBuildings().findSite(*m_singleBuildingId);
if (s) if (s)
{ {
// A periodic tick only advances construction progress, so update just the // 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<BuildingId>& ids) void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
{ {
m_singleBuildingId = kInvalidBuildingId; m_singleBuildingId = std::nullopt;
m_recipeSelectButton->hide(); m_recipeSelectButton->hide();
m_clearBeltBtn->hide(); m_clearBeltBtn->hide();
m_filterALabel->hide(); m_filterALabel->hide();
@@ -764,7 +763,7 @@ void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
void SelectedBuildingPanel::onSelectRecipeClicked() void SelectedBuildingPanel::onSelectRecipeClicked()
{ {
if (m_singleBuildingId == kInvalidBuildingId) if (!m_singleBuildingId.has_value())
{ {
return; return;
} }
@@ -775,7 +774,7 @@ void SelectedBuildingPanel::onSelectRecipeClicked()
// refreshBuffers() path picks up the new schematic (and shows the layout // refreshBuffers() path picks up the new schematic (and shows the layout
// preview + Configure Layout button) once the command has been applied. // preview + Configure Layout button) once the command has been applied.
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<RecipeSelectionRequestedEvent>(m_singleBuildingId)); std::make_shared<RecipeSelectionRequestedEvent>(*m_singleBuildingId));
rebuild(); rebuild();
} }
@@ -825,7 +824,7 @@ void SelectedBuildingPanel::buildSplitterFilters(
void SelectedBuildingPanel::onSplitterFilterChanged() void SelectedBuildingPanel::onSplitterFilterChanged()
{ {
if (m_singleBuildingId == kInvalidBuildingId) if (!m_singleBuildingId.has_value())
{ {
return; return;
} }
@@ -848,7 +847,7 @@ void SelectedBuildingPanel::onSplitterFilterChanged()
{ {
std::shared_ptr<SetSiteSplitterFiltersCommand> command = std::shared_ptr<SetSiteSplitterFiltersCommand> command =
std::make_shared<SetSiteSplitterFiltersCommand>(); std::make_shared<SetSiteSplitterFiltersCommand>();
command->id = m_singleBuildingId; command->id = *m_singleBuildingId;
command->filterA = collectFilter(m_filterAList); command->filterA = collectFilter(m_filterAList);
command->filterB = collectFilter(m_filterBList); command->filterB = collectFilter(m_filterBList);
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(

View File

@@ -109,7 +109,7 @@ private:
ShipLayoutPreview* m_layoutPreview; ShipLayoutPreview* m_layoutPreview;
QPushButton* m_configureLayoutBtn; QPushButton* m_configureLayoutBtn;
BuildingId m_singleBuildingId; std::optional<BuildingId> m_singleBuildingId;
bool m_singleIsSite = false; // selected single entity is a construction site bool m_singleIsSite = false; // selected single entity is a construction site
QPoint m_splitterTile; QPoint m_splitterTile;
std::string m_currentRecipeId; std::string m_currentRecipeId;

View File

@@ -83,7 +83,7 @@ public:
setFixedSize(cols * kCellSize + 1, rows * kCellSize + 1); setFixedSize(cols * kCellSize + 1, rows * kCellSize + 1);
} }
void setGhostData(int moduleIndex, Rotation rotation) void setGhostData(std::optional<int> moduleIndex, Rotation rotation)
{ {
m_ghostModuleIdx = moduleIndex; m_ghostModuleIdx = moduleIndex;
m_ghostRotation = rotation; m_ghostRotation = rotation;
@@ -111,9 +111,9 @@ protected:
{ {
painter.fillRect(cellRect, QColor(30, 30, 30)); 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); const ModuleDef* def = findModule(pm.moduleId);
QColor color(Qt::gray); QColor color(Qt::gray);
QString glyph; QString glyph;
@@ -140,9 +140,9 @@ protected:
} }
// Draw ghost // 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<std::string> mask = rotateMask(def.surfaceMask, m_ghostRotation); const std::vector<std::string> mask = rotateMask(def.surfaceMask, m_ghostRotation);
QColor ghostColor(QString::fromStdString(def.fillColor)); QColor ghostColor(QString::fromStdString(def.fillColor));
ghostColor.setAlpha(100); ghostColor.setAlpha(100);
@@ -238,7 +238,7 @@ private:
int m_cols = 0; int m_cols = 0;
const std::vector<PlacedModule>* m_placed = nullptr; const std::vector<PlacedModule>* m_placed = nullptr;
const GameConfig* m_config = nullptr; const GameConfig* m_config = nullptr;
int m_ghostModuleIdx = -2; std::optional<int> m_ghostModuleIdx;
Rotation m_ghostRotation = Rotation::East; Rotation m_ghostRotation = Rotation::East;
QPoint m_hoverCell = QPoint(-1, -1); QPoint m_hoverCell = QPoint(-1, -1);
}; };
@@ -374,7 +374,6 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
, m_rows(0) , m_rows(0)
, m_cols(0) , m_cols(0)
, m_placedModules(currentLayout.placedModules) , m_placedModules(currentLayout.placedModules)
, m_activeModuleIndex(-2)
, m_currentRotation(Rotation::East) , m_currentRotation(Rotation::East)
, m_removeButton(nullptr) , m_removeButton(nullptr)
, m_gridWidget(nullptr) , m_gridWidget(nullptr)
@@ -406,7 +405,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
} }
// Initialize grid. // Initialize grid.
m_grid.assign(m_rows, std::vector<CellInfo>(m_cols, {false, -1})); m_grid.assign(m_rows, std::vector<CellInfo>(m_cols, {false, std::nullopt}));
for (int r = 0; r < m_rows; ++r) for (int r = 0; r < m_rows; ++r)
{ {
for (int c = 0; c < static_cast<int>(m_shipLayout[r].size()); ++c) for (int c = 0; c < static_cast<int>(m_shipLayout[r].size()); ++c)
@@ -491,9 +490,9 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
} }
buttonGrid->addWidget(m_removeButton, row, 0, 1, kCols); buttonGrid->addWidget(m_removeButton, row, 0, 1, kCols);
connect(m_removeButton, &QPushButton::clicked, this, [this]() { connect(m_removeButton, &QPushButton::clicked, this, [this]() {
if (m_activeModuleIndex == -1) if (m_removeMode)
{ {
m_activeModuleIndex = -2; m_removeMode = false;
m_removeButton->setChecked(false); m_removeButton->setChecked(false);
} }
else else
@@ -502,7 +501,8 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
{ {
if (btn) { btn->setChecked(false); } if (btn) { btn->setChecked(false); }
} }
m_activeModuleIndex = -1; m_activeModuleIndex = std::nullopt;
m_removeMode = true;
m_removeButton->setChecked(true); m_removeButton->setChecked(true);
} }
updateGridWidget(); updateGridWidget();
@@ -540,20 +540,20 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
// Grid click handler. // Grid click handler.
connect(this, &ShipLayoutDialog::gridCellClicked, this, [this](QPoint cell) { connect(this, &ShipLayoutDialog::gridCellClicked, this, [this](QPoint cell) {
if (m_activeModuleIndex == -2) if (!m_removeMode && !m_activeModuleIndex.has_value())
{ {
return; return;
} }
if (m_activeModuleIndex == -1) if (m_removeMode)
{ {
// Remove mode: find and remove module at cell. // Remove mode: find and remove module at cell.
if (cell.y() >= 0 && cell.y() < m_rows && cell.x() >= 0 && cell.x() < m_cols) 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; const std::optional<int> idx = m_grid[cell.y()][cell.x()].moduleIndex;
if (idx >= 0) if (idx.has_value())
{ {
m_placedModules.erase(m_placedModules.begin() + idx); m_placedModules.erase(m_placedModules.begin() + *idx);
rebuildOccupancy(); rebuildOccupancy();
updateGridWidget(); updateGridWidget();
updateStats(); updateStats();
@@ -563,7 +563,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
} }
// Place module. // 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)) if (canPlaceModule(def, cell, m_currentRotation))
{ {
PlacedModule pm; PlacedModule pm;
@@ -622,7 +622,7 @@ void ShipLayoutDialog::onModuleButtonClicked(int index)
if (m_activeModuleIndex == index) if (m_activeModuleIndex == index)
{ {
if (m_moduleButtons[index]) { m_moduleButtons[index]->setChecked(false); } if (m_moduleButtons[index]) { m_moduleButtons[index]->setChecked(false); }
m_activeModuleIndex = -2; m_activeModuleIndex = std::nullopt;
} }
else else
{ {
@@ -631,6 +631,7 @@ void ShipLayoutDialog::onModuleButtonClicked(int index)
if (m_moduleButtons[i]) { m_moduleButtons[i]->setChecked(i == index); } if (m_moduleButtons[i]) { m_moduleButtons[i]->setChecked(i == index); }
} }
m_removeButton->setChecked(false); m_removeButton->setChecked(false);
m_removeMode = false;
m_activeModuleIndex = index; m_activeModuleIndex = index;
} }
updateGridWidget(); updateGridWidget();
@@ -656,7 +657,7 @@ void ShipLayoutDialog::rebuildOccupancy()
{ {
for (int c = 0; c < m_cols; ++c) 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; return false;
} }
if (m_grid[gr][gc].moduleIndex >= 0) if (m_grid[gr][gc].moduleIndex.has_value())
{ {
return false; return false;
} }

View File

@@ -47,7 +47,7 @@ public:
struct CellInfo struct CellInfo
{ {
bool buildable; bool buildable;
int moduleIndex; // -1 if empty std::optional<int> moduleIndex; // nullopt if empty
}; };
private: private:
@@ -69,7 +69,12 @@ private:
std::vector<PlacedModule> m_placedModules; std::vector<PlacedModule> m_placedModules;
std::vector<std::vector<CellInfo>> m_grid; std::vector<std::vector<CellInfo>> 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<int> m_activeModuleIndex;
bool m_removeMode = false;
Rotation m_currentRotation; Rotation m_currentRotation;
std::vector<QPushButton*> m_moduleButtons; std::vector<QPushButton*> m_moduleButtons;

View File

@@ -129,7 +129,7 @@ void ShipLayoutPreview::setShipAndLayout(const std::vector<std::string>& shipLay
} }
} }
m_grid.assign(m_rows, std::vector<CellInfo>(m_cols, {false, -1})); m_grid.assign(m_rows, std::vector<CellInfo>(m_cols, {false, std::nullopt}));
for (int r = 0; r < m_rows; ++r) for (int r = 0; r < m_rows; ++r)
{ {
for (int c = 0; c < static_cast<int>(shipLayout[r].size()); ++c) for (int c = 0; c < static_cast<int>(shipLayout[r].size()); ++c)
@@ -205,9 +205,9 @@ void ShipLayoutPreview::paintEvent(QPaintEvent* /*event*/)
{ {
painter.fillRect(cellRect, Qt::black); 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); const ModuleDef* def = findModuleDef(*m_modules, pm.moduleId);
QColor color(Qt::gray); QColor color(Qt::gray);
if (def) if (def)

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -31,7 +32,7 @@ private:
struct CellInfo struct CellInfo
{ {
bool buildable; bool buildable;
int moduleIndex; // -1 if empty std::optional<int> moduleIndex; // nullopt if empty
}; };
std::vector<std::vector<CellInfo>> m_grid; std::vector<std::vector<CellInfo>> m_grid;