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

@@ -146,11 +146,11 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
## Material Transport & Buffers ## Material Transport & Buffers
- REQ-MAT-BELT-ONLY: Materials are transported exclusively via belts, splitters, and tunnels. - REQ-MAT-BELT-ONLY: Materials are transported exclusively via belts, splitters, and tunnels, with one exception: two directly adjacent buildings whose output and input ports meet transfer items straight between them without an intervening transport tile (REQ-MAT-DIRECT-COUPLE).
- REQ-MAT-INPUT-PORTS: A building accepts items from any adjacent belt tile on any edge of its footprint (excluding cells occupied by output port(s)) whose direction points toward the building, provided the item is an input required by the currently selected recipe and the matching per-material input buffer has free space. An accepted item does not enter the building instantly; it is removed from the belt and travels inward across the input port's footprint cell on that port's own input belt before being added to the buffer (REQ-MAT-INPUT-INTAKE). - REQ-MAT-INPUT-PORTS: A building accepts items from any adjacent belt tile on any edge of its footprint (excluding cells occupied by output port(s)) whose direction points toward the building, provided the item is an input required by the currently selected recipe and the matching per-material input buffer has free space. An accepted item does not enter the building instantly; it is removed from the belt and travels inward across the input port's footprint cell on that port's own input belt before being added to the buffer (REQ-MAT-INPUT-INTAKE).
- REQ-MAT-INPUT-INTAKE: Accepted input items travel into a building as an animation rather than vanishing off the belt instantly — the input-side mirror of REQ-MAT-OUTPUT-EMERGE. Each input port has its own **input belt** — a virtual belt tile occupying the input port's footprint cell (the body cell the feeding belt points into), oriented in the port's inward flow direction, with progress 0.0 at the outer edge adjacent to the feeding belt and 0.5 at the tile centre. It reuses the belt subsystem: movement at belt speed (REQ-GW-BELT-SPEED), item rendering and spacing (REQ-GW-TILE-SIZE), and capacity/packing (REQ-GW-BELT-CAPACITY), but restricted to the 0.0→0.5 half of the tile. This applies to every building that pulls items from adjacent belts into an input buffer (Smelter, Assembler, Reprocessing Plant, Shipyard); a building may run several input belts at once when belts feed it from more than one side. The HQ is included with the one difference noted below. - REQ-MAT-INPUT-INTAKE: Accepted input items travel into a building as an animation rather than vanishing off the belt instantly — the input-side mirror of REQ-MAT-OUTPUT-EMERGE. Each input port has its own **input belt** — a virtual belt tile occupying the input port's footprint cell (the body cell the feeding belt points into), oriented in the port's inward flow direction, with progress 0.0 at the outer edge adjacent to the feeding belt and 0.5 at the tile centre. It reuses the belt subsystem: movement at belt speed (REQ-GW-BELT-SPEED), item rendering and spacing (REQ-GW-TILE-SIZE), and capacity/packing (REQ-GW-BELT-CAPACITY), but restricted to the 0.0→0.5 half of the tile. This applies to every building that pulls items from adjacent belts into an input buffer (Smelter, Assembler, Reprocessing Plant, Shipyard); a building may run several input belts at once when belts feed it from more than one side. The HQ is included with the one difference noted below.
- **Acceptance & reservation.** The acceptance test of REQ-MAT-INPUT-PORTS is unchanged — an item is accepted only if it is a required input whose per-material input buffer has space — except that "has space" now counts both the items already buffered **and** the items of that material currently travelling on the building's input belts (reserved but not yet arrived), so the total (buffered + in-transit) never exceeds that material's buffer cap (REQ-MAT-INPUT-BUFFER). An item that fails this test is not placed on an input belt and stays on the feeding belt exactly as before, so items that are not required inputs never enter the building. - **Acceptance & reservation.** The acceptance test of REQ-MAT-INPUT-PORTS is unchanged — an item is accepted only if it is a required input whose per-material input buffer has space — except that "has space" now counts both the items already buffered **and** the items of that material currently travelling on the building's input belts (reserved but not yet arrived), so the total (buffered + in-transit) never exceeds that material's buffer cap (REQ-MAT-INPUT-BUFFER). An item that fails this test is not placed on an input belt and stays on the feeding belt exactly as before, so items that are not required inputs never enter the building.
- **Feeding.** An accepted item is removed from the feeding belt on the same tick it would have been taken without this animation, and placed on the input belt at progress 0.0, reserving a slot in its per-material buffer. A new item is placed only when the input belt's entry slot at progress 0.0 is free (per REQ-GW-BELT-CAPACITY spacing — no in-transit item within a quarter tile of 0.0). The 0.0→0.5 span holds at most three in-transit items (progress 0.0, 0.25, 0.5); the reservation limit above may permit fewer. - **Feeding.** An accepted item is removed from the feeding belt on the same tick it would have been taken without this animation, and placed on the input belt at progress 0.0, reserving a slot in its per-material buffer. (An input belt may also be fed directly by an adjacent producer's output belt rather than by a real belt — see REQ-MAT-DIRECT-COUPLE — with the same reservation and entry rules.) A new item is placed only when the input belt's entry slot at progress 0.0 is free (per REQ-GW-BELT-CAPACITY spacing — no in-transit item within a quarter tile of 0.0). The 0.0→0.5 span holds at most three in-transit items (progress 0.0, 0.25, 0.5); the reservation limit above may permit fewer.
- **Travel & arrival.** An in-transit item advances from progress 0.0 to 0.5 at belt speed. On reaching progress 0.5 it leaves the input belt and is added to its per-material input buffer, turning its reservation into buffered stock; only then does it count toward starting a production cycle (REQ-MAT-CYCLE). Because the slot was reserved on entry, arrival always succeeds — there is no deadlock. - **Travel & arrival.** An in-transit item advances from progress 0.0 to 0.5 at belt speed. On reaching progress 0.5 it leaves the input belt and is added to its per-material input buffer, turning its reservation into buffered stock; only then does it count toward starting a production cycle (REQ-MAT-CYCLE). Because the slot was reserved on entry, arrival always succeeds — there is no deadlock.
- **Reservation may delay production.** A reserved item occupies buffer capacity for its whole 0.0→0.5 travel without yet being consumable, so an input-starved building may briefly wait for an in-transit item to arrive before it can start a cycle. This is accepted. - **Reservation may delay production.** A reserved item occupies buffer capacity for its whole 0.0→0.5 travel without yet being consumable, so an input-starved building may briefly wait for an in-transit item to arrive before it can start a cycle. This is accepted.
- **Clearing.** Clearing the input buffers on a recipe or schematic change (REQ-MAT-INPUT-BUFFER) also discards any items currently travelling on the input belts and releases their reservations. - **Clearing.** Clearing the input buffers on a recipe or schematic change (REQ-MAT-INPUT-BUFFER) also discards any items currently travelling on the input belts and releases their reservations.
@@ -160,9 +160,12 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
- REQ-MAT-OUTPUT-EMERGE: Items emerge from a building output port as an animation rather than popping directly onto the outgoing belt. Each output port has its own **output belt** — a virtual belt tile occupying the output port tile, oriented in the port's facing direction, with progress 0.0 at the tile's inner edge and 1.0 at the outer (port) edge adjacent to the next real belt tile. It reuses the belt subsystem: movement at belt speed (REQ-GW-BELT-SPEED), item rendering and spacing (REQ-GW-TILE-SIZE), and capacity/packing (REQ-GW-BELT-CAPACITY), but restricted to the 0.5→1.0 half of the tile. This applies to every building that outputs items onto belts (Miner, Smelter, Assembler, Reprocessing Plant, Salvage Bay); it does not apply to the Shipyard, which spawns a ship rather than a belt item (REQ-SHP-SPAWN-PLAYER). - REQ-MAT-OUTPUT-EMERGE: Items emerge from a building output port as an animation rather than popping directly onto the outgoing belt. Each output port has its own **output belt** — a virtual belt tile occupying the output port tile, oriented in the port's facing direction, with progress 0.0 at the tile's inner edge and 1.0 at the outer (port) edge adjacent to the next real belt tile. It reuses the belt subsystem: movement at belt speed (REQ-GW-BELT-SPEED), item rendering and spacing (REQ-GW-TILE-SIZE), and capacity/packing (REQ-GW-BELT-CAPACITY), but restricted to the 0.5→1.0 half of the tile. This applies to every building that outputs items onto belts (Miner, Smelter, Assembler, Reprocessing Plant, Salvage Bay); it does not apply to the Shipyard, which spawns a ship rather than a belt item (REQ-SHP-SPAWN-PLAYER).
- **Feeding.** While the output buffer (REQ-MAT-OUTPUT-BUFFER) holds an item that has not yet begun emerging and the output belt's entry slot at progress 0.5 is free (per REQ-GW-BELT-CAPACITY spacing — no emerging item within a quarter tile of progress 0.5), the next buffered item is placed on the output belt at progress 0.5. Because only the 0.5→1.0 span is used, the output belt holds at most three emerging items (progress 0.5, 0.75, 1.0); once that span is full the building places no further items on it even if the output buffer still holds more. - **Feeding.** While the output buffer (REQ-MAT-OUTPUT-BUFFER) holds an item that has not yet begun emerging and the output belt's entry slot at progress 0.5 is free (per REQ-GW-BELT-CAPACITY spacing — no emerging item within a quarter tile of progress 0.5), the next buffered item is placed on the output belt at progress 0.5. Because only the 0.5→1.0 span is used, the output belt holds at most three emerging items (progress 0.5, 0.75, 1.0); once that span is full the building places no further items on it even if the output buffer still holds more.
- **Cosmetic hold.** An emerging item still counts as residing in the output buffer (REQ-MAT-GLOBAL-STOCK) for the whole animation; it only leaves the building when it transfers onto a real belt tile at progress 1.0. The output belt therefore adds no inventory capacity beyond the output buffer, and clearing the output buffer on a recipe or schematic change (REQ-MAT-OUTPUT-BUFFER) also removes any items currently emerging. - **Cosmetic hold.** An emerging item still counts as residing in the output buffer (REQ-MAT-GLOBAL-STOCK) for the whole animation; it only leaves the building when it transfers onto a real belt tile at progress 1.0. The output belt therefore adds no inventory capacity beyond the output buffer, and clearing the output buffer on a recipe or schematic change (REQ-MAT-OUTPUT-BUFFER) also removes any items currently emerging.
- **Travel & handoff.** An emerging item advances from progress 0.5 to 1.0 at belt speed. At progress 1.0 it attempts to transfer onto the adjacent real belt tile using the normal belt hand-off and accept-direction rules (REQ-MAT-OUTPUT-PORT, REQ-MAT-ACCEPT-DIR): the transfer succeeds only if a transport tile exists there, is not oriented with its output edge facing back into the building, and has free space. On success the item leaves the output buffer and becomes an ordinary item on that belt tile. - **Travel & handoff.** An emerging item advances from progress 0.5 to 1.0 at belt speed. At progress 1.0 it attempts to transfer onto the adjacent real belt tile using the normal belt hand-off and accept-direction rules (REQ-MAT-OUTPUT-PORT, REQ-MAT-ACCEPT-DIR): the transfer succeeds only if a transport tile exists there, is not oriented with its output edge facing back into the building, and has free space. On success the item leaves the output buffer and becomes an ordinary item on that belt tile. If instead the output port tile is a directly adjacent building's input edge, the item transfers straight into that building (REQ-MAT-DIRECT-COUPLE).
- **Stuck items.** If there is no next real belt tile, or the transfer is refused or blocked, the emerging item stops at progress 1.0 and is rendered there (still counted in the output buffer). Following items pile up behind it at progress 0.75 and 0.5 per the packing above, and once the 0.5→1.0 span is full no further items emerge until the front item transfers. - **Stuck items.** If there is no next real belt tile and no directly-coupled building (REQ-MAT-DIRECT-COUPLE), or the transfer is refused or blocked, the emerging item stops at progress 1.0 and is rendered there (still counted in the output buffer). Following items pile up behind it at progress 0.75 and 0.5 per the packing above, and once the 0.5→1.0 span is full no further items emerge until the front item transfers.
- **Emergence rendering (no pop-in).** An emerging item must not simply appear at progress 0.5. The output port tile's building is rendered over the output belt, so an emerging item is occluded while inside the footprint and is revealed progressively as it slides past the port edge — appearing to physically emerge from the building. The portion of the item still within the output port tile is hidden; the portion past the outer edge is drawn. - **Emergence rendering (no pop-in).** An emerging item must not simply appear at progress 0.5. The output port tile's building is rendered over the output belt, so an emerging item is occluded while inside the footprint and is revealed progressively as it slides past the port edge — appearing to physically emerge from the building. The portion of the item still within the output port tile is hidden; the portion past the outer edge is drawn.
- REQ-MAT-DIRECT-COUPLE: **Direct port coupling.** Two directly adjacent buildings whose ports meet transfer items between them with no intervening transport tile. A direct coupling exists at a shared edge where a producer building's output port tile (the tile it pushes toward, REQ-MAT-OUTPUT-PORT) is a body cell of a consumer building, and the producer's output direction carries the item across that edge into the consumer through one of the consumer's input edges (any perimeter edge other than the consumer's own output port, per REQ-MAT-INPUT-PORTS). Over a direct coupling the two virtual belts chain end to end: an item that reaches progress 1.0 on the producer's output belt at the shared edge (REQ-MAT-OUTPUT-EMERGE) is handed, instead of onto a real belt tile, directly onto the consumer's input belt at progress 0.0 (REQ-MAT-INPUT-INTAKE) and continues inward to the consumer's buffer — so the item appears to slide continuously across the shared edge from one building into the next.
- **Acceptance.** The hand-off obeys the consumer's normal input rules (REQ-MAT-INPUT-PORTS, REQ-MAT-INPUT-INTAKE): it succeeds only if the item is a required input of the consumer whose per-material buffer has space (reservation-aware — buffered + in-transit below the cap) and the consumer's input belt entry at progress 0.0 is free. On success the item leaves the producer's output buffer and reserves a slot in the consumer's input buffer, exactly as a belt-fed intake would. If the consumer does not accept the item — it is not one of its inputs, or the buffer is full, or the input-belt entry is occupied — the item stays stuck at the producer's output port at progress 1.0, exactly as when a downstream belt is blocked (REQ-MAT-OUTPUT-EMERGE stuck items).
- **Scope.** Direct coupling is the only case in which materials move between buildings without a belt, splitter, or tunnel (REQ-MAT-BELT-ONLY); it bridges only two buildings that are directly adjacent with meeting output/input ports. Transport tiles feeding a building (belt, splitter, or tunnel exit) continue to work through the normal pull, and a producer still hands off to a transport tile placed in the gap as before; a single such tile between two buildings is unaffected by this requirement.
- REQ-MAT-ACCEPT-DIR: A transport tile (belt, splitter, tunnel entry, or tunnel exit) accepts an incoming item only through a non-output edge; an item that would enter through one of the tile's output edges is refused. For a belt or a tunnel entry/exit the sole output edge is the one in its facing direction; for a splitter either of its two output directions is an output edge. This applies both to items pushed from an adjacent transport tile and to items deposited by a building's output port (REQ-MAT-OUTPUT-PORT). - REQ-MAT-ACCEPT-DIR: A transport tile (belt, splitter, tunnel entry, or tunnel exit) accepts an incoming item only through a non-output edge; an item that would enter through one of the tile's output edges is refused. For a belt or a tunnel entry/exit the sole output edge is the one in its facing direction; for a splitter either of its two output directions is an output edge. This applies both to items pushed from an adjacent transport tile and to items deposited by a building's output port (REQ-MAT-OUTPUT-PORT).
- REQ-MAT-INPUT-BUFFER: Each building has one input buffer per required input material. Each per-material buffer holds up to twice that material's per-cycle requirement. When the player selects a new recipe or schematic, all items in all input buffers are cleared. - REQ-MAT-INPUT-BUFFER: Each building has one input buffer per required input material. Each per-material buffer holds up to twice that material's per-cycle requirement. When the player selects a new recipe or schematic, all items in all input buffers are cleared.
- REQ-MAT-OUTPUT-BUFFER: Each building has an output buffer that holds up to twice the quantity produced by one production cycle. If the output buffer is full, production stops until space is available. When the player selects a new recipe or schematic, all items in the output buffer are cleared (relevant when the adjacent belt is jammed and items have accumulated). - REQ-MAT-OUTPUT-BUFFER: Each building has an output buffer that holds up to twice the quantity produced by one production cycle. If the output buffer is full, production stops until space is available. When the player selects a new recipe or schematic, all items in the output buffer are cleared (relevant when the adjacent belt is jammed and items have accumulated).

View File

@@ -825,87 +825,86 @@ void BuildingSystem::tickBeltPull()
} }
} }
// 2. Feed newly accepted items from adjacent belts onto the input belts at // 2. Feed accepted items from adjacent belts onto the input belts at
// progress 0.0. HQ accepts building blocks into the global stock with no // progress 0.0. The acceptance rules — the HQ building-block case, the
// reservation; other buildings reserve a per-material buffer slot. // required-input check, and the reservation — live in canAcceptInput so
if (isHq) // direct coupling (REQ-MAT-DIRECT-COUPLE) shares them exactly.
{
for (std::size_t i = 0; i < building.inputPorts.size(); ++i) for (std::size_t i = 0; i < building.inputPorts.size(); ++i)
{ {
const Port& port = building.inputPorts[i]; const std::optional<ItemType> peeked = m_belts.peekItem(building.inputPorts[i]);
std::vector<BeltItemSlot>& lane = building.incomingItems[i]; if (!peeked) { continue; }
const std::optional<ItemType> peeked = m_belts.peekItem(port); if (!canAcceptInput(building, i, *peeked)) { continue; }
if (!peeked || peeked->id != "building_block") { continue; } const std::optional<Item> taken = m_belts.tryTakeItem(building.inputPorts[i]);
if (!inputLaneEntryFree(lane)) { continue; }
const std::optional<Item> taken = m_belts.tryTakeItem(port);
if (taken) if (taken)
{ {
lane.push_back(BeltItemSlot{*taken, 0.0}); depositToInputBelt(building, i, *taken);
} }
} }
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;
}
} }
} }
for (std::size_t i = 0; i < building.inputPorts.size(); ++i) bool BuildingSystem::canAcceptInput(const Building& consumer,
std::size_t inputPortIndex,
const ItemType& type) const
{ {
const Port& port = building.inputPorts[i]; if (inputPortIndex >= consumer.incomingItems.size()) { return false; }
std::vector<BeltItemSlot>& lane = building.incomingItems[i]; if (!inputLaneEntryFree(consumer.incomingItems[inputPortIndex])) { return false; }
const std::optional<ItemType> peeked = m_belts.peekItem(port); // The HQ has no input buffer; it accepts building blocks into the global stock
if (!peeked) // (REQ-HQ-BELT-INPUT) with no reservation.
if (consumer.type == BuildingType::Hq)
{ {
continue; return type.id == "building_block";
} }
const ItemType& type = *peeked; // 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).
// Accept only if this type is a required input and the buffer has space.
const std::map<ItemType, int>::const_iterator capIt = const std::map<ItemType, int>::const_iterator capIt =
building.inputBuffer.caps.find(type); consumer.inputBuffer.caps.find(type);
if (capIt == building.inputBuffer.caps.end() || capIt->second == 0) if (capIt == consumer.inputBuffer.caps.end() || capIt->second == 0)
{ {
continue; return false;
}
return consumer.pendingInputCount(type) < capIt->second;
} }
// Reservation-aware space test: buffered + in-transit must stay under void BuildingSystem::depositToInputBelt(Building& consumer,
// the cap (REQ-MAT-INPUT-INTAKE). std::size_t inputPortIndex,
if (building.pendingInputCount(type) >= capIt->second) const Item& item)
{ {
continue; consumer.incomingItems[inputPortIndex].push_back(BeltItemSlot{item, 0.0});
} }
if (!inputLaneEntryFree(lane)) bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId,
const Port& outputPort,
const Item& item)
{ {
continue; 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;
} }
const std::optional<Item> taken = m_belts.tryTakeItem(port); Building* consumer = findBuildingMutable(occIt->second);
if (taken) if (!consumer)
{ {
lane.push_back(BeltItemSlot{*taken, 0.0}); 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) void BuildingSystem::tickProduction(Tick currentTick)
@@ -1167,14 +1166,20 @@ void BuildingSystem::tickOutputBelts()
// caps to 0.5 / 0.75 / 1.0 for up to three items). // caps to 0.5 / 0.75 / 1.0 for up to three items).
advanceBeltSlots(lane, progressPerTick); advanceBeltSlots(lane, progressPerTick);
// 2. Hand the front item off onto the adjacent real belt once it reaches // 2. Hand the front item off once it reaches the output edge (progress
// the output edge (progress 1.0). On refusal — no belt, output-edge // 1.0): onto the adjacent real belt, or — if a building's input edge
// (REQ-MAT-ACCEPT-DIR), or a full belt — it stays stuck at 1.0. // meets this port — straight into that building (REQ-MAT-DIRECT-COUPLE).
if (!lane.empty() && lane.front().progress >= 1.0 // On refusal (no belt/coupling, output-edge per REQ-MAT-ACCEPT-DIR, or
&& m_belts.tryPutItem(port.tile, lane.front().item, port.direction)) // 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(building.id, port, item))
{ {
lane.erase(lane.begin()); lane.erase(lane.begin());
} }
}
// 3. Feed the next buffered item onto the lane at progress 0.5 when the // 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 // entry slot is free — the lane holds at most three items and a new
@@ -1250,6 +1255,18 @@ const Building* BuildingSystem::findBuilding(BuildingId id) const
return nullptr; 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 const ConstructionSite* BuildingSystem::findSite(BuildingId id) const
{ {
for (const ConstructionSite& site : m_constructionQueue) for (const ConstructionSite& site : m_constructionQueue)

View File

@@ -181,6 +181,25 @@ public:
void appendChecksum(Hasher& hasher) const; void appendChecksum(Hasher& hasher) const;
private: 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 BuildingDef* findBuildingDef(BuildingType type) const;
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const; const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
const ShipDef* findShipDef(const std::string& id) const; const ShipDef* findShipDef(const std::string& id) const;

View File

@@ -807,6 +807,84 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui
REQUIRE(item->type.id == "iron_ore"); 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 // setRecipe clears buffers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------