make recipe selection and buffers of smelter and reprocessing plant behave like other buildings, except that a recipe may be chosen automatically
This commit is contained in:
@@ -59,6 +59,8 @@ bool isConfigurableBuildingType(BuildingType type)
|
||||
{
|
||||
case BuildingType::Miner: // recipe (REQ-BLD-MINER)
|
||||
case BuildingType::Assembler: // recipe (REQ-BLD-ASSEMBLER)
|
||||
case BuildingType::Smelter: // recipe (REQ-BLD-AUTO-RECIPE)
|
||||
case BuildingType::ReprocessingPlant: // recipe (REQ-BLD-AUTO-RECIPE)
|
||||
case BuildingType::Shipyard: // schematic and layout (REQ-BLD-SHIPYARD, REQ-MOD-LAYOUT)
|
||||
case BuildingType::Splitter: // output filters (REQ-BLD-SPLITTER)
|
||||
return true;
|
||||
|
||||
@@ -30,9 +30,9 @@ std::optional<BuildingType> parseBuildingType(const std::string& id);
|
||||
// Canonical id string for a BuildingType. The inverse of parseBuildingType.
|
||||
std::string buildingTypeId(BuildingType type);
|
||||
|
||||
// Smelter and Reprocessing Plant have no player-selected recipe
|
||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). They auto-process whatever inputs
|
||||
// they receive, matching against every recipe of their building type.
|
||||
// Smelter and Reprocessing Plant pick a recipe for themselves from the first material
|
||||
// offered to them while they have none (REQ-BLD-AUTO-RECIPE). In every other respect
|
||||
// their recipe is selected and held exactly as any other building's.
|
||||
bool isAutoRecipeBuildingType(BuildingType type);
|
||||
|
||||
// Buildings that run a production cycle: Miner, Smelter, Assembler, Reprocessing
|
||||
@@ -45,7 +45,9 @@ bool isProductionBuildingType(BuildingType type);
|
||||
bool isBeltSubsystemType(BuildingType type);
|
||||
|
||||
// Building types with player-facing settings that a blueprint can carry and hand to an
|
||||
// existing building (REQ-UI-BLUEPRINT-TRANSFER): Miner and Assembler (recipe), Shipyard
|
||||
// (schematic and module layout), Splitter (output filters). Every other type has nothing
|
||||
// to configure, so a blueprint of one has nothing to transfer.
|
||||
// existing building (REQ-UI-BLUEPRINT-TRANSFER): Miner, Assembler, Smelter and
|
||||
// Reprocessing Plant (recipe -- the last two select their own when they have none,
|
||||
// REQ-BLD-AUTO-RECIPE), Shipyard (schematic and module layout), Splitter (output
|
||||
// filters). Every other type has nothing to configure, so a blueprint of one has
|
||||
// nothing to transfer.
|
||||
bool isConfigurableBuildingType(BuildingType type);
|
||||
|
||||
@@ -60,36 +60,6 @@ void initBuffers(Building& b, const RecipeDef& recipe)
|
||||
addOutputCaps(b.outputBuffer.caps, b.type, recipe);
|
||||
}
|
||||
|
||||
void initAutoBuffers(const GameConfig& config, Building& b)
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
b.inputBuffer.caps.clear();
|
||||
|
||||
b.outputBuffer.items.clear();
|
||||
b.outputBuffer.caps.clear();
|
||||
|
||||
// Union both sides over every recipe of this building type: the cap for each item is
|
||||
// twice the largest per-cycle amount across those recipes, on the input side as on
|
||||
// the output side (REQ-MAT-INPUT-BUFFER, REQ-MAT-OUTPUT-BUFFER).
|
||||
for (const RecipeDef& recipe : config.recipes.recipes)
|
||||
{
|
||||
if (recipe.building != b.type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const RecipeIngredient& ing : recipe.inputs)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts[type] = 0;
|
||||
b.inputBuffer.caps[type] =
|
||||
std::max(b.inputBuffer.caps[type], 2 * ing.amount);
|
||||
}
|
||||
|
||||
addOutputCaps(b.outputBuffer.caps, b.type, recipe);
|
||||
}
|
||||
}
|
||||
|
||||
void initShipyardBuffers(const GameConfig& config, Building& b)
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
|
||||
@@ -18,10 +18,6 @@
|
||||
// REQ-MAT-OUTPUT-BUFFER).
|
||||
void initBuffers(Building& b, const RecipeDef& recipe);
|
||||
|
||||
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant), unioned over
|
||||
// every recipe of its type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||
void initAutoBuffers(const GameConfig& config, Building& b);
|
||||
|
||||
// Buffers for a shipyard: its schematic's materials plus those of every placed
|
||||
// module (REQ-BLD-SHIPYARD).
|
||||
void initShipyardBuffers(const GameConfig& config, Building& b);
|
||||
|
||||
@@ -200,12 +200,6 @@ void BuildingSystem::setRecipe(FactoryState& state, BuildingId id, const std::st
|
||||
{
|
||||
if (site.id == id)
|
||||
{
|
||||
// Auto-recipe buildings have no player-selected recipe
|
||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING); ignore any attempt to set one.
|
||||
if (isAutoRecipeBuildingType(site.type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// No-op if the recipe is unchanged, so a redundant selection does
|
||||
// not wipe an already-configured ship layout.
|
||||
if (site.recipeId == recipeId)
|
||||
@@ -223,12 +217,6 @@ void BuildingSystem::setRecipe(FactoryState& state, BuildingId id, const std::st
|
||||
{
|
||||
if (building.id == id)
|
||||
{
|
||||
// Auto-recipe buildings have no player-selected recipe
|
||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING); ignore any attempt to set one.
|
||||
if (isAutoRecipeBuildingType(building.type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// No-op if the recipe is unchanged, so a redundant selection does
|
||||
// not wipe an already-configured ship layout or reset buffers.
|
||||
if (building.recipeId == recipeId)
|
||||
@@ -407,6 +395,10 @@ void BuildingSystem::tickBeltPull(FactoryState& state)
|
||||
{
|
||||
const std::optional<ItemType> peeked = m_belts.peekItem(building.inputPorts[i]);
|
||||
if (!peeked) { continue; }
|
||||
// A Smelter or Reprocessing Plant without a recipe takes the first material
|
||||
// offered to it as its selection (REQ-BLD-AUTO-RECIPE); the ports are walked
|
||||
// in order, so which offer comes first is fixed.
|
||||
selectAutoRecipeIfUnset(building, *peeked);
|
||||
if (!canAcceptInput(building, i, *peeked)) { continue; }
|
||||
const std::optional<Item> taken = m_belts.tryTakeItem(building.inputPorts[i]);
|
||||
if (taken)
|
||||
@@ -417,6 +409,25 @@ void BuildingSystem::tickBeltPull(FactoryState& state)
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::selectAutoRecipeIfUnset(Building& building, const ItemType& offered)
|
||||
{
|
||||
// Only while it holds none: once set, a recipe is the player's to change
|
||||
// (REQ-BLD-AUTO-RECIPE). Buildings that select their own recipe are the only ones
|
||||
// this applies to; everyone else ignores an offer they have no recipe for.
|
||||
if (!building.recipeId.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const RecipeDef* recipe = findAutoRecipeFor(m_config, building.type, offered);
|
||||
if (!recipe)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
building.recipeId = recipe->id;
|
||||
initBuffers(building, *recipe);
|
||||
}
|
||||
|
||||
bool BuildingSystem::canAcceptInput(const Building& consumer,
|
||||
std::size_t inputPortIndex,
|
||||
const ItemType& type) const
|
||||
@@ -477,6 +488,10 @@ bool BuildingSystem::tryDirectCoupleDeposit(FactoryState& state, BuildingId prod
|
||||
if (in.direction != outputPort.direction) { continue; }
|
||||
if (inputBodyTile(in.tile, in.direction) != outputPort.tile) { continue; }
|
||||
|
||||
// A coupling is an offer too, so an unset auto-recipe building selects from it
|
||||
// (REQ-BLD-AUTO-RECIPE). Without this a Smelter placed flush against a producer
|
||||
// would accept nothing and leave it stuck at its port for good.
|
||||
selectAutoRecipeIfUnset(*consumer, item.type);
|
||||
if (!canAcceptInput(*consumer, j, item.type)) { return false; }
|
||||
depositToInputBelt(*consumer, j, item);
|
||||
return true;
|
||||
@@ -502,15 +517,14 @@ void BuildingSystem::tickProduction(FactoryState& state, Tick currentTick)
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool autoRecipe = isAutoRecipeBuildingType(building.type);
|
||||
if (!autoRecipe && building.recipeId.empty())
|
||||
if (building.recipeId.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If a production cycle is active, check for completion. Completion only
|
||||
// needs the already-decided outputs, so it does not depend on which
|
||||
// recipe is selected or auto-chosen.
|
||||
// recipe is selected.
|
||||
if (building.production)
|
||||
{
|
||||
if (currentTick < building.production->completesAt)
|
||||
@@ -531,68 +545,64 @@ void BuildingSystem::tickProduction(FactoryState& state, Tick currentTick)
|
||||
// waits, exactly as it would have on the following tick.
|
||||
}
|
||||
|
||||
// Idle: gather the candidate recipes to try. Auto-recipe buildings
|
||||
// (Smelter, Reprocessing Plant) have no selected recipe and try every
|
||||
// recipe of their type in config order, running the first whose inputs
|
||||
// are satisfied (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). Other buildings
|
||||
// try only their selected recipe.
|
||||
const std::vector<const RecipeDef*> candidates =
|
||||
gatherCandidateRecipes(m_config, building);
|
||||
|
||||
for (const RecipeDef* recipe : candidates)
|
||||
// Idle: try to start the building's one selected recipe. Every type holds
|
||||
// exactly one, a Smelter and a Reprocessing Plant included -- they differ only
|
||||
// in how theirs first got set (REQ-BLD-AUTO-RECIPE).
|
||||
const RecipeDef* recipe = getSelectedRecipe(m_config, building);
|
||||
if (!recipe)
|
||||
{
|
||||
// 1. All required inputs present?
|
||||
if (!recipeInputsAvailable(building, *recipe))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Room for every output this cycle could produce -- checked before
|
||||
// anything is rolled (REQ-MAT-CYCLE). The roll below is committed the
|
||||
// moment the cycle starts, so a plant that could not store some outcome
|
||||
// must not start at all: that is what stops a stalled output belt from
|
||||
// biasing the distribution towards the outputs that still fit. Emerging
|
||||
// items count against their buffer (REQ-MAT-OUTPUT-EMERGE). The status
|
||||
// light asks the same question to decide yellow (REQ-UI-STATUS-LIGHT), so
|
||||
// the test lives in one place.
|
||||
if (!recipeOutputsFit(building, *recipe))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 1. All required inputs present?
|
||||
if (!recipeInputsAvailable(building, *recipe))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Determine chosen outputs (roll for reprocessing).
|
||||
std::vector<Item> chosen;
|
||||
if (building.type == BuildingType::ReprocessingPlant)
|
||||
// 2. Room for every output this cycle could produce -- checked before anything
|
||||
// is rolled (REQ-MAT-CYCLE). The roll below is committed the moment the cycle
|
||||
// starts, so a plant that could not store some outcome must not start at all:
|
||||
// that is what stops a stalled output belt from biasing the distribution
|
||||
// towards the outputs that still fit. Emerging items count against their
|
||||
// buffer (REQ-MAT-OUTPUT-EMERGE). The status light asks the same question to
|
||||
// decide yellow (REQ-UI-STATUS-LIGHT), so the test lives in one place.
|
||||
if (!recipeOutputsFit(building, *recipe))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Determine chosen outputs (roll for reprocessing).
|
||||
std::vector<Item> chosen;
|
||||
if (building.type == BuildingType::ReprocessingPlant)
|
||||
{
|
||||
chosen = rollReprocessingOutput(*recipe);
|
||||
if (chosen.empty()) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const RecipeOutput& out : recipe->outputs)
|
||||
{
|
||||
chosen = rollReprocessingOutput(*recipe);
|
||||
if (chosen.empty()) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const RecipeOutput& out : recipe->outputs)
|
||||
Item item;
|
||||
item.type.id = out.item;
|
||||
for (int i = 0; i < out.amount; ++i)
|
||||
{
|
||||
Item item;
|
||||
item.type.id = out.item;
|
||||
for (int i = 0; i < out.amount; ++i)
|
||||
{
|
||||
chosen.push_back(item);
|
||||
}
|
||||
chosen.push_back(item);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Consume inputs and start cycle.
|
||||
for (const RecipeIngredient& ing : recipe->inputs)
|
||||
{
|
||||
building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
|
||||
}
|
||||
|
||||
Production prod;
|
||||
prod.recipeId = recipe->id;
|
||||
prod.completesAt = currentTick + secondsToTicks(recipe->durationSeconds);
|
||||
prod.chosenOutputs = std::move(chosen);
|
||||
building.production = std::move(prod);
|
||||
break; // At most one cycle starts per tick.
|
||||
}
|
||||
|
||||
// 4. Consume inputs and start cycle.
|
||||
for (const RecipeIngredient& ing : recipe->inputs)
|
||||
{
|
||||
building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
|
||||
}
|
||||
|
||||
Production prod;
|
||||
prod.recipeId = recipe->id;
|
||||
prod.completesAt = currentTick + secondsToTicks(recipe->durationSeconds);
|
||||
prod.chosenOutputs = std::move(chosen);
|
||||
building.production = std::move(prod);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -188,6 +188,13 @@ private:
|
||||
// (on construction completion, or when un-queuing a deconstruction). No-op for
|
||||
// non-belt-subsystem types. Splitter filters are (re)applied after placement.
|
||||
|
||||
// Selects a recipe for an auto-recipe building that has none, from a material being
|
||||
// offered to it at one of its input ports (REQ-BLD-AUTO-RECIPE). No-op for every
|
||||
// other building, for one that already holds a recipe, and for a material none of
|
||||
// its recipes consumes. Called from both intake paths -- the belt pull and the
|
||||
// direct coupling -- since either can be where the first material arrives.
|
||||
void selectAutoRecipeIfUnset(Building& building,
|
||||
const ItemType& offered);
|
||||
// True if the consumer would accept `type` at the given input port right now:
|
||||
// it is a required input (or a building block for the HQ), the reservation-aware
|
||||
// buffer has room, and the input belt entry is free (REQ-MAT-INPUT-INTAKE).
|
||||
|
||||
@@ -67,12 +67,6 @@ void ConstructionSystem::tick(FactoryState& state, BeltSystem& belts, Tick curre
|
||||
{
|
||||
initSalvageBayBuffer(m_config, building);
|
||||
}
|
||||
else if (isAutoRecipeBuildingType(building.type))
|
||||
{
|
||||
// Smelter/Reprocessing Plant need no recipe selection; buffers are set
|
||||
// up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||
initAutoBuffers(m_config, building);
|
||||
}
|
||||
else if (!building.recipeId.empty())
|
||||
{
|
||||
if (building.type == BuildingType::Shipyard)
|
||||
|
||||
@@ -8,29 +8,37 @@
|
||||
#include "ModulesConfig.h"
|
||||
#include "ShipsConfig.h"
|
||||
|
||||
std::vector<const RecipeDef*>
|
||||
gatherCandidateRecipes(const GameConfig& config, const Building& b)
|
||||
const RecipeDef* getSelectedRecipe(const GameConfig& config, const Building& b)
|
||||
{
|
||||
std::vector<const RecipeDef*> candidates;
|
||||
if (isAutoRecipeBuildingType(b.type))
|
||||
if (b.recipeId.empty())
|
||||
{
|
||||
for (const RecipeDef& r : config.recipes.recipes)
|
||||
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 (r.building == b.type && !r.inputs.empty())
|
||||
if (ItemType{ing.item} == item)
|
||||
{
|
||||
candidates.push_back(&r);
|
||||
return &recipe;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const RecipeDef* recipe = config.recipes.findRecipeDef(b.recipeId, b.type);
|
||||
if (recipe)
|
||||
{
|
||||
candidates.push_back(recipe);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
return nullptr;
|
||||
}
|
||||
bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe)
|
||||
{
|
||||
@@ -130,17 +138,11 @@ bool hasInputsToStart(const GameConfig& config, const Building& b)
|
||||
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;
|
||||
// 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)
|
||||
@@ -192,14 +194,9 @@ bool canStartCycle(const GameConfig& config, const Building& b)
|
||||
return hasInputsToStart(config, b);
|
||||
}
|
||||
|
||||
for (const RecipeDef* recipe : gatherCandidateRecipes(config, b))
|
||||
{
|
||||
if (recipeInputsAvailable(b, *recipe) && recipeOutputsFit(b, *recipe))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
const RecipeDef* recipe = getSelectedRecipe(config, b);
|
||||
return recipe != nullptr && recipeInputsAvailable(b, *recipe)
|
||||
&& recipeOutputsFit(b, *recipe);
|
||||
}
|
||||
|
||||
std::optional<ProductionStatus>
|
||||
@@ -220,9 +217,10 @@ getProductionStatus(const GameConfig& config, const Building& building)
|
||||
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())
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -25,10 +25,16 @@ enum class ProductionStatus
|
||||
// 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<const RecipeDef*> gatherCandidateRecipes(const GameConfig& config,
|
||||
const Building& b);
|
||||
// The recipe this building runs, or null when it has none selected. Every building type
|
||||
// holds exactly one, a Smelter and a Reprocessing Plant included -- they only differ in
|
||||
// how theirs first gets set (REQ-BLD-AUTO-RECIPE).
|
||||
const RecipeDef* getSelectedRecipe(const GameConfig& config, const Building& b);
|
||||
|
||||
// The recipe an auto-recipe building adopts when this material is offered to it while it
|
||||
// has none: the first recipe of its type, in config order, that consumes the material
|
||||
// (REQ-BLD-AUTO-RECIPE). Null when no recipe of the type takes it.
|
||||
const RecipeDef* findAutoRecipeFor(const GameConfig& config, BuildingType type,
|
||||
const ItemType& item);
|
||||
|
||||
// True when the building's input buffer holds every ingredient the recipe needs.
|
||||
bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe);
|
||||
|
||||
Reference in New Issue
Block a user