LOGISTICS · STORAGE BUDGET AND EVENT IDENTITY

Use Storage Energy Without Crossing the Reserve or Misreading Transfers

CURATED PATH

These links are maintained for a concrete prerequisite, failure mode, debugging step, or next lesson instead of generic topic similarity.

Market / Industry

Recommended next steps

Move from local reserves into cross-room logisticsLogistics pathSet a resource budget for Factory productionIndustry budgetPreserve Energy for Tower defense and repairDefense budgetSet an Energy floor for Power processingResource consumption
Verification statusExisting English route: Preserved · Official docs: Checked — Room.storage, Store, withdraw(), transfer(), and Room.getEventLog() · Static API review: Passed — shared budgets, exact source-target identity, event window, and Store context

VERIFICATION

Evidence and test status

Existing English route
Preserved
Official docs
Checked — Room.storage, Store, withdraw(), transfer(), and Room.getEventLog()
Static API review
Passed — shared budgets, exact source-target identity, event window, and Store context
Offline coordinator review
Passed — reserve, target capacity, release, duplicate Creep, event missing, ambiguous, and missed-window states
Human editorial pass
Passed
Screeps Console test
Pending
Live multi-tick verification
Pending
Genuine room or Console screenshots
Pending
Official engine source
Checked screeps/engine 80977824199a596d174d392fd0cf8c458c21fcbd: withdraw processing re-reads empty space and source stock; transfer processing re-reads source stock and target capacity
Amount boundary
The coordinator reserves the requested amount; a matched EVENT_TRANSFER is used to report the processor-applied amount instead of assuming requested equals processed
Screeps Console test
Pending — no live same-tick Storage contention trace is claimed
Live requested-versus-processed comparison
Pending — no controlled competing withdraw/transfer run was collected
Last editorial review
August 17, 2026
Publication status
Published

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

CodeMeaningResponse
OKThe action was accepted.Record the exact IDs and inspect the next tick's event.
ERR_NOT_IN_RANGEThe Creep is not adjacent.Move without reserving the budget.
ERR_NOT_ENOUGH_RESOURCESThe source amount changed.Release the local reservation and refresh.
ERR_FULLThe receiving Store cannot accept the request.Release target capacity and reselect.
ERR_INVALID_TARGETThe selected object cannot perform that side of the transfer.Check source/target type and ownership.
ERR_INVALID_ARGSThe 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.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide keeps the technical scope and verification boundaries of its Chinese source. The verification record shows which checks were completed and which runtime observations are still pending.