Allow direct output-to-input port coupling between adjacent buildings

This commit is contained in:
2026-07-14 20:23:24 +02:00
parent c9f14970a1
commit 486296feee
4 changed files with 201 additions and 84 deletions

View File

@@ -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)

View File

@@ -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;