separate what buildings are from what flows through them
BuildingSystem was four unrelated jobs in one class: building lifecycle, building configuration, the per-tick material flow, and (until last commit) the checksum. The flow was the odd one out -- it is what the RNG, the ship spawner and the unlock test were held for, none of which placement, rotation or demolition has any business reaching. Tick steps 3 to 5 move to a new ProductionSystem: tickBeltPull, tickProduction, tickShipyardProduction, tickOutputBelts, their five private helpers and rollOutputGroup. The cut is clean in both directions -- nothing in the block called a topology or configuration method, and nothing outside it called the helpers -- so the bodies move verbatim; a scripted comparison against the old file confirms all nine differ only by class qualifier, the m_belts -> belts rename, and the two added parameters. (Seven em dashes in comments became `--`; the new file is ASCII, as the guidelines require.) Belts arrive per tick rather than being held, matching ConstructionSystem, and only the two methods that touch them take the parameter. BuildingSystem is left holding the config and the belts, and its constructor takes exactly those two. The arena constructs no ProductionSystem at all: it stages ships directly and never runs a factory. Determinism rests on the RNG stream: step 4's weighted output-group pick is the factory's only draw, so the four calls must keep their order and position in Simulation::tick. They do, and the tick-order section now says why, since no test can catch a reordering here. 913 lines of BuildingSystem.cpp become 444 there and 483 in ProductionSystem.cpp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
@@ -109,9 +109,11 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
|
|||||||
|
|
||||||
1. **Wave scheduler** — advance wave timer; on trigger, compute wave composition per REQ-WAV-TRIGGER and schedule spawn times across REQ-WAV-SPAWN-DURATION; spawn any enemy ships whose scheduled time has arrived this tick.
|
1. **Wave scheduler** — advance wave timer; on trigger, compute wave composition per REQ-WAV-TRIGGER and schedule spawn times across REQ-WAV-SPAWN-DURATION; spawn any enemy ships whose scheduled time has arrived this tick.
|
||||||
2. **Threat accumulation** — add `max(0, threat_rate_formula(t))` × tick_dt to threat level (REQ-WAV-THREAT-RATE).
|
2. **Threat accumulation** — add `max(0, threat_rate_formula(t))` × tick_dt to threat level (REQ-WAV-THREAT-RATE).
|
||||||
3. **Belt → building pull** — buildings drain eligible items from adjacent belt tiles into per-material input buffers (REQ-MAT-INPUT-PORTS).
|
3. **Belt → building pull** — buildings drain eligible items from adjacent belt tiles into per-material input buffers (REQ-MAT-INPUT-PORTS). `ProductionSystem::tickBeltPull`.
|
||||||
4. **Building production** — advance production timers; start new cycles when inputs and output-buffer space permit (REQ-MAT-CYCLE); on completion, deposit output.
|
4. **Building production** — advance production timers; start new cycles when inputs and output-buffer space permit (REQ-MAT-CYCLE); on completion, deposit output. `ProductionSystem::tickProduction`, then `tickShipyardProduction` for the shipyard's ship (REQ-BLD-SHIPYARD).
|
||||||
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT).
|
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT). `ProductionSystem::tickOutputBelts`.
|
||||||
|
|
||||||
|
Steps 3–5 are one system and must stay adjacent and in this order: an item arriving in step 3 is consumable in step 4, and an item produced in step 4 starts travelling in step 5. Step 4 is also the only place the factory draws from the RNG (an output group's weighted pick, REQ-LOCK-OUTPUT-POOL), so moving these calls relative to any other draw invalidates recorded replays.
|
||||||
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
|
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
|
||||||
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then the `AiSystem` runs three batched phases: every behavior **evaluator** scores its behavior and sets its target data; a **selection** pass records the highest-scoring behavior per ship in `SelectedBehaviorComponent`; each behavior **executor** runs for the winner, writing `MovementIntent` and preferred module targets. The module systems then perform world mutation: `SalvagerSystem` (scrap collection/delivery) and `RepairSystem` (healing). See Movement Arbitration.
|
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then the `AiSystem` runs three batched phases: every behavior **evaluator** scores its behavior and sets its target data; a **selection** pass records the highest-scoring behavior per ship in `SelectedBehaviorComponent`; each behavior **executor** runs for the winner, writing `MovementIntent` and preferred module targets. The module systems then perform world mutation: `SalvagerSystem` (scrap collection/delivery) and `RepairSystem` (healing). See Movement Arbitration.
|
||||||
8. **Combat resolution** — ships and defence stations validate/acquire targets, fire, apply damage; queue deaths. Each fire appends a `BeamFiredEvent` to the sim's beam-fired-event queue (REQ-SHP-FIRING-BEAM). The repair and salvage module systems (tick step 7d) append their own `BeamFiredEvent`s to the same queue when they start a cycle.
|
8. **Combat resolution** — ships and defence stations validate/acquire targets, fire, apply damage; queue deaths. Each fire appends a `BeamFiredEvent` to the sim's beam-fired-event queue (REQ-SHP-FIRING-BEAM). The repair and salvage module systems (tick step 7d) append their own `BeamFiredEvent`s to the same queue when they start a cycle.
|
||||||
@@ -241,6 +243,7 @@ struct Building {
|
|||||||
- The uniform "input buffer → production timer → output buffer" pattern across miner, smelter, assembler, reprocessing plant, and shipyard is driven by the recipe config, not by a class hierarchy.
|
- The uniform "input buffer → production timer → output buffer" pattern across miner, smelter, assembler, reprocessing plant, and shipyard is driven by the recipe config, not by a class hierarchy.
|
||||||
- Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances.
|
- Belts and splitters are separate types owned by the belt subsystem, not general `Building` instances.
|
||||||
- No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components.
|
- No ECS for buildings. A miner is never also an assembler; there is no composition benefit to decomposing buildings into components.
|
||||||
|
- **What buildings are is separate from what flows through them.** `BuildingSystem` places, demolishes, rotates and configures; `ProductionSystem` runs intake, production cycles and output (tick steps 3–5). The split follows what each needs: only the flow side draws from the RNG, tests unlock state, and spawns a ship into the entity model, so only it holds those. `BuildingSystem` holds the config and the belts and nothing else.
|
||||||
|
|
||||||
### Factory State and Queries
|
### Factory State and Queries
|
||||||
|
|
||||||
@@ -259,8 +262,8 @@ struct FactoryState {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Every system that touches the factory — `BuildingSystem`, `ConstructionSystem`,
|
Every system that touches the factory — `BuildingSystem`, `ProductionSystem`,
|
||||||
`DeconstructionSystem` — takes it as an argument and holds none of it, the same shape the
|
`ConstructionSystem`, `DeconstructionSystem` — takes it as an argument and holds none of it, the same shape the
|
||||||
`lib/ecs/system` classes have, where the world arrives per tick. This is why
|
`lib/ecs/system` classes have, where the world arrives per tick. This is why
|
||||||
`ConstructionSystem` can complete a building itself instead of handing the finished site
|
`ConstructionSystem` can complete a building itself instead of handing the finished site
|
||||||
back to `BuildingSystem`: with the state in the argument there is no owner to route
|
back to `BuildingSystem`: with the state in the argument there is no owner to route
|
||||||
|
|||||||
@@ -47,12 +47,9 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|||||||
{
|
{
|
||||||
m_factoryState = makeFactoryState(m_gameConfig);
|
m_factoryState = makeFactoryState(m_gameConfig);
|
||||||
|
|
||||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
// No ProductionSystem here: the arena stages ships directly and never runs a
|
||||||
m_gameConfig,
|
// factory, so nothing ticks material flow (ProductionSystem.h).
|
||||||
m_beltSystem,
|
m_buildingSystem = std::make_unique<BuildingSystem>(m_gameConfig, m_beltSystem);
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
|
||||||
[](const std::string&) -> bool { return true; },
|
|
||||||
m_rng);
|
|
||||||
|
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
|
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
|
||||||
// Arena fights are symmetric and aggressive: player-faction ships must not
|
// Arena fights are symmetric and aggressive: player-faction ships must not
|
||||||
|
|||||||
@@ -2,39 +2,17 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <limits>
|
|
||||||
#include <random>
|
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
#include "FactoryQueries.h"
|
#include "FactoryQueries.h"
|
||||||
#include "PlacementRules.h"
|
#include "PlacementRules.h"
|
||||||
#include "ProductionRules.h"
|
|
||||||
#include "PortGeometry.h"
|
#include "PortGeometry.h"
|
||||||
#include "SurfaceMask.h"
|
#include "SurfaceMask.h"
|
||||||
#include "tracing.h"
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// An input belt accepts a new item at progress 0.0 only when it holds fewer than
|
|
||||||
// three items and the entry slot is clear (nothing within a quarter tile of 0.0),
|
|
||||||
// matching the belt packing used elsewhere (REQ-GW-BELT-CAPACITY).
|
|
||||||
bool inputLaneEntryFree(const std::vector<BeltItemSlot>& lane)
|
|
||||||
{
|
|
||||||
return lane.size() < 3 && (lane.empty() || lane.back().progress >= 0.25);
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
BuildingSystem::BuildingSystem(const GameConfig& config,
|
BuildingSystem::BuildingSystem(const GameConfig& config, BeltSystem& belts)
|
||||||
BeltSystem& belts,
|
|
||||||
std::function<void(const std::string&, QVector2D,
|
|
||||||
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
|
||||||
std::function<bool(const std::string&)> isItemUnlocked,
|
|
||||||
std::mt19937& rng)
|
|
||||||
: m_config(config)
|
: m_config(config)
|
||||||
, m_belts(belts)
|
, m_belts(belts)
|
||||||
, m_spawnShip(std::move(spawnShip))
|
|
||||||
, m_isItemUnlocked(std::move(isItemUnlocked))
|
|
||||||
, m_rng(rng)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,63 +21,7 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// The items of one group, produced together (REQ-MAT-OUTPUT-GROUP).
|
|
||||||
std::vector<Item> itemsOf(const RecipeOutputGroup& group)
|
|
||||||
{
|
|
||||||
std::vector<Item> result;
|
|
||||||
for (const RecipeOutput& out : group.items)
|
|
||||||
{
|
|
||||||
Item item;
|
|
||||||
item.type.id = out.item;
|
|
||||||
for (int i = 0; i < out.amount; ++i)
|
|
||||||
{
|
|
||||||
result.push_back(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
std::vector<Item> BuildingSystem::rollOutputGroup(const RecipeDef& recipe)
|
|
||||||
{
|
|
||||||
// One group: nothing to choose, so no weight is read, no draw is made, and no
|
|
||||||
// eligibility is tested (REQ-MAT-OUTPUT-GROUP, REQ-LOCK-OUTPUT-POOL).
|
|
||||||
//
|
|
||||||
// Not drawing matters beyond speed. A draw here would consume entropy for every
|
|
||||||
// ordinary recipe, shifting every later random outcome and invalidating recorded
|
|
||||||
// replays. And eligibility must not apply either: implicit unlocking is derived from
|
|
||||||
// demand, so an ordinary recipe's output can be perfectly producible while nothing
|
|
||||||
// yet calls for it -- testing it here would stop the building producing at all.
|
|
||||||
if (recipe.outputGroups.size() == 1)
|
|
||||||
{
|
|
||||||
return itemsOf(recipe.outputGroups.front());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Several groups: only those whose items are all unlocked can be picked, and a group
|
|
||||||
// holding any locked item is dropped whole, since its items come together
|
|
||||||
// (REQ-LOCK-OUTPUT-POOL). Weights are renormalized over what is left by
|
|
||||||
// discrete_distribution.
|
|
||||||
std::vector<const RecipeOutputGroup*> eligible;
|
|
||||||
std::vector<double> weights;
|
|
||||||
for (const RecipeOutputGroup& group : recipe.outputGroups)
|
|
||||||
{
|
|
||||||
bool allUnlocked = true;
|
|
||||||
for (const RecipeOutput& out : group.items)
|
|
||||||
{
|
|
||||||
if (!m_isItemUnlocked(out.item)) { allUnlocked = false; break; }
|
|
||||||
}
|
|
||||||
if (!allUnlocked) { continue; }
|
|
||||||
eligible.push_back(&group);
|
|
||||||
weights.push_back(group.probability.value_or(1.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eligible.empty()) { return {}; }
|
|
||||||
|
|
||||||
std::discrete_distribution<int> dist(weights.begin(), weights.end());
|
|
||||||
return itemsOf(*eligible[static_cast<std::size_t>(dist(m_rng))]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Placement
|
// Placement
|
||||||
@@ -376,397 +298,6 @@ void BuildingSystem::cancelDeconstruction(FactoryState& state, BuildingId id)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildingSystem::tickBeltPull(FactoryState& state)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
// Same per-tick step as the belts, so items travel inward at belt speed
|
|
||||||
// (REQ-GW-BELT-SPEED, REQ-MAT-INPUT-INTAKE).
|
|
||||||
const double progressPerTick = m_belts.getProgressPerTick_tpt();
|
|
||||||
|
|
||||||
for (Building& building : state.buildings)
|
|
||||||
{
|
|
||||||
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
|
||||||
if (building.queuedForDeconstruction) { continue; }
|
|
||||||
|
|
||||||
const bool isHq = (building.type == BuildingType::Hq);
|
|
||||||
|
|
||||||
// 1. Advance every input belt and deliver arrivals (progress >= 0.5) into
|
|
||||||
// the input buffer — or the global stock for the HQ. Runs for all
|
|
||||||
// buildings so in-transit items keep moving even when feeding is gated
|
|
||||||
// off, and arrivals become consumable before tickProduction (step 4).
|
|
||||||
for (std::size_t i = 0; i < building.incomingItems.size(); ++i)
|
|
||||||
{
|
|
||||||
std::vector<BeltItemSlot>& lane = building.incomingItems[i];
|
|
||||||
advanceBeltSlots(lane, progressPerTick);
|
|
||||||
while (!lane.empty() && lane.front().progress >= 0.5)
|
|
||||||
{
|
|
||||||
const Item arrived = lane.front().item;
|
|
||||||
lane.erase(lane.begin());
|
|
||||||
if (isHq)
|
|
||||||
{
|
|
||||||
state.buildingBlocksStock += 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
building.inputBuffer.counts[arrived.type]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Feed accepted items from adjacent belts onto the input belts at
|
|
||||||
// progress 0.0. The acceptance rules — the HQ building-block case, the
|
|
||||||
// required-input check, and the reservation — live in canAcceptInput so
|
|
||||||
// direct coupling (REQ-MAT-DIRECT-COUPLE) shares them exactly.
|
|
||||||
for (std::size_t i = 0; i < building.inputPorts.size(); ++i)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
depositToInputBelt(building, i, *taken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
if (inputPortIndex >= consumer.incomingItems.size()) { return false; }
|
|
||||||
if (!inputLaneEntryFree(consumer.incomingItems[inputPortIndex])) { return false; }
|
|
||||||
|
|
||||||
// The HQ has no input buffer; it accepts building blocks into the global stock
|
|
||||||
// (REQ-HQ-BELT-INPUT) with no reservation.
|
|
||||||
if (consumer.type == BuildingType::Hq)
|
|
||||||
{
|
|
||||||
return type.id == "building_block";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everyone else: the item must be a required input whose reservation-aware
|
|
||||||
// buffer has room — buffered + in-transit below the cap (REQ-MAT-INPUT-INTAKE).
|
|
||||||
const std::map<ItemType, int>::const_iterator capIt =
|
|
||||||
consumer.inputBuffer.caps.find(type);
|
|
||||||
if (capIt == consumer.inputBuffer.caps.end() || capIt->second == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return consumer.pendingInputCount(type) < capIt->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildingSystem::depositToInputBelt(Building& consumer,
|
|
||||||
std::size_t inputPortIndex,
|
|
||||||
const Item& item)
|
|
||||||
{
|
|
||||||
consumer.incomingItems[inputPortIndex].push_back(BeltItemSlot{item, 0.0});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BuildingSystem::tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
|
|
||||||
const Port& outputPort,
|
|
||||||
const Item& item)
|
|
||||||
{
|
|
||||||
const std::optional<BuildingId> ownerId = state.grid.findOwner(outputPort.tile);
|
|
||||||
if (!ownerId.has_value() || *ownerId == producerId)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Building* consumer = findBuilding(state, *ownerId);
|
|
||||||
if (!consumer)
|
|
||||||
{
|
|
||||||
return false; // an unbuilt construction site, or not an operational building
|
|
||||||
}
|
|
||||||
if (consumer->queuedForDeconstruction)
|
|
||||||
{
|
|
||||||
return false; // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The coupling is the consumer input port meeting this output port: same flow
|
|
||||||
// direction, feeding the producer's output-port tile (REQ-MAT-DIRECT-COUPLE).
|
|
||||||
for (std::size_t j = 0; j < consumer->inputPorts.size(); ++j)
|
|
||||||
{
|
|
||||||
const Port& in = consumer->inputPorts[j];
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildingSystem::tickProduction(FactoryState& state, Tick currentTick)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
for (Building& building : state.buildings)
|
|
||||||
{
|
|
||||||
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
|
||||||
if (building.queuedForDeconstruction) { continue; }
|
|
||||||
|
|
||||||
// Skip types without a recipe-based production loop.
|
|
||||||
if (building.type == BuildingType::Belt ||
|
|
||||||
building.type == BuildingType::Splitter ||
|
|
||||||
building.type == BuildingType::Shipyard ||
|
|
||||||
building.type == BuildingType::SalvageBay ||
|
|
||||||
building.type == BuildingType::Hq)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
if (building.production)
|
|
||||||
{
|
|
||||||
if (currentTick < building.production->completesAt)
|
|
||||||
{
|
|
||||||
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: 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)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. All required inputs present?
|
|
||||||
if (!recipeInputsAvailable(building, *recipe))
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Settle what this cycle produces: its one output group, picked by weight only
|
|
||||||
// where the recipe has several (REQ-MAT-OUTPUT-GROUP). Empty means every group
|
|
||||||
// was ineligible, so there is nothing to run.
|
|
||||||
std::vector<Item> chosen = rollOutputGroup(*recipe);
|
|
||||||
if (chosen.empty()) { continue; }
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildingSystem::tickShipyardProduction(FactoryState& state, Tick currentTick)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
for (Building& building : state.buildings)
|
|
||||||
{
|
|
||||||
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
|
||||||
if (building.queuedForDeconstruction) { continue; }
|
|
||||||
|
|
||||||
if (building.type != BuildingType::Shipyard)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (building.recipeId.empty())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const ShipDef* shipDef = m_config.ships.findShipDef(building.recipeId);
|
|
||||||
if (!shipDef)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a cycle is in progress, check for completion.
|
|
||||||
if (building.production)
|
|
||||||
{
|
|
||||||
if (currentTick < building.production->completesAt)
|
|
||||||
{
|
|
||||||
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).
|
|
||||||
const std::map<std::string, int> requiredMaterials =
|
|
||||||
computeShipyardRequiredMaterials(m_config, building);
|
|
||||||
|
|
||||||
// Idle: check if all combined materials are available.
|
|
||||||
bool inputsOk = true;
|
|
||||||
for (const std::pair<const std::string, int>& req : requiredMaterials)
|
|
||||||
{
|
|
||||||
const ItemType type{req.first};
|
|
||||||
const std::map<ItemType, int>::const_iterator it =
|
|
||||||
building.inputBuffer.counts.find(type);
|
|
||||||
const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
|
|
||||||
if (have < req.second)
|
|
||||||
{
|
|
||||||
inputsOk = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!inputsOk)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consume combined materials and start the production cycle.
|
|
||||||
for (const std::pair<const std::string, int>& req : requiredMaterials)
|
|
||||||
{
|
|
||||||
building.inputBuffer.counts[ItemType{req.first}] -= req.second;
|
|
||||||
}
|
|
||||||
|
|
||||||
double totalTime = shipDef->schematic.productionTimeSeconds;
|
|
||||||
if (building.shipLayout.has_value())
|
|
||||||
{
|
|
||||||
for (const PlacedModule& pm : building.shipLayout->placedModules)
|
|
||||||
{
|
|
||||||
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
|
|
||||||
if (modDef)
|
|
||||||
{
|
|
||||||
totalTime += modDef->productionTimeSeconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Production prod;
|
|
||||||
prod.recipeId = building.recipeId;
|
|
||||||
prod.completesAt = currentTick + secondsToTicks(totalTime);
|
|
||||||
building.production = std::move(prod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildingSystem::tickOutputBelts(FactoryState& state)
|
|
||||||
{
|
|
||||||
TRACE();
|
|
||||||
// Use BeltSystem's own per-tick step so emerging items travel at exactly the
|
|
||||||
// same speed as real belts (REQ-GW-BELT-SPEED, REQ-MAT-OUTPUT-EMERGE).
|
|
||||||
const double progressPerTick = m_belts.getProgressPerTick_tpt();
|
|
||||||
|
|
||||||
for (Building& building : state.buildings)
|
|
||||||
{
|
|
||||||
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
|
||||||
if (building.queuedForDeconstruction) { continue; }
|
|
||||||
|
|
||||||
for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
|
|
||||||
{
|
|
||||||
const Port& port = building.outputPorts[p];
|
|
||||||
std::vector<BeltItemSlot>& lane = building.emergingItems[p];
|
|
||||||
|
|
||||||
// 1. Advance emerging items using the shared belt packing (progress
|
|
||||||
// caps to 0.5 / 0.75 / 1.0 for up to three items).
|
|
||||||
advanceBeltSlots(lane, progressPerTick);
|
|
||||||
|
|
||||||
// 2. Hand the front item off once it reaches the output edge (progress
|
|
||||||
// 1.0): onto the adjacent real belt, or — if a building's input edge
|
|
||||||
// meets this port — straight into that building (REQ-MAT-DIRECT-COUPLE).
|
|
||||||
// On refusal (no belt/coupling, output-edge per REQ-MAT-ACCEPT-DIR, or
|
|
||||||
// a full target) it stays stuck at 1.0.
|
|
||||||
if (!lane.empty() && lane.front().progress >= 1.0)
|
|
||||||
{
|
|
||||||
const Item item = lane.front().item;
|
|
||||||
if (m_belts.tryPutItem(port.tile, item, port.direction)
|
|
||||||
|| tryDirectCoupleDeposit(state, building.id, port, item))
|
|
||||||
{
|
|
||||||
lane.erase(lane.begin());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Feed the next buffered item onto the lane at progress 0.5 when the
|
|
||||||
// entry slot is free — the lane holds at most three items and a new
|
|
||||||
// one needs a quarter-tile clearance ahead of 0.5.
|
|
||||||
if (!building.outputBuffer.items.empty()
|
|
||||||
&& lane.size() < 3
|
|
||||||
&& (lane.empty() || lane.back().progress >= 0.75))
|
|
||||||
{
|
|
||||||
lane.push_back(BeltItemSlot{building.outputBuffer.items.front(), 0.5});
|
|
||||||
building.outputBuffer.items.erase(building.outputBuffer.items.begin());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildingSystem::rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation)
|
void BuildingSystem::rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation)
|
||||||
{
|
{
|
||||||
// Construction site path — just update rotation; no ports to recompute.
|
// Construction site path — just update rotation; no ports to recompute.
|
||||||
|
|||||||
@@ -4,13 +4,11 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <random>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QPointF>
|
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
@@ -19,7 +17,6 @@
|
|||||||
#include "BuildingBuffers.h"
|
#include "BuildingBuffers.h"
|
||||||
#include "DeconstructionSystem.h"
|
#include "DeconstructionSystem.h"
|
||||||
#include "PlacementRules.h"
|
#include "PlacementRules.h"
|
||||||
#include "ProductionRules.h"
|
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
@@ -29,19 +26,17 @@
|
|||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
// Manages building placement, construction queuing, and the per-tick
|
// What buildings are: placing them, demolishing them, rotating them, and configuring
|
||||||
// production loop (belt→building pull, production, building→belt push).
|
// what they will run. What flows through them once they stand -- intake, production
|
||||||
// All types including Belt and Splitter are stored as Building instances;
|
// cycles, output -- belongs to ProductionSystem, which took the RNG, the ship spawner and
|
||||||
// BeltSystem owns the per-tile simulation data (item slots, flow).
|
// the unlock test with it.
|
||||||
|
//
|
||||||
|
// All types including Belt and Splitter are stored as Building instances; BeltSystem owns
|
||||||
|
// the per-tile simulation data (item slots, flow).
|
||||||
class BuildingSystem
|
class BuildingSystem
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BuildingSystem(const GameConfig& config,
|
BuildingSystem(const GameConfig& config, BeltSystem& belts);
|
||||||
BeltSystem& belts,
|
|
||||||
std::function<void(const std::string&, QVector2D,
|
|
||||||
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
|
||||||
std::function<bool(const std::string&)> isItemUnlocked,
|
|
||||||
std::mt19937& rng);
|
|
||||||
|
|
||||||
// -- Placement / deconstruct ------------------------------------------------
|
// -- Placement / deconstruct ------------------------------------------------
|
||||||
// Returns the new entity id, or nullopt if the placement falls outside the
|
// Returns the new entity id, or nullopt if the placement falls outside the
|
||||||
@@ -92,17 +87,9 @@ public:
|
|||||||
const std::vector<ItemType>& filterA,
|
const std::vector<ItemType>& filterA,
|
||||||
const std::vector<ItemType>& filterB);
|
const std::vector<ItemType>& filterB);
|
||||||
|
|
||||||
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
// This system has no tick hook of its own: a building placed, configured or
|
||||||
// Advances every building's virtual input belts, delivers what arrives into the
|
// demolished is a player action, not something that advances every tick
|
||||||
// input buffers (into the global block stock for the HQ), and takes what the
|
// (ProductionSystem, ConstructionSystem, DeconstructionSystem tick instead).
|
||||||
// adjacent real belts offer (REQ-MAT-INPUT-INTAKE, REQ-HQ-BELT-INPUT).
|
|
||||||
void tickBeltPull(FactoryState& state);
|
|
||||||
void tickProduction(FactoryState& state, Tick currentTick);
|
|
||||||
void tickShipyardProduction(FactoryState& state, Tick currentTick);
|
|
||||||
// Advances each building's virtual output belts, hands finished items off onto
|
|
||||||
// the adjacent real belt, and feeds new buffered items into them
|
|
||||||
// (REQ-MAT-OUTPUT-EMERGE).
|
|
||||||
void tickOutputBelts(FactoryState& state);
|
|
||||||
|
|
||||||
// This system answers no queries: reading the factory needs none, the queries being
|
// This system answers no queries: reading the factory needs none, the queries being
|
||||||
// free functions over the state (FactoryQueries.h), the placement rules
|
// free functions over the state (FactoryQueries.h), the placement rules
|
||||||
@@ -133,46 +120,11 @@ public:
|
|||||||
void forEachBuilding(FactoryState& state, std::function<void(Building&)> fn);
|
void forEachBuilding(FactoryState& state, std::function<void(Building&)> fn);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Selects a recipe for an auto-recipe building that has none, from a material being
|
// No world data here: the factory arrives per call (FactoryState.h). The config says
|
||||||
// offered to it at one of its input ports (REQ-BLD-AUTO-RECIPE). No-op for every
|
// what a building costs, occupies and can run; the belts are what a placed, rotated or
|
||||||
// other building, for one that already holds a recipe, and for a material none of
|
// demolished belt tile must be registered with and unregistered from. Nothing else --
|
||||||
// its recipes consumes. Called from both intake paths -- the belt pull and the
|
// no RNG and no callbacks, those having gone to ProductionSystem with the material
|
||||||
// direct coupling -- since either can be where the first material arrives.
|
// flow that needed them.
|
||||||
void selectAutoRecipeIfUnset(Building& building,
|
const GameConfig& m_config;
|
||||||
const ItemType& offered);
|
BeltSystem& m_belts;
|
||||||
// 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).
|
|
||||||
bool canAcceptInput(const Building& consumer,
|
|
||||||
std::size_t inputPortIndex,
|
|
||||||
const ItemType& type) const;
|
|
||||||
// Places an accepted item onto the consumer's input belt at progress 0.0,
|
|
||||||
// reserving a per-material buffer slot (REQ-MAT-INPUT-INTAKE).
|
|
||||||
void depositToInputBelt(Building& consumer,
|
|
||||||
std::size_t inputPortIndex,
|
|
||||||
const Item& item);
|
|
||||||
// Attempts to hand an emerging output item straight into a directly adjacent
|
|
||||||
// building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
|
|
||||||
// Returns true if the item was accepted onto the consumer's input belt.
|
|
||||||
bool tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
|
|
||||||
const Port& outputPort,
|
|
||||||
const Item& item);
|
|
||||||
|
|
||||||
// What one cycle of this recipe produces: the items of its one output group
|
|
||||||
// (REQ-MAT-OUTPUT-GROUP). Where the recipe has several, one is picked by weight from
|
|
||||||
// those currently eligible (REQ-LOCK-OUTPUT-POOL) and the result is empty if none is;
|
|
||||||
// where it has one, that group is returned with no draw and no eligibility test.
|
|
||||||
std::vector<Item> rollOutputGroup(const RecipeDef& recipe);
|
|
||||||
|
|
||||||
// No world data among these: the factory arrives per call (FactoryState.h). What is
|
|
||||||
// left are the immutable config, the transport layer, the shared RNG, and two
|
|
||||||
// callbacks into what this system genuinely cannot reach -- the entity model a
|
|
||||||
// finished ship is spawned into, and the unlock state an output group is tested
|
|
||||||
// against. Neither is factory data, so neither belongs in the state.
|
|
||||||
const GameConfig& m_config;
|
|
||||||
BeltSystem& m_belts;
|
|
||||||
std::function<void(const std::string&, QVector2D,
|
|
||||||
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
|
||||||
std::function<bool(const std::string&)> m_isItemUnlocked;
|
|
||||||
std::mt19937& m_rng;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryChecksum.h
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryChecksum.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.h
|
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.h
|
||||||
@@ -50,6 +51,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryChecksum.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryChecksum.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.cpp
|
||||||
|
|||||||
483
src/lib/sim/ProductionSystem.cpp
Normal file
483
src/lib/sim/ProductionSystem.cpp
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
#include "ProductionSystem.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <optional>
|
||||||
|
#include <random>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "BuildingBuffers.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "PortGeometry.h"
|
||||||
|
#include "ProductionRules.h"
|
||||||
|
#include "tracing.h"
|
||||||
|
|
||||||
|
ProductionSystem::ProductionSystem(const GameConfig& config,
|
||||||
|
std::function<void(const std::string&, QVector2D,
|
||||||
|
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
||||||
|
std::function<bool(const std::string&)> isItemUnlocked,
|
||||||
|
std::mt19937& rng)
|
||||||
|
: m_config(config)
|
||||||
|
, m_spawnShip(std::move(spawnShip))
|
||||||
|
, m_isItemUnlocked(std::move(isItemUnlocked))
|
||||||
|
, m_rng(rng)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// An input belt accepts a new item at progress 0.0 only when it holds fewer than
|
||||||
|
// three items and the entry slot is clear (nothing within a quarter tile of 0.0),
|
||||||
|
// matching the belt packing used elsewhere (REQ-GW-BELT-CAPACITY).
|
||||||
|
bool inputLaneEntryFree(const std::vector<BeltItemSlot>& lane)
|
||||||
|
{
|
||||||
|
return lane.size() < 3 && (lane.empty() || lane.back().progress >= 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The items of one group, produced together (REQ-MAT-OUTPUT-GROUP).
|
||||||
|
std::vector<Item> itemsOf(const RecipeOutputGroup& group)
|
||||||
|
{
|
||||||
|
std::vector<Item> result;
|
||||||
|
for (const RecipeOutput& out : group.items)
|
||||||
|
{
|
||||||
|
Item item;
|
||||||
|
item.type.id = out.item;
|
||||||
|
for (int i = 0; i < out.amount; ++i)
|
||||||
|
{
|
||||||
|
result.push_back(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<Item> ProductionSystem::rollOutputGroup(const RecipeDef& recipe)
|
||||||
|
{
|
||||||
|
// One group: nothing to choose, so no weight is read, no draw is made, and no
|
||||||
|
// eligibility is tested (REQ-MAT-OUTPUT-GROUP, REQ-LOCK-OUTPUT-POOL).
|
||||||
|
//
|
||||||
|
// Not drawing matters beyond speed. A draw here would consume entropy for every
|
||||||
|
// ordinary recipe, shifting every later random outcome and invalidating recorded
|
||||||
|
// replays. And eligibility must not apply either: implicit unlocking is derived from
|
||||||
|
// demand, so an ordinary recipe's output can be perfectly producible while nothing
|
||||||
|
// yet calls for it -- testing it here would stop the building producing at all.
|
||||||
|
if (recipe.outputGroups.size() == 1)
|
||||||
|
{
|
||||||
|
return itemsOf(recipe.outputGroups.front());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Several groups: only those whose items are all unlocked can be picked, and a group
|
||||||
|
// holding any locked item is dropped whole, since its items come together
|
||||||
|
// (REQ-LOCK-OUTPUT-POOL). Weights are renormalized over what is left by
|
||||||
|
// discrete_distribution.
|
||||||
|
std::vector<const RecipeOutputGroup*> eligible;
|
||||||
|
std::vector<double> weights;
|
||||||
|
for (const RecipeOutputGroup& group : recipe.outputGroups)
|
||||||
|
{
|
||||||
|
bool allUnlocked = true;
|
||||||
|
for (const RecipeOutput& out : group.items)
|
||||||
|
{
|
||||||
|
if (!m_isItemUnlocked(out.item)) { allUnlocked = false; break; }
|
||||||
|
}
|
||||||
|
if (!allUnlocked) { continue; }
|
||||||
|
eligible.push_back(&group);
|
||||||
|
weights.push_back(group.probability.value_or(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eligible.empty()) { return {}; }
|
||||||
|
|
||||||
|
std::discrete_distribution<int> dist(weights.begin(), weights.end());
|
||||||
|
return itemsOf(*eligible[static_cast<std::size_t>(dist(m_rng))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductionSystem::tickBeltPull(FactoryState& state, BeltSystem& belts)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
// Same per-tick step as the belts, so items travel inward at belt speed
|
||||||
|
// (REQ-GW-BELT-SPEED, REQ-MAT-INPUT-INTAKE).
|
||||||
|
const double progressPerTick = belts.getProgressPerTick_tpt();
|
||||||
|
|
||||||
|
for (Building& building : state.buildings)
|
||||||
|
{
|
||||||
|
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||||
|
if (building.queuedForDeconstruction) { continue; }
|
||||||
|
|
||||||
|
const bool isHq = (building.type == BuildingType::Hq);
|
||||||
|
|
||||||
|
// 1. Advance every input belt and deliver arrivals (progress >= 0.5) into
|
||||||
|
// the input buffer -- or the global stock for the HQ. Runs for all
|
||||||
|
// buildings so in-transit items keep moving even when feeding is gated
|
||||||
|
// off, and arrivals become consumable before tickProduction (step 4).
|
||||||
|
for (std::size_t i = 0; i < building.incomingItems.size(); ++i)
|
||||||
|
{
|
||||||
|
std::vector<BeltItemSlot>& lane = building.incomingItems[i];
|
||||||
|
advanceBeltSlots(lane, progressPerTick);
|
||||||
|
while (!lane.empty() && lane.front().progress >= 0.5)
|
||||||
|
{
|
||||||
|
const Item arrived = lane.front().item;
|
||||||
|
lane.erase(lane.begin());
|
||||||
|
if (isHq)
|
||||||
|
{
|
||||||
|
state.buildingBlocksStock += 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
building.inputBuffer.counts[arrived.type]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Feed accepted items from adjacent belts onto the input belts at
|
||||||
|
// progress 0.0. The acceptance rules -- the HQ building-block case, the
|
||||||
|
// required-input check, and the reservation -- live in canAcceptInput so
|
||||||
|
// direct coupling (REQ-MAT-DIRECT-COUPLE) shares them exactly.
|
||||||
|
for (std::size_t i = 0; i < building.inputPorts.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::optional<ItemType> peeked = 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 = belts.tryTakeItem(building.inputPorts[i]);
|
||||||
|
if (taken)
|
||||||
|
{
|
||||||
|
depositToInputBelt(building, i, *taken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductionSystem::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 ProductionSystem::canAcceptInput(const Building& consumer,
|
||||||
|
std::size_t inputPortIndex,
|
||||||
|
const ItemType& type) const
|
||||||
|
{
|
||||||
|
if (inputPortIndex >= consumer.incomingItems.size()) { return false; }
|
||||||
|
if (!inputLaneEntryFree(consumer.incomingItems[inputPortIndex])) { return false; }
|
||||||
|
|
||||||
|
// The HQ has no input buffer; it accepts building blocks into the global stock
|
||||||
|
// (REQ-HQ-BELT-INPUT) with no reservation.
|
||||||
|
if (consumer.type == BuildingType::Hq)
|
||||||
|
{
|
||||||
|
return type.id == "building_block";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everyone else: the item must be a required input whose reservation-aware
|
||||||
|
// buffer has room -- buffered + in-transit below the cap (REQ-MAT-INPUT-INTAKE).
|
||||||
|
const std::map<ItemType, int>::const_iterator capIt =
|
||||||
|
consumer.inputBuffer.caps.find(type);
|
||||||
|
if (capIt == consumer.inputBuffer.caps.end() || capIt->second == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return consumer.pendingInputCount(type) < capIt->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductionSystem::depositToInputBelt(Building& consumer,
|
||||||
|
std::size_t inputPortIndex,
|
||||||
|
const Item& item)
|
||||||
|
{
|
||||||
|
consumer.incomingItems[inputPortIndex].push_back(BeltItemSlot{item, 0.0});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductionSystem::tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
|
||||||
|
const Port& outputPort,
|
||||||
|
const Item& item)
|
||||||
|
{
|
||||||
|
const std::optional<BuildingId> ownerId = state.grid.findOwner(outputPort.tile);
|
||||||
|
if (!ownerId.has_value() || *ownerId == producerId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Building* consumer = findBuilding(state, *ownerId);
|
||||||
|
if (!consumer)
|
||||||
|
{
|
||||||
|
return false; // an unbuilt construction site, or not an operational building
|
||||||
|
}
|
||||||
|
if (consumer->queuedForDeconstruction)
|
||||||
|
{
|
||||||
|
return false; // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The coupling is the consumer input port meeting this output port: same flow
|
||||||
|
// direction, feeding the producer's output-port tile (REQ-MAT-DIRECT-COUPLE).
|
||||||
|
for (std::size_t j = 0; j < consumer->inputPorts.size(); ++j)
|
||||||
|
{
|
||||||
|
const Port& in = consumer->inputPorts[j];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductionSystem::tickProduction(FactoryState& state, Tick currentTick)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
for (Building& building : state.buildings)
|
||||||
|
{
|
||||||
|
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||||
|
if (building.queuedForDeconstruction) { continue; }
|
||||||
|
|
||||||
|
// Skip types without a recipe-based production loop.
|
||||||
|
if (building.type == BuildingType::Belt ||
|
||||||
|
building.type == BuildingType::Splitter ||
|
||||||
|
building.type == BuildingType::Shipyard ||
|
||||||
|
building.type == BuildingType::SalvageBay ||
|
||||||
|
building.type == BuildingType::Hq)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
if (building.production)
|
||||||
|
{
|
||||||
|
if (currentTick < building.production->completesAt)
|
||||||
|
{
|
||||||
|
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: 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)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. All required inputs present?
|
||||||
|
if (!recipeInputsAvailable(building, *recipe))
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Settle what this cycle produces: its one output group, picked by weight only
|
||||||
|
// where the recipe has several (REQ-MAT-OUTPUT-GROUP). Empty means every group
|
||||||
|
// was ineligible, so there is nothing to run.
|
||||||
|
std::vector<Item> chosen = rollOutputGroup(*recipe);
|
||||||
|
if (chosen.empty()) { continue; }
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductionSystem::tickShipyardProduction(FactoryState& state, Tick currentTick)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
for (Building& building : state.buildings)
|
||||||
|
{
|
||||||
|
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||||
|
if (building.queuedForDeconstruction) { continue; }
|
||||||
|
|
||||||
|
if (building.type != BuildingType::Shipyard)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (building.recipeId.empty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ShipDef* shipDef = m_config.ships.findShipDef(building.recipeId);
|
||||||
|
if (!shipDef)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a cycle is in progress, check for completion.
|
||||||
|
if (building.production)
|
||||||
|
{
|
||||||
|
if (currentTick < building.production->completesAt)
|
||||||
|
{
|
||||||
|
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).
|
||||||
|
const std::map<std::string, int> requiredMaterials =
|
||||||
|
computeShipyardRequiredMaterials(m_config, building);
|
||||||
|
|
||||||
|
// Idle: check if all combined materials are available.
|
||||||
|
bool inputsOk = true;
|
||||||
|
for (const std::pair<const std::string, int>& req : requiredMaterials)
|
||||||
|
{
|
||||||
|
const ItemType type{req.first};
|
||||||
|
const std::map<ItemType, int>::const_iterator it =
|
||||||
|
building.inputBuffer.counts.find(type);
|
||||||
|
const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
|
||||||
|
if (have < req.second)
|
||||||
|
{
|
||||||
|
inputsOk = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!inputsOk)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume combined materials and start the production cycle.
|
||||||
|
for (const std::pair<const std::string, int>& req : requiredMaterials)
|
||||||
|
{
|
||||||
|
building.inputBuffer.counts[ItemType{req.first}] -= req.second;
|
||||||
|
}
|
||||||
|
|
||||||
|
double totalTime = shipDef->schematic.productionTimeSeconds;
|
||||||
|
if (building.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& pm : building.shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
|
||||||
|
if (modDef)
|
||||||
|
{
|
||||||
|
totalTime += modDef->productionTimeSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Production prod;
|
||||||
|
prod.recipeId = building.recipeId;
|
||||||
|
prod.completesAt = currentTick + secondsToTicks(totalTime);
|
||||||
|
building.production = std::move(prod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductionSystem::tickOutputBelts(FactoryState& state, BeltSystem& belts)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
// Use BeltSystem's own per-tick step so emerging items travel at exactly the
|
||||||
|
// same speed as real belts (REQ-GW-BELT-SPEED, REQ-MAT-OUTPUT-EMERGE).
|
||||||
|
const double progressPerTick = belts.getProgressPerTick_tpt();
|
||||||
|
|
||||||
|
for (Building& building : state.buildings)
|
||||||
|
{
|
||||||
|
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||||
|
if (building.queuedForDeconstruction) { continue; }
|
||||||
|
|
||||||
|
for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
|
||||||
|
{
|
||||||
|
const Port& port = building.outputPorts[p];
|
||||||
|
std::vector<BeltItemSlot>& lane = building.emergingItems[p];
|
||||||
|
|
||||||
|
// 1. Advance emerging items using the shared belt packing (progress
|
||||||
|
// caps to 0.5 / 0.75 / 1.0 for up to three items).
|
||||||
|
advanceBeltSlots(lane, progressPerTick);
|
||||||
|
|
||||||
|
// 2. Hand the front item off once it reaches the output edge (progress
|
||||||
|
// 1.0): onto the adjacent real belt, or -- if a building's input edge
|
||||||
|
// meets this port -- straight into that building (REQ-MAT-DIRECT-COUPLE).
|
||||||
|
// On refusal (no belt/coupling, output-edge per REQ-MAT-ACCEPT-DIR, or
|
||||||
|
// a full target) it stays stuck at 1.0.
|
||||||
|
if (!lane.empty() && lane.front().progress >= 1.0)
|
||||||
|
{
|
||||||
|
const Item item = lane.front().item;
|
||||||
|
if (belts.tryPutItem(port.tile, item, port.direction)
|
||||||
|
|| tryDirectCoupleDeposit(state, building.id, port, item))
|
||||||
|
{
|
||||||
|
lane.erase(lane.begin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Feed the next buffered item onto the lane at progress 0.5 when the
|
||||||
|
// entry slot is free -- the lane holds at most three items and a new
|
||||||
|
// one needs a quarter-tile clearance ahead of 0.5.
|
||||||
|
if (!building.outputBuffer.items.empty()
|
||||||
|
&& lane.size() < 3
|
||||||
|
&& (lane.empty() || lane.back().progress >= 0.75))
|
||||||
|
{
|
||||||
|
lane.push_back(BeltItemSlot{building.outputBuffer.items.front(), 0.5});
|
||||||
|
building.outputBuffer.items.erase(building.outputBuffer.items.begin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/lib/sim/ProductionSystem.h
Normal file
105
src/lib/sim/ProductionSystem.h
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QVector2D>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Item.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "Port.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
class BeltSystem;
|
||||||
|
|
||||||
|
// The building side of material flow, end to end: what a building takes in, what it makes
|
||||||
|
// of it, and what it puts back out. Its four tick hooks are steps 3 to 5 of the tick order
|
||||||
|
// (docs/architecture.md), run back to back and in this order --
|
||||||
|
//
|
||||||
|
// tickBeltPull belt -> building (REQ-MAT-INPUT-PORTS, REQ-MAT-INPUT-INTAKE)
|
||||||
|
// tickProduction one cycle (REQ-MAT-CYCLE, REQ-MAT-OUTPUT-GROUP)
|
||||||
|
// tickShipyardProduction one ship (REQ-BLD-SHIPYARD)
|
||||||
|
// tickOutputBelts building -> belt (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-OUTPUT-PORT)
|
||||||
|
//
|
||||||
|
// -- so that an item arriving this tick is consumable this tick and a produced item starts
|
||||||
|
// travelling the same tick it appears.
|
||||||
|
//
|
||||||
|
// Split out of BuildingSystem, which keeps what buildings *are* -- placement, demolition,
|
||||||
|
// rotation, recipe and layout configuration. This system keeps what flows through them,
|
||||||
|
// and with it the three things that flow needs and building topology never did: the RNG an
|
||||||
|
// output group is drawn with, the unlock test that decides which groups are eligible, and
|
||||||
|
// the entity model a finished ship is spawned into.
|
||||||
|
//
|
||||||
|
// Holds no world data: the factory and the transport layer arrive per tick, as they do for
|
||||||
|
// ConstructionSystem (FactoryState.h).
|
||||||
|
class ProductionSystem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ProductionSystem(const GameConfig& config,
|
||||||
|
std::function<void(const std::string&, QVector2D,
|
||||||
|
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
||||||
|
std::function<bool(const std::string&)> isItemUnlocked,
|
||||||
|
std::mt19937& rng);
|
||||||
|
|
||||||
|
// Advances every building's virtual input belts, delivers what arrives into the input
|
||||||
|
// buffers (into the global block stock for the HQ), and takes what the adjacent real
|
||||||
|
// belts offer (REQ-MAT-INPUT-INTAKE, REQ-HQ-BELT-INPUT).
|
||||||
|
void tickBeltPull(FactoryState& state, BeltSystem& belts);
|
||||||
|
void tickProduction(FactoryState& state, Tick currentTick);
|
||||||
|
void tickShipyardProduction(FactoryState& state, Tick currentTick);
|
||||||
|
// Advances each building's virtual output belts, hands finished items off onto the
|
||||||
|
// adjacent real belt, and feeds new buffered items into them (REQ-MAT-OUTPUT-EMERGE).
|
||||||
|
void tickOutputBelts(FactoryState& state, BeltSystem& belts);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// 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).
|
||||||
|
bool canAcceptInput(const Building& consumer,
|
||||||
|
std::size_t inputPortIndex,
|
||||||
|
const ItemType& type) const;
|
||||||
|
// Places an accepted item onto the consumer's input belt at progress 0.0,
|
||||||
|
// reserving a per-material buffer slot (REQ-MAT-INPUT-INTAKE).
|
||||||
|
void depositToInputBelt(Building& consumer,
|
||||||
|
std::size_t inputPortIndex,
|
||||||
|
const Item& item);
|
||||||
|
// Attempts to hand an emerging output item straight into a directly adjacent
|
||||||
|
// building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
|
||||||
|
// Returns true if the item was accepted onto the consumer's input belt.
|
||||||
|
bool tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
|
||||||
|
const Port& outputPort,
|
||||||
|
const Item& item);
|
||||||
|
|
||||||
|
// What one cycle of this recipe produces: the items of its one output group
|
||||||
|
// (REQ-MAT-OUTPUT-GROUP). Where the recipe has several, one is picked by weight from
|
||||||
|
// those currently eligible (REQ-LOCK-OUTPUT-POOL) and the result is empty if none is;
|
||||||
|
// where it has one, that group is returned with no draw and no eligibility test.
|
||||||
|
std::vector<Item> rollOutputGroup(const RecipeDef& recipe);
|
||||||
|
|
||||||
|
const GameConfig& m_config;
|
||||||
|
// Spawning a finished ship reaches into the entity model, and an output group's
|
||||||
|
// eligibility into the unlock state; neither is factory data, so both arrive as
|
||||||
|
// callbacks rather than living in FactoryState.
|
||||||
|
std::function<void(const std::string&, QVector2D,
|
||||||
|
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
||||||
|
std::function<bool(const std::string&)> m_isItemUnlocked;
|
||||||
|
// The simulation's one RNG, by reference: a draw here shares the stream with every
|
||||||
|
// other draw in the run, which is what makes a replay reproducible (docs/replay_design.md).
|
||||||
|
std::mt19937& m_rng;
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
|
#include "ProductionSystem.h"
|
||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
#include "DynamicBodySystem.h"
|
#include "DynamicBodySystem.h"
|
||||||
@@ -107,9 +108,9 @@ void Simulation::reset(unsigned int seed)
|
|||||||
|
|
||||||
void Simulation::initializeSubsystems()
|
void Simulation::initializeSubsystems()
|
||||||
{
|
{
|
||||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
m_buildingSystem = std::make_unique<BuildingSystem>(m_config, m_beltSystem);
|
||||||
|
m_productionSystem = std::make_unique<ProductionSystem>(
|
||||||
m_config,
|
m_config,
|
||||||
m_beltSystem,
|
|
||||||
[this](const std::string& id, QVector2D pos,
|
[this](const std::string& id, QVector2D pos,
|
||||||
const std::optional<ShipLayoutConfig>& layout) {
|
const std::optional<ShipLayoutConfig>& layout) {
|
||||||
if (!isSchematicUnlocked(id))
|
if (!isSchematicUnlocked(id))
|
||||||
@@ -244,10 +245,10 @@ void Simulation::tick()
|
|||||||
// Construction + production pipeline
|
// Construction + production pipeline
|
||||||
m_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick);
|
m_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick);
|
||||||
m_deconstructionSystem->tick(m_factoryState, m_currentTick); // parallel to construction
|
m_deconstructionSystem->tick(m_factoryState, m_currentTick); // parallel to construction
|
||||||
m_buildingSystem->tickBeltPull(m_factoryState); // step 3
|
m_productionSystem->tickBeltPull(m_factoryState, m_beltSystem); // step 3
|
||||||
m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4
|
m_productionSystem->tickProduction(m_factoryState, m_currentTick); // step 4
|
||||||
m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b
|
m_productionSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b
|
||||||
m_buildingSystem->tickOutputBelts(m_factoryState); // step 5
|
m_productionSystem->tickOutputBelts(m_factoryState, m_beltSystem); // step 5
|
||||||
m_beltSystem.tick(); // step 6
|
m_beltSystem.tick(); // step 6
|
||||||
|
|
||||||
// Step 7: ship behavior systems (movement arbitration via intent priority)
|
// Step 7: ship behavior systems (movement arbitration via intent priority)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class AiSystem;
|
|||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
class ConstructionSystem;
|
class ConstructionSystem;
|
||||||
class DeconstructionSystem;
|
class DeconstructionSystem;
|
||||||
|
class ProductionSystem;
|
||||||
struct Command;
|
struct Command;
|
||||||
class Hasher;
|
class Hasher;
|
||||||
class CombatSystem;
|
class CombatSystem;
|
||||||
@@ -218,6 +219,7 @@ private:
|
|||||||
FactoryState m_factoryState;
|
FactoryState m_factoryState;
|
||||||
BeltSystem m_beltSystem;
|
BeltSystem m_beltSystem;
|
||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
|
std::unique_ptr<ProductionSystem> m_productionSystem;
|
||||||
std::unique_ptr<ConstructionSystem> m_constructionSystem;
|
std::unique_ptr<ConstructionSystem> m_constructionSystem;
|
||||||
std::unique_ptr<DeconstructionSystem> m_deconstructionSystem;
|
std::unique_ptr<DeconstructionSystem> m_deconstructionSystem;
|
||||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||||
|
|||||||
@@ -77,10 +77,7 @@ struct Fixture
|
|||||||
: cfg(loadTestConfig())
|
: cfg(loadTestConfig())
|
||||||
, belts(cfg.world.beltSpeed_tps)
|
, belts(cfg.world.beltSpeed_tps)
|
||||||
, rng(42)
|
, rng(42)
|
||||||
, buildings(cfg, belts,
|
, buildings(cfg, belts)
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
|
||||||
[](const std::string&) -> bool { return true; },
|
|
||||||
rng)
|
|
||||||
, construction(cfg)
|
, construction(cfg)
|
||||||
, ships(cfg, admin)
|
, ships(cfg, admin)
|
||||||
, ai(cfg)
|
, ai(cfg)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingBuffers.h"
|
#include "BuildingBuffers.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
|
#include "ProductionSystem.h"
|
||||||
#include "ConstructionSystem.h"
|
#include "ConstructionSystem.h"
|
||||||
#include "DeconstructionSystem.h"
|
#include "DeconstructionSystem.h"
|
||||||
#include "FactoryState.h"
|
#include "FactoryState.h"
|
||||||
@@ -57,16 +58,16 @@ static Port westPort(QPoint tile)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run N full sim ticks: construction, belt-pull, production, belt-push, belt tick.
|
// Run N full sim ticks: construction, belt-pull, production, belt-push, belt tick.
|
||||||
static void runTicks(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state_bs,
|
static void runTicks(ProductionSystem& production, const GameConfig& cfg,
|
||||||
BeltSystem& belts, int n, Tick& tick)
|
FactoryState& state_bs, BeltSystem& belts, int n, Tick& tick)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i)
|
||||||
{
|
{
|
||||||
ConstructionSystem(cfg).tick(state_bs, belts, tick);
|
ConstructionSystem(cfg).tick(state_bs, belts, tick);
|
||||||
DeconstructionSystem(cfg).tick(state_bs, tick);
|
DeconstructionSystem(cfg).tick(state_bs, tick);
|
||||||
bs.tickBeltPull(state_bs);
|
production.tickBeltPull(state_bs, belts);
|
||||||
bs.tickProduction(state_bs, tick);
|
production.tickProduction(state_bs, tick);
|
||||||
bs.tickOutputBelts(state_bs);
|
production.tickOutputBelts(state_bs, belts);
|
||||||
belts.tick();
|
belts.tick();
|
||||||
++tick;
|
++tick;
|
||||||
}
|
}
|
||||||
@@ -99,7 +100,8 @@ struct PlacementFixture
|
|||||||
FactoryState state = makeFactoryState(cfg);
|
FactoryState state = makeFactoryState(cfg);
|
||||||
BeltSystem belts;
|
BeltSystem belts;
|
||||||
std::mt19937 rng{0};
|
std::mt19937 rng{0};
|
||||||
BuildingSystem bs;
|
BuildingSystem bs;
|
||||||
|
ProductionSystem production;
|
||||||
|
|
||||||
// Blocks credited back since the run began. The state is seeded with the configured
|
// Blocks credited back since the run began. The state is seeded with the configured
|
||||||
// starting stock (FactoryState.h), so a refund reads as a delta rather than a total.
|
// starting stock (FactoryState.h), so a refund reads as a delta rather than a total.
|
||||||
@@ -116,7 +118,8 @@ struct PlacementFixture
|
|||||||
std::optional<double> beltSpeed_tps = std::nullopt,
|
std::optional<double> beltSpeed_tps = std::nullopt,
|
||||||
std::function<bool(const std::string&)> isItemUnlocked = nullptr)
|
std::function<bool(const std::string&)> isItemUnlocked = nullptr)
|
||||||
: belts(beltSpeed_tps.value_or(cfg.world.beltSpeed_tps))
|
: belts(beltSpeed_tps.value_or(cfg.world.beltSpeed_tps))
|
||||||
, bs(cfg, belts,
|
, bs(cfg, belts)
|
||||||
|
, production(cfg,
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
isItemUnlocked ? std::move(isItemUnlocked)
|
isItemUnlocked ? std::move(isItemUnlocked)
|
||||||
: std::function<bool(const std::string&)>(
|
: std::function<bool(const std::string&)>(
|
||||||
@@ -258,7 +261,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
|
|||||||
|
|
||||||
// Complete construction (1 s).
|
// Complete construction (1 s).
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
||||||
|
|
||||||
REQUIRE(f.belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East));
|
REQUIRE(f.belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East));
|
||||||
REQUIRE(getAllBuildings(f.state).size() == 1);
|
REQUIRE(getAllBuildings(f.state).size() == 1);
|
||||||
@@ -327,7 +330,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
|
|||||||
// Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300.
|
// Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300.
|
||||||
// We need to process tick 300 itself, so run 301 ticks (ticks 0..300).
|
// We need to process tick 300 itself, so run 301 ticks (ticks 0..300).
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||||
|
|
||||||
REQUIRE(getAllSites(f.state).empty());
|
REQUIRE(getAllSites(f.state).empty());
|
||||||
REQUIRE(findBuilding(f.state, id) != nullptr);
|
REQUIRE(findBuilding(f.state, id) != nullptr);
|
||||||
@@ -342,7 +345,7 @@ static void runUntilBuilt(PlacementFixture& f, BuildingId id, Tick& tick)
|
|||||||
{
|
{
|
||||||
for (int i = 0; i < 100000 && findBuilding(f.state, id) == nullptr; ++i)
|
for (int i = 0; i < 100000 && findBuilding(f.state, id) == nullptr; ++i)
|
||||||
{
|
{
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, 1, tick);
|
||||||
}
|
}
|
||||||
REQUIRE(findBuilding(f.state, id) != nullptr);
|
REQUIRE(findBuilding(f.state, id) != nullptr);
|
||||||
}
|
}
|
||||||
@@ -366,7 +369,7 @@ TEST_CASE("BuildingSystem: deconstructing a built building queues it; refund cre
|
|||||||
|
|
||||||
// After the deconstruction time (0.1s = 3 ticks) it is removed and the partial
|
// After the deconstruction time (0.1s = 3 ticks) it is removed and the partial
|
||||||
// refund (15 * 75 / 100 = 11) is credited exactly once.
|
// refund (15 * 75 / 100 = 11) is credited exactly once.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
|
||||||
REQUIRE(findBuilding(f.state, id) == nullptr);
|
REQUIRE(findBuilding(f.state, id) == nullptr);
|
||||||
REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0)));
|
REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0)));
|
||||||
REQUIRE(f.getRefundedBlocks() == 15 * f.cfg.world.refundPercentage / 100);
|
REQUIRE(f.getRefundedBlocks() == 15 * f.cfg.world.refundPercentage / 100);
|
||||||
@@ -390,14 +393,14 @@ TEST_CASE("BuildingSystem: deconstruction queue removes one building at a time",
|
|||||||
|
|
||||||
// After one deconstruction interval only the front building is gone; the
|
// After one deconstruction interval only the front building is gone; the
|
||||||
// second is still queued and its refund not yet credited.
|
// second is still queued and its refund not yet credited.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
|
||||||
REQUIRE(findBuilding(f.state, a) == nullptr);
|
REQUIRE(findBuilding(f.state, a) == nullptr);
|
||||||
REQUIRE(findBuilding(f.state, b) != nullptr);
|
REQUIRE(findBuilding(f.state, b) != nullptr);
|
||||||
REQUIRE(isQueuedForDeconstruction(f.state, b));
|
REQUIRE(isQueuedForDeconstruction(f.state, b));
|
||||||
REQUIRE(f.getRefundedBlocks() == 15 * f.cfg.world.refundPercentage / 100);
|
REQUIRE(f.getRefundedBlocks() == 15 * f.cfg.world.refundPercentage / 100);
|
||||||
|
|
||||||
// The second drains next.
|
// The second drains next.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 2, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 2, tick);
|
||||||
REQUIRE(findBuilding(f.state, b) == nullptr);
|
REQUIRE(findBuilding(f.state, b) == nullptr);
|
||||||
REQUIRE(f.getRefundedBlocks() == 2 * (15 * f.cfg.world.refundPercentage / 100));
|
REQUIRE(f.getRefundedBlocks() == 2 * (15 * f.cfg.world.refundPercentage / 100));
|
||||||
}
|
}
|
||||||
@@ -423,7 +426,7 @@ TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with n
|
|||||||
REQUIRE(f.getRefundedBlocks() == 0);
|
REQUIRE(f.getRefundedBlocks() == 0);
|
||||||
|
|
||||||
// It is never removed even after more than a deconstruction interval passes.
|
// It is never removed even after more than a deconstruction interval passes.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 5, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(0.1)) + 5, tick);
|
||||||
REQUIRE(findBuilding(f.state, id) != nullptr);
|
REQUIRE(findBuilding(f.state, id) != nullptr);
|
||||||
REQUIRE(f.getRefundedBlocks() == 0);
|
REQUIRE(f.getRefundedBlocks() == 0);
|
||||||
}
|
}
|
||||||
@@ -482,7 +485,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
|
|||||||
|
|
||||||
// Process through tick 300 to complete first miner's construction.
|
// Process through tick 300 to complete first miner's construction.
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||||
|
|
||||||
REQUIRE(getAllSites(f.state).size() == 1);
|
REQUIRE(getAllSites(f.state).size() == 1);
|
||||||
REQUIRE(getAllSites(f.state).front().id == id2);
|
REQUIRE(getAllSites(f.state).front().id == id2);
|
||||||
@@ -503,7 +506,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
|
|||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
// Construction completes on tick 300; production cycle starts tick 300,
|
// Construction completes on tick 300; production cycle starts tick 300,
|
||||||
// completes on tick 330. Process through tick 330: 331 ticks total.
|
// completes on tick 330. Process through tick 330: 331 ticks total.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
|
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
|
||||||
tick);
|
tick);
|
||||||
|
|
||||||
@@ -529,7 +532,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
|||||||
// (completesAt=360). Cycle 2 completes at tick 360: deposit item -> 2 items held,
|
// (completesAt=360). Cycle 2 completes at tick 360: deposit item -> 2 items held,
|
||||||
// which fills the buffer (capacity 2), so cycle 3 cannot start.
|
// which fills the buffer (capacity 2), so cycle 3 cannot start.
|
||||||
// Need to process through tick 360: 361 ticks total.
|
// Need to process through tick 360: 361 ticks total.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(10.0))
|
static_cast<int>(secondsToTicks(10.0))
|
||||||
+ 2 * static_cast<int>(secondsToTicks(1.0)) + 1,
|
+ 2 * static_cast<int>(secondsToTicks(1.0)) + 1,
|
||||||
tick);
|
tick);
|
||||||
@@ -558,7 +561,7 @@ TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one comple
|
|||||||
|
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
// Construction completes at tick 300 and cycle 1 starts in that same tick.
|
// Construction completes at tick 300 and cycle 1 starts in that same tick.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||||
const Building* b = findBuilding(f.state, id);
|
const Building* b = findBuilding(f.state, id);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
@@ -567,7 +570,7 @@ TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one comple
|
|||||||
|
|
||||||
// Process up to and including that completion tick: the next cycle is already
|
// 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.
|
// running, due exactly one duration later rather than one duration plus a tick.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(cycleTicks), tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(cycleTicks), tick);
|
||||||
b = findBuilding(f.state, id);
|
b = findBuilding(f.state, id);
|
||||||
REQUIRE(b->getOutputItemCount() == 1);
|
REQUIRE(b->getOutputItemCount() == 1);
|
||||||
REQUIRE(b->production.has_value());
|
REQUIRE(b->production.has_value());
|
||||||
@@ -579,7 +582,7 @@ TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one comple
|
|||||||
building.outputBuffer.items.clear();
|
building.outputBuffer.items.clear();
|
||||||
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
|
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
|
||||||
});
|
});
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(cycleTicks), tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(cycleTicks), tick);
|
||||||
b = findBuilding(f.state, id);
|
b = findBuilding(f.state, id);
|
||||||
REQUIRE(b->production.has_value());
|
REQUIRE(b->production.has_value());
|
||||||
REQUIRE(b->production->completesAt == firstCompletesAt + 2 * cycleTicks);
|
REQUIRE(b->production->completesAt == firstCompletesAt + 2 * cycleTicks);
|
||||||
@@ -603,10 +606,10 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
|
|||||||
|
|
||||||
// The queue builds one at a time: miner (10s) completes at tick 300, then
|
// The queue builds one at a time: miner (10s) completes at tick 300, then
|
||||||
// the smelter (15s) starts and completes at tick 300 + 450 = 750.
|
// the smelter (15s) starts and completes at tick 300 + 450 = 750.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||||
REQUIRE(getProductionBuildingCount(f.state) == 1);
|
REQUIRE(getProductionBuildingCount(f.state) == 1);
|
||||||
|
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)), tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)), tick);
|
||||||
REQUIRE(getProductionBuildingCount(f.state) == 2);
|
REQUIRE(getProductionBuildingCount(f.state) == 2);
|
||||||
|
|
||||||
// Neither is producing yet: the miner has no recipe selected, and the
|
// Neither is producing yet: the miner has no recipe selected, and the
|
||||||
@@ -614,7 +617,7 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
|
|||||||
REQUIRE(getActiveProductionBuildingCount(f.state) == 0);
|
REQUIRE(getActiveProductionBuildingCount(f.state) == 0);
|
||||||
|
|
||||||
f.bs.setRecipe(f.state, minerId, "mine_iron_ore");
|
f.bs.setRecipe(f.state, minerId, "mine_iron_ore");
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, 1, tick);
|
||||||
REQUIRE(getActiveProductionBuildingCount(f.state) == 1);
|
REQUIRE(getActiveProductionBuildingCount(f.state) == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,12 +634,12 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
|
|||||||
REQUIRE(getActiveProductionBuildingCount(f.state) == 0);
|
REQUIRE(getActiveProductionBuildingCount(f.state) == 0);
|
||||||
|
|
||||||
// Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330).
|
// Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330).
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||||
REQUIRE(getActiveProductionBuildingCount(f.state) == 1);
|
REQUIRE(getActiveProductionBuildingCount(f.state) == 1);
|
||||||
|
|
||||||
// Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the
|
// Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the
|
||||||
// output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER).
|
// output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER).
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, 2 * static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, 2 * static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, id);
|
const Building* b = findBuilding(f.state, id);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
@@ -663,7 +666,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
|||||||
|
|
||||||
// Complete construction (15s → tick 450+1 = 451 ticks).
|
// Complete construction (15s → tick 450+1 = 451 ticks).
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||||
REQUIRE(findBuilding(f.state, sid)->recipeId.empty());
|
REQUIRE(findBuilding(f.state, sid)->recipeId.empty());
|
||||||
|
|
||||||
// Place west-flowing belt at (2,0): belt flows West, delivers to smelter.
|
// Place west-flowing belt at (2,0): belt flows West, delivers to smelter.
|
||||||
@@ -671,7 +674,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
|||||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
|
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, sid);
|
const Building* b = findBuilding(f.state, sid);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
@@ -697,12 +700,12 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf
|
|||||||
|
|
||||||
const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
|
const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||||
|
|
||||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state); // accepts the item onto the input belt at progress 0.0
|
f.production.tickBeltPull(f.state, f.belts); // accepts the item onto the input belt at progress 0.0
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, sid);
|
const Building* b = findBuilding(f.state, sid);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
@@ -714,7 +717,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf
|
|||||||
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1);
|
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1);
|
||||||
|
|
||||||
// One more pull tick advances the input belt to the centre; the item arrives.
|
// One more pull tick advances the input belt to the centre; the item arrives.
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
REQUIRE(b->inputBuffer.counts.at(ItemType{"iron_ore"}) == 1);
|
REQUIRE(b->inputBuffer.counts.at(ItemType{"iron_ore"}) == 1);
|
||||||
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1);
|
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1);
|
||||||
}
|
}
|
||||||
@@ -729,7 +732,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th
|
|||||||
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
|
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
|
||||||
QPoint(0, 0), Rotation::East, 0).value();
|
QPoint(0, 0), Rotation::East, 0).value();
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
||||||
|
|
||||||
// Feed scrap via an input belt without ever running production (only pull), so
|
// Feed scrap via an input belt without ever running production (only pull), so
|
||||||
// the buffer fills and stays full. Try to over-fill it well past the cap.
|
// the buffer fills and stays full. Try to over-fill it well past the cap.
|
||||||
@@ -738,7 +741,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th
|
|||||||
{
|
{
|
||||||
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
|
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, id);
|
const Building* b = findBuilding(f.state, id);
|
||||||
@@ -761,7 +764,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
|
|||||||
const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
|
const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||||
|
|
||||||
// Feed 2 iron_ore (the test-config iron_ingot recipe needs 2) via a
|
// Feed 2 iron_ore (the test-config iron_ingot recipe needs 2) via a
|
||||||
// west-flowing belt at input port (2,0).
|
// west-flowing belt at input port (2,0).
|
||||||
@@ -770,11 +773,11 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
|
|||||||
{
|
{
|
||||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// iron_ingot recipe cycle is 2s; run to completion.
|
// iron_ingot recipe cycle is 2s; run to completion.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(2.0)) + 2, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(2.0)) + 2, tick);
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, sid);
|
const Building* b = findBuilding(f.state, sid);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
@@ -797,7 +800,7 @@ TEST_CASE("BuildingSystem: mixed ore on one belt leaves the smelter on the first
|
|||||||
const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
|
const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||||
|
|
||||||
// Feed 1 iron_ore, then 2 copper_ore, via the west-flowing input belt.
|
// Feed 1 iron_ore, then 2 copper_ore, via the west-flowing input belt.
|
||||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||||
@@ -806,10 +809,10 @@ TEST_CASE("BuildingSystem: mixed ore on one belt leaves the smelter on the first
|
|||||||
{
|
{
|
||||||
f.belts.tryPutItem(QPoint(2, 0), makeItem(id));
|
f.belts.tryPutItem(QPoint(2, 0), makeItem(id));
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
}
|
}
|
||||||
|
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(2.5)) + 2, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(2.5)) + 2, tick);
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, sid);
|
const Building* b = findBuilding(f.state, sid);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
@@ -847,13 +850,13 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui
|
|||||||
|
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
// Construction (10s) + 1 production cycle (1s) + 1 extra tick.
|
// Construction (10s) + 1 production cycle (1s) + 1 extra tick.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
|
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
|
||||||
tick);
|
tick);
|
||||||
|
|
||||||
// Item should have been pushed onto the belt this tick or a subsequent one.
|
// Item should have been pushed onto the belt this tick or a subsequent one.
|
||||||
// Run one more tick to ensure tickBeltPush fires after the deposit tick.
|
// Run one more tick to ensure tickBeltPush fires after the deposit tick.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, 1, tick);
|
||||||
|
|
||||||
const std::optional<Item> item = f.belts.tryTakeItem(eastPort(QPoint(1, 1)));
|
const std::optional<Item> item = f.belts.tryTakeItem(eastPort(QPoint(1, 1)));
|
||||||
REQUIRE(item.has_value());
|
REQUIRE(item.has_value());
|
||||||
@@ -878,7 +881,7 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p
|
|||||||
|
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
// Smelter build (15s) + margin for coupling and a smelt cycle.
|
// Smelter build (15s) + margin for coupling and a smelt cycle.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(30.0)), tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(30.0)), tick);
|
||||||
|
|
||||||
const Building* smelter = findBuilding(f.state, smelterId);
|
const Building* smelter = findBuilding(f.state, smelterId);
|
||||||
REQUIRE(smelter != nullptr);
|
REQUIRE(smelter != nullptr);
|
||||||
@@ -907,7 +910,7 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu
|
|||||||
|
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
// Both miners build sequentially (10s each), then the producer runs and jams.
|
// Both miners build sequentially (10s each), then the producer runs and jams.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)), tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)), tick);
|
||||||
|
|
||||||
const Building* miner = findBuilding(f.state, minerId);
|
const Building* miner = findBuilding(f.state, minerId);
|
||||||
const Building* sink = findBuilding(f.state, sinkId);
|
const Building* sink = findBuilding(f.state, sinkId);
|
||||||
@@ -933,7 +936,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
|
|||||||
|
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
// Run until first item is in output buffer.
|
// Run until first item is in output buffer.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
|
static_cast<int>(secondsToTicks(10.0)) + static_cast<int>(secondsToTicks(1.0)) + 1,
|
||||||
tick);
|
tick);
|
||||||
|
|
||||||
@@ -998,8 +1001,8 @@ TEST_CASE("BuildingSystem: a single-group recipe consumes no randomness", "[buil
|
|||||||
advanced.bs.setRecipe(advanced.state, b, "mine_iron_ore");
|
advanced.bs.setRecipe(advanced.state, b, "mine_iron_ore");
|
||||||
|
|
||||||
const int ticks = static_cast<int>(secondsToTicks(10.0)) + 40;
|
const int ticks = static_cast<int>(secondsToTicks(10.0)) + 40;
|
||||||
runTicks(quiet.bs, quiet.cfg, quiet.state, quiet.belts, ticks, tickA);
|
runTicks(quiet.production, quiet.cfg, quiet.state, quiet.belts, ticks, tickA);
|
||||||
runTicks(advanced.bs, advanced.cfg, advanced.state, advanced.belts, ticks, tickB);
|
runTicks(advanced.production, advanced.cfg, advanced.state, advanced.belts, ticks, tickB);
|
||||||
|
|
||||||
const Building* minerA = findBuilding(quiet.state, a);
|
const Building* minerA = findBuilding(quiet.state, a);
|
||||||
const Building* minerB = findBuilding(advanced.state, b);
|
const Building* minerB = findBuilding(advanced.state, b);
|
||||||
@@ -1056,7 +1059,7 @@ TEST_CASE("BuildingSystem: a group with a locked item is never picked", "[buildi
|
|||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
|
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
|
||||||
QPoint(0, 0), Rotation::East, 0).value();
|
QPoint(0, 0), Rotation::East, 0).value();
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
||||||
f.bs.setRecipe(f.state, id, "reprocessing_cycle");
|
f.bs.setRecipe(f.state, id, "reprocessing_cycle");
|
||||||
|
|
||||||
@@ -1072,7 +1075,7 @@ TEST_CASE("BuildingSystem: a group with a locked item is never picked", "[buildi
|
|||||||
building.outputBuffer.items.clear();
|
building.outputBuffer.items.clear();
|
||||||
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
|
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
|
||||||
});
|
});
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(3.0)) + 1, tick);
|
static_cast<int>(secondsToTicks(3.0)) + 1, tick);
|
||||||
|
|
||||||
for (const Item& item : outputSideItems(*findBuilding(f.state, id)))
|
for (const Item& item : outputSideItems(*findBuilding(f.state, id)))
|
||||||
@@ -1094,7 +1097,7 @@ TEST_CASE("BuildingSystem: reprocessing plant sizes one output buffer per possib
|
|||||||
|
|
||||||
// Complete construction (25s).
|
// Complete construction (25s).
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
||||||
|
|
||||||
// A plant holds no buffers until it has a recipe (REQ-BLD-AUTO-RECIPE); selecting
|
// A plant holds no buffers until it has a recipe (REQ-BLD-AUTO-RECIPE); selecting
|
||||||
// one sizes them, exactly as the first scrap offered to it would.
|
// one sizes them, exactly as the first scrap offered to it would.
|
||||||
@@ -1124,7 +1127,7 @@ TEST_CASE("BuildingSystem: one full output buffer stops the plant even when the
|
|||||||
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
|
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
|
||||||
QPoint(0, 0), Rotation::East, 0).value();
|
QPoint(0, 0), Rotation::East, 0).value();
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
||||||
|
|
||||||
// Feed a full cycle's scrap (5) so only the output side can hold it back.
|
// Feed a full cycle's scrap (5) so only the output side can hold it back.
|
||||||
@@ -1133,7 +1136,7 @@ TEST_CASE("BuildingSystem: one full output buffer stops the plant even when the
|
|||||||
{
|
{
|
||||||
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
|
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill the iron_ingot buffer to its cap and leave the other two empty.
|
// Fill the iron_ingot buffer to its cap and leave the other two empty.
|
||||||
@@ -1146,7 +1149,7 @@ TEST_CASE("BuildingSystem: one full output buffer stops the plant even when the
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, 5, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, 5, tick);
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, id);
|
const Building* b = findBuilding(f.state, id);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
@@ -1171,7 +1174,7 @@ TEST_CASE("BuildingSystem: reprocessing plant runs a second cycle while holding
|
|||||||
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
|
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
|
||||||
QPoint(0, 0), Rotation::East, 0).value();
|
QPoint(0, 0), Rotation::East, 0).value();
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
||||||
|
|
||||||
// Two cycles' worth of scrap (5 each), which is exactly the input cap.
|
// Two cycles' worth of scrap (5 each), which is exactly the input cap.
|
||||||
@@ -1180,13 +1183,13 @@ TEST_CASE("BuildingSystem: reprocessing plant runs a second cycle while holding
|
|||||||
{
|
{
|
||||||
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
|
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
}
|
}
|
||||||
REQUIRE(findBuilding(f.state, id)->pendingInputCount(ItemType{"scrap"}) == 10);
|
REQUIRE(findBuilding(f.state, id)->pendingInputCount(ItemType{"scrap"}) == 10);
|
||||||
|
|
||||||
// No belt carries the output away, so the first cycle's result is still held.
|
// No belt carries the output away, so the first cycle's result is still held.
|
||||||
// reprocessing_cycle runs 3s; run through the completion tick.
|
// reprocessing_cycle runs 3s; run through the completion tick.
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(3.0)) + 1, tick);
|
static_cast<int>(secondsToTicks(3.0)) + 1, tick);
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, id);
|
const Building* b = findBuilding(f.state, id);
|
||||||
@@ -1207,7 +1210,7 @@ static BuildingId buildSmelter(PlacementFixture& f, QPoint anchor, Tick& tick)
|
|||||||
{
|
{
|
||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
f.bs.place(f.state, BuildingType::Smelter, anchor, Rotation::East, 0).value();
|
f.bs.place(f.state, BuildingType::Smelter, anchor, Rotation::East, 0).value();
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts,
|
runTicks(f.production, f.cfg, f.state, f.belts,
|
||||||
static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -1241,13 +1244,13 @@ TEST_CASE("BuildingSystem: a set recipe is never replaced by a later material",
|
|||||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
REQUIRE(findBuilding(f.state, id)->recipeId == "iron_ingot");
|
REQUIRE(findBuilding(f.state, id)->recipeId == "iron_ingot");
|
||||||
|
|
||||||
// Copper ore next: refused, and the recipe stands.
|
// Copper ore next: refused, and the recipe stands.
|
||||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("copper_ore"));
|
f.belts.tryPutItem(QPoint(2, 0), makeItem("copper_ore"));
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, id);
|
const Building* b = findBuilding(f.state, id);
|
||||||
REQUIRE(b->recipeId == "iron_ingot");
|
REQUIRE(b->recipeId == "iron_ingot");
|
||||||
@@ -1270,7 +1273,7 @@ TEST_CASE("BuildingSystem: a manually selected recipe is not overridden", "[buil
|
|||||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, id);
|
const Building* b = findBuilding(f.state, id);
|
||||||
REQUIRE(b->recipeId == "copper_ingot");
|
REQUIRE(b->recipeId == "copper_ingot");
|
||||||
@@ -1291,8 +1294,8 @@ TEST_CASE("BuildingSystem: selecting a different recipe frees a stuck auto-recip
|
|||||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, 30, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, 30, tick);
|
||||||
|
|
||||||
const Building* stuck = findBuilding(f.state, id);
|
const Building* stuck = findBuilding(f.state, id);
|
||||||
REQUIRE(stuck->recipeId == "iron_ingot");
|
REQUIRE(stuck->recipeId == "iron_ingot");
|
||||||
@@ -1322,7 +1325,7 @@ TEST_CASE("BuildingSystem: selecting (Auto) returns the building to automatic se
|
|||||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
|
|
||||||
REQUIRE(findBuilding(f.state, id)->recipeId == "iron_ingot");
|
REQUIRE(findBuilding(f.state, id)->recipeId == "iron_ingot");
|
||||||
}
|
}
|
||||||
@@ -1340,7 +1343,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
|||||||
|
|
||||||
// Complete construction (25s).
|
// Complete construction (25s).
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
||||||
|
|
||||||
// Feed 5 scrap into the building via a belt at an input port.
|
// Feed 5 scrap into the building via a belt at an input port.
|
||||||
// Reprocessing plant body (East rotation) = 3×3 at (0,0).
|
// Reprocessing plant body (East rotation) = 3×3 at (0,0).
|
||||||
@@ -1350,7 +1353,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
|||||||
{
|
{
|
||||||
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
|
f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
|
||||||
f.belts.tick();
|
f.belts.tick();
|
||||||
f.bs.tickBeltPull(f.state);
|
f.production.tickBeltPull(f.state, f.belts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify all five scrap were accepted; some may still be travelling inward on
|
// Verify all five scrap were accepted; some may still be travelling inward on
|
||||||
@@ -1362,7 +1365,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run production cycle (3s = 90 ticks + 1 for the completion tick).
|
// Run production cycle (3s = 90 ticks + 1 for the completion tick).
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(3.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(3.0)) + 1, tick);
|
||||||
|
|
||||||
const Building* b = findBuilding(f.state, id);
|
const Building* b = findBuilding(f.state, id);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
@@ -1406,7 +1409,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
|
|||||||
const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
|
const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
||||||
REQUIRE(getAllSites(f.state).empty());
|
REQUIRE(getAllSites(f.state).empty());
|
||||||
|
|
||||||
const std::optional<BuildingId> result =
|
const std::optional<BuildingId> result =
|
||||||
@@ -1806,7 +1809,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct
|
|||||||
const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
|
const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
||||||
REQUIRE(findBuilding(f.state, id) != nullptr);
|
REQUIRE(findBuilding(f.state, id) != nullptr);
|
||||||
|
|
||||||
const Building& before = *findBuilding(f.state, id);
|
const Building& before = *findBuilding(f.state, id);
|
||||||
@@ -1827,7 +1830,7 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste
|
|||||||
const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
|
const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
||||||
|
|
||||||
f.bs.rotateInPlace(f.state, id, Rotation::North);
|
f.bs.rotateInPlace(f.state, id, Rotation::North);
|
||||||
|
|
||||||
@@ -1847,7 +1850,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the output filters of a split
|
|||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
while (getAllBuildings(f.state).empty() && tick < 100000)
|
while (getAllBuildings(f.state).empty() && tick < 100000)
|
||||||
{
|
{
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, 1, tick);
|
||||||
}
|
}
|
||||||
REQUIRE(getAllBuildings(f.state).size() == 1);
|
REQUIRE(getAllBuildings(f.state).size() == 1);
|
||||||
|
|
||||||
@@ -1892,7 +1895,7 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
|
|||||||
Tick tick = 0;
|
Tick tick = 0;
|
||||||
while (getAllBuildings(f.state).empty() && tick < 100000)
|
while (getAllBuildings(f.state).empty() && tick < 100000)
|
||||||
{
|
{
|
||||||
runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick);
|
runTicks(f.production, f.cfg, f.state, f.belts, 1, tick);
|
||||||
}
|
}
|
||||||
REQUIRE(getAllBuildings(f.state).size() == 1);
|
REQUIRE(getAllBuildings(f.state).size() == 1);
|
||||||
REQUIRE(getAllBuildings(f.state)[0].type == BuildingType::Splitter);
|
REQUIRE(getAllBuildings(f.state)[0].type == BuildingType::Splitter);
|
||||||
@@ -2199,12 +2202,13 @@ namespace
|
|||||||
|
|
||||||
// Advances the sim until the given site becomes an operational building, or a
|
// Advances the sim until the given site becomes an operational building, or a
|
||||||
// safety cap is reached.
|
// safety cap is reached.
|
||||||
void buildToCompletion(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state,
|
void buildToCompletion(ProductionSystem& production, const GameConfig& cfg,
|
||||||
BeltSystem& belts, BuildingId id, Tick& tick)
|
FactoryState& state, BeltSystem& belts, BuildingId id,
|
||||||
|
Tick& tick)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 20000 && findBuilding(state, id) == nullptr; ++i)
|
for (int i = 0; i < 20000 && findBuilding(state, id) == nullptr; ++i)
|
||||||
{
|
{
|
||||||
runTicks(bs, cfg, state, belts, 1, tick);
|
runTicks(production, cfg, state, belts, 1, tick);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2239,7 +2243,7 @@ TEST_CASE("BuildingSystem: getInputPorts matches between a site and the built bu
|
|||||||
const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
const std::vector<Port> sitePorts = getInputPorts(f.state, f.cfg, id);
|
const std::vector<Port> sitePorts = getInputPorts(f.state, f.cfg, id);
|
||||||
buildToCompletion(f.bs, f.cfg, f.state, f.belts, id, tick);
|
buildToCompletion(f.production, f.cfg, f.state, f.belts, id, tick);
|
||||||
REQUIRE(findBuilding(f.state, id) != nullptr);
|
REQUIRE(findBuilding(f.state, id) != nullptr);
|
||||||
const std::vector<Port> builtPorts = getInputPorts(f.state, f.cfg, id);
|
const std::vector<Port> builtPorts = getInputPorts(f.state, f.cfg, id);
|
||||||
|
|
||||||
|
|||||||
@@ -65,10 +65,7 @@ struct CombatFixture
|
|||||||
, rng(42)
|
, rng(42)
|
||||||
, belts(cfg.world.beltSpeed_tps)
|
, belts(cfg.world.beltSpeed_tps)
|
||||||
, ships(cfg, admin)
|
, ships(cfg, admin)
|
||||||
, buildings(cfg, belts,
|
, buildings(cfg, belts)
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
|
||||||
[](const std::string&) -> bool { return true; },
|
|
||||||
rng)
|
|
||||||
, combat(cfg)
|
, combat(cfg)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user