#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; }