move the placement rules and the config-dependent queries off BuildingSystem
This commit is contained in:
@@ -20,6 +20,7 @@ SET(HDRS
|
|||||||
SET(SRCS
|
SET(SRCS
|
||||||
${SRCS}
|
${SRCS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PortGeometry.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
||||||
|
|||||||
57
src/lib/core/PortGeometry.cpp
Normal file
57
src/lib/core/PortGeometry.cpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#include "PortGeometry.h"
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
std::vector<Port> computeInputPorts(
|
||||||
|
const std::vector<QPoint>& bodyCells,
|
||||||
|
const std::vector<Port>& outputPorts)
|
||||||
|
{
|
||||||
|
// Build lookup sets for quick membership checks.
|
||||||
|
std::set<std::pair<int, int>> bodySet;
|
||||||
|
for (const QPoint& cell : bodyCells)
|
||||||
|
{
|
||||||
|
bodySet.insert({cell.x(), cell.y()});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::pair<int, int>> outputPortTiles;
|
||||||
|
for (const Port& port : outputPorts)
|
||||||
|
{
|
||||||
|
outputPortTiles.insert({port.tile.x(), port.tile.y()});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neighbour deltas and the corresponding "inward" belt direction.
|
||||||
|
const int dx[4] = {-1, 1, 0, 0};
|
||||||
|
const int dy[4] = { 0, 0, -1, 1};
|
||||||
|
const Rotation inward[4] = {
|
||||||
|
Rotation::East, // neighbour is to the West; belt flows East toward building
|
||||||
|
Rotation::West, // neighbour is to the East; belt flows West toward building
|
||||||
|
Rotation::South, // neighbour is above (row-1); belt flows South toward building
|
||||||
|
Rotation::North // neighbour is below (row+1); belt flows North toward building
|
||||||
|
};
|
||||||
|
|
||||||
|
std::set<std::pair<int, int>> seen;
|
||||||
|
std::vector<Port> inputPorts;
|
||||||
|
|
||||||
|
for (const QPoint& cell : bodyCells)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; ++i)
|
||||||
|
{
|
||||||
|
const int nx = cell.x() + dx[i];
|
||||||
|
const int ny = cell.y() + dy[i];
|
||||||
|
const std::pair<int, int> neighbor = {nx, ny};
|
||||||
|
|
||||||
|
if (bodySet.count(neighbor)) { continue; }
|
||||||
|
if (outputPortTiles.count(neighbor)){ continue; }
|
||||||
|
if (seen.count(neighbor)) { continue; }
|
||||||
|
|
||||||
|
seen.insert(neighbor);
|
||||||
|
Port port;
|
||||||
|
port.tile = QPoint(nx, ny);
|
||||||
|
port.direction = inward[i];
|
||||||
|
inputPorts.push_back(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputPorts;
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "Port.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
|
||||||
// Geometry of a building's input/output ports. A Port names the tile *outside* the
|
// Geometry of a building's input/output ports. A Port names the tile *outside* the
|
||||||
@@ -43,3 +46,10 @@ inline QPoint inputBodyTile(QPoint portTile, Rotation inwardDirection)
|
|||||||
}
|
}
|
||||||
return portTile;
|
return portTile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every belt-facing edge of a footprint that is not already an output port — the
|
||||||
|
// tiles a belt can feed the building from, with the direction items must flow to
|
||||||
|
// enter (REQ-MAT-INPUT-PORTS, REQ-BLD-BELT-DRAG). bodyCells and outputPorts are
|
||||||
|
// in absolute tile coordinates, and so is the result.
|
||||||
|
std::vector<Port> computeInputPorts(const std::vector<QPoint>& bodyCells,
|
||||||
|
const std::vector<Port>& outputPorts);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
#include "FactoryQueries.h"
|
#include "FactoryQueries.h"
|
||||||
|
#include "PlacementRules.h"
|
||||||
#include "ProductionRules.h"
|
#include "ProductionRules.h"
|
||||||
#include "PortGeometry.h"
|
#include "PortGeometry.h"
|
||||||
#include "StateChecksum.h"
|
#include "StateChecksum.h"
|
||||||
@@ -182,87 +183,6 @@ void BuildingSystem::initSalvageBayBuffer(Building& b) const
|
|||||||
(def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
|
(def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
|
||||||
{
|
|
||||||
return computeInputPorts(b.bodyCells, b.outputPorts);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Port> BuildingSystem::computeInputPorts(
|
|
||||||
const std::vector<QPoint>& bodyCells,
|
|
||||||
const std::vector<Port>& outputPorts) const
|
|
||||||
{
|
|
||||||
// Build lookup sets for quick membership checks.
|
|
||||||
std::set<std::pair<int, int>> bodySet;
|
|
||||||
for (const QPoint& cell : bodyCells)
|
|
||||||
{
|
|
||||||
bodySet.insert({cell.x(), cell.y()});
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::pair<int, int>> outputPortTiles;
|
|
||||||
for (const Port& port : outputPorts)
|
|
||||||
{
|
|
||||||
outputPortTiles.insert({port.tile.x(), port.tile.y()});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neighbour deltas and the corresponding "inward" belt direction.
|
|
||||||
const int dx[4] = {-1, 1, 0, 0};
|
|
||||||
const int dy[4] = { 0, 0, -1, 1};
|
|
||||||
const Rotation inward[4] = {
|
|
||||||
Rotation::East, // neighbour is to the West; belt flows East toward building
|
|
||||||
Rotation::West, // neighbour is to the East; belt flows West toward building
|
|
||||||
Rotation::South, // neighbour is above (row-1); belt flows South toward building
|
|
||||||
Rotation::North // neighbour is below (row+1); belt flows North toward building
|
|
||||||
};
|
|
||||||
|
|
||||||
std::set<std::pair<int, int>> seen;
|
|
||||||
std::vector<Port> inputPorts;
|
|
||||||
|
|
||||||
for (const QPoint& cell : bodyCells)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 4; ++i)
|
|
||||||
{
|
|
||||||
const int nx = cell.x() + dx[i];
|
|
||||||
const int ny = cell.y() + dy[i];
|
|
||||||
const std::pair<int, int> neighbor = {nx, ny};
|
|
||||||
|
|
||||||
if (bodySet.count(neighbor)) { continue; }
|
|
||||||
if (outputPortTiles.count(neighbor)){ continue; }
|
|
||||||
if (seen.count(neighbor)) { continue; }
|
|
||||||
|
|
||||||
seen.insert(neighbor);
|
|
||||||
Port port;
|
|
||||||
port.tile = QPoint(nx, ny);
|
|
||||||
port.direction = inward[i];
|
|
||||||
inputPorts.push_back(port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputPorts;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Port> BuildingSystem::getInputPorts(BuildingId id) const
|
|
||||||
{
|
|
||||||
if (const Building* building = findBuilding(m_state, id))
|
|
||||||
{
|
|
||||||
return building->inputPorts;
|
|
||||||
}
|
|
||||||
if (const ConstructionSite* site = findSite(m_state, id))
|
|
||||||
{
|
|
||||||
// A site stores no ports; derive its output ports from the mask (absolute)
|
|
||||||
// and run the same input-edge scan (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS).
|
|
||||||
const BuildingDef* def = m_config.buildings.findBuildingDef(site->type);
|
|
||||||
if (def == nullptr) { return {}; }
|
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, site->rotation);
|
|
||||||
std::vector<Port> outputPortsAbsolute;
|
|
||||||
outputPortsAbsolute.reserve(mask.outputPorts.size());
|
|
||||||
for (const Port& port : mask.outputPorts)
|
|
||||||
{
|
|
||||||
outputPortsAbsolute.push_back(Port{ site->anchor + port.tile, port.direction });
|
|
||||||
}
|
|
||||||
return computeInputPorts(site->bodyCells, outputPortsAbsolute);
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
|
std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
|
||||||
{
|
{
|
||||||
@@ -301,7 +221,7 @@ std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor
|
|||||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
||||||
|
|
||||||
// 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(m_state, m_config, mask.bodyCells, anchor))
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
@@ -337,70 +257,6 @@ std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BuildingSystem::bodyCellsWithinWorldBounds(const std::vector<QPoint>& bodyCells,
|
|
||||||
QPoint anchor) const
|
|
||||||
{
|
|
||||||
const int heightTiles = m_config.world.heightTiles;
|
|
||||||
const int leftEdgeX = -m_state.asteroidWidth_tiles;
|
|
||||||
for (const QPoint& cell : bodyCells)
|
|
||||||
{
|
|
||||||
const QPoint worldCell = anchor + cell;
|
|
||||||
if (worldCell.y() < 0 || worldCell.y() >= heightTiles)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (worldCell.x() < leftEdgeX)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BuildingSystem::isPlacementValid(BuildingType type, QPoint anchor,
|
|
||||||
Rotation rotation) const
|
|
||||||
{
|
|
||||||
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
|
||||||
if (def == nullptr)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
|
||||||
|
|
||||||
if (!bodyCellsWithinWorldBounds(mask.bodyCells, anchor))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Terrain: ship-dock (S) cells must sit in space (x >= 0); all other body
|
|
||||||
// (A) cells must sit on the asteroid (x < 0). (REQ-BLD-PLACE-VALID)
|
|
||||||
for (const QPoint& cell : mask.bodyCells)
|
|
||||||
{
|
|
||||||
const QPoint worldCell = anchor + cell;
|
|
||||||
bool isShipDock = false;
|
|
||||||
for (const QPoint& dock : mask.shipDockCells)
|
|
||||||
{
|
|
||||||
if (dock == cell)
|
|
||||||
{
|
|
||||||
isShipDock = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isShipDock)
|
|
||||||
{
|
|
||||||
if (worldCell.x() < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (worldCell.x() >= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Deconstruct
|
// Deconstruct
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -595,29 +451,6 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<BeltSystem::SplitterInfo>
|
|
||||||
BuildingSystem::getSiteSplitterInfo(BuildingId id) const
|
|
||||||
{
|
|
||||||
for (const ConstructionSite& site : m_state.constructionQueue)
|
|
||||||
{
|
|
||||||
if (site.id != id) { continue; }
|
|
||||||
if (site.type != BuildingType::Splitter) { return std::nullopt; }
|
|
||||||
|
|
||||||
const BuildingDef* def = m_config.buildings.findBuildingDef(site.type);
|
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(
|
|
||||||
def ? def->surfaceMask : std::vector<std::string>{}, site.rotation);
|
|
||||||
if (mask.outputPorts.size() < 2) { return std::nullopt; }
|
|
||||||
|
|
||||||
BeltSystem::SplitterInfo info;
|
|
||||||
info.outputA = mask.outputPorts[0].direction;
|
|
||||||
info.outputB = mask.outputPorts[1].direction;
|
|
||||||
info.filterA = site.splitterFilterA;
|
|
||||||
info.filterB = site.splitterFilterB;
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildingSystem::setSiteSplitterFilters(BuildingId id,
|
void BuildingSystem::setSiteSplitterFilters(BuildingId id,
|
||||||
const std::vector<ItemType>& filterA,
|
const std::vector<ItemType>& filterA,
|
||||||
const std::vector<ItemType>& filterB)
|
const std::vector<ItemType>& filterB)
|
||||||
@@ -690,7 +523,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
building.outputPorts.push_back(absPort);
|
building.outputPorts.push_back(absPort);
|
||||||
}
|
}
|
||||||
building.emergingItems.resize(building.outputPorts.size());
|
building.emergingItems.resize(building.outputPorts.size());
|
||||||
building.inputPorts = computeInputPorts(building);
|
building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts);
|
||||||
building.incomingItems.assign(building.inputPorts.size(), {});
|
building.incomingItems.assign(building.inputPorts.size(), {});
|
||||||
|
|
||||||
if (building.type == BuildingType::SalvageBay)
|
if (building.type == BuildingType::SalvageBay)
|
||||||
@@ -1263,56 +1096,6 @@ void BuildingSystem::forEachIncomingItem(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
std::optional<BuildingId> BuildingSystem::findRotateInPlaceTarget(
|
|
||||||
BuildingType type, QPoint anchor, Rotation rot) const
|
|
||||||
{
|
|
||||||
// Tunnel Entries and Tunnel Exits cannot be rotated in place; re-orienting a
|
|
||||||
// tunnel requires deconstructing and re-placing it (REQ-BLD-ROTATE-IN-PLACE).
|
|
||||||
if (type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit)
|
|
||||||
{
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
|
||||||
if (!def) { return std::nullopt; }
|
|
||||||
|
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rot);
|
|
||||||
if (mask.bodyCells.empty()) { return std::nullopt; }
|
|
||||||
|
|
||||||
// All body cells must be occupied by the same entity.
|
|
||||||
const QPoint firstAbs = anchor + mask.bodyCells[0];
|
|
||||||
const std::optional<BuildingId> firstOwner = m_state.grid.findOwner(firstAbs);
|
|
||||||
if (!firstOwner.has_value()) { return std::nullopt; }
|
|
||||||
const BuildingId candidateId = *firstOwner;
|
|
||||||
|
|
||||||
for (const QPoint& rel : mask.bodyCells)
|
|
||||||
{
|
|
||||||
const std::optional<BuildingId> owner = m_state.grid.findOwner(anchor + rel);
|
|
||||||
if (!owner.has_value() || *owner != candidateId)
|
|
||||||
{
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the candidate is the same building type with the same cell count.
|
|
||||||
for (const ConstructionSite& site : m_state.constructionQueue)
|
|
||||||
{
|
|
||||||
if (site.id != candidateId) { continue; }
|
|
||||||
if (site.type != type) { return std::nullopt; }
|
|
||||||
if (site.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
|
|
||||||
return candidateId;
|
|
||||||
}
|
|
||||||
for (const Building& b : m_state.buildings)
|
|
||||||
{
|
|
||||||
if (b.id != candidateId) { continue; }
|
|
||||||
if (b.type != type) { return std::nullopt; }
|
|
||||||
if (b.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
|
|
||||||
return candidateId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
||||||
{
|
{
|
||||||
// Construction site path — just update rotation; no ports to recompute.
|
// Construction site path — just update rotation; no ports to recompute.
|
||||||
@@ -1348,7 +1131,7 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
|||||||
// the lanes to the new port set (REQ-MAT-OUTPUT-EMERGE).
|
// the lanes to the new port set (REQ-MAT-OUTPUT-EMERGE).
|
||||||
b.emergingItems.clear();
|
b.emergingItems.clear();
|
||||||
b.emergingItems.resize(b.outputPorts.size());
|
b.emergingItems.resize(b.outputPorts.size());
|
||||||
b.inputPorts = computeInputPorts(b);
|
b.inputPorts = computeInputPorts(b.bodyCells, b.outputPorts);
|
||||||
// Likewise discard in-transit input items and re-size the input belts to
|
// Likewise discard in-transit input items and re-size the input belts to
|
||||||
// the new port set (REQ-MAT-INPUT-INTAKE).
|
// the new port set (REQ-MAT-INPUT-INTAKE).
|
||||||
b.incomingItems.assign(b.inputPorts.size(), {});
|
b.incomingItems.assign(b.inputPorts.size(), {});
|
||||||
@@ -1406,7 +1189,7 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type,
|
|||||||
building.outputPorts.push_back(absPort);
|
building.outputPorts.push_back(absPort);
|
||||||
}
|
}
|
||||||
building.emergingItems.resize(building.outputPorts.size());
|
building.emergingItems.resize(building.outputPorts.size());
|
||||||
building.inputPorts = computeInputPorts(building);
|
building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts);
|
||||||
building.incomingItems.assign(building.inputPorts.size(), {});
|
building.incomingItems.assign(building.inputPorts.size(), {});
|
||||||
|
|
||||||
if (type == BuildingType::SalvageBay)
|
if (type == BuildingType::SalvageBay)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "FactoryState.h"
|
#include "FactoryState.h"
|
||||||
|
#include "PlacementRules.h"
|
||||||
#include "ProductionRules.h"
|
#include "ProductionRules.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
@@ -60,8 +61,6 @@ public:
|
|||||||
// other body (A) cell sits on the asteroid (x < 0 and x >= the left edge),
|
// other body (A) cell sits on the asteroid (x < 0 and x >= the left edge),
|
||||||
// and every cell has 0 <= y < world.height_tiles. There is no right-side
|
// and every cell has 0 <= y < world.height_tiles. There is no right-side
|
||||||
// bound — space extends rightward. Tile occupancy is NOT checked here.
|
// bound — space extends rightward. Tile occupancy is NOT checked here.
|
||||||
bool isPlacementValid(BuildingType type, QPoint anchor,
|
|
||||||
Rotation rotation) const;
|
|
||||||
|
|
||||||
// Sets the current buildable asteroid width in tiles. Grows the left
|
// Sets the current buildable asteroid width in tiles. Grows the left
|
||||||
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
|
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
|
||||||
@@ -99,7 +98,6 @@ public:
|
|||||||
// output directions (derived from its surface mask) and stored filters, or
|
// output directions (derived from its surface mask) and stored filters, or
|
||||||
// nullopt if the id is not a Splitter site. The stored filters are applied
|
// nullopt if the id is not a Splitter site. The stored filters are applied
|
||||||
// to BeltSystem when the splitter finishes building (tickConstruction).
|
// to BeltSystem when the splitter finishes building (tickConstruction).
|
||||||
std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(BuildingId id) const;
|
|
||||||
void setSiteSplitterFilters(BuildingId id,
|
void setSiteSplitterFilters(BuildingId id,
|
||||||
const std::vector<ItemType>& filterA,
|
const std::vector<ItemType>& filterA,
|
||||||
const std::vector<ItemType>& filterB);
|
const std::vector<ItemType>& filterB);
|
||||||
@@ -145,13 +143,6 @@ public:
|
|||||||
void forEachIncomingItem(
|
void forEachIncomingItem(
|
||||||
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
||||||
|
|
||||||
// Returns the entity id of the building or construction site whose footprint
|
|
||||||
// exactly coincides with the ghost (type, anchor, rot) and is of the same
|
|
||||||
// building type. Returns nullopt otherwise.
|
|
||||||
std::optional<BuildingId> findRotateInPlaceTarget(BuildingType type,
|
|
||||||
QPoint anchor,
|
|
||||||
Rotation rot) const;
|
|
||||||
|
|
||||||
// Rotate an existing building or construction site to newRotation in place.
|
// Rotate an existing building or construction site to newRotation in place.
|
||||||
// For belt-type operational buildings, re-registers with BeltSystem (items
|
// For belt-type operational buildings, re-registers with BeltSystem (items
|
||||||
// currently on the tile are discarded by BeltSystem::removeTile).
|
// currently on the tile are discarded by BeltSystem::removeTile).
|
||||||
@@ -162,7 +153,6 @@ public:
|
|||||||
// (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS): each returned Port.tile is the
|
// (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS): each returned Port.tile is the
|
||||||
// outside adjacent tile and Port.direction is the belt facing that points into
|
// outside adjacent tile and Port.direction is the belt facing that points into
|
||||||
// the target. Output-port edges are excluded. Empty for an unknown id.
|
// the target. Output-port edges are excluded. Empty for an unknown id.
|
||||||
std::vector<Port> getInputPorts(BuildingId id) const;
|
|
||||||
|
|
||||||
// Register / unregister tile occupancy for ECS station entities.
|
// Register / unregister tile occupancy for ECS station entities.
|
||||||
void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
|
void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
|
||||||
@@ -238,14 +228,8 @@ private:
|
|||||||
void initAutoBuffers(Building& b) const;
|
void initAutoBuffers(Building& b) const;
|
||||||
void initShipyardBuffers(Building& b) const;
|
void initShipyardBuffers(Building& b) const;
|
||||||
void initSalvageBayBuffer(Building& b) const;
|
void initSalvageBayBuffer(Building& b) const;
|
||||||
std::vector<Port> computeInputPorts(const Building& b) const;
|
|
||||||
// Core input-edge scan shared by operational buildings and construction sites.
|
// Core input-edge scan shared by operational buildings and construction sites.
|
||||||
std::vector<Port> computeInputPorts(const std::vector<QPoint>& bodyCells,
|
|
||||||
const std::vector<Port>& outputPorts) const;
|
|
||||||
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
|
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
|
||||||
bool bodyCellsWithinWorldBounds(
|
|
||||||
const std::vector<QPoint>& bodyCells,
|
|
||||||
QPoint anchor) const;
|
|
||||||
|
|
||||||
const GameConfig& m_config;
|
const GameConfig& m_config;
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
||||||
@@ -43,6 +44,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
|
#include "PortGeometry.h"
|
||||||
|
#include "SurfaceMask.h"
|
||||||
|
|
||||||
#include "Item.h"
|
#include "Item.h"
|
||||||
#include "ItemType.h"
|
#include "ItemType.h"
|
||||||
|
|
||||||
@@ -126,3 +129,53 @@ bool deliverScrapToSalvageBay(FactoryState& state, BuildingId bayId)
|
|||||||
bay->outputBuffer.items.push_back(Item{ItemType{"scrap"}});
|
bay->outputBuffer.items.push_back(Item{ItemType{"scrap"}});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<Port> getInputPorts(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingId id)
|
||||||
|
{
|
||||||
|
if (const Building* building = findBuilding(state, id))
|
||||||
|
{
|
||||||
|
return building->inputPorts;
|
||||||
|
}
|
||||||
|
if (const ConstructionSite* site = findSite(state, id))
|
||||||
|
{
|
||||||
|
// A site stores no ports; derive its output ports from the mask (absolute)
|
||||||
|
// and run the same input-edge scan (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS).
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(site->type);
|
||||||
|
if (def == nullptr) { return {}; }
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, site->rotation);
|
||||||
|
std::vector<Port> outputPortsAbsolute;
|
||||||
|
outputPortsAbsolute.reserve(mask.outputPorts.size());
|
||||||
|
for (const Port& port : mask.outputPorts)
|
||||||
|
{
|
||||||
|
outputPortsAbsolute.push_back(Port{ site->anchor + port.tile, port.direction });
|
||||||
|
}
|
||||||
|
return computeInputPorts(site->bodyCells, outputPortsAbsolute);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<BeltSystem::SplitterInfo>
|
||||||
|
getSiteSplitterInfo(const FactoryState& state, const GameConfig& config, BuildingId id)
|
||||||
|
{
|
||||||
|
for (const ConstructionSite& site : state.constructionQueue)
|
||||||
|
{
|
||||||
|
if (site.id != id) { continue; }
|
||||||
|
if (site.type != BuildingType::Splitter) { return std::nullopt; }
|
||||||
|
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(site.type);
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||||
|
def ? def->surfaceMask : std::vector<std::string>{}, site.rotation);
|
||||||
|
if (mask.outputPorts.size() < 2) { return std::nullopt; }
|
||||||
|
|
||||||
|
BeltSystem::SplitterInfo info;
|
||||||
|
info.outputA = mask.outputPorts[0].direction;
|
||||||
|
info.outputB = mask.outputPorts[1].direction;
|
||||||
|
info.filterA = site.splitterFilterA;
|
||||||
|
info.filterB = site.splitterFilterB;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,19 @@
|
|||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
|
#include "BeltSystem.h"
|
||||||
#include "FactoryState.h"
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Port.h"
|
||||||
|
|
||||||
// Queries and operations over the factory's world data that need nothing but that
|
// Queries and operations over the factory's world data that need nothing but that
|
||||||
// data — no config, no belts, no RNG. Free functions rather than BuildingSystem
|
// data — no config, no belts, no RNG. Free functions rather than BuildingSystem
|
||||||
// methods so that callers depend on the data they read instead of on the system
|
// methods so that callers depend on the data they read instead of on the system
|
||||||
// that happens to tick it (see FactoryState.h).
|
// that happens to tick it (see FactoryState.h).
|
||||||
//
|
//
|
||||||
// A helper belongs here only if it is a pure function of FactoryState. Anything
|
// Most need nothing but the state. The two at the bottom also take the config,
|
||||||
// needing GameConfig or the asteroid bound stays on BuildingSystem for now.
|
// because answering them means reading a building definition — but still no belts,
|
||||||
|
// no RNG and no system.
|
||||||
|
|
||||||
// The building with the given id, or nullptr when no building has it. Construction
|
// The building with the given id, or nullptr when no building has it. Construction
|
||||||
// sites are not buildings yet — use findSite for those.
|
// sites are not buildings yet — use findSite for those.
|
||||||
@@ -52,3 +56,16 @@ const Building* findNearestBuilding(const FactoryState& state, QVector2D worldPo
|
|||||||
// (REQ-BLD-DECON-QUEUE), or its holding capacity is already taken — emerging
|
// (REQ-BLD-DECON-QUEUE), or its holding capacity is already taken — emerging
|
||||||
// scrap counts against that capacity (REQ-MAT-OUTPUT-EMERGE).
|
// scrap counts against that capacity (REQ-MAT-OUTPUT-EMERGE).
|
||||||
bool deliverScrapToSalvageBay(FactoryState& state, BuildingId bayId);
|
bool deliverScrapToSalvageBay(FactoryState& state, BuildingId bayId);
|
||||||
|
|
||||||
|
// Every belt-facing edge of the building or site with this id (REQ-MAT-INPUT-PORTS,
|
||||||
|
// REQ-BLD-BELT-DRAG). Empty when the id is unknown. A site has no stored ports, so
|
||||||
|
// they are derived from its surface mask.
|
||||||
|
std::vector<Port> getInputPorts(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingId id);
|
||||||
|
|
||||||
|
// The two output directions and stored filters of a queued Splitter site
|
||||||
|
// (REQ-BLD-SITE-CONFIG), or nullopt if the id is not one. Operational splitters are
|
||||||
|
// configured through BeltSystem by tile instead.
|
||||||
|
std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(const FactoryState& state,
|
||||||
|
const GameConfig& config,
|
||||||
|
BuildingId id);
|
||||||
|
|||||||
121
src/lib/sim/PlacementRules.cpp
Normal file
121
src/lib/sim/PlacementRules.cpp
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#include "PlacementRules.h"
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "SurfaceMask.h"
|
||||||
|
|
||||||
|
bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,const std::vector<QPoint>& bodyCells,
|
||||||
|
QPoint anchor)
|
||||||
|
{
|
||||||
|
const int heightTiles = config.world.heightTiles;
|
||||||
|
const int leftEdgeX = -state.asteroidWidth_tiles;
|
||||||
|
for (const QPoint& cell : bodyCells)
|
||||||
|
{
|
||||||
|
const QPoint worldCell = anchor + cell;
|
||||||
|
if (worldCell.y() < 0 || worldCell.y() >= heightTiles)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (worldCell.x() < leftEdgeX)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPlacementValid(const FactoryState& state, const GameConfig& config,BuildingType type, QPoint anchor,
|
||||||
|
Rotation rotation)
|
||||||
|
{
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(type);
|
||||||
|
if (def == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
||||||
|
|
||||||
|
if (!bodyCellsWithinWorldBounds(state, config, mask.bodyCells, anchor))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terrain: ship-dock (S) cells must sit in space (x >= 0); all other body
|
||||||
|
// (A) cells must sit on the asteroid (x < 0). (REQ-BLD-PLACE-VALID)
|
||||||
|
for (const QPoint& cell : mask.bodyCells)
|
||||||
|
{
|
||||||
|
const QPoint worldCell = anchor + cell;
|
||||||
|
bool isShipDock = false;
|
||||||
|
for (const QPoint& dock : mask.shipDockCells)
|
||||||
|
{
|
||||||
|
if (dock == cell)
|
||||||
|
{
|
||||||
|
isShipDock = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isShipDock)
|
||||||
|
{
|
||||||
|
if (worldCell.x() < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (worldCell.x() >= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingType type, QPoint anchor, Rotation rot)
|
||||||
|
{
|
||||||
|
// Tunnel Entries and Tunnel Exits cannot be rotated in place; re-orienting a
|
||||||
|
// tunnel requires deconstructing and re-placing it (REQ-BLD-ROTATE-IN-PLACE).
|
||||||
|
if (type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(type);
|
||||||
|
if (!def) { return std::nullopt; }
|
||||||
|
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rot);
|
||||||
|
if (mask.bodyCells.empty()) { return std::nullopt; }
|
||||||
|
|
||||||
|
// All body cells must be occupied by the same entity.
|
||||||
|
const QPoint firstAbs = anchor + mask.bodyCells[0];
|
||||||
|
const std::optional<BuildingId> firstOwner = state.grid.findOwner(firstAbs);
|
||||||
|
if (!firstOwner.has_value()) { return std::nullopt; }
|
||||||
|
const BuildingId candidateId = *firstOwner;
|
||||||
|
|
||||||
|
for (const QPoint& rel : mask.bodyCells)
|
||||||
|
{
|
||||||
|
const std::optional<BuildingId> owner = state.grid.findOwner(anchor + rel);
|
||||||
|
if (!owner.has_value() || *owner != candidateId)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the candidate is the same building type with the same cell count.
|
||||||
|
for (const ConstructionSite& site : state.constructionQueue)
|
||||||
|
{
|
||||||
|
if (site.id != candidateId) { continue; }
|
||||||
|
if (site.type != type) { return std::nullopt; }
|
||||||
|
if (site.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
|
||||||
|
return candidateId;
|
||||||
|
}
|
||||||
|
for (const Building& b : state.buildings)
|
||||||
|
{
|
||||||
|
if (b.id != candidateId) { continue; }
|
||||||
|
if (b.type != type) { return std::nullopt; }
|
||||||
|
if (b.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
|
||||||
|
return candidateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
38
src/lib/sim/PlacementRules.h
Normal file
38
src/lib/sim/PlacementRules.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
// Where a building may be placed, and what is already sitting on those tiles.
|
||||||
|
// Free functions over the factory state and the config — they read no other
|
||||||
|
// system state, so they do not belong to BuildingSystem.
|
||||||
|
|
||||||
|
// True if every body cell lies inside the world: 0 <= y < world.height_tiles and
|
||||||
|
// x >= the current asteroid left edge (REQ-BLD-PLACE-VALID). Terrain type is not
|
||||||
|
// checked — see isPlacementValid for the full rule.
|
||||||
|
bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,
|
||||||
|
const std::vector<QPoint>& bodyCells, QPoint anchor);
|
||||||
|
|
||||||
|
// True if the placement satisfies REQ-BLD-PLACE-VALID terrain and world-bounds
|
||||||
|
// rules: every ship-dock (S) cell sits in space (x >= 0), every other body (A)
|
||||||
|
// cell sits on the asteroid (x < 0 and x >= the left edge), and every cell has
|
||||||
|
// 0 <= y < world.height_tiles. There is no right-side bound — space extends
|
||||||
|
// rightward. Tile occupancy is NOT checked here.
|
||||||
|
bool isPlacementValid(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingType type, QPoint anchor, Rotation rotation);
|
||||||
|
|
||||||
|
// The building or site that a ghost of the given type/anchor/rotation would
|
||||||
|
// rotate in place rather than replace: same type, same body cells, one owner
|
||||||
|
// (REQ-BLD-ROTATE-IN-PLACE). Tunnels never qualify.
|
||||||
|
std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state,
|
||||||
|
const GameConfig& config,
|
||||||
|
BuildingType type, QPoint anchor,
|
||||||
|
Rotation rot);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
#include "FactoryQueries.h"
|
#include "FactoryQueries.h"
|
||||||
|
#include "PlacementRules.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
@@ -860,7 +861,7 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_buildingSystem->isPlacementValid(type, anchor, rotation))
|
if (!isPlacementValid(m_factoryState, m_config, type, anchor, rotation))
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
#include "PlacementRules.h"
|
||||||
#include "FactoryQueries.h"
|
#include "FactoryQueries.h"
|
||||||
#include "ProductionRules.h"
|
#include "ProductionRules.h"
|
||||||
|
|
||||||
@@ -201,19 +202,18 @@ TEST_CASE("BuildingSystem: isPlacementValid enforces terrain and world bounds",
|
|||||||
const int leftEdgeX = -f.cfg.world.regions.asteroidWidth_tiles;
|
const int leftEdgeX = -f.cfg.world.regions.asteroidWidth_tiles;
|
||||||
|
|
||||||
// Miner is all-asteroid (A): valid only fully on the asteroid (x < 0).
|
// Miner is all-asteroid (A): valid only fully on the asteroid (x < 0).
|
||||||
REQUIRE(f.bs.isPlacementValid(BuildingType::Miner, QPoint(-3, 0), Rotation::East));
|
REQUIRE(isPlacementValid(f.state, f.cfg, BuildingType::Miner, QPoint(-3, 0), Rotation::East));
|
||||||
REQUIRE_FALSE(f.bs.isPlacementValid(BuildingType::Miner, QPoint(0, 0), Rotation::East)); // A cells in space
|
REQUIRE_FALSE(isPlacementValid(f.state, f.cfg, BuildingType::Miner, QPoint(0, 0), Rotation::East)); // A cells in space
|
||||||
REQUIRE_FALSE(f.bs.isPlacementValid(BuildingType::Miner, QPoint(0, -1), Rotation::East)); // above world
|
REQUIRE_FALSE(isPlacementValid(f.state, f.cfg, BuildingType::Miner, QPoint(0, -1), Rotation::East)); // above world
|
||||||
REQUIRE(f.bs.isPlacementValid(BuildingType::Miner, QPoint(leftEdgeX, 0), Rotation::East));
|
REQUIRE(isPlacementValid(f.state, f.cfg, BuildingType::Miner, QPoint(leftEdgeX, 0), Rotation::East));
|
||||||
REQUIRE_FALSE(f.bs.isPlacementValid(BuildingType::Miner,
|
REQUIRE_FALSE(isPlacementValid(f.state, f.cfg, BuildingType::Miner, QPoint(leftEdgeX - 1, 0), Rotation::East)); // past left edge
|
||||||
QPoint(leftEdgeX - 1, 0), Rotation::East)); // past left edge
|
|
||||||
|
|
||||||
// Shipyard mask ["AAAS>","AAAS "] straddles the boundary: A cells on the
|
// Shipyard mask ["AAAS>","AAAS "] straddles the boundary: A cells on the
|
||||||
// asteroid, the S (dock) cell in space. At anchor (-3,0) the A cells land at
|
// asteroid, the S (dock) cell in space. At anchor (-3,0) the A cells land at
|
||||||
// x=-3..-1 and the dock at x=0.
|
// x=-3..-1 and the dock at x=0.
|
||||||
REQUIRE(f.bs.isPlacementValid(BuildingType::Shipyard, QPoint(-3, 0), Rotation::East));
|
REQUIRE(isPlacementValid(f.state, f.cfg, BuildingType::Shipyard, QPoint(-3, 0), Rotation::East));
|
||||||
REQUIRE_FALSE(f.bs.isPlacementValid(BuildingType::Shipyard, QPoint(0, 0), Rotation::East)); // A cells in space
|
REQUIRE_FALSE(isPlacementValid(f.state, f.cfg, BuildingType::Shipyard, QPoint(0, 0), Rotation::East)); // A cells in space
|
||||||
REQUIRE_FALSE(f.bs.isPlacementValid(BuildingType::Shipyard, QPoint(-4, 0), Rotation::East)); // dock on asteroid
|
REQUIRE_FALSE(isPlacementValid(f.state, f.cfg, BuildingType::Shipyard, QPoint(-4, 0), Rotation::East)); // dock on asteroid
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after construction",
|
TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after construction",
|
||||||
@@ -1192,7 +1192,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is
|
|||||||
rng);
|
rng);
|
||||||
|
|
||||||
REQUIRE_FALSE(
|
REQUIRE_FALSE(
|
||||||
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::East).has_value());
|
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Belt, QPoint(0, 0), Rotation::East).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a queued belt (same type, different rotation)",
|
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a queued belt (same type, different rotation)",
|
||||||
@@ -1214,7 +1214,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que
|
|||||||
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
|
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);
|
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Belt, QPoint(0, 0), Rotation::North);
|
||||||
REQUIRE(result.has_value());
|
REQUIRE(result.has_value());
|
||||||
REQUIRE(*result == id);
|
REQUIRE(*result == id);
|
||||||
}
|
}
|
||||||
@@ -1242,7 +1242,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
|
|||||||
REQUIRE(getAllSites(state_bs).empty());
|
REQUIRE(getAllSites(state_bs).empty());
|
||||||
|
|
||||||
const std::optional<BuildingId> result =
|
const std::optional<BuildingId> result =
|
||||||
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::South);
|
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Belt, QPoint(0, 0), Rotation::South);
|
||||||
REQUIRE(result.has_value());
|
REQUIRE(result.has_value());
|
||||||
REQUIRE(*result == id);
|
REQUIRE(*result == id);
|
||||||
}
|
}
|
||||||
@@ -1267,7 +1267,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building
|
|||||||
|
|
||||||
// Querying with Splitter at the same tile — type mismatch → nullopt.
|
// Querying with Splitter at the same tile — type mismatch → nullopt.
|
||||||
REQUIRE_FALSE(
|
REQUIRE_FALSE(
|
||||||
bs.findRotateInPlaceTarget(BuildingType::Splitter, QPoint(0, 0), Rotation::East).has_value());
|
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Splitter, QPoint(0, 0), Rotation::East).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in place",
|
TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in place",
|
||||||
@@ -1292,9 +1292,9 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in pla
|
|||||||
bs.place(BuildingType::TunnelExit, QPoint(-2, 0), Rotation::East, 0);
|
bs.place(BuildingType::TunnelExit, QPoint(-2, 0), Rotation::East, 0);
|
||||||
|
|
||||||
REQUIRE_FALSE(
|
REQUIRE_FALSE(
|
||||||
bs.findRotateInPlaceTarget(BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::North).has_value());
|
findRotateInPlaceTarget(state_bs, cfg, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::North).has_value());
|
||||||
REQUIRE_FALSE(
|
REQUIRE_FALSE(
|
||||||
bs.findRotateInPlaceTarget(BuildingType::TunnelExit, QPoint(-2, 0), Rotation::North).has_value());
|
findRotateInPlaceTarget(state_bs, cfg, BuildingType::TunnelExit, QPoint(-2, 0), Rotation::North).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprints only partially overlap",
|
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprints only partially overlap",
|
||||||
@@ -1319,7 +1319,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprin
|
|||||||
// Ghost anchored at (1,0) would cover (1,0),(2,0),(1,1),(2,1):
|
// Ghost anchored at (1,0) would cover (1,0),(2,0),(1,1),(2,1):
|
||||||
// only (1,0) and (1,1) are occupied — not a full coincidence.
|
// only (1,0) and (1,1) are occupied — not a full coincidence.
|
||||||
REQUIRE_FALSE(
|
REQUIRE_FALSE(
|
||||||
bs.findRotateInPlaceTarget(BuildingType::Smelter, QPoint(1, 0), Rotation::East).has_value());
|
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Smelter, QPoint(1, 0), Rotation::East).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-tile building with rotated ghost",
|
TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-tile building with rotated ghost",
|
||||||
@@ -1343,7 +1343,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
|
|||||||
const BuildingId id = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
|
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);
|
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Smelter, QPoint(0, 0), Rotation::North);
|
||||||
REQUIRE(result.has_value());
|
REQUIRE(result.has_value());
|
||||||
REQUIRE(*result == id);
|
REQUIRE(*result == id);
|
||||||
}
|
}
|
||||||
@@ -1507,7 +1507,7 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
|
|||||||
|
|
||||||
// The site reports its two output directions and the stored filters before
|
// The site reports its two output directions and the stored filters before
|
||||||
// it is built; it is not yet registered with BeltSystem.
|
// it is built; it is not yet registered with BeltSystem.
|
||||||
const std::optional<BeltSystem::SplitterInfo> siteInfo = f.bs.getSiteSplitterInfo(id);
|
const std::optional<BeltSystem::SplitterInfo> siteInfo = getSiteSplitterInfo(f.state, f.cfg, id);
|
||||||
REQUIRE(siteInfo.has_value());
|
REQUIRE(siteInfo.has_value());
|
||||||
REQUIRE(siteInfo->filterA == filterA);
|
REQUIRE(siteInfo->filterA == filterA);
|
||||||
REQUIRE(siteInfo->filterB.empty());
|
REQUIRE(siteInfo->filterB.empty());
|
||||||
@@ -1687,7 +1687,7 @@ TEST_CASE("BuildingSystem: getInputPorts on a miner site lists every input edge"
|
|||||||
// Miner mask ["AA","A>"] East → body (0,0),(1,0),(0,1); output tile (1,1) East.
|
// Miner mask ["AA","A>"] East → body (0,0),(1,0),(0,1); output tile (1,1) East.
|
||||||
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
const std::vector<Port> ports = f.bs.getInputPorts(id);
|
const std::vector<Port> ports = getInputPorts(f.state, f.cfg, id);
|
||||||
|
|
||||||
// Every perimeter edge except the output-port edge at (1,1), each pointing in.
|
// Every perimeter edge except the output-port edge at (1,1), each pointing in.
|
||||||
REQUIRE(ports.size() == 6);
|
REQUIRE(ports.size() == 6);
|
||||||
@@ -1710,10 +1710,10 @@ TEST_CASE("BuildingSystem: getInputPorts matches between a site and the built bu
|
|||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
const std::vector<Port> sitePorts = f.bs.getInputPorts(id);
|
const std::vector<Port> sitePorts = getInputPorts(f.state, f.cfg, id);
|
||||||
buildToCompletion(f.bs, f.state, f.belts, id, tick);
|
buildToCompletion(f.bs, f.state, f.belts, id, tick);
|
||||||
REQUIRE(findBuilding(f.state, id) != nullptr);
|
REQUIRE(findBuilding(f.state, id) != nullptr);
|
||||||
const std::vector<Port> builtPorts = f.bs.getInputPorts(id);
|
const std::vector<Port> builtPorts = getInputPorts(f.state, f.cfg, id);
|
||||||
|
|
||||||
// The operational path (stored inputPorts) agrees with the site path (mask-derived).
|
// The operational path (stored inputPorts) agrees with the site path (mask-derived).
|
||||||
REQUIRE(builtPorts.size() == sitePorts.size());
|
REQUIRE(builtPorts.size() == sitePorts.size());
|
||||||
@@ -1733,7 +1733,7 @@ TEST_CASE("BuildingSystem: getInputPorts invariants hold for a rotated site", "[
|
|||||||
std::set<std::pair<int, int>> bodySet;
|
std::set<std::pair<int, int>> bodySet;
|
||||||
for (const QPoint& cell : site->bodyCells) { bodySet.insert({cell.x(), cell.y()}); }
|
for (const QPoint& cell : site->bodyCells) { bodySet.insert({cell.x(), cell.y()}); }
|
||||||
|
|
||||||
const std::vector<Port> ports = f.bs.getInputPorts(id);
|
const std::vector<Port> ports = getInputPorts(f.state, f.cfg, id);
|
||||||
REQUIRE_FALSE(ports.empty());
|
REQUIRE_FALSE(ports.empty());
|
||||||
for (const Port& port : ports)
|
for (const Port& port : ports)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "GameWorldView.h"
|
#include "GameWorldView.h"
|
||||||
|
#include "PlacementRules.h"
|
||||||
#include "FactoryQueries.h"
|
#include "FactoryQueries.h"
|
||||||
#include "ProductionRules.h"
|
#include "ProductionRules.h"
|
||||||
|
|
||||||
@@ -662,7 +663,7 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
|
|||||||
// Terrain and world-bounds validity are owned by the simulation
|
// Terrain and world-bounds validity are owned by the simulation
|
||||||
// (REQ-BLD-PLACE-VALID); the presentation layer only adds the occupancy /
|
// (REQ-BLD-PLACE-VALID); the presentation layer only adds the occupancy /
|
||||||
// rotate-in-place check.
|
// rotate-in-place check.
|
||||||
if (!m_sim->getBuildings().isPlacementValid(type, anchor, rot))
|
if (!isPlacementValid(m_sim->getFactoryState(), m_sim->getConfig(), type, anchor, rot))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -683,7 +684,7 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
|
|||||||
|
|
||||||
if (anyOccupied)
|
if (anyOccupied)
|
||||||
{
|
{
|
||||||
return m_sim->getBuildings().findRotateInPlaceTarget(type, anchor, rot).has_value();
|
return findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, anchor, rot).has_value();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -865,8 +866,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
|||||||
for (const BlueprintBuilding& bb : bp.buildings)
|
for (const BlueprintBuilding& bb : bp.buildings)
|
||||||
{
|
{
|
||||||
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
||||||
if (m_sim->getBuildings().findRotateInPlaceTarget(
|
if (findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), bb.type, center + bb.offset, bb.rotation).has_value())
|
||||||
bb.type, center + bb.offset, bb.rotation).has_value())
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -880,7 +880,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
|||||||
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
||||||
const QPoint anchor = center + bb.offset;
|
const QPoint anchor = center + bb.offset;
|
||||||
const std::optional<BuildingId> rotateTarget =
|
const std::optional<BuildingId> rotateTarget =
|
||||||
m_sim->getBuildings().findRotateInPlaceTarget(bb.type, anchor, bb.rotation);
|
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), bb.type, anchor, bb.rotation);
|
||||||
if (rotateTarget.has_value())
|
if (rotateTarget.has_value())
|
||||||
{
|
{
|
||||||
std::shared_ptr<RotateInPlaceCommand> rotateCommand =
|
std::shared_ptr<RotateInPlaceCommand> rotateCommand =
|
||||||
@@ -1016,7 +1016,7 @@ void GameWorldView::placeAtTile(QPoint tile)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::optional<BuildingId> rotateTarget =
|
const std::optional<BuildingId> rotateTarget =
|
||||||
m_sim->getBuildings().findRotateInPlaceTarget(type, tile, m_ghostRotation);
|
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, tile, m_ghostRotation);
|
||||||
if (rotateTarget.has_value())
|
if (rotateTarget.has_value())
|
||||||
{
|
{
|
||||||
std::shared_ptr<RotateInPlaceCommand> command =
|
std::shared_ptr<RotateInPlaceCommand> command =
|
||||||
@@ -1080,7 +1080,7 @@ void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
|
|||||||
if (targetId.has_value() && targetType.has_value()
|
if (targetId.has_value() && targetType.has_value()
|
||||||
&& *targetType != BuildingType::Belt)
|
&& *targetType != BuildingType::Belt)
|
||||||
{
|
{
|
||||||
const std::vector<Port> inputPorts = m_sim->getBuildings().getInputPorts(*targetId);
|
const std::vector<Port> inputPorts = getInputPorts(m_sim->getFactoryState(), m_sim->getConfig(), *targetId);
|
||||||
std::optional<Port> best;
|
std::optional<Port> best;
|
||||||
float bestDistanceSq = 0.0f;
|
float bestDistanceSq = 0.0f;
|
||||||
for (const Port& port : inputPorts)
|
for (const Port& port : inputPorts)
|
||||||
@@ -1124,8 +1124,7 @@ std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath(
|
|||||||
{
|
{
|
||||||
BeltDragResolved item;
|
BeltDragResolved item;
|
||||||
const std::optional<BuildingId> rotateTarget =
|
const std::optional<BuildingId> rotateTarget =
|
||||||
m_sim->getBuildings().findRotateInPlaceTarget(
|
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), BuildingType::Belt, entry.tile, entry.rotation);
|
||||||
BuildingType::Belt, entry.tile, entry.rotation);
|
|
||||||
if (rotateTarget.has_value())
|
if (rotateTarget.has_value())
|
||||||
{
|
{
|
||||||
// A tile holding only a belt (or belt site) is re-oriented, no cost.
|
// A tile holding only a belt (or belt site) is re-oriented, no cost.
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
|
|||||||
std::optional<BeltSystem::SplitterInfo> info;
|
std::optional<BeltSystem::SplitterInfo> info;
|
||||||
if (m_singleIsSite)
|
if (m_singleIsSite)
|
||||||
{
|
{
|
||||||
info = m_sim->getBuildings().getSiteSplitterInfo(id);
|
info = getSiteSplitterInfo(m_sim->getFactoryState(), m_sim->getConfig(), id);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user