#pragma once #include #include #include #include #include #include "BuildingId.h" #include "BuildingType.h" #include "ItemType.h" #include "Rotation.h" #include "ShipLayout.h" class GameConfig; // Player intent, resolved to domain ids / tile coordinates and serializable, that // mutates the simulation. Every sim mutation during play flows through a Command // applied at the single Simulation::apply chokepoint, so it can be recorded and // replayed (see docs/replay_design.md). // // Commands form a closed set dispatched by kind. They reference stable, // deterministic ids (BuildingId, tile coordinates, choice indices) — never raw // entt handles — so a recorded command resolves to the same target on replay. enum class CommandKind { PlaceBuilding, Deconstruct, CancelDeconstruction, RotateInPlace, SetRecipe, SetShipLayout, SetSiteSplitterFilters, SetSplitterFilters, ClearBeltTiles, ApplySchematicChoice, ExpandAsteroid, Reset }; struct Command { explicit Command(CommandKind kind) : kind(kind) {} virtual ~Command() = default; CommandKind kind; // Source of the command. Always 0 in single-player; lockstep multiplayer // later merges commands from multiple sources into one ordered stream. int playerId = 0; }; // Places a building and, atomically, configures it. The configuration fields are // bundled here (rather than as follow-up commands) because commands are applied // at a deferred tick boundary, so the caller never sees the new BuildingId — the // place-and-configure must happen as one unit inside apply(). struct PlaceBuildingCommand : Command { PlaceBuildingCommand() : Command(CommandKind::PlaceBuilding) {} BuildingType type = BuildingType::Miner; QPoint anchor; Rotation rotation = Rotation::East; // Optional configuration applied to the freshly placed (still-construction) // building. The caller (UI) is responsible for unlock/validity pre-filtering; // only fields that should apply are set. std::optional recipeId; std::optional shipLayout; bool hasSplitterFilters = false; std::vector splitterFilterA; std::vector splitterFilterB; }; // Marks a building for demolition: a construction site is removed instantly, a // built building is queued for deconstruction (REQ-BLD-DECONSTRUCT, REQ-BLD-DECON-QUEUE). struct DeconstructCommand : Command { DeconstructCommand() : Command(CommandKind::Deconstruct) {} std::optional id; }; // Takes a building back out of the deconstruction queue (REQ-BLD-DECONSTRUCT-CLICK, // REQ-BLD-DECONSTRUCT-BOX). No-op if the id is not currently queued. struct CancelDeconstructionCommand : Command { CancelDeconstructionCommand() : Command(CommandKind::CancelDeconstruction) {} std::optional id; }; struct RotateInPlaceCommand : Command { RotateInPlaceCommand() : Command(CommandKind::RotateInPlace) {} std::optional id; Rotation newRotation = Rotation::East; }; struct SetRecipeCommand : Command { SetRecipeCommand() : Command(CommandKind::SetRecipe) {} std::optional id; std::string recipeId; }; struct SetShipLayoutCommand : Command { SetShipLayoutCommand() : Command(CommandKind::SetShipLayout) {} std::optional id; ShipLayoutConfig layout; }; // Splitter filters for a queued / under-construction Splitter site (configured by // BuildingSystem before the splitter is registered with BeltSystem). struct SetSiteSplitterFiltersCommand : Command { SetSiteSplitterFiltersCommand() : Command(CommandKind::SetSiteSplitterFilters) {} std::optional id; std::vector filterA; std::vector filterB; }; // Splitter filters for an operational splitter, configured by tile via BeltSystem. struct SetSplitterFiltersCommand : Command { SetSplitterFiltersCommand() : Command(CommandKind::SetSplitterFilters) {} QPoint tile; std::vector filterA; std::vector filterB; }; struct ClearBeltTilesCommand : Command { ClearBeltTilesCommand() : Command(CommandKind::ClearBeltTiles) {} std::vector tiles; }; struct ApplySchematicChoiceCommand : Command { ApplySchematicChoiceCommand() : Command(CommandKind::ApplySchematicChoice) {} int choiceIndex = 0; }; // Unlocks the next asteroid expansion (REQ-EXP-UNLOCK). Carries no payload: the // simulation derives the cost and column count from its own expansion counter // and config, so a recorded command replays identically. struct ExpandAsteroidCommand : Command { ExpandAsteroidCommand() : Command(CommandKind::ExpandAsteroid) {} }; // Restart boundary: reinitializes the simulation with a fresh seed and, if // config is set, a reloaded config (GameConfig is move-only, so it is carried by // shared_ptr and moved into the sim on apply). A null config keeps the current // config. One replay file = one run between Reset boundaries. struct ResetCommand : Command { ResetCommand() : Command(CommandKind::Reset) {} std::shared_ptr config; // null = keep current config unsigned int seed = 0; };