#pragma once #include #include #include #include #include #include #include #include #include #include #include #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 allocateBuildingId, std::function&)> spawnShip, std::function 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 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& filterA, const std::vector& 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& cells, BuildingId ownerPlaceholder); void unregisterTileOccupancy(FactoryState& state, const std::vector& 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& 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 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 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 m_allocateBuildingId; std::function&)> m_spawnShip; std::function m_isItemUnlocked; std::mt19937& m_rng; };