isPlacementValid, findRotateInPlaceTarget and the bodyCellsWithinWorldBounds helper become PlacementRules.h — where a building may go and what already sits on those tiles, answered from the factory state and the config. getInputPorts and getSiteSplitterInfo join FactoryQueries.h, whose header comment now says plainly that the last two also take the config because answering them means reading a building definition. computeInputPorts goes to PortGeometry.h alongside outputBodyTile/inputBodyTile: it needs only Port and QPoint, so it belongs in core rather than in sim. BuildingSystem is left with no query that reads the factory — its remaining const methods are the emerging/incoming item walks, the checksum fold, and the buffer initialisers. It changes the factory now; it no longer describes it. Verified with a golden-checksum capture before and after — all four sample ticks identical. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
182 lines
5.1 KiB
C++
182 lines
5.1 KiB
C++
#include "FactoryQueries.h"
|
|
|
|
#include <limits>
|
|
|
|
#include "PortGeometry.h"
|
|
#include "SurfaceMask.h"
|
|
|
|
#include "Item.h"
|
|
#include "ItemType.h"
|
|
|
|
const Building* findBuilding(const FactoryState& state, BuildingId id)
|
|
{
|
|
for (const Building& building : state.buildings)
|
|
{
|
|
if (building.id == id)
|
|
{
|
|
return &building;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Building* findBuilding(FactoryState& state, BuildingId id)
|
|
{
|
|
for (Building& building : state.buildings)
|
|
{
|
|
if (building.id == id)
|
|
{
|
|
return &building;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const ConstructionSite* findSite(const FactoryState& state, BuildingId id)
|
|
{
|
|
for (const ConstructionSite& site : state.constructionQueue)
|
|
{
|
|
if (site.id == id)
|
|
{
|
|
return &site;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<Building> getAllBuildings(const FactoryState& state)
|
|
{
|
|
return state.buildings;
|
|
}
|
|
|
|
std::vector<ConstructionSite> getAllSites(const FactoryState& state)
|
|
{
|
|
return std::vector<ConstructionSite>(state.constructionQueue.begin(),
|
|
state.constructionQueue.end());
|
|
}
|
|
|
|
int getProductionBuildingCount(const FactoryState& state)
|
|
{
|
|
int count = 0;
|
|
for (const Building& b : state.buildings)
|
|
{
|
|
if (isProductionBuildingType(b.type)) { ++count; }
|
|
}
|
|
return count;
|
|
}
|
|
|
|
int getActiveProductionBuildingCount(const FactoryState& state)
|
|
{
|
|
int count = 0;
|
|
for (const Building& b : state.buildings)
|
|
{
|
|
if (isProductionBuildingType(b.type) && b.production.has_value()) { ++count; }
|
|
}
|
|
return count;
|
|
}
|
|
|
|
bool isTileOccupied(const FactoryState& state, QPoint tile)
|
|
{
|
|
return state.grid.isOccupied(tile);
|
|
}
|
|
|
|
bool isQueuedForDeconstruction(const FactoryState& state, BuildingId id)
|
|
{
|
|
const Building* building = findBuilding(state, id);
|
|
return building && building->queuedForDeconstruction;
|
|
}
|
|
|
|
const Building* findNearestBuilding(const FactoryState& state, QVector2D worldPos,
|
|
BuildingType type)
|
|
{
|
|
const Building* best = nullptr;
|
|
float bestDist = std::numeric_limits<float>::max();
|
|
for (const Building& b : state.buildings)
|
|
{
|
|
if (b.type != type)
|
|
{
|
|
continue;
|
|
}
|
|
QVector2D center(b.anchor.x() + b.footprint.width() / 2.0f,
|
|
b.anchor.y() + b.footprint.height() / 2.0f);
|
|
float dist = (center - worldPos).length();
|
|
if (dist < bestDist)
|
|
{
|
|
bestDist = dist;
|
|
best = &b;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
bool deliverScrapToSalvageBay(FactoryState& state, BuildingId bayId)
|
|
{
|
|
Building* bay = findBuilding(state, bayId);
|
|
if (!bay || bay->type != BuildingType::SalvageBay)
|
|
{
|
|
return false;
|
|
}
|
|
if (bay->queuedForDeconstruction)
|
|
{
|
|
return false; // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
|
|
}
|
|
// Emerging scrap still counts against the bay's holding capacity
|
|
// (REQ-MAT-OUTPUT-EMERGE).
|
|
if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
|
|
{
|
|
return false;
|
|
}
|
|
bay->outputBuffer.items.push_back(Item{ItemType{"scrap"}});
|
|
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;
|
|
}
|
|
|