From 2ddf13238cfcdee0962c67f3636eff2bf35f5b52 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Thu, 9 Jul 2026 20:11:12 +0200 Subject: [PATCH] Fix Salvage Bay drop-off not working by adding config-driven buffer capacity --- bin/app/data/config/buildings.toml | 1 + bin/test/data/config/buildings.toml | 1 + docs/requirements.md | 2 +- src/lib/config/BuildingsConfig.h | 5 ++++ src/lib/config/ConfigLoader.cpp | 6 +++++ src/lib/sim/BuildingSystem.cpp | 21 +++++++++++++++- src/lib/sim/BuildingSystem.h | 1 + src/test/BehaviorSystemTest.cpp | 39 +++++++++++++++++++++++++++++ src/test/ConfigLoaderTest.cpp | 9 +++++++ 9 files changed, 83 insertions(+), 2 deletions(-) diff --git a/bin/app/data/config/buildings.toml b/bin/app/data/config/buildings.toml index b68e8bd..57ec733 100644 --- a/bin/app/data/config/buildings.toml +++ b/bin/app/data/config/buildings.toml @@ -81,6 +81,7 @@ id = "salvage_bay" cost = 25 player_placeable = true construction_time_seconds = 1 +output_buffer_capacity = 20 surface_mask = [ "", diff --git a/docs/requirements.md b/docs/requirements.md index 9ae545e..39ce94f 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -120,7 +120,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des - REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`. Only implicitly unlocked recipes are available for selection (REQ-LOCK-UI-RECIPE). - REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE); the pool of eligible outputs is restricted to implicitly unlocked item types (REQ-LOCK-REPROCESSING-POOL). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING. - REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a schematic. When all required materials — the ship's base materials (`[ship.schematic].materials`) plus the materials of all modules in the configured layout (REQ-MOD-MATERIALS) — are present in its input buffer, the shipyard consumes them and begins a production cycle lasting the ship's base `[ship.schematic].production_time_seconds` plus the sum of production times contributed by all module instances in the configured layout (REQ-MOD-PRODUCTION-TIME). One ship of that type is spawned with the configured modules when the cycle completes. The shipyard cannot start a new cycle while one is in progress. If the player confirms a layout change (REQ-MOD-UI-DIALOG) while a production cycle is in progress, the current cycle is cancelled and all consumed materials are discarded; the shipyard returns to idle with the new layout configuration. -- REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. Scrap delivered here is placed onto connected output belts. +- REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. It has an output buffer whose holding capacity is defined by the `output_buffer_capacity` field of the `salvage_bay` entry in `buildings.toml` (rather than by a production cycle, since the Salvage Bay has no recipe). A ship at the bay hands over one unit of scrap per tick while the buffer has free space; a full buffer blocks further drop-off until space frees up (consistent with the buffer-full semantics of REQ-MAT-OUTPUT-BUFFER). Held scrap is pushed onto connected output belts. - REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED). - REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel; only implicitly unlocked item types are available as filter options (REQ-LOCK-UI-SPLITTER). Routing rules: - An item matching only one output's filter is routed to that output. diff --git a/src/lib/config/BuildingsConfig.h b/src/lib/config/BuildingsConfig.h index c6fac17..260d9b2 100644 --- a/src/lib/config/BuildingsConfig.h +++ b/src/lib/config/BuildingsConfig.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -18,6 +19,10 @@ struct BuildingDef // Stored as raw strings here; parsing into per-cell tiles + output ports // happens when buildings are placed, not at load time. std::vector surfaceMask; + + // Output-buffer holding size for buildings without a recipe-driven buffer. + // Only the Salvage Bay sets this (REQ-BLD-SALVAGE-BAY). + std::optional outputBufferCapacity; }; struct BuildingsConfig diff --git a/src/lib/config/ConfigLoader.cpp b/src/lib/config/ConfigLoader.cpp index e89c2ba..3eb31de 100644 --- a/src/lib/config/ConfigLoader.cpp +++ b/src/lib/config/ConfigLoader.cpp @@ -337,6 +337,12 @@ BuildingsConfig ConfigLoader::loadBuildings(const std::string& path) def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds"); def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask"); + if (mt.contains("output_buffer_capacity")) + { + def.outputBufferCapacity = static_cast( + requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity")); + } + const std::optional parsedType = parseBuildingType(def.id); if (!parsedType) { diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 85df1d2..ba86d05 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -154,6 +154,16 @@ void BuildingSystem::initShipyardBuffers(Building& b) const } } +void BuildingSystem::initSalvageBayBuffer(Building& b) const +{ + // Salvage Bay has no recipe-driven buffer; its output-buffer holding size for + // ship drop-off is config-defined (REQ-BLD-SALVAGE-BAY). + b.outputBuffer.items.clear(); + const BuildingDef* def = findBuildingDef(BuildingType::SalvageBay); + b.outputBuffer.capacity = + (def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0; +} + std::vector BuildingSystem::computeInputPorts(const Building& b) const { // Build lookup sets for quick membership checks. @@ -577,7 +587,11 @@ void BuildingSystem::tickConstruction(Tick currentTick) } building.inputPorts = computeInputPorts(building); - if (!building.recipeId.empty()) + if (building.type == BuildingType::SalvageBay) + { + initSalvageBayBuffer(building); + } + else if (!building.recipeId.empty()) { if (building.type == BuildingType::Shipyard) { @@ -1238,6 +1252,11 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type, } building.inputPorts = computeInputPorts(building); + if (type == BuildingType::SalvageBay) + { + initSalvageBayBuffer(building); + } + m_buildings.push_back(std::move(building)); return id; } diff --git a/src/lib/sim/BuildingSystem.h b/src/lib/sim/BuildingSystem.h index cff3eea..c3e80bd 100644 --- a/src/lib/sim/BuildingSystem.h +++ b/src/lib/sim/BuildingSystem.h @@ -170,6 +170,7 @@ private: const ModuleDef* findModuleDef(const std::string& id) const; void initBuffers(Building& b, const RecipeDef& recipe) const; void initShipyardBuffers(Building& b) const; + void initSalvageBayBuffer(Building& b) const; std::vector computeInputPorts(const Building& b) const; std::vector rollReprocessingOutput(const RecipeDef& recipe); bool bodyCellsWithinWorldBounds( diff --git a/src/test/BehaviorSystemTest.cpp b/src/test/BehaviorSystemTest.cpp index b093158..2be6191 100644 --- a/src/test/BehaviorSystemTest.cpp +++ b/src/test/BehaviorSystemTest.cpp @@ -984,6 +984,45 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b REQUIRE(i.target.x() < pos(f.admin, ship).value.x()); } +TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo", "[behavior]") +{ + Fixture f; + + const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay, + QPoint(-4, 0), Rotation::East, 0); + Tick t = 0; + for (int i = 0; i < 500; ++i) + { + f.buildings.tickConstruction(t++); + if (f.buildings.findBuilding(bayId) != nullptr) { break; } + } + const Building* bay = f.buildings.findBuilding(bayId); + REQUIRE(bay != nullptr); + // Config-driven output-buffer capacity is applied on placement (REQ-BLD-SALVAGE-BAY). + REQUIRE(bay->outputBuffer.capacity == 20); + + const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f, + bay->anchor.y() + bay->footprint.height() / 2.0f); + + const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager"); + const entt::entity ship = f.ships.spawn("salvage_ship", bayCenter, false, salvageLayout); + f.admin.get(ship).value = bayCenter; + CargoComponent& cargo = f.admin.get(ship); + cargo.current = cargo.maxCapacity; // full cargo + const int before = cargo.current; + REQUIRE(before > 0); + f.admin.get(ship).deliveryBay = bayId; + + f.salvageTick(); + + // One unit handed over from cargo into the bay's output buffer. + REQUIRE(f.admin.get(ship).current == before - 1); + const Building* bayAfter = f.buildings.findBuilding(bayId); + REQUIRE(bayAfter != nullptr); + REQUIRE(bayAfter->outputBuffer.items.size() == 1); + REQUIRE(bayAfter->outputBuffer.items.front().type.id == "scrap"); +} + // --------------------------------------------------------------------------- // Collection range (per-module) // --------------------------------------------------------------------------- diff --git a/src/test/ConfigLoaderTest.cpp b/src/test/ConfigLoaderTest.cpp index 99c93bd..45c0d82 100644 --- a/src/test/ConfigLoaderTest.cpp +++ b/src/test/ConfigLoaderTest.cpp @@ -103,6 +103,15 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c REQUIRE(minerIt != cfg.buildings.buildings.end()); REQUIRE(minerIt->cost == 15); REQUIRE(minerIt->surfaceMask.size() == 2); + // Miner has no output-buffer-capacity override; the Salvage Bay does. + REQUIRE_FALSE(minerIt->outputBufferCapacity.has_value()); + + const auto salvageBayIt = std::find_if( + cfg.buildings.buildings.begin(), cfg.buildings.buildings.end(), + [](const BuildingDef& b) { return b.type == BuildingType::SalvageBay; }); + REQUIRE(salvageBayIt != cfg.buildings.buildings.end()); + REQUIRE(salvageBayIt->outputBufferCapacity.has_value()); + REQUIRE(*salvageBayIt->outputBufferCapacity == 20); // recipes.toml — reprocessing cycle has three weighted outputs. const auto reproIt = std::find_if(