From 3272431353aa468de7369e5dc96585cfc149410f Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Tue, 4 Aug 2026 22:17:01 +0200 Subject: [PATCH] extract the production rules as free functions over config and building MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gatherCandidateRecipes, recipeInputsAvailable, computeShipyardRequiredMaterials, hasInputsToStart and getProductionStatus read no factory state — they answer "what can this building produce, and can it start" from the config and the Building alone. They move to ProductionRules.h as free functions, and the ProductionStatus enum goes with them since it is that group's return type. Two of the five are pure in their arguments; the other three need GameConfig through gatherCandidateRecipes and computeShipyardRequiredMaterials, so config is a parameter rather than the group being split across two headers. Only two callers outside BuildingSystem existed — the status light in GameWorldView and one test lambda — so this is nearly all internal. Verified with a golden-checksum capture before and after — all four sample ticks identical. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG --- src/lib/sim/BuildingSystem.cpp | 141 +------------------------------ src/lib/sim/BuildingSystem.h | 19 +---- src/lib/sim/CMakeLists.txt | 2 + src/lib/sim/ProductionRules.cpp | 142 ++++++++++++++++++++++++++++++++ src/lib/sim/ProductionRules.h | 47 +++++++++++ src/test/BuildingTest.cpp | 3 +- src/ui/GameWorldView.cpp | 3 +- 7 files changed, 199 insertions(+), 158 deletions(-) create mode 100644 src/lib/sim/ProductionRules.cpp create mode 100644 src/lib/sim/ProductionRules.h diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 9761702..7155153 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -7,6 +7,7 @@ #include #include "FactoryQueries.h" +#include "ProductionRules.h" #include "PortGeometry.h" #include "StateChecksum.h" #include "SurfaceMask.h" @@ -1008,7 +1009,7 @@ void BuildingSystem::tickProduction(Tick currentTick) // are satisfied (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). Other buildings // try only their selected recipe. const std::vector candidates = - gatherCandidateRecipes(building); + gatherCandidateRecipes(m_config, building); for (const RecipeDef* recipe : candidates) { @@ -1112,7 +1113,7 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick) // Build combined materials list (base + modules). const std::map requiredMaterials = - computeShipyardRequiredMaterials(building); + computeShipyardRequiredMaterials(m_config, building); // Idle: check if all combined materials are available. bool inputsOk = true; @@ -1257,146 +1258,10 @@ void BuildingSystem::forEachIncomingItem( // Queries // --------------------------------------------------------------------------- -std::vector -BuildingSystem::gatherCandidateRecipes(const Building& b) const -{ - std::vector candidates; - if (isAutoRecipeBuildingType(b.type)) - { - for (const RecipeDef& r : m_config.recipes.recipes) - { - if (r.building == b.type && !r.inputs.empty()) - { - candidates.push_back(&r); - } - } - } - else - { - const RecipeDef* recipe = m_config.recipes.findRecipeDef(b.recipeId, b.type); - if (recipe) - { - candidates.push_back(recipe); - } - } - return candidates; -} -bool BuildingSystem::recipeInputsAvailable(const Building& b, - const RecipeDef& recipe) const -{ - for (const RecipeIngredient& ing : recipe.inputs) - { - const std::map::const_iterator it = - b.inputBuffer.counts.find(ItemType{ing.item}); - const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0; - if (have < ing.amount) - { - return false; - } - } - return true; -} -std::map -BuildingSystem::computeShipyardRequiredMaterials(const Building& b) const -{ - std::map requiredMaterials; - const ShipDef* shipDef = m_config.ships.findShipDef(b.recipeId); - if (!shipDef) - { - return requiredMaterials; - } - for (const RecipeIngredient& ing : shipDef->schematic.materials) - { - requiredMaterials[ing.item] += ing.amount; - } - if (b.shipLayout.has_value()) - { - for (const PlacedModule& pm : b.shipLayout->placedModules) - { - const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId); - if (!modDef) - { - continue; - } - for (const RecipeIngredient& ing : modDef->materials) - { - requiredMaterials[ing.item] += ing.amount; - } - } - } - return requiredMaterials; -} -bool BuildingSystem::hasInputsToStart(const Building& b) const -{ - if (b.type == BuildingType::Shipyard) - { - const std::map required = - computeShipyardRequiredMaterials(b); - for (const std::pair& req : required) - { - const std::map::const_iterator it = - b.inputBuffer.counts.find(ItemType{req.first}); - const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0; - if (have < req.second) - { - return false; - } - } - return true; - } - // Recipe buildings: startable if any candidate recipe's inputs are satisfied. - // A Miner recipe has no inputs, so an idle Miner is always startable and its - // only idle reason is a full output buffer. - for (const RecipeDef* recipe : gatherCandidateRecipes(b)) - { - if (recipeInputsAvailable(b, *recipe)) - { - return true; - } - } - return false; -} - -std::optional -BuildingSystem::getProductionStatus(const Building& building) const -{ - // Salvage Bay has no recipe or production cycle (REQ-BLD-SALVAGE-BAY): it is - // "producing" while it holds scrap to push out, and starved when empty. - if (building.type == BuildingType::SalvageBay) - { - return building.getOutputItemCount() >= 1 ? ProductionStatus::Producing - : ProductionStatus::Starved; - } - - // Only the five recipe/cycle production types show a status light besides the - // Salvage Bay; belts, splitters, tunnels, HQ, and stations show none. - if (!isProductionBuildingType(building.type)) - { - return std::nullopt; - } - - // Grey only applies to player-configured types; auto-recipe buildings - // (Smelter, Reprocessing Plant) always run an implicit recipe. - if (!isAutoRecipeBuildingType(building.type) && building.recipeId.empty()) - { - return ProductionStatus::Unconfigured; - } - - if (building.production.has_value()) - { - return ProductionStatus::Producing; - } - - // Idle: missing inputs (red) take precedence over a full output buffer - // (yellow). If inputs are present yet the building is idle, the only remaining - // reason it could not start a cycle is a full output buffer (REQ-MAT-CYCLE). - return hasInputsToStart(building) ? ProductionStatus::Blocked - : ProductionStatus::Starved; -} std::vector BuildingSystem::getAllBeltTiles() const { diff --git a/src/lib/sim/BuildingSystem.h b/src/lib/sim/BuildingSystem.h index 4e70846..69bd119 100644 --- a/src/lib/sim/BuildingSystem.h +++ b/src/lib/sim/BuildingSystem.h @@ -16,6 +16,7 @@ #include "BeltSystem.h" #include "Building.h" #include "FactoryState.h" +#include "ProductionRules.h" #include "BuildingType.h" #include "BuildingId.h" #include "GameConfig.h" @@ -27,18 +28,6 @@ class Hasher; -// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT). -// The simulation owns the classification so it stays in sync with the -// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill -// color. -enum class ProductionStatus -{ - Unconfigured, // no recipe/schematic selected (grey) - Producing, // a production cycle is active (green) - Starved, // idle: a required input is missing / Salvage Bay empty (red) - Blocked, // idle: output buffer full, inputs otherwise present (yellow) -}; - // 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; @@ -150,7 +139,6 @@ public: // nullopt for building types that show no light (belts, splitters, tunnels, // HQ, defence stations). The Salvage Bay is a two-state special case: // Producing while its output buffer holds scrap, Starved when empty. - std::optional getProductionStatus(const Building& building) const; std::vector getAllBeltTiles() const; // Visits every item currently emerging from a building output port on its @@ -245,17 +233,12 @@ private: // building (Smelter, Reprocessing Plant) offers every recipe of its type with // inputs; other buildings offer only their selected recipe. Shared by // tickProduction and the status classifier (REQ-MAT-CYCLE, REQ-UI-STATUS-LIGHT). - std::vector gatherCandidateRecipes(const Building& b) const; // True if every input of `recipe` is present in `b`'s input buffers in the // required per-cycle amount (REQ-MAT-CYCLE input check). - bool recipeInputsAvailable(const Building& b, - const RecipeDef& recipe) const; // Combined base + module materials a shipyard needs per ship (REQ-BLD-SHIPYARD). - std::map computeShipyardRequiredMaterials(const Building& b) const; // True if the building currently has all inputs/materials to start a cycle // (ignoring output-buffer space); drives the Starved/Blocked distinction of // the status light (REQ-UI-STATUS-LIGHT). - bool hasInputsToStart(const Building& b) const; void initBuffers(Building& b, const RecipeDef& recipe) const; // Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input diff --git a/src/lib/sim/CMakeLists.txt b/src/lib/sim/CMakeLists.txt index 793e500..479074e 100644 --- a/src/lib/sim/CMakeLists.txt +++ b/src/lib/sim/CMakeLists.txt @@ -15,6 +15,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h ${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h ${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h + ${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h @@ -41,6 +42,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp ${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp diff --git a/src/lib/sim/ProductionRules.cpp b/src/lib/sim/ProductionRules.cpp new file mode 100644 index 0000000..ee4d191 --- /dev/null +++ b/src/lib/sim/ProductionRules.cpp @@ -0,0 +1,142 @@ +#include "ProductionRules.h" + +#include "BuildingType.h" +#include "ItemType.h" +#include "ModulesConfig.h" +#include "ShipsConfig.h" + +std::vector +gatherCandidateRecipes(const GameConfig& config, const Building& b) +{ + std::vector candidates; + if (isAutoRecipeBuildingType(b.type)) + { + for (const RecipeDef& r : config.recipes.recipes) + { + if (r.building == b.type && !r.inputs.empty()) + { + candidates.push_back(&r); + } + } + } + else + { + const RecipeDef* recipe = config.recipes.findRecipeDef(b.recipeId, b.type); + if (recipe) + { + candidates.push_back(recipe); + } + } + return candidates; +} +bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe) +{ + for (const RecipeIngredient& ing : recipe.inputs) + { + const std::map::const_iterator it = + b.inputBuffer.counts.find(ItemType{ing.item}); + const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0; + if (have < ing.amount) + { + return false; + } + } + return true; +} +std::map +computeShipyardRequiredMaterials(const GameConfig& config, const Building& b) +{ + std::map requiredMaterials; + const ShipDef* shipDef = config.ships.findShipDef(b.recipeId); + if (!shipDef) + { + return requiredMaterials; + } + for (const RecipeIngredient& ing : shipDef->schematic.materials) + { + requiredMaterials[ing.item] += ing.amount; + } + if (b.shipLayout.has_value()) + { + for (const PlacedModule& pm : b.shipLayout->placedModules) + { + const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId); + if (!modDef) + { + continue; + } + for (const RecipeIngredient& ing : modDef->materials) + { + requiredMaterials[ing.item] += ing.amount; + } + } + } + return requiredMaterials; +} +bool hasInputsToStart(const GameConfig& config, const Building& b) +{ + if (b.type == BuildingType::Shipyard) + { + const std::map required = + computeShipyardRequiredMaterials(config, b); + for (const std::pair& req : required) + { + const std::map::const_iterator it = + b.inputBuffer.counts.find(ItemType{req.first}); + const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0; + if (have < req.second) + { + return false; + } + } + return true; + } + + // Recipe buildings: startable if any candidate recipe's inputs are satisfied. + // A Miner recipe has no inputs, so an idle Miner is always startable and its + // only idle reason is a full output buffer. + for (const RecipeDef* recipe : gatherCandidateRecipes(config, b)) + { + if (recipeInputsAvailable(b, *recipe)) + { + return true; + } + } + return false; +} +std::optional +getProductionStatus(const GameConfig& config, const Building& building) +{ + // Salvage Bay has no recipe or production cycle (REQ-BLD-SALVAGE-BAY): it is + // "producing" while it holds scrap to push out, and starved when empty. + if (building.type == BuildingType::SalvageBay) + { + return building.getOutputItemCount() >= 1 ? ProductionStatus::Producing + : ProductionStatus::Starved; + } + + // Only the five recipe/cycle production types show a status light besides the + // Salvage Bay; belts, splitters, tunnels, HQ, and stations show none. + if (!isProductionBuildingType(building.type)) + { + return std::nullopt; + } + + // Grey only applies to player-configured types; auto-recipe buildings + // (Smelter, Reprocessing Plant) always run an implicit recipe. + if (!isAutoRecipeBuildingType(building.type) && building.recipeId.empty()) + { + return ProductionStatus::Unconfigured; + } + + if (building.production.has_value()) + { + return ProductionStatus::Producing; + } + + // Idle: missing inputs (red) take precedence over a full output buffer + // (yellow). If inputs are present yet the building is idle, the only remaining + // reason it could not start a cycle is a full output buffer (REQ-MAT-CYCLE). + return hasInputsToStart(config, building) ? ProductionStatus::Blocked + : ProductionStatus::Starved; +} diff --git a/src/lib/sim/ProductionRules.h b/src/lib/sim/ProductionRules.h new file mode 100644 index 0000000..caba2ec --- /dev/null +++ b/src/lib/sim/ProductionRules.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include "Building.h" +#include "GameConfig.h" +#include "RecipesConfig.h" + +// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT). +// The simulation owns the classification so it stays in sync with the +// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill +// color. +enum class ProductionStatus +{ + Unconfigured, // no recipe/schematic selected (grey) + Producing, // a production cycle is active (green) + Starved, // idle: a required input is missing / Salvage Bay empty (red) + Blocked, // idle: output buffer full, inputs otherwise present (yellow) +}; + +// The rules deciding what a building can produce and whether it can start. +// Pure functions of the config and the building itself — they read no factory +// state, so they are free functions rather than BuildingSystem members. + +// Recipes this building could run: every recipe of its type for an auto-recipe +// building (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), otherwise just its selected one. +std::vector gatherCandidateRecipes(const GameConfig& config, + const Building& b); + +// True when the building's input buffer holds every ingredient the recipe needs. +bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe); + +// Total materials a shipyard needs for its schematic plus its placed modules +// (REQ-BLD-SHIPYARD), keyed by item id. +std::map computeShipyardRequiredMaterials(const GameConfig& config, + const Building& b); + +// True when a production cycle could start right now, ignoring output-buffer space. +bool hasInputsToStart(const GameConfig& config, const Building& b); + +// Status light for a building, or nullopt for types that show none — belts, +// splitters, tunnels, HQ and defence stations (REQ-UI-STATUS-LIGHT). +std::optional getProductionStatus(const GameConfig& config, + const Building& building); diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index d557c51..2184151 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -1,5 +1,6 @@ #include "catch.hpp" #include "FactoryQueries.h" +#include "ProductionRules.h" #include #include @@ -1562,7 +1563,7 @@ TEST_CASE("BuildingSystem: getProductionStatus classifies production state", "[b } REQUIRE_FALSE(shipId.empty()); - const auto statusOf = [&f](const Building& b) { return f.bs.getProductionStatus(b); }; + const auto statusOf = [&f](const Building& b) { return getProductionStatus(f.cfg, b); }; SECTION("non-production buildings show no status light") { diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index f07f7cb..12ebe23 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -1,5 +1,6 @@ #include "GameWorldView.h" #include "FactoryQueries.h" +#include "ProductionRules.h" #include #include @@ -1341,7 +1342,7 @@ void GameWorldView::drawBuildings(QPainter& painter) // building footprints are rectangular, so a corner of the axis-aligned // bounding box is the true corner. if (const std::optional status = - m_sim->getBuildings().getProductionStatus(b)) + getProductionStatus(m_sim->getConfig(), b)) { const float px = getTilePx(); const float r = px * 0.18f;