Quick diagnosis
If room.energyAvailable stays below room.energyCapacityAvailable, do not begin by debugging Storage, Containers, Towers, or your total room Energy. These Room values describe the Energy currently available in your owned Spawn-and-Extension network.
Start with four questions:
- Which active owned Spawn or Extension still has free Energy capacity?
- Does your filler Creep actually carry Energy?
- What exact value did
creep.transfer()return? - If it returned
OK, did the target fill on the next processed tick even if the room aggregate stayed flat?
Below capacity is a symptom. Find the missing structure first, preserve the action result second, and use next-tick evidence before blaming the room aggregate.
The most likely causes
| What you observe | Likely cause | What to inspect next |
|---|---|---|
| One Spawn or Extension still has free capacity | The fill route has not reached that structure yet. | Filler Store, saved target, movement result, then transfer() result. |
| The filler keeps walking but rarely transfers | The target changes while the Creep is moving, or the path keeps failing. | Keep one target ID stable and preserve moveTo() separately. |
transfer() returns ERR_NOT_IN_RANGE | The Creep is not adjacent. | Move to range 1, then retry on a later tick. |
transfer() returns ERR_FULL | The target is full now, or the requested amount no longer fits its remaining capacity. | Re-read target free capacity, clear stale target state, and recompute the amount. |
transfer() returns OK but the Room value does not rise | The transfer may have processed while spawning or another fill changed the same network. | Check the target and the previous-tick EVENT_TRANSFER; do not use room delta alone as proof. |
| No active Spawn/Extension appears to have free capacity, but the Room values still disagree | Your assumption about active structures or the captured tick may be stale. | Reconcile the exact active owned structures and preserve the snapshot before escalating. |
Run one read-only diagnostic first
State impact: read-only. Pass the visible Room you are debugging into this helper. It does not move a Creep or submit a transfer.
function diagnoseRoomEnergy(room) {
const structures = room.find(FIND_MY_STRUCTURES, {
filter: structure =>
(
structure.structureType === STRUCTURE_SPAWN
|| structure.structureType === STRUCTURE_EXTENSION
)
&& structure.isActive()
&& structure.store.getCapacity(RESOURCE_ENERGY) > 0
});
const fillTargets = structures
.map(structure => ({
id: structure.id,
type: structure.structureType,
used: structure.store.getUsedCapacity(RESOURCE_ENERGY),
free: structure.store.getFreeCapacity(RESOURCE_ENERGY),
capacity: structure.store.getCapacity(RESOURCE_ENERGY)
}))
.filter(structure => structure.free > 0)
.sort((left, right) =>
left.type.localeCompare(right.type)
|| left.id.localeCompare(right.id)
);
return {
tick: Game.time,
roomName: room.name,
energyAvailable: room.energyAvailable,
energyCapacityAvailable: room.energyCapacityAvailable,
missingEnergy: Math.max(
0,
room.energyCapacityAvailable - room.energyAvailable
),
fillTargets
};
}
Interpret the result before changing code. If fillTargets contains a structure, you have a concrete destination to trace. If it is empty while the Room aggregate is still below capacity, move to the deeper structure reconciliation later in this guide instead of repeatedly changing filler logic.
Fix the branch you actually found, then retest
Use the diagnostic result to choose one branch rather than changing several systems at once:
- Filler has no Energy: fix the acquisition/withdrawal side first. A delivery loop cannot repair a missing supply.
- Target exists but is out of range: keep that target stable, preserve the movement result, and retry after movement has actually changed position.
- Target filled before your transfer: clear the stale target and select again from current free capacity.
- Transfer returns
OK: do not immediately rewrite the route because the room aggregate stayed flat. Verify the processed target/event on the next tick. - No active target explains the gap: reconcile the exact active Spawn/Extension Stores and Controller context before treating the discrepancy as unusual.
Retest one controlled fill with the same target ID and the exact action result preserved. This turns “room Energy looks stuck” into a specific state, movement, action, or concurrency problem.
What the two Room values actually measure
room.energyAvailable is the current Energy available in the room's Spawn-and-Extension network. room.energyCapacityAvailable is that network's corresponding total capacity. Energy in Storage, Containers, Terminals, Links, Towers, Labs, or a Creep's Store does not directly increase either Room aggregate.
The official Room API defines the values in terms of Spawns and Extensions. In the current checked engine, the runtime Room snapshot accumulates owned Spawn/Extension Energy and capacity only for structures that are not internally marked off. Player code should not depend on that private field; use the public structure.isActive() check.
A Room aggregate below capacity also does not prove that a previous fill failed. Spawn consumption and other fillers can change the same network around the same observation window. A target can gain Energy while room.energyAvailable stays flat or falls.
Deeper structure reconciliation
If the minimal diagnostic reports no free active target but the aggregates still disagree, preserve a full same-tick snapshot:
function reconcileRoomEnergy(room) {
const structures = room.find(FIND_MY_STRUCTURES, {
filter: structure =>
(
structure.structureType === STRUCTURE_SPAWN
|| structure.structureType === STRUCTURE_EXTENSION
)
&& structure.isActive()
&& structure.store.getCapacity(RESOURCE_ENERGY) > 0
}).map(structure => ({
id: structure.id,
type: structure.structureType,
used: structure.store.getUsedCapacity(RESOURCE_ENERGY),
capacity: structure.store.getCapacity(RESOURCE_ENERGY),
free: structure.store.getFreeCapacity(RESOURCE_ENERGY)
}));
const measuredUsed = structures.reduce(
(sum, structure) => sum + structure.used,
0
);
const measuredCapacity = structures.reduce(
(sum, structure) => sum + structure.capacity,
0
);
return {
tick: Game.time,
roomName: room.name,
roomEnergyAvailable: room.energyAvailable,
roomEnergyCapacityAvailable: room.energyCapacityAvailable,
measuredUsed,
measuredCapacity,
usedDifference: room.energyAvailable - measuredUsed,
capacityDifference:
room.energyCapacityAvailable - measuredCapacity,
controllerLevel: room.controller?.level ?? null,
controllerMy: room.controller?.my ?? null,
structures
};
}
An unexplained difference is worth recording, but it is not enough by itself to call an engine bug. Keep the exact tick, Controller state, and per-structure values so the observation can be reproduced.
Keep one fill target stable while moving
A common logistics failure is target churn: a filler chooses Extension A, another Creep changes the free-capacity ordering, and the first filler switches to B before reaching A. Save one target ID while it remains valid, and only select again when it becomes invalid or full.
function isSpawnEnergyStructure(structure) {
return structure.structureType === STRUCTURE_SPAWN
|| structure.structureType === STRUCTURE_EXTENSION;
}
function resolveFillTarget(creep, targetId) {
if (!targetId) return null;
const target = Game.getObjectById(targetId);
if (
!target
|| target.room?.name !== creep.room.name
|| target.my !== true
|| !isSpawnEnergyStructure(target)
|| !target.isActive()
|| target.store.getFreeCapacity(RESOURCE_ENERGY) <= 0
) {
return null;
}
return target;
}
function selectFillTarget(creep) {
const candidates = creep.room.find(FIND_MY_STRUCTURES, {
filter: target =>
isSpawnEnergyStructure(target)
&& target.isActive()
&& target.store.getFreeCapacity(RESOURCE_ENERGY) > 0
});
return creep.pos.findClosestByPath(candidates) ?? null;
}
findClosestByPath() is a project choice, not a universal logistics scheduler. It is useful here because a pure range sort can prefer a structure the current pathfinder cannot reach.
Verify the processed transfer on the next tick
Keep submission evidence and processed-result evidence separate. creep.transfer() returning OK means the transfer intent was accepted for that tick. The checked processor re-reads target capacity later and can reduce the processed amount if the target filled before processing. Its EVENT_TRANSFER records the amount that actually moved.
Save the target and before-state when transfer() returns OK, then check the previous tick from the same Room on the next run:
function rememberSubmittedFill(creep, target, requestedAmount) {
Memory.energyFillPending ??= {};
Memory.energyFillPending[creep.name] = {
tick: Game.time,
roomName: creep.room.name,
creepId: creep.id,
targetId: target.id,
requestedAmount,
targetBefore:
target.store.getUsedCapacity(RESOURCE_ENERGY),
creepBefore:
creep.store.getUsedCapacity(RESOURCE_ENERGY),
roomBefore: creep.room.energyAvailable
};
}
function verifyPreviousFill(creep) {
const pending = Memory.energyFillPending?.[creep.name];
if (!pending || pending.tick >= Game.time) return null;
const room = Game.rooms[pending.roomName];
const target = Game.getObjectById(pending.targetId);
const event = room?.getEventLog().find(item =>
item.event === EVENT_TRANSFER
&& item.objectId === pending.creepId
&& item.data?.targetId === pending.targetId
&& item.data?.resourceType === RESOURCE_ENERGY
);
const result = {
verifiedAt: Game.time,
submittedAt: pending.tick,
targetId: pending.targetId,
requestedAmount: pending.requestedAmount,
processedAmount: event?.data?.amount ?? null,
targetNow: target?.store
? target.store.getUsedCapacity(RESOURCE_ENERGY)
: null,
creepNow:
creep.store.getUsedCapacity(RESOURCE_ENERGY),
roomNow: room?.energyAvailable ?? null
};
delete Memory.energyFillPending[creep.name];
return result;
}
Room.getEventLog() exposes events from the previous tick, so this verification belongs on the later run. Using the saved Room name also avoids accidentally asking a different Room for the event if the Creep moved after submission.
For one submitted fill, the evidence order is: exact matching EVENT_TRANSFER first; matching target/Creep Store changes second; room-level delta only as context. Do not require room.energyAvailable > previousValue as proof that one transfer worked.
transfer() return-code checklist
| Code | Meaning in this workflow | Response |
|---|---|---|
OK | The transfer intent was accepted for the current tick. | Verify the processed event or later Store state next tick. |
ERR_NOT_OWNER | The sending Creep is not yours. | Stop; do not retry an ownership error as logistics. |
ERR_BUSY | The Creep is still spawning. | Wait until spawning is complete. |
ERR_NOT_ENOUGH_RESOURCES | The Creep does not carry the requested resource amount. | Re-read carried Energy and recompute the request. |
ERR_INVALID_TARGET | The target cannot receive this transfer/resource. | Clear the target and rebuild the candidate set. |
ERR_FULL | The target is full, or the explicit requested amount exceeds its current remaining capacity. | Re-read free capacity and recompute before retrying. |
ERR_NOT_IN_RANGE | The target is not adjacent. | Move to range 1 and preserve the movement result separately. |
ERR_INVALID_ARGS | The resource type or amount is invalid. | Rebuild the call from current Store values. |
Less common and evidence boundaries
The official Room API, Creep.transfer(), Structure.isActive(), Room.getEventLog(), and current screeps/engine 4.3.2 source were rechecked on August 29, 2026. The checked engine master remains 80977824199a596d174d392fd0cf8c458c21fcbd.
Engine-source boundary: the current runtime builds the two Room Energy aggregates from owned Spawn/Extension objects that are not internally off. This article uses the public isActive() API rather than relying on that internal field.
Concurrency boundary: an exact transfer event proves that one transfer processed, not that the room total had to rise by the same amount. Spawn consumption and other transfers can overlap the same observation window.
Live evidence: Screeps Console test: Pending. Live multi-hauler contention trace: Pending. Live transfer-plus-spawn-consumption trace: Pending. No live result is fabricated.
If your next problem is production cost rather than filling, continue with dynamic Creep bodies or spawnCreep() return codes. For processed event inspection, use the Room event log guide. For Energy outside the Spawn/Extension network, use the Storage Energy policy guide.