Files
dota_factory/src/lib/sim/BuildingSystem.h
Malte Langkabel 780d5e5052 put the block stock where the blocks are
The global building block stock lived on Simulation while being factory data
through and through: placement spends it, deconstruction refunds it, and blocks
delivered to the HQ by belt add to it. Every system that credited it therefore
held a std::function back into Simulation to do so -- BuildingSystem and
DeconstructionSystem each carried one, and the arena and three test fixtures had
to pass a stub.

It moves into FactoryState, seeded by makeFactoryState from
world.starting_building_blocks, and both callbacks are gone: the HQ's belt intake
and the deconstruction refund now credit the state they are already holding.

BuildingSystem::deconstruct stops returning a refund for its caller to remember
to credit. It had grown asymmetric -- the queued path credits itself through
DeconstructionSystem while the instant path handed a number back -- so it now
credits the site's full cost directly and returns void.

Checksum order is untouched: Simulation still folds the stock at exactly the
point it always did, reading it from the state. The arena no longer discards
refunds into a no-op sink; nothing there reads the stock either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
2026-08-19 17:23:46 +02:00

187 lines
10 KiB
C++

#pragma once
#include <deque>
#include <functional>
#include <map>
#include <optional>
#include <random>
#include <string>
#include <utility>
#include <vector>
#include <QPoint>
#include <QPointF>
#include <QVector2D>
#include "BeltSystem.h"
#include "Building.h"
#include "FactoryState.h"
#include "BuildingBuffers.h"
#include "DeconstructionSystem.h"
#include "PlacementRules.h"
#include "ProductionRules.h"
#include "BuildingType.h"
#include "BuildingId.h"
#include "GameConfig.h"
#include "Rotation.h"
#include "ModulesConfig.h"
#include "ShipLayout.h"
#include "ShipsConfig.h"
#include "Tick.h"
class Hasher;
// Manages building placement, construction queuing, and the per-tick
// production loop (belt→building pull, production, building→belt push).
// All types including Belt and Splitter are stored as Building instances;
// BeltSystem owns the per-tile simulation data (item slots, flow).
class BuildingSystem
{
public:
BuildingSystem(const GameConfig& config,
BeltSystem& belts,
std::function<BuildingId()> allocateBuildingId,
std::function<void(const std::string&, QVector2D,
const std::optional<ShipLayoutConfig>&)> spawnShip,
std::function<bool(const std::string&)> isItemUnlocked,
std::mt19937& rng);
// -- Placement / deconstruct ------------------------------------------------
// Returns the new entity id, or nullopt if the placement falls outside the
// world bounds (vertical extent and asteroid left edge). Belt and Splitter
// register with BeltSystem directly; other types enter the construction
// queue. Terrain type (A vs S) is NOT checked here so that tests can stage
// arbitrary layouts; the player-facing entry point
// (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid.
std::optional<BuildingId> place(FactoryState& state, BuildingType type, QPoint anchor, Rotation rotation,
Tick currentTick);
// Sets the current buildable asteroid width in tiles. Grows the left
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
// Defaults to world.regions.asteroid_width_tiles at construction.
void setAsteroidWidth_tiles(FactoryState& state, int widthTiles) const
{ state.asteroidWidth_tiles = widthTiles; }
// Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT).
// A construction site is removed instantly and its full cost credited back to the
// block stock. A fully-built building is instead appended to the deconstruction
// queue (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
// credited later, on completion, by DeconstructionSystem. No-op for unknown ids and
// for a building already queued.
void deconstruct(FactoryState& state, BuildingId id, Tick currentTick);
// Take a building back out of the deconstruction queue before it is removed
// (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation
// (re-registering belt/tunnel/splitter tiles); discards deconstruction
// progress and credits no refund. No-op if the id is not queued.
void cancelDeconstruction(FactoryState& state, BuildingId id);
// Set the recipe (or schematic id for shipyard) on a building or queued
// construction site. Clears both buffers on an operational building.
void setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId);
// Set the module layout for a shipyard. Cancels in-progress production
// (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
void setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout);
// Splitter filter configuration for a queued/under-construction Splitter
// site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through
// BeltSystem by tile; these mirror that for sites, which are not yet
// registered with BeltSystem. getSiteSplitterInfo returns the site's two
// 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
// to BeltSystem when the splitter finishes building (tickConstruction).
void setSiteSplitterFilters(FactoryState& state, BuildingId id,
const std::vector<ItemType>& filterA,
const std::vector<ItemType>& filterB);
// -- Tick hooks (called from Simulation::tick in the documented order) ---
// Advances every building's virtual input belts, delivers what arrives into the
// input buffers (into the global block stock for the HQ), and takes what the
// adjacent real belts offer (REQ-MAT-INPUT-INTAKE, REQ-HQ-BELT-INPUT).
void tickBeltPull(FactoryState& state);
void tickProduction(FactoryState& state, Tick currentTick);
void tickShipyardProduction(FactoryState& state, Tick currentTick);
// Advances each building's virtual output belts, hands finished items off onto
// the adjacent real belt, and feeds new buffered items into them
// (REQ-MAT-OUTPUT-EMERGE).
void tickOutputBelts(FactoryState& state);
// This system answers no queries: reading the factory needs none, the queries being
// free functions over the state (FactoryQueries.h), the placement rules
// (PlacementRules.h) and the production rules (ProductionRules.h). What is left here
// mutates.
// Rotate an existing building or construction site to newRotation in place.
// For belt-type operational buildings, re-registers with BeltSystem (items
// currently on the tile are discarded by BeltSystem::removeTile).
void rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation);
// Register / unregister tile occupancy for ECS station entities.
void registerTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
void unregisterTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells);
// Bypass the construction queue and create a fully-operational Building
// immediately. Used for pre-placed structures (HQ, defence stations).
// surfaceMask comes from the relevant config struct.
BuildingId placeImmediate(FactoryState& state, BuildingType type,
const std::vector<std::string>& surfaceMask,
QPoint anchor, Rotation rotation);
// Remove an operational building by id without refund (used for deaths).
// Returns true if found and removed.
bool removeBuilding(FactoryState& state, BuildingId id);
// Mutable iteration over all operational buildings.
void forEachBuilding(FactoryState& state, std::function<void(Building&)> fn);
// -- Determinism ---------------------------------------------------------
// Folds all building, construction-site, and tile-occupancy state into the
// hasher in deterministic order (see docs/replay_design.md).
void appendChecksum(const FactoryState& state, Hasher& hasher) const;
private:
// Selects a recipe for an auto-recipe building that has none, from a material being
// offered to it at one of its input ports (REQ-BLD-AUTO-RECIPE). No-op for every
// other building, for one that already holds a recipe, and for a material none of
// its recipes consumes. Called from both intake paths -- the belt pull and the
// direct coupling -- since either can be where the first material arrives.
void selectAutoRecipeIfUnset(Building& building,
const ItemType& offered);
// True if the consumer would accept `type` at the given input port right now:
// it is a required input (or a building block for the HQ), the reservation-aware
// buffer has room, and the input belt entry is free (REQ-MAT-INPUT-INTAKE).
bool canAcceptInput(const Building& consumer,
std::size_t inputPortIndex,
const ItemType& type) const;
// Places an accepted item onto the consumer's input belt at progress 0.0,
// reserving a per-material buffer slot (REQ-MAT-INPUT-INTAKE).
void depositToInputBelt(Building& consumer,
std::size_t inputPortIndex,
const Item& item);
// Attempts to hand an emerging output item straight into a directly adjacent
// building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
// Returns true if the item was accepted onto the consumer's input belt.
bool tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
const Port& outputPort,
const Item& item);
// What one cycle of this recipe produces: the items of its one output group
// (REQ-MAT-OUTPUT-GROUP). Where the recipe has several, one is picked by weight from
// those currently eligible (REQ-LOCK-OUTPUT-POOL) and the result is empty if none is;
// where it has one, that group is returned with no draw and no eligibility test.
std::vector<Item> rollOutputGroup(const RecipeDef& recipe);
// No world data among these: the factory arrives per call (FactoryState.h). What is
// left are the immutable config, the transport layer, the shared RNG, and four
// callbacks into what this system cannot reach on its own -- the id counter and the
// block stock, which live on Simulation, and the entity and unlock sides.
const GameConfig& m_config;
BeltSystem& m_belts;
std::function<BuildingId()> m_allocateBuildingId;
std::function<void(const std::string&, QVector2D,
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
std::function<bool(const std::string&)> m_isItemUnlocked;
std::mt19937& m_rng;
};