start the next production cycle in the tick the last one completed

This commit is contained in:
2026-08-12 21:27:25 +02:00
parent dd061082fb
commit 0b859bd1a4
2 changed files with 83 additions and 32 deletions

View File

@@ -513,17 +513,22 @@ void BuildingSystem::tickProduction(FactoryState& state, Tick currentTick)
// recipe is selected or auto-chosen.
if (building.production)
{
if (currentTick >= building.production->completesAt)
if (currentTick < building.production->completesAt)
{
continue;
}
for (const Item& item : building.production->chosenOutputs)
{
building.outputBuffer.items.push_back(item);
}
building.production = std::nullopt;
}
// Whether we just completed or are still running, do not start
// another cycle in the same tick.
continue;
// Fall through to the start attempt below rather than idling for a tick,
// so a cycle takes exactly its recipe duration and a building fed to
// capacity produces at the configured rate (REQ-MAT-CYCLE). The start
// code runs once per building per tick, so at most one cycle begins here
// even when a duration rounds to zero ticks. The outputs just deposited
// count against the space check, so a cycle whose output no longer fits
// waits, exactly as it would have on the following tick.
}
// Idle: gather the candidate recipes to try. Auto-recipe buildings
@@ -612,8 +617,10 @@ void BuildingSystem::tickShipyardProduction(FactoryState& state, Tick currentTic
// If a cycle is in progress, check for completion.
if (building.production)
{
if (currentTick >= building.production->completesAt)
if (currentTick < building.production->completesAt)
{
continue;
}
if (!building.outputPorts.empty())
{
const Port& p = building.outputPorts[0];
@@ -630,8 +637,9 @@ void BuildingSystem::tickShipyardProduction(FactoryState& state, Tick currentTic
m_spawnShip(building.recipeId, spawnPos, layout);
}
building.production = std::nullopt;
}
continue;
// Fall through and start the next cycle in this same tick, so a ship takes
// exactly its computed production time (REQ-BLD-SHIPYARD), as for the
// recipe buildings in tickProduction.
}
// Build combined materials list (base + modules).

View File

@@ -495,13 +495,13 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
Tick tick = 0;
// Construction (10s) then cycle 1 starts at tick 300 (completesAt=330).
// Cycle 1 completes at tick 330: deposit item, continue (no same-tick restart).
// Cycle 2 starts at tick 331 (completesAt=361).
// Cycle 2 completes at tick 361: deposit item → buffer=2, cycle 3 stalls.
// Need to process through tick 361: 362 ticks total.
// Cycle 1 completes at tick 330 and cycle 2 starts in that same tick
// (completesAt=360). Cycle 2 completes at tick 360: deposit item -> 2 items held,
// which fills the buffer (capacity 2), so cycle 3 cannot start.
// Need to process through tick 360: 361 ticks total.
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock,
static_cast<int>(secondsToTicks(10.0))
+ 2 * static_cast<int>(secondsToTicks(1.0)) + 2,
+ 2 * static_cast<int>(secondsToTicks(1.0)) + 1,
tick);
const Building* b = findBuilding(f.state, id);
@@ -512,6 +512,49 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
REQUIRE_FALSE(b->production.has_value());
}
TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one completed",
"[building]")
{
// A cycle takes exactly its recipe duration, so a building whose output keeps
// draining produces at the configured rate (REQ-MAT-CYCLE). An idle tick between
// cycles would cost a one-second recipe about 3% of its throughput.
PlacementFixture f;
const BuildingId id =
f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
f.bs.setRecipe(f.state, id, "mine_iron_ore");
const Tick cycleTicks = secondsToTicks(1.0); // mine_iron_ore duration
Tick tick = 0;
// Construction completes at tick 300 and cycle 1 starts in that same tick.
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock,
static_cast<int>(secondsToTicks(10.0)) + 1, tick);
const Building* b = findBuilding(f.state, id);
REQUIRE(b != nullptr);
REQUIRE(b->production.has_value());
const Tick firstCompletesAt = b->production->completesAt;
// Process up to and including that completion tick: the next cycle is already
// running, due exactly one duration later rather than one duration plus a tick.
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(cycleTicks), tick);
b = findBuilding(f.state, id);
REQUIRE(b->getOutputItemCount() == 1);
REQUIRE(b->production.has_value());
REQUIRE(b->production->completesAt == firstCompletesAt + cycleTicks);
// Nothing hauls the ore away here, so the buffer (capacity 2) would stall the third
// cycle. Drain it and confirm the cadence holds across the next boundary too.
f.bs.forEachBuilding(f.state, [](Building& building) {
building.outputBuffer.items.clear();
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
});
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(cycleTicks), tick);
b = findBuilding(f.state, id);
REQUIRE(b->production.has_value());
REQUIRE(b->production->completesAt == firstCompletesAt + 2 * cycleTicks);
}
// ---------------------------------------------------------------------------
// REQ-UI-DEBUG-OVERLAY production counts
// ---------------------------------------------------------------------------