Allow direct output-to-input port coupling between adjacent buildings
This commit is contained in:
@@ -825,89 +825,88 @@ void BuildingSystem::tickBeltPull()
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Feed newly accepted items from adjacent belts onto the input belts at
|
||||
// progress 0.0. HQ accepts building blocks into the global stock with no
|
||||
// reservation; other buildings reserve a per-material buffer slot.
|
||||
if (isHq)
|
||||
{
|
||||
for (std::size_t i = 0; i < building.inputPorts.size(); ++i)
|
||||
{
|
||||
const Port& port = building.inputPorts[i];
|
||||
std::vector<BeltItemSlot>& lane = building.incomingItems[i];
|
||||
const std::optional<ItemType> peeked = m_belts.peekItem(port);
|
||||
if (!peeked || peeked->id != "building_block") { continue; }
|
||||
if (!inputLaneEntryFree(lane)) { continue; }
|
||||
const std::optional<Item> taken = m_belts.tryTakeItem(port);
|
||||
if (taken)
|
||||
{
|
||||
lane.push_back(BeltItemSlot{*taken, 0.0});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Auto-recipe buildings (Smelter, Reprocessing Plant) accept any item
|
||||
// that is an input to one of their recipes; their caps already span the
|
||||
// union of those inputs (initAutoBuffers), so no recipe lookup is needed.
|
||||
if (!isAutoRecipeBuildingType(building.type))
|
||||
{
|
||||
if (building.recipeId.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (building.type != BuildingType::Shipyard)
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
if (!recipe || recipe->inputs.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 Port& port = building.inputPorts[i];
|
||||
std::vector<BeltItemSlot>& lane = building.incomingItems[i];
|
||||
|
||||
const std::optional<ItemType> peeked = m_belts.peekItem(port);
|
||||
if (!peeked)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const ItemType& type = *peeked;
|
||||
|
||||
// Accept only if this type is a required input and the buffer has space.
|
||||
const std::map<ItemType, int>::const_iterator capIt =
|
||||
building.inputBuffer.caps.find(type);
|
||||
if (capIt == building.inputBuffer.caps.end() || capIt->second == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reservation-aware space test: buffered + in-transit must stay under
|
||||
// the cap (REQ-MAT-INPUT-INTAKE).
|
||||
if (building.pendingInputCount(type) >= capIt->second)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inputLaneEntryFree(lane))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::optional<Item> taken = m_belts.tryTakeItem(port);
|
||||
const std::optional<ItemType> peeked = m_belts.peekItem(building.inputPorts[i]);
|
||||
if (!peeked) { continue; }
|
||||
if (!canAcceptInput(building, i, *peeked)) { continue; }
|
||||
const std::optional<Item> taken = m_belts.tryTakeItem(building.inputPorts[i]);
|
||||
if (taken)
|
||||
{
|
||||
lane.push_back(BeltItemSlot{*taken, 0.0});
|
||||
depositToInputBelt(building, i, *taken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(BuildingId producerId,
|
||||
const Port& outputPort,
|
||||
const Item& item)
|
||||
{
|
||||
const std::map<std::pair<int, int>, BuildingId>::const_iterator occIt =
|
||||
m_tileOccupancy.find({outputPort.tile.x(), outputPort.tile.y()});
|
||||
if (occIt == m_tileOccupancy.end() || occIt->second == producerId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Building* consumer = findBuildingMutable(occIt->second);
|
||||
if (!consumer)
|
||||
{
|
||||
return false; // an unbuilt construction site, or not an operational building
|
||||
}
|
||||
|
||||
// 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; }
|
||||
|
||||
if (!canAcceptInput(*consumer, j, item.type)) { return false; }
|
||||
depositToInputBelt(*consumer, j, item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BuildingSystem::tickProduction(Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
@@ -1167,13 +1166,19 @@ void BuildingSystem::tickOutputBelts()
|
||||
// caps to 0.5 / 0.75 / 1.0 for up to three items).
|
||||
advanceBeltSlots(lane, progressPerTick);
|
||||
|
||||
// 2. Hand the front item off onto the adjacent real belt once it reaches
|
||||
// the output edge (progress 1.0). On refusal — no belt, output-edge
|
||||
// (REQ-MAT-ACCEPT-DIR), or a full belt — it stays stuck at 1.0.
|
||||
if (!lane.empty() && lane.front().progress >= 1.0
|
||||
&& m_belts.tryPutItem(port.tile, lane.front().item, port.direction))
|
||||
// 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)
|
||||
{
|
||||
lane.erase(lane.begin());
|
||||
const Item item = lane.front().item;
|
||||
if (m_belts.tryPutItem(port.tile, item, port.direction)
|
||||
|| tryDirectCoupleDeposit(building.id, port, item))
|
||||
{
|
||||
lane.erase(lane.begin());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Feed the next buffered item onto the lane at progress 0.5 when the
|
||||
@@ -1250,6 +1255,18 @@ const Building* BuildingSystem::findBuilding(BuildingId id) const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Building* BuildingSystem::findBuildingMutable(BuildingId id)
|
||||
{
|
||||
for (Building& building : m_buildings)
|
||||
{
|
||||
if (building.id == id)
|
||||
{
|
||||
return &building;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ConstructionSite* BuildingSystem::findSite(BuildingId id) const
|
||||
{
|
||||
for (const ConstructionSite& site : m_constructionQueue)
|
||||
|
||||
@@ -181,6 +181,25 @@ public:
|
||||
void appendChecksum(Hasher& hasher) const;
|
||||
|
||||
private:
|
||||
Building* findBuildingMutable(BuildingId id);
|
||||
// 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(BuildingId producerId,
|
||||
const Port& outputPort,
|
||||
const Item& item);
|
||||
|
||||
const BuildingDef* findBuildingDef(BuildingType type) const;
|
||||
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
|
||||
const ShipDef* findShipDef(const std::string& id) const;
|
||||
|
||||
@@ -807,6 +807,84 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui
|
||||
REQUIRE(item->type.id == "iron_ore");
|
||||
}
|
||||
|
||||
// Two directly adjacent buildings whose ports meet transfer items with no belt in
|
||||
// between: a miner's iron_ore output feeds straight into a smelter, which smelts it
|
||||
// (REQ-MAT-DIRECT-COUPLE).
|
||||
TEST_CASE("BuildingSystem: output port couples directly into an adjacent input port",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
BuildingId nextBuildingId = 1;
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextBuildingId]() { return nextBuildingId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||
[](const std::string&) -> bool { return true; },
|
||||
rng);
|
||||
|
||||
// Miner at (0,0): body (0,0),(1,0),(0,1); output port tile (1,1) flowing East.
|
||||
const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
bs.setRecipe(minerId, "mine_iron_ore");
|
||||
// Smelter anchored at (1,1): body (1,1),(2,1),(1,2),(2,2). Its body cell (1,1) is
|
||||
// the miner's output-port tile, and its west input edge there faces East, so the
|
||||
// two ports meet — no belt placed anywhere.
|
||||
const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(1, 1), Rotation::East, 0);
|
||||
|
||||
Tick tick = 0;
|
||||
// Smelter build (15s) + margin for coupling and a smelt cycle.
|
||||
runTicks(bs, belts, static_cast<int>(secondsToTicks(30.0)), tick);
|
||||
|
||||
const Building* smelter = bs.findBuilding(smelterId);
|
||||
REQUIRE(smelter != nullptr);
|
||||
// iron_ore reached the smelter over the direct coupling and was smelted.
|
||||
bool hasIronIngot = false;
|
||||
for (const Item& produced : outputSideItems(*smelter))
|
||||
{
|
||||
if (produced.type.id == "iron_ingot") { hasIronIngot = true; }
|
||||
}
|
||||
REQUIRE(hasIronIngot);
|
||||
}
|
||||
|
||||
// A producer coupled to a building that cannot accept its item delivers nothing; the
|
||||
// item stays stuck at the producer's output port (REQ-MAT-DIRECT-COUPLE acceptance).
|
||||
TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stuck",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
BuildingId nextBuildingId = 1;
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextBuildingId]() { return nextBuildingId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||
[](const std::string&) -> bool { return true; },
|
||||
rng);
|
||||
|
||||
// Producing miner at (0,0), output port (1,1) East.
|
||||
const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
bs.setRecipe(minerId, "mine_iron_ore");
|
||||
// A second, idle miner anchored at (1,1) occupies the output-port tile but takes
|
||||
// no inputs, so it cannot accept the iron_ore.
|
||||
const BuildingId sinkId = bs.place(BuildingType::Miner, QPoint(1, 1), Rotation::East, 0);
|
||||
|
||||
Tick tick = 0;
|
||||
// Both miners build sequentially (10s each), then the producer runs and jams.
|
||||
runTicks(bs, belts, static_cast<int>(secondsToTicks(25.0)), tick);
|
||||
|
||||
const Building* miner = bs.findBuilding(minerId);
|
||||
const Building* sink = bs.findBuilding(sinkId);
|
||||
REQUIRE(miner != nullptr);
|
||||
REQUIRE(sink != nullptr);
|
||||
// Nothing was delivered, and the producer's output side has backed up to its cap.
|
||||
REQUIRE(sink->pendingInputCount(ItemType{"iron_ore"}) == 0);
|
||||
REQUIRE(miner->outputItemCount() == miner->outputBuffer.capacity);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// setRecipe clears buffers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user