Files
dota_factory/src/lib/sim/BuildingSystem.cpp

1042 lines
38 KiB
C++

#include "BuildingSystem.h"
#include <algorithm>
#include <cassert>
#include <limits>
#include <random>
#include <set>
#include "FactoryQueries.h"
#include "PlacementRules.h"
#include "ProductionRules.h"
#include "PortGeometry.h"
#include "StateChecksum.h"
#include "SurfaceMask.h"
#include "tracing.h"
namespace
{
// An input belt accepts a new item at progress 0.0 only when it holds fewer than
// three items and the entry slot is clear (nothing within a quarter tile of 0.0),
// matching the belt packing used elsewhere (REQ-GW-BELT-CAPACITY).
bool inputLaneEntryFree(const std::vector<BeltItemSlot>& lane)
{
return lane.size() < 3 && (lane.empty() || lane.back().progress >= 0.25);
}
} // namespace
BuildingSystem::BuildingSystem(const GameConfig& config,
BeltSystem& belts,
std::function<BuildingId()> allocateBuildingId,
std::function<void(int)> addBuildingBlocks,
std::function<void(const std::string&, QVector2D,
const std::optional<ShipLayoutConfig>&)> spawnShip,
std::function<bool(const std::string&)> isItemUnlocked,
std::mt19937& rng)
: m_config(config)
, m_belts(belts)
, m_allocateBuildingId(std::move(allocateBuildingId))
, m_addBuildingBlocks(std::move(addBuildingBlocks))
, m_spawnShip(std::move(spawnShip))
, m_isItemUnlocked(std::move(isItemUnlocked))
, m_rng(rng)
{
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
{
std::vector<const RecipeOutput*> eligible;
std::vector<double> weights;
for (const RecipeOutput& out : recipe.outputs)
{
if (!m_isItemUnlocked(out.item)) { continue; }
eligible.push_back(&out);
weights.push_back(out.probability.value_or(1.0));
}
if (eligible.empty()) { return {}; }
std::discrete_distribution<int> dist(weights.begin(), weights.end());
const RecipeOutput& chosen = *eligible[static_cast<std::size_t>(dist(m_rng))];
std::vector<Item> result;
Item item;
item.type.id = chosen.item;
for (int i = 0; i < chosen.amount; ++i)
{
result.push_back(item);
}
return result;
}
// ---------------------------------------------------------------------------
// Placement
// ---------------------------------------------------------------------------
std::optional<BuildingId> BuildingSystem::place(FactoryState& state, BuildingType type, QPoint anchor,
Rotation rotation, Tick currentTick)
{
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
assert(def != nullptr);
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
// Reject placements that fall outside the world (REQ-BLD-PLACE-VALID).
if (!bodyCellsWithinWorldBounds(state, m_config, mask.bodyCells, anchor))
{
return std::nullopt;
}
const BuildingId id = m_allocateBuildingId();
// Record tile occupancy for body cells.
for (const QPoint& cell : mask.bodyCells)
{
const QPoint absCell = anchor + cell;
state.grid.occupy(absCell, id);
}
// Build construction site.
ConstructionSite site;
site.id = id;
site.anchor = anchor;
site.footprint = mask.footprint;
site.rotation = rotation;
site.type = type;
for (const QPoint& cell : mask.bodyCells)
{
site.bodyCells.push_back(anchor + cell);
}
if (state.constructionQueue.empty())
{
site.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
}
// else: completesAt remains 0 (queued, not yet started).
state.constructionQueue.push_back(std::move(site));
return id;
}
// ---------------------------------------------------------------------------
// Deconstruct
// ---------------------------------------------------------------------------
int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick currentTick)
{
// Construction site? Removed instantly with the full refund; never queued
// for deconstruction (REQ-BLD-DECONSTRUCT).
for (std::deque<ConstructionSite>::iterator it = state.constructionQueue.begin();
it != state.constructionQueue.end();
++it)
{
if (it->id == id)
{
const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
state.grid.release(it->bodyCells);
state.constructionQueue.erase(it);
if (def)
{
return def->cost;
}
return 0;
}
}
// Operational building? Append it to the deconstruction queue rather than
// removing it now; the partial refund is credited on completion in
// tickDeconstruction (REQ-BLD-DECON-QUEUE).
for (Building& building : state.buildings)
{
if (building.id != id) { continue; }
if (building.queuedForDeconstruction) { return 0; } // already queued
building.queuedForDeconstruction = true;
DeconstructionEntry entry;
entry.id = id;
// A queued belt/tunnel/splitter stops transporting at once: capture a
// splitter's filters (so an un-queue can restore them), then unregister
// its tile, discarding items on it and re-pairing tunnels as if it were
// gone (REQ-BLD-TUNNEL-PAIR).
if (building.type == BuildingType::Splitter)
{
if (const std::optional<BeltSystem::SplitterInfo> info =
m_belts.getSplitterInfo(building.anchor))
{
entry.splitterFilterA = info->filterA;
entry.splitterFilterB = info->filterB;
}
}
if (isBeltSubsystemType(building.type))
{
m_belts.removeTile(building.anchor);
}
const bool wasEmpty = state.deconstructionQueue.empty();
state.deconstructionQueue.push_back(std::move(entry));
if (wasEmpty)
{
startFrontDeconstruction(state, m_config, currentTick);
}
return 0;
}
return 0;
}
// ---------------------------------------------------------------------------
// Set recipe
// ---------------------------------------------------------------------------
void BuildingSystem::setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId)
{
// Construction site: store recipe for when building completes.
for (ConstructionSite& site : state.constructionQueue)
{
if (site.id == id)
{
// Auto-recipe buildings have no player-selected recipe
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING); ignore any attempt to set one.
if (isAutoRecipeBuildingType(site.type))
{
return;
}
// No-op if the recipe is unchanged, so a redundant selection does
// not wipe an already-configured ship layout.
if (site.recipeId == recipeId)
{
return;
}
site.recipeId = recipeId;
site.shipLayout = std::nullopt;
return;
}
}
// Operational building: clear buffers and re-init.
for (Building& building : state.buildings)
{
if (building.id == id)
{
// Auto-recipe buildings have no player-selected recipe
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING); ignore any attempt to set one.
if (isAutoRecipeBuildingType(building.type))
{
return;
}
// No-op if the recipe is unchanged, so a redundant selection does
// not wipe an already-configured ship layout or reset buffers.
if (building.recipeId == recipeId)
{
return;
}
building.recipeId = recipeId;
building.shipLayout = std::nullopt;
building.inputBuffer.counts.clear();
building.inputBuffer.caps.clear();
building.outputBuffer.items.clear();
building.outputBuffer.capacity = 0;
// Emerging items are part of the output buffer, so clearing it on a
// recipe change discards them too (REQ-MAT-OUTPUT-EMERGE); in-transit
// input items are discarded and their reservations released
// (REQ-MAT-INPUT-INTAKE).
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
for (std::vector<BeltItemSlot>& lane : building.incomingItems) { lane.clear(); }
building.production = std::nullopt;
if (!recipeId.empty())
{
if (building.type == BuildingType::Shipyard)
{
initShipyardBuffers(m_config, building);
}
else
{
const RecipeDef* recipe = m_config.recipes.findRecipeDef(recipeId, building.type);
if (recipe)
{
initBuffers(building, *recipe);
}
}
}
return;
}
}
}
void BuildingSystem::setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout)
{
for (ConstructionSite& site : state.constructionQueue)
{
if (site.id == id)
{
site.shipLayout = layout;
return;
}
}
for (Building& building : state.buildings)
{
if (building.id == id)
{
// No-op if the layout is unchanged, so re-applying the layout a shipyard
// already has does not cancel its production cycle or wipe its buffers
// (REQ-MAT-INPUT-BUFFER, REQ-BLD-SHIPYARD). Confirming the layout dialog
// without editing anything, and a blueprint configuration transfer onto an
// already-matching shipyard (REQ-UI-BLUEPRINT-TRANSFER), both land here.
// An unset layout counts as an empty one: the two are equivalent for
// buffers, production, and the spawned ship (see the spawn path below),
// so an empty layout arriving at an unconfigured shipyard changes nothing.
const bool unchanged = building.shipLayout.has_value()
? *building.shipLayout == layout
: layout.placedModules.empty();
if (unchanged)
{
return;
}
if (building.production.has_value())
{
building.production = std::nullopt;
}
building.shipLayout = layout;
building.inputBuffer.counts.clear();
building.inputBuffer.caps.clear();
building.outputBuffer.items.clear();
building.outputBuffer.capacity = 0;
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
for (std::vector<BeltItemSlot>& lane : building.incomingItems) { lane.clear(); }
if (!building.recipeId.empty() && building.type == BuildingType::Shipyard)
{
initShipyardBuffers(m_config, building);
}
return;
}
}
}
void BuildingSystem::setSiteSplitterFilters(FactoryState& state, BuildingId id,
const std::vector<ItemType>& filterA,
const std::vector<ItemType>& filterB)
{
for (ConstructionSite& site : state.constructionQueue)
{
if (site.id == id && site.type == BuildingType::Splitter)
{
site.splitterFilterA = filterA;
site.splitterFilterB = filterB;
return;
}
}
}
// ---------------------------------------------------------------------------
// Tick hooks
// ---------------------------------------------------------------------------
void BuildingSystem::cancelDeconstruction(FactoryState& state, BuildingId id)
{
for (std::deque<DeconstructionEntry>::iterator it = state.deconstructionQueue.begin();
it != state.deconstructionQueue.end();
++it)
{
if (it->id != id) { continue; }
// Resume operation: clear the flag and re-register belt/tunnel/splitter
// tiles that were unregistered at enqueue (which re-pairs tunnels,
// REQ-BLD-TUNNEL-PAIR). Deconstruction progress is discarded; no refund.
if (Building* building = findBuilding(state, id))
{
building->queuedForDeconstruction = false;
reregisterBeltTile(m_belts, m_config, *building, it->splitterFilterA, it->splitterFilterB);
}
state.deconstructionQueue.erase(it);
// If the running front was removed, the new front (completesAt == 0) has
// its timer started by the next tickDeconstruction guard.
return;
}
}
void BuildingSystem::tickBeltPull(FactoryState& state)
{
TRACE();
// Same per-tick step as the belts, so items travel inward at belt speed
// (REQ-GW-BELT-SPEED, REQ-MAT-INPUT-INTAKE).
const double progressPerTick = m_belts.getProgressPerTick_tpt();
for (Building& building : state.buildings)
{
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
if (building.queuedForDeconstruction) { continue; }
const bool isHq = (building.type == BuildingType::Hq);
// 1. Advance every input belt and deliver arrivals (progress >= 0.5) into
// the input buffer — or the global stock for the HQ. Runs for all
// buildings so in-transit items keep moving even when feeding is gated
// off, and arrivals become consumable before tickProduction (step 4).
for (std::size_t i = 0; i < building.incomingItems.size(); ++i)
{
std::vector<BeltItemSlot>& lane = building.incomingItems[i];
advanceBeltSlots(lane, progressPerTick);
while (!lane.empty() && lane.front().progress >= 0.5)
{
const Item arrived = lane.front().item;
lane.erase(lane.begin());
if (isHq)
{
m_addBuildingBlocks(1);
}
else
{
building.inputBuffer.counts[arrived.type]++;
}
}
}
// 2. Feed accepted items from adjacent belts onto the input belts at
// progress 0.0. The acceptance rules — the HQ building-block case, the
// required-input check, and the reservation — live in canAcceptInput so
// direct coupling (REQ-MAT-DIRECT-COUPLE) shares them exactly.
for (std::size_t i = 0; i < building.inputPorts.size(); ++i)
{
const std::optional<ItemType> peeked = m_belts.peekItem(building.inputPorts[i]);
if (!peeked) { continue; }
if (!canAcceptInput(building, i, *peeked)) { continue; }
const std::optional<Item> taken = m_belts.tryTakeItem(building.inputPorts[i]);
if (taken)
{
depositToInputBelt(building, i, *taken);
}
}
}
}
bool BuildingSystem::canAcceptInput(const Building& consumer,
std::size_t inputPortIndex,
const ItemType& type) const
{
if (inputPortIndex >= consumer.incomingItems.size()) { return false; }
if (!inputLaneEntryFree(consumer.incomingItems[inputPortIndex])) { return false; }
// The HQ has no input buffer; it accepts building blocks into the global stock
// (REQ-HQ-BELT-INPUT) with no reservation.
if (consumer.type == BuildingType::Hq)
{
return type.id == "building_block";
}
// Everyone else: the item must be a required input whose reservation-aware
// buffer has room — buffered + in-transit below the cap (REQ-MAT-INPUT-INTAKE).
const std::map<ItemType, int>::const_iterator capIt =
consumer.inputBuffer.caps.find(type);
if (capIt == consumer.inputBuffer.caps.end() || capIt->second == 0)
{
return false;
}
return consumer.pendingInputCount(type) < capIt->second;
}
void BuildingSystem::depositToInputBelt(Building& consumer,
std::size_t inputPortIndex,
const Item& item)
{
consumer.incomingItems[inputPortIndex].push_back(BeltItemSlot{item, 0.0});
}
bool BuildingSystem::tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
const Port& outputPort,
const Item& item)
{
const std::optional<BuildingId> ownerId = state.grid.findOwner(outputPort.tile);
if (!ownerId.has_value() || *ownerId == producerId)
{
return false;
}
Building* consumer = findBuilding(state, *ownerId);
if (!consumer)
{
return false; // an unbuilt construction site, or not an operational building
}
if (consumer->queuedForDeconstruction)
{
return false; // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
}
// The coupling is the consumer input port meeting this output port: same flow
// direction, feeding the producer's output-port tile (REQ-MAT-DIRECT-COUPLE).
for (std::size_t j = 0; j < consumer->inputPorts.size(); ++j)
{
const Port& in = consumer->inputPorts[j];
if (in.direction != outputPort.direction) { continue; }
if (inputBodyTile(in.tile, in.direction) != outputPort.tile) { continue; }
if (!canAcceptInput(*consumer, j, item.type)) { return false; }
depositToInputBelt(*consumer, j, item);
return true;
}
return false;
}
void BuildingSystem::tickProduction(FactoryState& state, Tick currentTick)
{
TRACE();
for (Building& building : state.buildings)
{
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
if (building.queuedForDeconstruction) { continue; }
// Skip types without a recipe-based production loop.
if (building.type == BuildingType::Belt ||
building.type == BuildingType::Splitter ||
building.type == BuildingType::Shipyard ||
building.type == BuildingType::SalvageBay ||
building.type == BuildingType::Hq)
{
continue;
}
const bool autoRecipe = isAutoRecipeBuildingType(building.type);
if (!autoRecipe && building.recipeId.empty())
{
continue;
}
// If a production cycle is active, check for completion. Completion only
// needs the already-decided outputs, so it does not depend on which
// recipe is selected or auto-chosen.
if (building.production)
{
if (currentTick >= building.production->completesAt)
{
for (const Item& item : building.production->chosenOutputs)
{
building.outputBuffer.items.push_back(item);
}
building.production = std::nullopt;
}
// Whether we just completed or are still running, do not start
// another cycle in the same tick.
continue;
}
// Idle: gather the candidate recipes to try. Auto-recipe buildings
// (Smelter, Reprocessing Plant) have no selected recipe and try every
// recipe of their type in config order, running the first whose inputs
// are satisfied (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). Other buildings
// try only their selected recipe.
const std::vector<const RecipeDef*> candidates =
gatherCandidateRecipes(m_config, building);
for (const RecipeDef* recipe : candidates)
{
// 1. All required inputs present?
if (!recipeInputsAvailable(building, *recipe))
{
continue;
}
// 2. Determine chosen outputs (roll for reprocessing).
std::vector<Item> chosen;
if (building.type == BuildingType::ReprocessingPlant)
{
chosen = rollReprocessingOutput(*recipe);
if (chosen.empty()) { continue; }
}
else
{
for (const RecipeOutput& out : recipe->outputs)
{
Item item;
item.type.id = out.item;
for (int i = 0; i < out.amount; ++i)
{
chosen.push_back(item);
}
}
}
// 3. Output buffer has space for chosen outputs? Emerging items still
// count against the buffer (REQ-MAT-OUTPUT-EMERGE).
const int newSize = building.getOutputItemCount()
+ static_cast<int>(chosen.size());
if (newSize > building.outputBuffer.capacity)
{
continue;
}
// 4. Consume inputs and start cycle.
for (const RecipeIngredient& ing : recipe->inputs)
{
building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
}
Production prod;
prod.recipeId = recipe->id;
prod.completesAt = currentTick + secondsToTicks(recipe->durationSeconds);
prod.chosenOutputs = std::move(chosen);
building.production = std::move(prod);
break; // At most one cycle starts per tick.
}
}
}
void BuildingSystem::tickShipyardProduction(FactoryState& state, Tick currentTick)
{
TRACE();
for (Building& building : state.buildings)
{
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
if (building.queuedForDeconstruction) { continue; }
if (building.type != BuildingType::Shipyard)
{
continue;
}
if (building.recipeId.empty())
{
continue;
}
const ShipDef* shipDef = m_config.ships.findShipDef(building.recipeId);
if (!shipDef)
{
continue;
}
// If a cycle is in progress, check for completion.
if (building.production)
{
if (currentTick >= building.production->completesAt)
{
if (!building.outputPorts.empty())
{
const Port& p = building.outputPorts[0];
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
// A shipyard builds exactly what the player configured and
// paid for. When no layout is set it produces a bare hull, so
// pass an explicit empty layout rather than nullopt: the latter
// would make ShipSystem fall back to the schematic's
// defaultModules (a wave-only loadout) and yield free weapons.
const std::optional<ShipLayoutConfig> layout =
building.shipLayout.has_value()
? building.shipLayout
: std::make_optional<ShipLayoutConfig>();
m_spawnShip(building.recipeId, spawnPos, layout);
}
building.production = std::nullopt;
}
continue;
}
// Build combined materials list (base + modules).
const std::map<std::string, int> requiredMaterials =
computeShipyardRequiredMaterials(m_config, building);
// Idle: check if all combined materials are available.
bool inputsOk = true;
for (const std::pair<const std::string, int>& req : requiredMaterials)
{
const ItemType type{req.first};
const std::map<ItemType, int>::const_iterator it =
building.inputBuffer.counts.find(type);
const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
if (have < req.second)
{
inputsOk = false;
break;
}
}
if (!inputsOk)
{
continue;
}
// Consume combined materials and start the production cycle.
for (const std::pair<const std::string, int>& req : requiredMaterials)
{
building.inputBuffer.counts[ItemType{req.first}] -= req.second;
}
double totalTime = shipDef->schematic.productionTimeSeconds;
if (building.shipLayout.has_value())
{
for (const PlacedModule& pm : building.shipLayout->placedModules)
{
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
if (modDef)
{
totalTime += modDef->productionTimeSeconds;
}
}
}
Production prod;
prod.recipeId = building.recipeId;
prod.completesAt = currentTick + secondsToTicks(totalTime);
building.production = std::move(prod);
}
}
void BuildingSystem::tickOutputBelts(FactoryState& state)
{
TRACE();
// Use BeltSystem's own per-tick step so emerging items travel at exactly the
// same speed as real belts (REQ-GW-BELT-SPEED, REQ-MAT-OUTPUT-EMERGE).
const double progressPerTick = m_belts.getProgressPerTick_tpt();
for (Building& building : state.buildings)
{
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
if (building.queuedForDeconstruction) { continue; }
for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
{
const Port& port = building.outputPorts[p];
std::vector<BeltItemSlot>& lane = building.emergingItems[p];
// 1. Advance emerging items using the shared belt packing (progress
// caps to 0.5 / 0.75 / 1.0 for up to three items).
advanceBeltSlots(lane, progressPerTick);
// 2. Hand the front item off once it reaches the output edge (progress
// 1.0): onto the adjacent real belt, or — if a building's input edge
// meets this port — straight into that building (REQ-MAT-DIRECT-COUPLE).
// On refusal (no belt/coupling, output-edge per REQ-MAT-ACCEPT-DIR, or
// a full target) it stays stuck at 1.0.
if (!lane.empty() && lane.front().progress >= 1.0)
{
const Item item = lane.front().item;
if (m_belts.tryPutItem(port.tile, item, port.direction)
|| tryDirectCoupleDeposit(state, building.id, port, item))
{
lane.erase(lane.begin());
}
}
// 3. Feed the next buffered item onto the lane at progress 0.5 when the
// entry slot is free — the lane holds at most three items and a new
// one needs a quarter-tile clearance ahead of 0.5.
if (!building.outputBuffer.items.empty()
&& lane.size() < 3
&& (lane.empty() || lane.back().progress >= 0.75))
{
lane.push_back(BeltItemSlot{building.outputBuffer.items.front(), 0.5});
building.outputBuffer.items.erase(building.outputBuffer.items.begin());
}
}
}
}
void BuildingSystem::forEachEmergingItem(const FactoryState& state,
const std::function<void(const ItemType&, QPointF)>& visit) const
{
for (const Building& building : state.buildings)
{
for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
{
const Port& port = building.outputPorts[p];
const QPoint bodyTile = outputBodyTile(port.tile, port.direction);
const std::vector<BeltItemSlot>& lane = building.emergingItems[p];
// Render least-progressed first (bottom) → most-progressed last (top),
// matching belt item ordering (REQ-GW-TILE-SIZE).
for (int i = static_cast<int>(lane.size()) - 1; i >= 0; --i)
{
visit(lane[i].item.type,
beltSlotWorldPos(bodyTile, port.direction, lane[i].progress));
}
}
}
}
void BuildingSystem::forEachIncomingItem(const FactoryState& state,
const std::function<void(const ItemType&, QPointF)>& visit) const
{
for (const Building& building : state.buildings)
{
for (std::size_t p = 0; p < building.inputPorts.size(); ++p)
{
const Port& port = building.inputPorts[p];
const QPoint bodyTile = inputBodyTile(port.tile, port.direction);
const std::vector<BeltItemSlot>& lane = building.incomingItems[p];
// Render least-progressed first (bottom) → most-progressed last (top),
// matching belt item ordering (REQ-GW-TILE-SIZE).
for (int i = static_cast<int>(lane.size()) - 1; i >= 0; --i)
{
visit(lane[i].item.type,
beltSlotWorldPos(bodyTile, port.direction, lane[i].progress));
}
}
}
}
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
void BuildingSystem::rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation)
{
// Construction site path — just update rotation; no ports to recompute.
for (ConstructionSite& site : state.constructionQueue)
{
if (site.id == id)
{
site.rotation = newRotation;
return;
}
}
// Operational building path.
for (Building& b : state.buildings)
{
if (b.id != id) { continue; }
b.rotation = newRotation;
const BuildingDef* def = m_config.buildings.findBuildingDef(b.type);
if (!def) { return; }
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, newRotation);
b.outputPorts.clear();
for (const Port& port : mask.outputPorts)
{
Port absPort;
absPort.tile = b.anchor + port.tile;
absPort.direction = port.direction;
b.outputPorts.push_back(absPort);
}
// The output ports moved; discard any in-flight emerging items and re-size
// the lanes to the new port set (REQ-MAT-OUTPUT-EMERGE).
b.emergingItems.clear();
b.emergingItems.resize(b.outputPorts.size());
b.inputPorts = computeInputPorts(b.bodyCells, b.outputPorts);
// Likewise discard in-transit input items and re-size the input belts to
// the new port set (REQ-MAT-INPUT-INTAKE).
b.incomingItems.assign(b.inputPorts.size(), {});
// Re-register with BeltSystem (items on tile are discarded). A splitter's
// filters live in BeltSystem and would be lost by removeTile, so capture
// them first and hand them back to reregisterBeltTile (REQ-BLD-SPLITTER).
if (isBeltSubsystemType(b.type))
{
std::vector<ItemType> splitterFilterA;
std::vector<ItemType> splitterFilterB;
if (b.type == BuildingType::Splitter)
{
if (const std::optional<BeltSystem::SplitterInfo> info =
m_belts.getSplitterInfo(b.anchor))
{
splitterFilterA = info->filterA;
splitterFilterB = info->filterB;
}
}
m_belts.removeTile(b.anchor);
reregisterBeltTile(m_belts, m_config, b, splitterFilterA, splitterFilterB);
}
return;
}
}
BuildingId BuildingSystem::placeImmediate(FactoryState& state, BuildingType type,
const std::vector<std::string>& surfaceMask,
QPoint anchor, Rotation rotation)
{
const BuildingId id = m_allocateBuildingId();
const ParsedSurfaceMask mask = parseSurfaceMask(surfaceMask, rotation);
Building building;
building.id = id;
building.anchor = anchor;
building.footprint = mask.footprint;
building.rotation = rotation;
building.type = type;
for (const QPoint& cell : mask.bodyCells)
{
const QPoint absCell = anchor + cell;
building.bodyCells.push_back(absCell);
state.grid.occupy(absCell, id);
}
for (const Port& port : mask.outputPorts)
{
Port absPort;
absPort.tile = anchor + port.tile;
absPort.direction = port.direction;
building.outputPorts.push_back(absPort);
}
building.emergingItems.resize(building.outputPorts.size());
building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts);
building.incomingItems.assign(building.inputPorts.size(), {});
if (type == BuildingType::SalvageBay)
{
initSalvageBayBuffer(m_config, building);
}
state.buildings.push_back(std::move(building));
return id;
}
bool BuildingSystem::removeBuilding(FactoryState& state, BuildingId id)
{
for (std::vector<Building>::iterator it = state.buildings.begin();
it != state.buildings.end();
++it)
{
if (it->id == id)
{
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter
|| it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
{
m_belts.removeTile(it->anchor);
}
state.grid.release(it->bodyCells);
state.buildings.erase(it);
return true;
}
}
return false;
}
void BuildingSystem::forEachBuilding(FactoryState& state, std::function<void(Building&)> fn)
{
for (Building& b : state.buildings)
{
fn(b);
}
}
void BuildingSystem::registerTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells,
BuildingId ownerPlaceholder)
{
state.grid.occupy(cells, ownerPlaceholder);
}
void BuildingSystem::unregisterTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells)
{
state.grid.release(cells);
}
namespace
{
void appendItems(Hasher& hasher, const std::vector<Item>& items)
{
hasher.append(items.size());
for (const Item& item : items)
{
hasher.append(item.type.id);
}
}
void appendInputBuffer(Hasher& hasher, const InputBuffer& buffer)
{
// std::map<ItemType, int> iterates in sorted-id order (ItemType::operator<).
hasher.append(buffer.counts.size());
for (const std::pair<const ItemType, int>& entry : buffer.counts)
{
hasher.append(entry.first.id);
hasher.append(entry.second);
}
hasher.append(buffer.caps.size());
for (const std::pair<const ItemType, int>& entry : buffer.caps)
{
hasher.append(entry.first.id);
hasher.append(entry.second);
}
}
} // namespace
void BuildingSystem::appendChecksum(const FactoryState& state, Hasher& hasher) const
{
// state.buildings keeps a stable, deterministic order (append on build, swap-free
// erase aside — both runs perform identical operations, so order matches).
hasher.append(state.buildings.size());
for (const Building& b : state.buildings)
{
hasher.append(b.id);
hasher.append(b.anchor);
hasher.append(b.footprint.width());
hasher.append(b.footprint.height());
hasher.append(b.rotation);
hasher.append(b.type);
hasher.append(b.recipeId);
appendInputBuffer(hasher, b.inputBuffer);
appendItems(hasher, b.outputBuffer.items);
hasher.append(b.outputBuffer.capacity);
hasher.append(b.emergingItems.size());
for (const std::vector<BeltItemSlot>& lane : b.emergingItems)
{
hasher.append(lane.size());
for (const BeltItemSlot& slot : lane)
{
hasher.append(slot.item.type.id);
hasher.append(slot.progress);
}
}
hasher.append(b.incomingItems.size());
for (const std::vector<BeltItemSlot>& lane : b.incomingItems)
{
hasher.append(lane.size());
for (const BeltItemSlot& slot : lane)
{
hasher.append(slot.item.type.id);
hasher.append(slot.progress);
}
}
hasher.append(b.production.has_value());
if (b.production.has_value())
{
hasher.append(b.production->recipeId);
hasher.append(b.production->completesAt);
appendItems(hasher, b.production->chosenOutputs);
}
hasher.append(b.shipLayout.has_value());
hasher.append(b.queuedForDeconstruction);
}
hasher.append(state.constructionQueue.size());
for (const ConstructionSite& s : state.constructionQueue)
{
hasher.append(s.id);
hasher.append(s.anchor);
hasher.append(s.footprint.width());
hasher.append(s.footprint.height());
hasher.append(s.rotation);
hasher.append(s.type);
hasher.append(s.recipeId);
hasher.append(s.completesAt);
hasher.append(s.shipLayout.has_value());
hasher.append(s.splitterFilterA.size());
for (const ItemType& type : s.splitterFilterA) { hasher.append(type.id); }
hasher.append(s.splitterFilterB.size());
for (const ItemType& type : s.splitterFilterB) { hasher.append(type.id); }
}
hasher.append(state.deconstructionQueue.size());
for (const DeconstructionEntry& e : state.deconstructionQueue)
{
hasher.append(e.id);
hasher.append(e.completesAt);
hasher.append(e.splitterFilterA.size());
for (const ItemType& type : e.splitterFilterA) { hasher.append(type.id); }
hasher.append(e.splitterFilterB.size());
for (const ItemType& type : e.splitterFilterB) { hasher.append(type.id); }
}
state.grid.appendChecksum(hasher);
}