From 9c3be0fbd0c227c2f5f97b934a0db48b30cfb1e2 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Wed, 5 Aug 2026 06:49:15 +0200 Subject: [PATCH] extract the production rules as free functions over config and building --- 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;