Add deconstruction queue

This commit is contained in:
2026-07-22 21:37:56 +02:00
parent a9082c57f3
commit b2ce20e6ad
17 changed files with 551 additions and 85 deletions

View File

@@ -129,5 +129,10 @@ struct Building
// Module layout for shipyards (REQ-MOD-LAYOUT).
std::optional<ShipLayoutConfig> shipLayout;
// True while this building sits in the deconstruction queue (REQ-BLD-DECON-QUEUE).
// A queued building stops operating immediately (all tick loops skip it) but keeps
// occupying its tiles until its deconstruction completes.
bool queuedForDeconstruction = false;
};

View File

@@ -21,6 +21,17 @@ bool isAutoRecipeBuildingType(BuildingType type)
|| type == BuildingType::ReprocessingPlant;
}
// Belts, splitters, and tunnel ends keep their runtime data in the belt subsystem
// rather than in the Building instance, so placing/removing them must register or
// unregister a tile with BeltSystem.
bool isBeltSubsystemType(BuildingType type)
{
return type == BuildingType::Belt
|| type == BuildingType::Splitter
|| type == BuildingType::TunnelEntry
|| type == BuildingType::TunnelExit;
}
// The building body tile that owns an output port, given the port's outside tile
// (port.tile) and its facing direction. The virtual output belt occupies this tile
// and flows toward port.tile (REQ-MAT-OUTPUT-EMERGE).
@@ -489,9 +500,10 @@ bool BuildingSystem::isPlacementValid(BuildingType type, QPoint anchor,
// Demolish
// ---------------------------------------------------------------------------
int BuildingSystem::demolish(BuildingId id)
int BuildingSystem::demolish(BuildingId id, Tick currentTick)
{
// Construction queue?
// Construction site? Removed instantly with the full refund; never queued
// for deconstruction (REQ-BLD-DEMOLISH).
for (std::deque<ConstructionSite>::iterator it = m_constructionQueue.begin();
it != m_constructionQueue.end();
++it)
@@ -512,35 +524,60 @@ int BuildingSystem::demolish(BuildingId id)
}
}
// Operational building?
for (std::vector<Building>::iterator it = m_buildings.begin();
it != m_buildings.end();
++it)
// Operational building? Append it to the deconstruction queue rather than
// removing it now; the partial refund is credited on completion in
// tickDeconstruction (REQ-BLD-DECON-QUEUE).
for (Building& building : m_buildings)
{
if (it->id == id)
if (building.id != id) { continue; }
if (building.queuedForDeconstruction) { return 0; } // already queued
building.queuedForDeconstruction = true;
DeconstructionEntry entry;
entry.id = id;
// A queued belt/tunnel/splitter stops transporting at once: capture a
// splitter's filters (so an un-queue can restore them), then unregister
// its tile, discarding items on it and re-pairing tunnels as if it were
// gone (REQ-BLD-TUNNEL-PAIR).
if (building.type == BuildingType::Splitter)
{
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter
|| it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
if (const std::optional<BeltSystem::SplitterInfo> info =
m_belts.getSplitterInfo(building.anchor))
{
m_belts.removeTile(it->anchor);
entry.splitterFilterA = info->filterA;
entry.splitterFilterB = info->filterB;
}
const BuildingDef* def = findBuildingDef(it->type);
for (const QPoint& cell : it->bodyCells)
{
m_tileOccupancy.erase({cell.x(), cell.y()});
}
m_buildings.erase(it);
if (def)
{
return def->cost * m_config.world.refundPercentage / 100;
}
return 0;
}
if (isBeltSubsystemType(building.type))
{
m_belts.removeTile(building.anchor);
}
const bool wasEmpty = m_deconstructionQueue.empty();
m_deconstructionQueue.push_back(std::move(entry));
if (wasEmpty)
{
startFrontDeconstruction(currentTick);
}
return 0;
}
return 0;
}
void BuildingSystem::startFrontDeconstruction(Tick currentTick)
{
if (m_deconstructionQueue.empty()) { return; }
DeconstructionEntry& front = m_deconstructionQueue.front();
if (front.completesAt == 0)
{
front.completesAt =
currentTick + secondsToTicks(m_config.world.deconstructionTimeSeconds);
}
}
// ---------------------------------------------------------------------------
// Set recipe
// ---------------------------------------------------------------------------
@@ -780,31 +817,9 @@ void BuildingSystem::tickConstruction(Tick currentTick)
}
}
// Register with BeltSystem before the move (mask stays valid).
if (front.type == BuildingType::Belt)
{
m_belts.placeBelt(front.anchor, front.rotation);
}
else if (front.type == BuildingType::Splitter)
{
assert(mask.outputPorts.size() >= 2);
m_belts.placeSplitter(front.anchor,
mask.outputPorts[0].direction,
mask.outputPorts[1].direction);
// Carry over any filters configured while under construction
// (REQ-BLD-SITE-CONFIG).
m_belts.setSplitterFilters(front.anchor,
front.splitterFilterA,
front.splitterFilterB);
}
else if (front.type == BuildingType::TunnelEntry)
{
m_belts.placeTunnelEntry(front.anchor, front.rotation, m_config.world.tunnelMaxDistance_tiles);
}
else if (front.type == BuildingType::TunnelExit)
{
m_belts.placeTunnelExit(front.anchor, front.rotation);
}
// Register with BeltSystem before the move (mask/building stays valid). Any
// filters configured while under construction carry over (REQ-BLD-SITE-CONFIG).
reregisterBeltTile(building, front.splitterFilterA, front.splitterFilterB);
m_buildings.push_back(std::move(building));
@@ -822,6 +837,114 @@ void BuildingSystem::tickConstruction(Tick currentTick)
}
}
void BuildingSystem::reregisterBeltTile(const Building& building,
const std::vector<ItemType>& splitterFilterA,
const std::vector<ItemType>& splitterFilterB)
{
switch (building.type)
{
case BuildingType::Belt:
m_belts.placeBelt(building.anchor, building.rotation);
break;
case BuildingType::Splitter:
assert(building.outputPorts.size() >= 2);
m_belts.placeSplitter(building.anchor,
building.outputPorts[0].direction,
building.outputPorts[1].direction);
m_belts.setSplitterFilters(building.anchor, splitterFilterA, splitterFilterB);
break;
case BuildingType::TunnelEntry:
m_belts.placeTunnelEntry(building.anchor, building.rotation,
m_config.world.tunnelMaxDistance_tiles);
break;
case BuildingType::TunnelExit:
m_belts.placeTunnelExit(building.anchor, building.rotation);
break;
default:
break;
}
}
void BuildingSystem::tickDeconstruction(Tick currentTick)
{
TRACE();
if (m_deconstructionQueue.empty())
{
return;
}
DeconstructionEntry& front = m_deconstructionQueue.front();
// Guard: if the front entry's timer was never started, start it now.
if (front.completesAt == 0)
{
startFrontDeconstruction(currentTick);
return;
}
if (currentTick < front.completesAt)
{
return;
}
// Remove the building from the world and credit its refund (REQ-BLD-DEMOLISH).
// Belt/tunnel/splitter tiles were already unregistered when the building was
// queued (see demolish), so only tile occupancy and the record remain.
for (std::vector<Building>::iterator it = m_buildings.begin();
it != m_buildings.end();
++it)
{
if (it->id != front.id) { continue; }
const BuildingDef* def = findBuildingDef(it->type);
for (const QPoint& cell : it->bodyCells)
{
m_tileOccupancy.erase({cell.x(), cell.y()});
}
m_buildings.erase(it);
if (def)
{
m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
}
break;
}
m_deconstructionQueue.pop_front();
// Start the next queued deconstruction, if any.
startFrontDeconstruction(currentTick);
}
void BuildingSystem::cancelDeconstruction(BuildingId id)
{
for (std::deque<DeconstructionEntry>::iterator it = m_deconstructionQueue.begin();
it != m_deconstructionQueue.end();
++it)
{
if (it->id != id) { continue; }
// Resume operation: clear the flag and re-register belt/tunnel/splitter
// tiles that were unregistered at enqueue (which re-pairs tunnels,
// REQ-BLD-TUNNEL-PAIR). Deconstruction progress is discarded; no refund.
if (Building* building = findBuildingMutable(id))
{
building->queuedForDeconstruction = false;
reregisterBeltTile(*building, it->splitterFilterA, it->splitterFilterB);
}
m_deconstructionQueue.erase(it);
// If the running front was removed, the new front (completesAt == 0) has
// its timer started by the next tickDeconstruction guard.
return;
}
}
bool BuildingSystem::isQueuedForDeconstruction(BuildingId id) const
{
const Building* building = findBuilding(id);
return building && building->queuedForDeconstruction;
}
void BuildingSystem::tickBeltPull()
{
TRACE();
@@ -831,6 +954,9 @@ void BuildingSystem::tickBeltPull()
for (Building& building : m_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
@@ -922,6 +1048,10 @@ bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId,
{
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).
@@ -943,6 +1073,9 @@ void BuildingSystem::tickProduction(Tick currentTick)
TRACE();
for (Building& building : m_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 ||
@@ -1043,6 +1176,9 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
TRACE();
for (Building& building : m_buildings)
{
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
if (building.queuedForDeconstruction) { continue; }
if (building.type != BuildingType::Shipyard)
{
continue;
@@ -1140,6 +1276,9 @@ void BuildingSystem::tickOutputBelts()
for (Building& building : m_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];
@@ -1647,6 +1786,10 @@ bool BuildingSystem::deliverScrapToSalvageBay(BuildingId bayId)
{
return false;
}
if (bay->queuedForDeconstruction)
{
return false; // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
}
// Emerging scrap still counts against the bay's holding capacity
// (REQ-MAT-OUTPUT-EMERGE).
if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
@@ -1820,6 +1963,7 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
appendItems(hasher, b.production->chosenOutputs);
}
hasher.append(b.shipLayout.has_value());
hasher.append(b.queuedForDeconstruction);
}
hasher.append(m_constructionQueue.size());
@@ -1840,6 +1984,17 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
for (const ItemType& type : s.splitterFilterB) { hasher.append(type.id); }
}
hasher.append(m_deconstructionQueue.size());
for (const DeconstructionEntry& e : m_deconstructionQueue)
{
hasher.append(e.id);
hasher.append(e.completesAt);
hasher.append(e.splitterFilterA.size());
for (const ItemType& type : e.splitterFilterA) { hasher.append(type.id); }
hasher.append(e.splitterFilterB.size());
for (const ItemType& type : e.splitterFilterB) { hasher.append(type.id); }
}
// std::map iterates in sorted key order.
hasher.append(m_tileOccupancy.size());
for (const std::pair<const std::pair<int, int>, BuildingId>& entry : m_tileOccupancy)

View File

@@ -77,10 +77,22 @@ public:
// Defaults to world.regions.asteroid_width_tiles at construction.
void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; }
// Remove a building or construction site by id. Returns the refund in
// building blocks (floor(cost * refundPercentage / 100)). Returns 0 for
// unknown ids.
int demolish(BuildingId id);
// Mark a building or construction site for demolition (REQ-BLD-DEMOLISH).
// A construction site is removed instantly and the full cost is returned.
// A fully-built building is instead appended to the deconstruction queue
// (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
// credited later, on completion in tickDeconstruction, so this returns 0 for
// it. Returns 0 for unknown ids and for a building already queued.
int demolish(BuildingId id, Tick currentTick);
// Take a building back out of the deconstruction queue before it is removed
// (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation
// (re-registering belt/tunnel/splitter tiles); discards deconstruction
// progress and credits no refund. No-op if the id is not queued.
void cancelDeconstruction(BuildingId id);
// True if the building is currently in the deconstruction queue.
bool isQueuedForDeconstruction(BuildingId id) const;
// Set the recipe (or schematic id for shipyard) on a building or queued
// construction site. Clears both buffers on an operational building.
@@ -104,6 +116,10 @@ public:
// -- Tick hooks (called from Simulation::tick in the documented order) ---
void tickConstruction(Tick currentTick);
// Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a
// time, in parallel with tickConstruction. Removes the front building and
// credits its refund when its timer elapses.
void tickDeconstruction(Tick currentTick);
void tickBeltPull();
void tickProduction(Tick currentTick);
void tickShipyardProduction(Tick currentTick);
@@ -205,6 +221,17 @@ public:
void appendChecksum(Hasher& hasher) const;
private:
// Starts the front deconstruction-queue entry's timer if not yet started
// (mirrors how tickConstruction starts a queued construction site).
void startFrontDeconstruction(Tick currentTick);
// Registers a belt/splitter/tunnel building's tile with the belt subsystem
// (on construction completion, or when un-queuing a deconstruction). No-op for
// non-belt-subsystem types. Splitter filters are (re)applied after placement.
void reregisterBeltTile(const Building& building,
const std::vector<ItemType>& splitterFilterA,
const std::vector<ItemType>& splitterFilterB);
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
@@ -273,6 +300,19 @@ private:
std::vector<Building> m_buildings;
std::deque<ConstructionSite> m_constructionQueue;
// One pending demolition of a fully-built building (REQ-BLD-DECON-QUEUE).
// completesAt == 0 means "queued but its timer has not started yet"
// (mirrors ConstructionSite). For a Splitter, the filters it had are captured
// here so cancelDeconstruction can restore them on re-registration.
struct DeconstructionEntry
{
BuildingId id = kInvalidBuildingId;
Tick completesAt = 0;
std::vector<ItemType> splitterFilterA;
std::vector<ItemType> splitterFilterB;
};
std::deque<DeconstructionEntry> m_deconstructionQueue;
// Maps every occupied body-cell coordinate to the entity that owns it.
std::map<std::pair<int, int>, BuildingId> m_tileOccupancy;
};

View File

@@ -28,6 +28,7 @@ enum class CommandKind
{
PlaceBuilding,
Demolish,
CancelDeconstruction,
RotateInPlace,
SetRecipe,
SetShipLayout,
@@ -72,12 +73,22 @@ struct PlaceBuildingCommand : Command
std::vector<ItemType> splitterFilterB;
};
// Marks a building for demolition: a construction site is removed instantly, a
// built building is queued for deconstruction (REQ-BLD-DEMOLISH, REQ-BLD-DECON-QUEUE).
struct DemolishCommand : Command
{
DemolishCommand() : Command(CommandKind::Demolish) {}
std::optional<BuildingId> id;
};
// Takes a building back out of the deconstruction queue (REQ-BLD-DEMOLISH-CLICK,
// REQ-BLD-DEMOLISH-BOX). No-op if the id is not currently queued.
struct CancelDeconstructionCommand : Command
{
CancelDeconstructionCommand() : Command(CommandKind::CancelDeconstruction) {}
std::optional<BuildingId> id;
};
struct RotateInPlaceCommand : Command
{
RotateInPlaceCommand() : Command(CommandKind::RotateInPlace) {}

View File

@@ -131,6 +131,10 @@ std::string serializeCommand(const Command& command)
case CommandKind::Demolish:
out << "demolish " << static_cast<const DemolishCommand&>(command).id.value();
break;
case CommandKind::CancelDeconstruction:
out << "cancel_deconstruct "
<< static_cast<const CancelDeconstructionCommand&>(command).id.value();
break;
case CommandKind::RotateInPlace:
{
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
@@ -247,6 +251,15 @@ std::shared_ptr<Command> parseCommand(const std::string& tokens)
c->id = id;
return c;
}
if (verb == "cancel_deconstruct")
{
std::shared_ptr<CancelDeconstructionCommand> c =
std::make_shared<CancelDeconstructionCommand>();
BuildingId id = 0;
if (!(in >> id)) { return nullptr; }
c->id = id;
return c;
}
if (verb == "rotate")
{
std::shared_ptr<RotateInPlaceCommand> c = std::make_shared<RotateInPlaceCommand>();

View File

@@ -234,6 +234,9 @@ void Simulation::apply(const Command& command)
case CommandKind::Demolish:
demolish(*static_cast<const DemolishCommand&>(command).id);
break;
case CommandKind::CancelDeconstruction:
cancelDeconstruction(*static_cast<const CancelDeconstructionCommand&>(command).id);
break;
case CommandKind::RotateInPlace:
{
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
@@ -307,6 +310,7 @@ void Simulation::tick()
// Construction + production pipeline
m_buildingSystem->tickConstruction(m_currentTick);
m_buildingSystem->tickDeconstruction(m_currentTick); // parallel to construction
m_buildingSystem->tickBeltPull(); // step 3
m_buildingSystem->tickProduction(m_currentTick); // step 4
m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b
@@ -1196,7 +1200,12 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
void Simulation::demolish(BuildingId id)
{
m_buildingBlocksStock += m_buildingSystem->demolish(id);
m_buildingBlocksStock += m_buildingSystem->demolish(id, m_currentTick);
}
void Simulation::cancelDeconstruction(BuildingId id)
{
m_buildingSystem->cancelDeconstruction(id);
}
BuildingSystem& Simulation::getBuildingsMutable()

View File

@@ -139,9 +139,14 @@ private:
// Returns the new entity id, or nullopt if blocks are insufficient.
std::optional<BuildingId> tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
// Demolishes the building with the given id and refunds building blocks.
// Marks the building with the given id for demolition: a construction site is
// removed instantly (full refund), a built building is queued for timed
// deconstruction (REQ-BLD-DECON-QUEUE), refunded on completion.
void demolish(BuildingId id);
// Takes a queued building back out of the deconstruction queue (REQ-BLD-DECON-QUEUE).
void cancelDeconstruction(BuildingId id);
// Applies the player's chosen schematic from the pending choices.
// choiceIndex must be in [0, pendingChoices.size()).
// Clears the pending choices after application.