Files
dota_factory/src/lib/sim/ProductionRules.cpp
Malte Langkabel 9c275e283c give every recipe one shape: a list of output groups
Implements REQ-MAT-OUTPUT-GROUP. A recipe had two shapes -- outputs produced
together, or outputs of which exactly one happened -- and every rule over them
was written twice, selected by `building == ReprocessingPlant`: sizing a
buffer, deciding whether a cycle fits, resolving what a cycle makes, costing an
item. RecipeDef now holds output groups, each a weight and a list of items, and
a cycle yields exactly one group. One group is the ordinary recipe, so the old
two cases are the same shape with one and with several, and all four rules
collapse to one expression apiece with no building-type test left.

rollReprocessingOutput becomes rollOutputGroup, where a single group returns
without drawing or testing eligibility. That early-out is load-bearing twice
over. Drawing there would consume entropy for every ordinary recipe and shift
every later random outcome; and eligibility must not apply either, since
implicit unlocking is demand-derived, so an ordinary recipe's output can be
producible while nothing yet calls for it -- testing it would stop the building
producing rather than gate a drop. Past the early-out a group is eligible only
when all of its items are unlocked, being produced whole.

Threat follows the recipe's shape rather than the building, and the per-unit
value now divides by the group's amount as well as its odds. That moves no
number today: every item resolved through this path has amount 1, which is why
the threat expectations are untouched.

Config keeps `outputs = [...]` as the single-group form, so only the two
reprocessing recipes change shape. The recipe summary gains "/" between groups
and keeps "+" within one, which also fixes the plant reading as though a cycle
produced all of its items at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
2026-08-17 12:47:09 +02:00

237 lines
7.8 KiB
C++

#include "ProductionRules.h"
#include <algorithm>
#include <map>
#include "BuildingType.h"
#include "ItemType.h"
#include "ModulesConfig.h"
#include "ShipsConfig.h"
const RecipeDef* getSelectedRecipe(const GameConfig& config, const Building& b)
{
if (b.recipeId.empty())
{
return nullptr;
}
return config.recipes.findRecipeDef(b.recipeId, b.type);
}
const RecipeDef* findAutoRecipeFor(const GameConfig& config, BuildingType type,
const ItemType& item)
{
if (!isAutoRecipeBuildingType(type))
{
return nullptr;
}
// Config order decides where a material feeds more than one recipe of the type, so
// the same offer always picks the same recipe (REQ-BLD-AUTO-RECIPE).
for (const RecipeDef& recipe : config.recipes.recipes)
{
if (recipe.building != type) { continue; }
for (const RecipeIngredient& ing : recipe.inputs)
{
if (ItemType{ing.item} == item)
{
return &recipe;
}
}
}
return nullptr;
}
bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe)
{
for (const RecipeIngredient& ing : recipe.inputs)
{
const std::map<ItemType, int>::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<std::string, int>
computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
{
return computeShipyardRequiredMaterials(config, b.recipeId, b.shipLayout);
}
std::map<std::string, int>
computeShipyardRequiredMaterials(const GameConfig& config,
const std::string& recipeId,
const std::optional<ShipLayoutConfig>& shipLayout)
{
std::map<std::string, int> 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<ShipLayoutConfig>& 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<std::string, int> required =
computeShipyardRequiredMaterials(config, b);
for (const std::pair<const std::string, int>& req : required)
{
const std::map<ItemType, int>::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 the selected 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.
const RecipeDef* recipe = getSelectedRecipe(config, b);
return recipe != nullptr && recipeInputsAvailable(b, *recipe);
}
bool outputBufferHasRoom(const Building& b, const ItemType& type, int itemCount)
{
const std::map<ItemType, int>::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)
{
for (const RecipeOutputGroup& group : recipe.outputGroups)
{
// A group's items come together, so an item listed twice in one is produced in the
// sum of those amounts and judged once, as a sum.
std::map<ItemType, int> perCycle;
for (const RecipeOutput& out : group.items)
{
perCycle[ItemType{out.item}] += out.amount;
}
for (const std::pair<const ItemType, int>& 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);
}
const RecipeDef* recipe = getSelectedRecipe(config, b);
return recipe != nullptr && recipeInputsAvailable(b, *recipe)
&& recipeOutputsFit(b, *recipe);
}
std::optional<ProductionStatus>
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;
}
// Every production building can be unconfigured, an auto-recipe building included:
// it holds no recipe until one is offered to it, and the player can hand it back to
// automatic selection (REQ-BLD-AUTO-RECIPE, REQ-UI-STATUS-LIGHT).
if (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;
}