Separate accepted destruction from removal proof
Structure.destroy() returns OK after queuing a room-level destruction intent. The official engine checks that the room has your Controller and that no hostile regular Creep or hostile Power Creep is present. It does not create a Room event for the removal.
The next-tick verifier must answer two different questions: did the original Structure ID disappear, and what now occupies the reviewed tile? A replacement Extension at the same coordinate does not mean the original destruction failed.
Bind confirmation to the exact Extension
function buildDestroyConfirmation(request) {
return [
'DESTROY_STRUCTURE',
request.requestId,
request.structureId,
request.roomName,
request.x,
request.y,
request.expectedType
].join('_');
}
Memory.destroyRequests ??= {};
Memory.destroyRequests['remove-extension-7'] = {
requestId: 'remove-extension-7',
enabled: true,
structureId: 'replace-with-extension-id',
roomName: 'W1N1',
x: 20,
y: 20,
expectedType: STRUCTURE_EXTENSION,
requestedAt: Game.time,
confirmation:
'DESTROY_STRUCTURE_remove-extension-7_' +
'replace-with-extension-id_W1N1_20_20_extension'
};
A static phrase such as DESTROY_EXTENSION proves only that someone typed a phrase. It does not prove which Extension was reviewed. Bind the exact ID, room, coordinate and allowed type.
Validate room authority and hostiles
function evaluateDestroyOperation(
request,
structure
) {
if (!request || request.enabled !== true) {
return { ready: false, status: 'request-disabled' };
}
if (
typeof request.requestId !== 'string'
|| request.requestId.length === 0
|| typeof request.structureId !== 'string'
|| typeof request.roomName !== 'string'
|| !Number.isInteger(request.x)
|| !Number.isInteger(request.y)
|| request.x < 0
|| request.x > 49
|| request.y < 0
|| request.y > 49
|| request.expectedType !== STRUCTURE_EXTENSION
|| !Number.isInteger(request.requestedAt)
) {
return { ready: false, status: 'request-invalid' };
}
if (request.confirmation !== buildDestroyConfirmation(request)) {
return { ready: false, status: 'confirmation-mismatch' };
}
if (!structure) {
return { ready: false, status: 'structure-unavailable' };
}
if (
structure.id !== request.structureId
|| structure.structureType !== request.expectedType
|| structure.pos.roomName !== request.roomName
|| structure.pos.x !== request.x
|| structure.pos.y !== request.y
) {
return { ready: false, status: 'structure-identity-mismatch' };
}
const room = structure.room;
const controller = room?.controller || null;
if (!room || !controller || controller.my !== true) {
return { ready: false, status: 'room-controller-not-owned' };
}
const hostileCreepCount =
room.find(FIND_HOSTILE_CREEPS).length;
const hostilePowerCreepCount =
room.find(FIND_HOSTILE_POWER_CREEPS).length;
if (
hostileCreepCount > 0
|| hostilePowerCreepCount > 0
) {
return {
ready: false,
status: 'hostiles-present',
hostileCreepCount,
hostilePowerCreepCount
};
}
return {
ready: true,
status: 'destroy-ready',
room,
hostileCreepCount,
hostilePowerCreepCount
};
}
The method's ownership boundary is the room Controller, not a generic assumption that every destroyable Structure must appear in Game.structures. This guide still restricts the operation to one reviewed Extension in one owned room.
Queue only one destructive operation
function createDestructionDispatcher() {
let operationReserved = false;
const reservedStructureIds = new Set();
return {
reserve(structureId) {
if (operationReserved) {
return {
ready: false,
status: 'destruction-slot-already-used'
};
}
if (reservedStructureIds.has(structureId)) {
return {
ready: false,
status: 'structure-already-reserved'
};
}
operationReserved = true;
reservedStructureIds.add(structureId);
return {
ready: true,
status: 'destruction-reserved'
};
},
release(structureId) {
operationReserved = false;
reservedStructureIds.delete(structureId);
}
};
}
The engine can queue more than one room destruction intent in a tick. This guide intentionally permits one reviewed destructive operation per dispatcher run. Expand that policy only with a batch-level dependency review.
Record only an accepted destroy call
function submitDestroyOperation(
dispatcher,
requestId
) {
const request =
Memory.destroyRequests?.[requestId];
if (!request || request.requestId !== requestId) {
return { status: 'request-identity-mismatch' };
}
const structure =
typeof request.structureId === 'string'
? Game.getObjectById(request.structureId)
: null;
const decision = evaluateDestroyOperation(
request,
structure
);
if (!decision.ready) {
return decision;
}
const reservation =
dispatcher.reserve(structure.id);
if (!reservation.ready) {
return reservation;
}
Memory.pendingStructureDestructions ??= {};
if (
Memory.pendingStructureDestructions[
structure.id
]
) {
dispatcher.release(structure.id);
return { status: 'pending-operation-exists' };
}
request.enabled = false;
request.lastAttemptAt = Game.time;
const before = {
structureType: structure.structureType,
roomName: structure.pos.roomName,
x: structure.pos.x,
y: structure.pos.y,
energyCapacityAvailable:
decision.room.energyCapacityAvailable,
hostileCreepCount:
decision.hostileCreepCount,
hostilePowerCreepCount:
decision.hostilePowerCreepCount
};
const result = structure.destroy();
request.lastResult = result;
if (result !== OK) {
dispatcher.release(structure.id);
request.status =
'destroy-rejected-review-required';
return {
status: request.status,
result
};
}
Memory.pendingStructureDestructions[
structure.id
] = {
submittedAt: Game.time,
requestId: request.requestId,
structureId: structure.id,
roomName: structure.pos.roomName,
x: structure.pos.x,
y: structure.pos.y,
expectedType: structure.structureType,
before
};
request.status =
'accepted-awaiting-verification';
return {
status: request.status,
result,
structureId: structure.id
};
}
A pending record exists only after OK. A rejected request remains disabled for review instead of firing automatically after hostiles leave.
Distinguish removal from replacement
function verifyDestroyedStructure(pending) {
if (!pending) {
return { status: 'no-pending-operation' };
}
const expectedTick = pending.submittedAt + 1;
if (Game.time < expectedTick) {
return { status: 'waiting-for-next-tick' };
}
if (Game.time > expectedTick) {
return { status: 'verification-window-missed' };
}
const original =
Game.getObjectById(pending.structureId);
if (original) {
return {
status: 'accepted-original-still-observed',
structureId: original.id
};
}
const room = Game.rooms[pending.roomName];
if (!room) {
return {
status: 'original-gone-tile-evidence-unavailable'
};
}
const structures = room.lookForAt(
LOOK_STRUCTURES,
pending.x,
pending.y
);
const replacement = structures.find(structure =>
structure.structureType
=== pending.expectedType
) || null;
return replacement
? {
status: 'original-destroyed-replacement-present',
replacementId: replacement.id
}
: {
status: 'original-destroyed-tile-empty'
};
}
Both terminal states prove that the original ID is gone. The replacement state is operationally important, but it must not retroactively invalidate the original removal.
Preserve destructive-operation states
| Status | Meaning | Response |
|---|---|---|
room-controller-not-owned | The room does not provide current destroy authority | Stop without guessing from Structure properties |
hostiles-present | A hostile regular Creep or Power Creep is visible | Keep the request unsubmitted |
destruction-slot-already-used | Another destructive operation was selected this tick | Defer without submitting |
accepted-original-still-observed | The saved ID still resolves next tick | Preserve the discrepancy |
original-destroyed-replacement-present | The original ID is gone and the tile contains another Extension | Review the replacement separately |
Review capacity and layout impact
An Extension contributes to room Energy capacity and may be referenced by logistics, Rampart coverage, path matrices and blueprints. Record the pre-action capacity for impact review, but do not treat a later capacity change as exact destruction proof. This guide makes no refund claim and does not rebuild automatically.
Production integration boundary
Route all destructive requests through one final queue, verify accepted operations before opening another slot, and require a new target-bound confirmation after any blueprint or object change. Console execution, hostile Power Creep behavior, capacity impact, replacement timing, genuine screenshots and CPU measurements remain pending.
Official documentation
Review the official Screeps API for Structure.destroy(), room Controllers, hostile Creeps, hostile Power Creeps, object IDs, room look methods and return codes. The official engine implementation clarifies the room-authority and queued-intent boundaries.