Use this guide when
Use this guide when owned Creeps withdraw Energy from an owned room Storage and deliver it to room consumers while preserving a deliberate reserve. It treats the reserve as a budget policy and the Creep action as a separate execution step.
Choose the Link coordination guide when multiple Links share a receiver. Choose the Controller downgrade guide when emergency upgrading should temporarily outrank the ordinary Storage budget.
Calculate one shared withdrawal budget
If several haulers each read storage.store independently, each can believe the same Energy above reserve is available. Build one coordinator per room and tick, then reserve from that shared budget before an adjacent Creep calls withdraw().
function createStorageEnergyCoordinator(
room,
reserveEnergy
) {
const storage = room?.storage;
const storageEnergy = storage
? storage.store.getUsedCapacity(RESOURCE_ENERGY)
: 0;
return {
tick: Game.time,
roomName: room?.name ?? null,
storageId: storage?.id ?? null,
reserveEnergy,
withdrawalRemaining: Math.max(
0,
storageEnergy - reserveEnergy
),
targetReservations: Object.create(null),
actedCreepIds: new Set()
};
}
function reserveWithdrawal(coordinator, requested) {
const amount = Math.min(
Math.max(0, requested),
coordinator.withdrawalRemaining
);
coordinator.withdrawalRemaining -= amount;
return amount;
}
function releaseWithdrawal(coordinator, amount) {
coordinator.withdrawalRemaining += Math.max(0, amount);
}
The reserve value is a room policy, not an official optimum. A controller emergency, defense reserve, Terminal hub, Factory, or Power Spawn may require a different budget.
Reserve target capacity in the same coordinator
function reserveTargetCapacity(
coordinator,
target,
requested
) {
const currentFree = target.store.getFreeCapacity(
RESOURCE_ENERGY
);
const alreadyReserved =
coordinator.targetReservations[target.id] ?? 0;
const available = Math.max(
0,
currentFree - alreadyReserved
);
const amount = Math.min(
Math.max(0, requested),
available
);
coordinator.targetReservations[target.id] =
alreadyReserved + amount;
return amount;
}
function releaseTargetCapacity(
coordinator,
targetId,
amount
) {
coordinator.targetReservations[targetId] =
Math.max(
0,
(coordinator.targetReservations[targetId] ?? 0)
- Math.max(0, amount)
);
}
This reservation is local planning state. It is not a server lock and does not protect against another independent module that bypasses the coordinator.
Choose a target with an explicit policy
const ENERGY_TARGET_PRIORITY = {
[STRUCTURE_SPAWN]: 0,
[STRUCTURE_EXTENSION]: 1
};
function selectStorageEnergyTarget(
coordinator,
creep
) {
return creep.room.find(FIND_MY_STRUCTURES, {
filter: structure => {
const priority =
ENERGY_TARGET_PRIORITY[structure.structureType];
if (!Number.isInteger(priority)) {
return false;
}
const currentFree =
structure.store.getFreeCapacity(
RESOURCE_ENERGY
);
const reserved =
coordinator.targetReservations[
structure.id
] ?? 0;
return currentFree - reserved > 0;
}
}).sort((left, right) =>
ENERGY_TARGET_PRIORITY[left.structureType]
- ENERGY_TARGET_PRIORITY[right.structureType]
|| creep.pos.getRangeTo(left)
- creep.pos.getRangeTo(right)
|| left.id.localeCompare(right.id)
)[0] ?? null;
}
Spawn-first is a transparent example policy, not a universal room strategy. Stable tie-breaking prevents the target from changing for no meaningful reason.
Record only accepted actions
For an accepted withdrawal, the transfer event's logical source is the Storage and its target is the Creep. For an accepted delivery, the source is the Creep and the target is the structure. Save those exact IDs after the action returns OK.
function recordStorageEnergyAction(
room,
action
) {
if (!Array.isArray(room.memory.storageEnergyPending)) {
room.memory.storageEnergyPending = [];
}
room.memory.storageEnergyPending.push({
requestId: [
Game.time,
action.kind,
action.sourceId,
action.targetId
].join(':'),
submittedAt: Game.time,
kind: action.kind,
sourceId: action.sourceId,
targetId: action.targetId,
amount: action.amount,
sourceEnergyBefore: action.sourceEnergyBefore,
targetEnergyBefore: action.targetEnergyBefore
});
room.memory.storageEnergyPending =
room.memory.storageEnergyPending.slice(-20);
}
Execute one coordinated hauler action
function runStorageEnergyHauler(
coordinator,
creep
) {
if (
!creep
|| creep.my !== true
|| creep.spawning === true
|| creep.room.name !== coordinator.roomName
) {
return { status: 'creep-unavailable' };
}
if (coordinator.actedCreepIds.has(creep.id)) {
return { status: 'creep-already-planned' };
}
coordinator.actedCreepIds.add(creep.id);
const room = creep.room;
const storage = room.storage;
if (
!storage
|| storage.id !== coordinator.storageId
) {
return { status: 'storage-unavailable' };
}
const carried = creep.store.getUsedCapacity(
RESOURCE_ENERGY
);
if (carried === 0) {
if (!creep.pos.isNearTo(storage)) {
return {
status: 'moving-to-storage',
moveResult: creep.moveTo(storage, {
range: 1,
reusePath: 10
})
};
}
const requested = Math.min(
creep.store.getFreeCapacity(RESOURCE_ENERGY),
coordinator.withdrawalRemaining
);
const amount = reserveWithdrawal(
coordinator,
requested
);
if (amount <= 0) {
return { status: 'storage-reserve-protected' };
}
const sourceEnergyBefore =
storage.store.getUsedCapacity(
RESOURCE_ENERGY
);
const targetEnergyBefore =
creep.store.getUsedCapacity(
RESOURCE_ENERGY
);
const result = creep.withdraw(
storage,
RESOURCE_ENERGY,
amount
);
if (result !== OK) {
releaseWithdrawal(coordinator, amount);
return {
status: 'withdraw-rejected',
amount,
result
};
}
recordStorageEnergyAction(room, {
kind: 'withdraw',
sourceId: storage.id,
targetId: creep.id,
amount,
sourceEnergyBefore,
targetEnergyBefore
});
return {
status: 'withdraw-accepted',
amount,
result
};
}
const target = selectStorageEnergyTarget(
coordinator,
creep
);
if (!target) {
return {
status: 'delivery-target-not-found',
carried
};
}
if (!creep.pos.isNearTo(target)) {
return {
status: 'moving-to-target',
targetId: target.id,
moveResult: creep.moveTo(target, {
range: 1,
reusePath: 10
})
};
}
const amount = reserveTargetCapacity(
coordinator,
target,
carried
);
if (amount <= 0) {
return {
status: 'target-capacity-reserved',
targetId: target.id
};
}
const sourceEnergyBefore =
creep.store.getUsedCapacity(
RESOURCE_ENERGY
);
const targetEnergyBefore =
target.store.getUsedCapacity(
RESOURCE_ENERGY
);
const result = creep.transfer(
target,
RESOURCE_ENERGY,
amount
);
if (result !== OK) {
releaseTargetCapacity(
coordinator,
target.id,
amount
);
return {
status: 'transfer-rejected',
targetId: target.id,
amount,
result
};
}
recordStorageEnergyAction(room, {
kind: 'transfer',
sourceId: creep.id,
targetId: target.id,
amount,
sourceEnergyBefore,
targetEnergyBefore
});
return {
status: 'transfer-accepted',
targetId: target.id,
amount,
result
};
}
Movement is handled before reserving Energy or capacity. If the action is rejected, the local reservation is released so later haulers in the same tick do not inherit a false commitment.
Treat the coordinator amount as a request ceiling
The amount saved by this coordinator is the amount requested at submission time, not a promise that the processor will move exactly that much. In the checked engine snapshot, withdraw() starts from the requested amount but caps processing to the Creep's current empty space and the source's current stock. For transfer(), the source must still cover the requested amount, while the processed amount can be truncated to the target's current free capacity.
This distinction matters when another same-tick action changes a Store after your call returned OK. Keep the coordinator reservation conservative for the rest of that tick; do not recycle an assumed difference before processing. On the next tick, a matched EVENT_TRANSFER reports the processed amount for that exact source-target pair. Missing events and Store deltas remain separate evidence states.
Verify the exact event on the next tick
function verifyStorageEnergyAction(
room,
pending
) {
if (Game.time <= pending.submittedAt) {
return { status: 'accepted-this-tick' };
}
if (Game.time !== pending.submittedAt + 1) {
return {
status: 'verification-window-missed',
submittedAt: pending.submittedAt,
checkedAt: Game.time
};
}
const matches = room.getEventLog()
.filter(event =>
event.event === EVENT_TRANSFER
&& event.objectId === pending.sourceId
&& event.data?.targetId === pending.targetId
&& event.data?.resourceType === RESOURCE_ENERGY
);
if (matches.length === 0) {
return { status: 'matching-event-not-found' };
}
if (matches.length > 1) {
return {
status: 'matching-event-ambiguous',
matchCount: matches.length
};
}
const event = matches[0];
const source = Game.getObjectById(
pending.sourceId
);
const target = Game.getObjectById(
pending.targetId
);
return {
status: 'transfer-event-verified',
kind: pending.kind,
requestedAmount: pending.amount,
processedAmount: event.data?.amount ?? null,
sourceEnergyNow: source?.store
? source.store.getUsedCapacity(
RESOURCE_ENERGY
)
: null,
targetEnergyNow: target?.store
? target.store.getUsedCapacity(
RESOURCE_ENERGY
)
: null
};
}
The event identifies the accepted source-target pair, and its amount is the processor-observed transfer amount for that event. It does not have to equal the coordinator's saved request when same-tick state changed. Current Stores are supporting context only: other haulers, Links, Spawns, Extensions, Towers, or consumers can change the same objects during the tick.
Process pending records once
function verifyPendingStorageActions(room) {
const pending = Array.isArray(
room.memory.storageEnergyPending
)
? room.memory.storageEnergyPending
: [];
const results = [];
const keep = [];
for (const action of pending) {
const result = verifyStorageEnergyAction(
room,
action
);
results.push({
requestId: action.requestId,
...result
});
if (result.status === 'accepted-this-tick') {
keep.push(action);
}
}
room.memory.storageEnergyPending = keep;
return results;
}
Run verification before submitting new actions so the one-tick event window is not lost. Production code may retain failed results in a bounded diagnostic history instead of deleting them immediately.
Return-code boundaries
| Code | Meaning | Response |
|---|---|---|
OK | The action was accepted. | Record the exact IDs and inspect the next tick's event. |
ERR_NOT_IN_RANGE | The Creep is not adjacent. | Move without reserving the budget. |
ERR_NOT_ENOUGH_RESOURCES | The source amount changed. | Release the local reservation and refresh. |
ERR_FULL | The receiving Store cannot accept the request. | Release target capacity and reselect. |
ERR_INVALID_TARGET | The selected object cannot perform that side of the transfer. | Check source/target type and ownership. |
ERR_INVALID_ARGS | The resource or amount is invalid. | Inspect the captured request. |
Production adaptation notes
Build the coordinator once per room and tick, pass it to every eligible hauler, and keep final action calls in one logistics layer. Extend target priorities only after the room budget decides that the Energy may be spent. A Storage reserve protects stock; it does not by itself decide whether upgrading, defense, spawning, Labs, Factory, Power processing, or Terminal operations should win.
Verification status and evidence boundary
The reserve math, same-tick reservation rules, exact event identity, and missed-window states were checked statically and offline. No live Storage race, multiple-hauler event log, room traffic, or Console output was available. Console and live multi-tick verification remain pending.