Files
dota_factory/src/lib/sim/Simulation.h
Malte Langkabel 46649e7dd1 separate what buildings are from what flows through them
BuildingSystem was four unrelated jobs in one class: building lifecycle,
building configuration, the per-tick material flow, and (until last commit) the
checksum. The flow was the odd one out -- it is what the RNG, the ship spawner
and the unlock test were held for, none of which placement, rotation or
demolition has any business reaching.

Tick steps 3 to 5 move to a new ProductionSystem: tickBeltPull, tickProduction,
tickShipyardProduction, tickOutputBelts, their five private helpers and
rollOutputGroup. The cut is clean in both directions -- nothing in the block
called a topology or configuration method, and nothing outside it called the
helpers -- so the bodies move verbatim; a scripted comparison against the old
file confirms all nine differ only by class qualifier, the m_belts -> belts
rename, and the two added parameters. (Seven em dashes in comments became `--`;
the new file is ASCII, as the guidelines require.)

Belts arrive per tick rather than being held, matching ConstructionSystem, and
only the two methods that touch them take the parameter. BuildingSystem is left
holding the config and the belts, and its constructor takes exactly those two.
The arena constructs no ProductionSystem at all: it stages ships directly and
never runs a factory.

Determinism rests on the RNG stream: step 4's weighted output-group pick is the
factory's only draw, so the four calls must keep their order and position in
Simulation::tick. They do, and the tick-order section now says why, since no
test can catch a reordering here.

913 lines of BuildingSystem.cpp become 444 there and 483 in ProductionSystem.cpp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
2026-08-20 07:41:20 +02:00

238 lines
9.8 KiB
C++

#pragma once
#include <memory>
#include <optional>
#include <random>
#include <string>
#include <vector>
#include <QPoint>
#include "BeltSystem.h"
#include "FactoryState.h"
#include "EntityAdmin.h"
#include "entt/entity/entity.hpp"
#include "SchematicChoiceOption.h"
#include "BuildingType.h"
#include "BuildingId.h"
#include "EventHandler.h"
#include "BeamFiredEvent.h"
#include "GameConfig.h"
#include "Rotation.h"
#include "Tick.h"
#include "TracePrintRequestedEvent.h"
#include "UnlockState.h"
class AiSystem;
class BuildingSystem;
class ConstructionSystem;
class DeconstructionSystem;
class ProductionSystem;
struct Command;
class Hasher;
class CombatSystem;
class DynamicBodySystem;
class MovementIntentSystem;
class RepairSystem;
class SalvagerSystem;
class ShipSystem;
class DebrisSystem;
class WaveSystem;
class Simulation: public CombinedEventHandler<TracePrintRequestedEvent>
{
public:
explicit Simulation(GameConfig config, unsigned int seed = 0);
~Simulation();
const GameConfig& getConfig() const;
// Reinitializes all simulation state as if constructed fresh.
void reset(unsigned int seed = 0);
// Reloads config then reinitializes all simulation state.
void reset(GameConfig newConfig, unsigned int seed = 0);
// Advances the simulation by one tick. Tick order per architecture.md §Tick Order.
void tick();
// The single command chokepoint: applies one player command by dispatching
// to the underlying mutators. Every sim mutation during play must flow
// through here so it can be recorded and replayed (see docs/replay_design.md
// and CommandManager). Reached via CommandManager::drain.
void apply(const Command& command);
// Returns all fire events accumulated since the last drain, clearing the
// internal queue. Call once per rendered frame (REQ-SHP-FIRING-BEAM).
std::vector<BeamFiredEvent> drainBeamFiredEvents();
// Returns the pending schematic choices (empty if no drop is pending).
const std::vector<SchematicChoiceOption>& getPendingSchematicChoices() const;
// Returns true if there are pending schematic choices waiting for player input.
bool hasSchematicChoicesPending() const;
Tick getCurrentTick() const;
// The seed this run was (re)initialized with; written to the replay header.
unsigned int getSeed() const;
int getBuildingBlocksStock() const;
// Current asteroid width in tiles = base width + purchased expansions
// (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND).
int getCurrentAsteroidWidth_tiles() const;
// Building block cost of the next expansion, floored to an integer
// (REQ-EXP-COST); x = number of expansions already purchased.
int getCurrentExpansionCost() const;
bool isGameOver() const;
bool isWon() const;
int getArtifactCount() const;
double getThreatLevel() const;
double getThreatAccumulationRate() const;
double getMaxFactoryProductionThreatRate() const;
double getCurrentFactoryProductionThreatRate() const;
int getBossWaveCounter() const;
Tick getBossCountdownTicks() const;
Tick getNormalGapRemainingTicks() const;
// Ship schematic state query.
bool isSchematicUnlocked(const std::string& shipId) const;
// Module schematic state query.
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
// Implicit recipe/item unlock queries (REQ-LOCK-IMPLICIT).
bool isRecipeUnlocked(const std::string& recipeId) const;
bool isItemUnlocked(const std::string& itemId) const;
// Building unlock query (REQ-LOCK-BUILDING). True if the building type is not
// gated by any unlock group, or its granting group has been awarded.
bool isBuildingUnlocked(BuildingType type) const;
// -- Determinism (see docs/replay_design.md) -----------------------------
// 64-bit fingerprint of the RNG stream state. Cheap; written to the replay
// file periodically + after each command for desync detection.
unsigned long long getRngFingerprint() const;
// 64-bit fingerprint of the full simulation state (RNG, scalars, buildings,
// belts, and ECS component state). Used by the double-run determinism test;
// a superset of getRngFingerprint().
unsigned long long computeStateChecksum() const;
// Const subsystem accessors (queries only). The mutable counterparts are
// private and reachable only through Simulation::apply (the command
// chokepoint) or, in tests, SimulationTestAccess — so production code cannot
// mutate the factory outside the recorded command path (docs/replay_design.md).
// BuildingSystem has no const accessor: it answers no queries, so nothing outside
// the command path has a reason to reach it (FactoryQueries.h).
// The factory's world data, for the free queries in FactoryQueries.h.
const FactoryState& getFactoryState() const;
const BeltSystem& getBelts() const;
ShipSystem& getShips();
const ShipSystem& getShips() const;
DebrisSystem& getDebrisSystem();
const DebrisSystem& getDebrisSystem() const;
EntityAdmin& getAdmin();
const EntityAdmin& getAdmin() const;
private:
// Grants tests access to the private player-action mutators below without
// opening them to production code (see src/test/SimulationTestAccess.h).
friend struct SimulationTestAccess;
// -- Player-action mutators (command chokepoint only) --------------------
// Reached during play exclusively via apply(); never called by UI/app code.
// Checks affordability, deducts building blocks, and places the building.
// Returns the new entity id, or nullopt if blocks are insufficient.
std::optional<BuildingId> tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
// Marks the building with the given id for demolition: a construction site is
// removed instantly (full refund), a built building is queued for timed
// deconstruction (REQ-BLD-DECON-QUEUE), refunded on completion.
void deconstruct(BuildingId id);
// Takes a queued building back out of the deconstruction queue (REQ-BLD-DECON-QUEUE).
void cancelDeconstruction(BuildingId id);
// Applies the player's chosen schematic from the pending choices.
// choiceIndex must be in [0, pendingChoices.size()).
// Clears the pending choices after application.
void applySchematicChoice(int choiceIndex);
// Unlocks one asteroid expansion if affordable (REQ-EXP-UNLOCK): checks the
// current cost against the stock, deducts it, increments the expansion
// counter, and widens the buildable asteroid. No-op if blocks are short.
void tryExpandAsteroid();
// Mutable subsystem accessors; same chokepoint rule as the mutators above.
BuildingSystem& getBuildingsMutable();
BeltSystem& getBeltsMutable();
void handleEvent(std::shared_ptr<const TracePrintRequestedEvent> event) override;
// (Re-)create every owned subsystem. Shared by the constructor and reset();
// the construction order is load-bearing for determinism, so both paths must
// go through here. Only called before the first tick of a run.
void initializeSubsystems();
// Populate HQ, player defence stations, and the first enemy station set.
void placeInitialStructures();
// Place two enemy defence stations for the given generation level.
// Stores their IDs in m_currentEnemyStationIds.
void placeEnemyStationSet(int generation);
// Tick step 9: remove dead ships and buildings, drop debris, handle push.
void tickDeathsAndLoot();
// Generate up to 3 schematic choices (REQ-DEF-SCHEMATIC-DROP) for the player.
void generateSchematicChoices(int destroyedStationLevel);
GameConfig m_config;
std::mt19937 m_rng;
unsigned int m_seed;
Tick m_currentTick;
Tick m_nextDepartureTick;
int m_expansionsPurchased = 0; // REQ-EXP-COST formula variable x
bool m_gameOver = false;
bool m_isWon = false;
int m_artifactCount = 0;
// Pre-placed structure IDs.
std::optional<BuildingId> m_hqBuildingId; // Building id (for belt integration)
entt::entity m_hqProxyEntity; // ECS entity (HP, targeting)
entt::entity m_playerStation1Entity;
entt::entity m_playerStation2Entity;
entt::entity m_currentEnemyStationEntities[2];
// Schematic/unlock bookkeeping (REQ-DEF-SCHEMATIC-DROP, REQ-LOCK-EXPLICIT,
// REQ-LOCK-IMPLICIT, REQ-LOCK-BUILDING, REQ-LOCK-PREREQ). Constructed before
// initializeSubsystems() runs since BuildingSystem's spawn-gating lambda
// calls into it (see initializeSubsystems()).
UnlockState m_unlockState;
EntityAdmin m_admin;
// The factory's world data. Owned here, not by BuildingSystem, so the systems
// that operate on it can be handed the same state (see FactoryState.h).
FactoryState m_factoryState;
BeltSystem m_beltSystem;
std::unique_ptr<BuildingSystem> m_buildingSystem;
std::unique_ptr<ProductionSystem> m_productionSystem;
std::unique_ptr<ConstructionSystem> m_constructionSystem;
std::unique_ptr<DeconstructionSystem> m_deconstructionSystem;
std::unique_ptr<ShipSystem> m_shipSystem;
std::unique_ptr<AiSystem> m_aiSystem;
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem;
std::unique_ptr<DebrisSystem> m_debrisSystem;
std::unique_ptr<SalvagerSystem> m_salvagerSystem;
std::unique_ptr<RepairSystem> m_repairSystem;
std::unique_ptr<WaveSystem> m_waveSystem;
std::unique_ptr<CombatSystem> m_combatSystem;
std::vector<BeamFiredEvent> m_beamFiredEvents;
std::vector<SchematicChoiceOption> m_pendingSchematicChoices;
};