start the next production cycle in the tick the last one completed
This commit is contained in:
@@ -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)
|
||||
{
|
||||
for (const Item& item : building.production->chosenOutputs)
|
||||
{
|
||||
building.outputBuffer.items.push_back(item);
|
||||
}
|
||||
building.production = std::nullopt;
|
||||
continue;
|
||||
}
|
||||
// Whether we just completed or are still running, do not start
|
||||
// another cycle in the same tick.
|
||||
continue;
|
||||
for (const Item& item : building.production->chosenOutputs)
|
||||
{
|
||||
building.outputBuffer.items.push_back(item);
|
||||
}
|
||||
building.production = std::nullopt;
|
||||
// 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,26 +617,29 @@ 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)
|
||||
{
|
||||
if (!building.outputPorts.empty())
|
||||
{
|
||||
const Port& p = building.outputPorts[0];
|
||||
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
|
||||
// A shipyard builds exactly what the player configured and
|
||||
// paid for. When no layout is set it produces a bare hull, so
|
||||
// pass an explicit empty layout rather than nullopt: the latter
|
||||
// would make ShipSystem fall back to the schematic's
|
||||
// defaultModules (a wave-only loadout) and yield free weapons.
|
||||
const std::optional<ShipLayoutConfig> layout =
|
||||
building.shipLayout.has_value()
|
||||
? building.shipLayout
|
||||
: std::make_optional<ShipLayoutConfig>();
|
||||
m_spawnShip(building.recipeId, spawnPos, layout);
|
||||
}
|
||||
building.production = std::nullopt;
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
if (!building.outputPorts.empty())
|
||||
{
|
||||
const Port& p = building.outputPorts[0];
|
||||
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
|
||||
// A shipyard builds exactly what the player configured and
|
||||
// paid for. When no layout is set it produces a bare hull, so
|
||||
// pass an explicit empty layout rather than nullopt: the latter
|
||||
// would make ShipSystem fall back to the schematic's
|
||||
// defaultModules (a wave-only loadout) and yield free weapons.
|
||||
const std::optional<ShipLayoutConfig> layout =
|
||||
building.shipLayout.has_value()
|
||||
? building.shipLayout
|
||||
: std::make_optional<ShipLayoutConfig>();
|
||||
m_spawnShip(building.recipeId, spawnPos, layout);
|
||||
}
|
||||
building.production = std::nullopt;
|
||||
// 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).
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user