fix issue where status light was flickering to yellow for one tick between cycles
This commit is contained in:
@@ -563,10 +563,10 @@ void BuildingSystem::tickProduction(FactoryState& state, Tick currentTick)
|
||||
}
|
||||
|
||||
// 3. Output buffer has space for chosen outputs? Emerging items still
|
||||
// count against the buffer (REQ-MAT-OUTPUT-EMERGE).
|
||||
const int newSize = building.getOutputItemCount()
|
||||
+ static_cast<int>(chosen.size());
|
||||
if (newSize > building.outputBuffer.capacity)
|
||||
// count against the 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 (!outputBufferHasRoom(building, static_cast<int>(chosen.size())))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,47 @@
|
||||
#include "ProductionRules.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
#include "BuildingType.h"
|
||||
#include "ItemType.h"
|
||||
#include "ModulesConfig.h"
|
||||
#include "ShipsConfig.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Items one cycle of this recipe would deposit into the output buffer. A Reprocessing
|
||||
// Plant rolls exactly one of its outputs per cycle (REQ-BLD-REPROCESSING) and the roll
|
||||
// happens in the simulation, so the smallest amount any roll could yield is what decides
|
||||
// whether a cycle could start at all; a larger roll may still not fit.
|
||||
int getCycleOutputItemCount(const Building& b, const RecipeDef& recipe)
|
||||
{
|
||||
if (recipe.outputs.empty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (b.type == BuildingType::ReprocessingPlant)
|
||||
{
|
||||
int smallest = std::numeric_limits<int>::max();
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
smallest = std::min(smallest, out.amount);
|
||||
}
|
||||
return smallest;
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
total += out.amount;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<const RecipeDef*>
|
||||
gatherCandidateRecipes(const GameConfig& config, const Building& b)
|
||||
{
|
||||
@@ -128,8 +165,8 @@ bool hasInputsToStart(const GameConfig& config, const Building& b)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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))
|
||||
@@ -139,6 +176,32 @@ bool hasInputsToStart(const GameConfig& config, const Building& b)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool outputBufferHasRoom(const Building& b, int outputItemCount)
|
||||
{
|
||||
return b.getOutputItemCount() + outputItemCount <= b.outputBuffer.capacity;
|
||||
}
|
||||
|
||||
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)
|
||||
&& outputBufferHasRoom(b, getCycleOutputItemCount(b, *recipe)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<ProductionStatus>
|
||||
getProductionStatus(const GameConfig& config, const Building& building)
|
||||
{
|
||||
@@ -169,9 +232,18 @@ getProductionStatus(const GameConfig& config, const Building& building)
|
||||
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).
|
||||
// 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;
|
||||
: ProductionStatus::Starved;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,16 @@ double computeShipyardProductionTimeSeconds(
|
||||
// True when a production cycle could start right now, ignoring output-buffer space.
|
||||
bool hasInputsToStart(const GameConfig& config, const Building& b);
|
||||
|
||||
// True when the building's output side can take `outputItemCount` more items beside
|
||||
// what it already holds. An emerging item has not left the building yet and so still
|
||||
// counts against the capacity (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-OUTPUT-BUFFER).
|
||||
bool outputBufferHasRoom(const Building& b, int outputItemCount);
|
||||
|
||||
// True when a production cycle could actually start right now: some candidate recipe
|
||||
// has its inputs *and* its output fits (REQ-MAT-CYCLE). Stricter than
|
||||
// hasInputsToStart, which looks at the input buffers alone.
|
||||
bool canStartCycle(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<ProductionStatus> getProductionStatus(const GameConfig& config,
|
||||
|
||||
@@ -1515,36 +1515,167 @@ TEST_CASE("BuildingSystem: getProductionStatus classifies production state", "[b
|
||||
miner.production = Production{};
|
||||
REQUIRE(statusOf(miner) == ProductionStatus::Producing); // active cycle -> green
|
||||
|
||||
// A miner has no inputs, so its only idle reason is a full output buffer.
|
||||
// A miner has no inputs, so its only idle reason is an output buffer with no
|
||||
// room for the next cycle's output.
|
||||
miner.production = std::nullopt;
|
||||
miner.outputBuffer.capacity = 2;
|
||||
miner.outputBuffer.items = { makeItem("iron_ore"), makeItem("iron_ore") };
|
||||
REQUIRE(statusOf(miner) == ProductionStatus::Blocked); // -> yellow
|
||||
|
||||
// One item handed off: the next cycle fits again, so the idle tick between two
|
||||
// cycles reads as producing rather than blinking yellow (REQ-UI-STATUS-LIGHT).
|
||||
miner.outputBuffer.items.pop_back();
|
||||
REQUIRE(statusOf(miner) == ProductionStatus::Producing); // -> green
|
||||
|
||||
// An emerging item has not left the building, so it fills the freed slot and
|
||||
// blocks the cycle again (REQ-MAT-OUTPUT-EMERGE).
|
||||
miner.emergingItems.push_back({ BeltItemSlot{ makeItem("iron_ore"), 0.5 } });
|
||||
REQUIRE(miner.getOutputItemCount() == 2);
|
||||
REQUIRE(statusOf(miner) == ProductionStatus::Blocked); // -> yellow
|
||||
}
|
||||
|
||||
SECTION("Assembler: starved vs blocked, input-missing takes precedence")
|
||||
SECTION("Assembler: starved, the transient between cycles, then blocked")
|
||||
{
|
||||
Building assembler; assembler.type = BuildingType::Assembler;
|
||||
assembler.recipeId = assemblerRecipe->id;
|
||||
|
||||
int cycleOutput = 0;
|
||||
for (const RecipeOutput& out : assemblerRecipe->outputs)
|
||||
{
|
||||
cycleOutput += out.amount;
|
||||
}
|
||||
REQUIRE(cycleOutput > 0);
|
||||
// The buffer the simulation would give it (REQ-MAT-OUTPUT-BUFFER).
|
||||
assembler.outputBuffer.capacity = 2 * cycleOutput;
|
||||
|
||||
// Idle with inputs missing -> red.
|
||||
REQUIRE(statusOf(assembler) == ProductionStatus::Starved);
|
||||
|
||||
// Inputs present but idle -> the only remaining reason is a full output
|
||||
// buffer -> yellow.
|
||||
// Inputs present and the output fits: nothing blocks a cycle, so the building
|
||||
// is merely between cycles -> green, not yellow (REQ-UI-STATUS-LIGHT).
|
||||
for (const RecipeIngredient& ing : assemblerRecipe->inputs)
|
||||
{
|
||||
assembler.inputBuffer.counts[ItemType{ing.item}] = ing.amount;
|
||||
}
|
||||
REQUIRE(statusOf(assembler) == ProductionStatus::Producing);
|
||||
|
||||
// Filled to within less than one cycle's output of capacity: no cycle can start
|
||||
// -> yellow.
|
||||
for (int i = 0; i < cycleOutput + 1; ++i)
|
||||
{
|
||||
assembler.outputBuffer.items.push_back(makeItem("x"));
|
||||
}
|
||||
REQUIRE(statusOf(assembler) == ProductionStatus::Blocked);
|
||||
|
||||
// Inputs missing AND output full -> red wins over yellow.
|
||||
// Inputs missing AND output blocked -> red wins over yellow.
|
||||
assembler.inputBuffer.counts.clear();
|
||||
assembler.outputBuffer.capacity = 2;
|
||||
assembler.outputBuffer.items = { makeItem("x"), makeItem("x") };
|
||||
REQUIRE(statusOf(assembler) == ProductionStatus::Starved);
|
||||
}
|
||||
|
||||
SECTION("A multi-item cycle blocks before the output buffer is full")
|
||||
{
|
||||
// Free space smaller than one cycle's output stops the cycle even though the
|
||||
// buffer still has room, so yellow is not the same as "full" (REQ-MAT-CYCLE).
|
||||
const RecipeDef* multiOutputRecipe = nullptr;
|
||||
for (const RecipeDef& r : f.cfg.recipes.recipes)
|
||||
{
|
||||
if (r.building != BuildingType::Assembler || r.inputs.empty()) { continue; }
|
||||
int total = 0;
|
||||
for (const RecipeOutput& out : r.outputs) { total += out.amount; }
|
||||
if (total >= 2) { multiOutputRecipe = &r; break; }
|
||||
}
|
||||
REQUIRE(multiOutputRecipe != nullptr);
|
||||
|
||||
int cycleOutput = 0;
|
||||
for (const RecipeOutput& out : multiOutputRecipe->outputs)
|
||||
{
|
||||
cycleOutput += out.amount;
|
||||
}
|
||||
|
||||
Building assembler; assembler.type = BuildingType::Assembler;
|
||||
assembler.recipeId = multiOutputRecipe->id;
|
||||
assembler.outputBuffer.capacity = 2 * cycleOutput;
|
||||
for (const RecipeIngredient& ing : multiOutputRecipe->inputs)
|
||||
{
|
||||
assembler.inputBuffer.counts[ItemType{ing.item}] = ing.amount;
|
||||
}
|
||||
|
||||
// One item short of a full cycle's worth of free space.
|
||||
for (int i = 0; i < cycleOutput + 1; ++i)
|
||||
{
|
||||
assembler.outputBuffer.items.push_back(makeItem("x"));
|
||||
}
|
||||
REQUIRE(assembler.getOutputItemCount() < assembler.outputBuffer.capacity);
|
||||
REQUIRE(statusOf(assembler) == ProductionStatus::Blocked);
|
||||
|
||||
// Exactly one cycle's worth of free space: the cycle fits again.
|
||||
assembler.outputBuffer.items.pop_back();
|
||||
REQUIRE(statusOf(assembler) == ProductionStatus::Producing);
|
||||
}
|
||||
|
||||
SECTION("Reprocessing Plant: judged by the smallest output a roll could yield")
|
||||
{
|
||||
// The plant rolls one of its outputs per cycle (REQ-BLD-REPROCESSING) and the
|
||||
// roll belongs to the simulation, so the status can only say whether *some*
|
||||
// roll could start: it is blocked once not even the smallest output fits.
|
||||
const RecipeDef* reprocessingRecipe = nullptr;
|
||||
for (const RecipeDef& r : f.cfg.recipes.recipes)
|
||||
{
|
||||
if (r.building == BuildingType::ReprocessingPlant && !r.inputs.empty())
|
||||
{
|
||||
reprocessingRecipe = &r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(reprocessingRecipe != nullptr);
|
||||
|
||||
int smallestOutput = 0;
|
||||
int largestOutput = 0;
|
||||
for (const RecipeOutput& out : reprocessingRecipe->outputs)
|
||||
{
|
||||
if (smallestOutput == 0 || out.amount < smallestOutput)
|
||||
{
|
||||
smallestOutput = out.amount;
|
||||
}
|
||||
if (out.amount > largestOutput) { largestOutput = out.amount; }
|
||||
}
|
||||
REQUIRE(smallestOutput > 0);
|
||||
|
||||
Building plant; plant.type = BuildingType::ReprocessingPlant;
|
||||
// One cycle's largest output, as initAutoBuffers sizes it
|
||||
// (REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
|
||||
plant.outputBuffer.capacity = largestOutput;
|
||||
for (const RecipeIngredient& ing : reprocessingRecipe->inputs)
|
||||
{
|
||||
plant.inputBuffer.counts[ItemType{ing.item}] = ing.amount;
|
||||
}
|
||||
|
||||
// Empty buffer: a roll fits -> green.
|
||||
REQUIRE(statusOf(plant) == ProductionStatus::Producing);
|
||||
|
||||
// Room for the smallest output but not for the largest: some roll can still
|
||||
// start, so the plant is waiting on the roll rather than blocked.
|
||||
if (smallestOutput < largestOutput)
|
||||
{
|
||||
plant.outputBuffer.items.push_back(makeItem("iron_ingot"));
|
||||
REQUIRE(plant.getOutputItemCount() + largestOutput
|
||||
> plant.outputBuffer.capacity);
|
||||
REQUIRE(statusOf(plant) == ProductionStatus::Producing);
|
||||
plant.outputBuffer.items.pop_back();
|
||||
}
|
||||
|
||||
// Filled so that not even the smallest output fits -> yellow.
|
||||
for (int i = 0; i < largestOutput - smallestOutput + 1; ++i)
|
||||
{
|
||||
plant.outputBuffer.items.push_back(makeItem("iron_ingot"));
|
||||
}
|
||||
REQUIRE(statusOf(plant) == ProductionStatus::Blocked);
|
||||
|
||||
// Without the scrap it is starved regardless of the buffer.
|
||||
plant.inputBuffer.counts.clear();
|
||||
REQUIRE(statusOf(plant) == ProductionStatus::Starved);
|
||||
}
|
||||
|
||||
SECTION("Smelter (auto-recipe) is never grey")
|
||||
{
|
||||
Building smelter; smelter.type = BuildingType::Smelter;
|
||||
|
||||
Reference in New Issue
Block a user