SPAWN SYSTEM · ENERGY DIAGNOSTICS

Why room.energyAvailable Stays Below Capacity in Screeps

Verification statusOfficial documentation: Checked August 29, 2026 — Room.energyAvailable, Room.energyCapacityAvailable, Structure.isActive(), Creep.transfer(), and Room.getEventLog() · Engine source: screeps/engine 4.3.2 · 80977824199a596d174d392fd0cf8c458c21fcbd · Static code review: Passed — symptom-first diagnostic, active Spawn/Extension reconciliation, stable target identity, separate movement/transfer results, saved-room event verification, and requested-versus-processed amount separation

VERIFICATION

Evidence and test status

Official documentation
Checked August 29, 2026 — Room.energyAvailable, Room.energyCapacityAvailable, Structure.isActive(), Creep.transfer(), and Room.getEventLog()
Engine source
screeps/engine 4.3.2 · 80977824199a596d174d392fd0cf8c458c21fcbd
Static code review
Passed — symptom-first diagnostic, active Spawn/Extension reconciliation, stable target identity, separate movement/transfer results, saved-room event verification, and requested-versus-processed amount separation
Evidence model
Exact EVENT_TRANSFER first; matching Store deltas second; room aggregate only as concurrent-demand context
Screeps Console test
Pending — no real-account Console transcript was collected for this revision
Live multi-tick verification pending
Pending — no live multi-hauler contention or transfer-plus-spawn-consumption trace was collected

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:

  1. Which active owned Spawn or Extension still has free Energy capacity?
  2. Does your filler Creep actually carry Energy?
  3. What exact value did creep.transfer() return?
  4. 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 observeLikely causeWhat to inspect next
One Spawn or Extension still has free capacityThe fill route has not reached that structure yet.Filler Store, saved target, movement result, then transfer() result.
The filler keeps walking but rarely transfersThe 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_RANGEThe Creep is not adjacent.Move to range 1, then retry on a later tick.
transfer() returns ERR_FULLThe 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 riseThe 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 disagreeYour 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

CodeMeaning in this workflowResponse
OKThe transfer intent was accepted for the current tick.Verify the processed event or later Store state next tick.
ERR_NOT_OWNERThe sending Creep is not yours.Stop; do not retry an ownership error as logistics.
ERR_BUSYThe Creep is still spawning.Wait until spawning is complete.
ERR_NOT_ENOUGH_RESOURCESThe Creep does not carry the requested resource amount.Re-read carried Energy and recompute the request.
ERR_INVALID_TARGETThe target cannot receive this transfer/resource.Clear the target and rebuild the candidate set.
ERR_FULLThe target is full, or the explicit requested amount exceeds its current remaining capacity.Re-read free capacity and recompute before retrying.
ERR_NOT_IN_RANGEThe target is not adjacent.Move to range 1 and preserve the movement result separately.
ERR_INVALID_ARGSThe 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.

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.