#include "ProductionRules.h" #include #include #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) { return computeShipyardRequiredMaterials(config, b.recipeId, b.shipLayout); } std::map computeShipyardRequiredMaterials(const GameConfig& config, const std::string& recipeId, const std::optional& shipLayout) { std::map requiredMaterials; const ShipDef* shipDef = config.ships.findShipDef(recipeId); if (!shipDef) { return requiredMaterials; } for (const RecipeIngredient& ing : shipDef->schematic.materials) { requiredMaterials[ing.item] += ing.amount; } if (shipLayout.has_value()) { for (const PlacedModule& pm : 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; } double computeShipyardProductionTimeSeconds( const GameConfig& config, const std::string& recipeId, const std::optional& shipLayout) { const ShipDef* shipDef = config.ships.findShipDef(recipeId); if (!shipDef) { return 0.0; } double seconds = shipDef->schematic.productionTimeSeconds; if (shipLayout.has_value()) { for (const PlacedModule& pm : shipLayout->placedModules) { const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId); if (!modDef) { continue; } seconds += modDef->productionTimeSeconds; } } return seconds; } 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 here and its // only idle reason is an output buffer without room for the next cycle. for (const RecipeDef* recipe : gatherCandidateRecipes(config, b)) { if (recipeInputsAvailable(b, *recipe)) { return true; } } return false; } bool outputBufferHasRoom(const Building& b, const ItemType& type, int itemCount) { const std::map::const_iterator capIt = b.outputBuffer.caps.find(type); const int cap = (capIt != b.outputBuffer.caps.end()) ? capIt->second : 0; return b.getOutputItemCount(type) + itemCount <= cap; } bool recipeOutputsFit(const Building& b, const RecipeDef& recipe) { if (b.type == BuildingType::ReprocessingPlant) { // One roll yields one of these, so each is measured on its own -- but all of them // have to fit, since which one it will be is not known yet. for (const RecipeOutput& out : recipe.outputs) { if (!outputBufferHasRoom(b, ItemType{out.item}, out.amount)) { return false; } } return true; } // A deterministic cycle deposits all of its outputs together. An item listed more // than once is produced in the sum of those amounts, so it is judged once, as a sum. std::map perCycle; for (const RecipeOutput& out : recipe.outputs) { perCycle[ItemType{out.item}] += out.amount; } for (const std::pair& entry : perCycle) { if (!outputBufferHasRoom(b, entry.first, entry.second)) { return false; } } return true; } bool canStartCycle(const GameConfig& config, const Building& b) { // A shipyard's completed cycle spawns a ship instead of filling an output buffer // (REQ-BLD-SHIPYARD), so holding the materials is the whole condition. if (b.type == BuildingType::Shipyard) { return hasInputsToStart(config, b); } for (const RecipeDef* recipe : gatherCandidateRecipes(config, b)) { if (recipeInputsAvailable(b, *recipe) && recipeOutputsFit(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, but blocked by neither condition: the building is only between cycles and // the simulation starts the next one on a following tick. A building running back // to back sits here for exactly one tick per cycle, since tickProduction never // starts a cycle in the tick one completed, so this must read as producing rather // than blink (REQ-UI-STATUS-LIGHT, REQ-MAT-CYCLE). if (canStartCycle(config, building)) { return ProductionStatus::Producing; } // Idle for a reason: a missing input (red) takes precedence over an output buffer // with no room for the next cycle's output (yellow). return hasInputsToStart(config, building) ? ProductionStatus::Blocked : ProductionStatus::Starved; }