CONTROLLER · FIXED UPGRADER DIAGNOSTICS

Why a Fixed Screeps Upgrader Is Not Upgrading

CURATED PATH

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

Controller

Recommended next steps

upgradeController basicsPrerequisite pathController Link Energy deliveryEnergy sourceVerify upgrade results with Event LogRuntime evidenceCheck downgrade risk when upgrading stallsRisk escalation
Verification statusDocumentation: Official API and game-loop references checked · Syntax: Complete JavaScript example checked offline · Offline cases: 24 passed

VERIFICATION

Evidence and test status

Documentation
Official API and game-loop references checked
Syntax
Complete JavaScript example checked offline
Offline cases
24 passed
Live shard
Pending

Separate the Upgrader from its supply chain

A fixed Upgrader is the final consumer in a longer Energy chain: Source or Storage, hauler or source Link, Controller Link, Upgrader Store, then upgradeController(). A stationary Creep is not automatically broken. It may be waiting for Link Energy, missing an active WORK part, standing on the wrong tile, or facing a blocked Controller.

This guide is different from the beginner Source-to-Controller loop and the generic Link transfer guide. It diagnoses the combined fixed-position system.

LayerRequired conditionFailure signal
AnchorWithin range 3 of Controller and range 1 of LinkRange or walkability reason
BodyEnergy capacity and active WORK; MOVE until anchoredNo capacity, WORK, or MOVE
Controller LinkOwned, active, same room, with Energycontroller-link-empty
ControllerOwned and not upgrade-blockedcontroller-upgrade-blocked
EvidenceLater position, Store delta, or exact eventAccepted command without observed result

Run a 30-second read-only triage

Before changing the anchor or replacing your loop, inspect the objects that already exist. Replace the two placeholder values, then run this in the Console. It does not issue movement, withdraw, transfer, or upgrade commands.

const UPGRADER_NAME = 'Upgrader1';
const CONTROLLER_LINK_ID = 'CONTROLLER_LINK_ID';

const creep = Game.creeps[UPGRADER_NAME];
const controller = creep?.room.controller ?? null;
const link = Game.getObjectById(CONTROLLER_LINK_ID);

console.log(JSON.stringify({
  creepFound: Boolean(creep),
  spawning: creep?.spawning ?? null,
  activeWork: creep?.getActiveBodyparts(WORK) ?? null,
  energy: creep?.store.getUsedCapacity(RESOURCE_ENERGY) ?? null,
  controllerOwned: controller?.my ?? null,
  controllerRange: creep && controller
    ? creep.pos.getRangeTo(controller)
    : null,
  upgradeBlocked: controller?.upgradeBlocked ?? null,
  linkFound: Boolean(link),
  linkOwned: link?.my ?? null,
  linkType: link?.structureType ?? null,
  linkActive: typeof link?.isActive === 'function'
    ? link.isActive()
    : null,
  linkEnergy: link?.store?.getUsedCapacity(RESOURCE_ENERGY) ?? null,
  linkRange: creep && link
    ? creep.pos.getRangeTo(link)
    : null
}));
Probe resultMost likely branchNext step
creepFound: false or spawning: trueThe worker is missing or not ready.Fix the Creep name or wait for spawning to finish.
activeWork: 0The Creep cannot perform Controller work.Inspect its body and damage before changing logistics.
controllerRange > 3The current position cannot upgrade the Controller.Validate the anchor geometry below.
linkRange > 1The current position cannot withdraw from the Controller Link.Validate the same anchor against the Link.
energy: 0 and linkEnergy: 0The Upgrader is starved upstream.Repair Link supply instead of changing upgradeController().
upgradeBlocked > 0The Controller is temporarily blocked from upgrading.Wait for the block to expire; code changes will not bypass it.
Ranges, body, ownership, Link Energy, and Controller state all look validThe next useful signal is the exact action return code.Use the checklist below before replacing the whole implementation.

If the Creep is not currently on its intended fixed tile, treat the two range values as current-position diagnostics only. The next section checks the configured anchor itself.

Validate one exact anchor tile

The chosen tile must satisfy both documented action ranges:

anchor.getRangeTo(controller) <= 3;
anchor.getRangeTo(controllerLink) <= 1;

The tile must also be walkable. Moving only to range: 3 from the Controller does not guarantee adjacency to the Controller Link. Store the Link ID and anchor coordinates explicitly instead of depending on a structure array position.

Memory.fixedUpgraders ??= {};
Memory.fixedUpgraders.W1N1 = {
  enabled: true,
  creepName: 'Upgrader1',
  controllerLinkId: 'CONTROLLER_LINK_ID',
  anchor: { x: 24, y: 25 },
  pending: null,
  history: []
};

Use one diagnosable action per tick

not anchored -> move only
anchored and empty -> withdraw only
anchored with Energy -> upgrade only

Screeps commands are queued against the tick's starting state and settle later. Separating the states is not the only valid architecture, but it makes return codes and next-tick observations easier to interpret.

If the Controller Link is empty, keep the Upgrader fixed and repair upstream logistics. Use the hauling throughput planner and the Link guide instead of silently turning the Upgrader into a Storage hauler.

Return-code checklist

MethodImportant resultsPrimary diagnosis
moveTo()OK, ERR_TIRED, ERR_NO_PATH, ERR_NO_BODYPARTTraffic, fatigue, path, active MOVE
withdraw()OK, ERR_NOT_ENOUGH_RESOURCES, ERR_FULL, ERR_NOT_IN_RANGELink stock, Creep capacity, range 1
upgradeController()OK, ERR_NOT_ENOUGH_RESOURCES, ERR_INVALID_TARGET, ERR_NOT_IN_RANGE, ERR_NO_BODYPART, ERR_ACCESS_DENIEDEnergy, Controller state, range 3, active WORK

Preserve the actual result. OK means the command was accepted, not that the same code block can already read the settled state. If the short probe already isolates the problem, stop there; the complete implementation below is a hardened reference, not the first diagnostic step.

Complete fixed-Upgrader example

The implementation below validates object identity, ownership, anchor geometry, walkability, active body parts, Link state, upgradeBlocked, and one later observation. History is bounded to 20 records.

const HISTORY_LIMIT = 20;

function evaluateFixedUpgraderState(input) {
  const {
    enabled, creepExists, creepOwned, spawning,
    activeWork, energyCapacity, energy, activeMove,
    atAnchor, anchorWalkable, anchorControllerRange,
    anchorLinkRange, controllerExists, controllerOwned,
    upgradeBlocked, linkExists, linkOwned, linkActive,
    linkEnergy
  } = input;

  if (enabled !== true) return { action: 'none', reason: 'config-disabled' };
  if (!creepExists || !creepOwned) return { action: 'none', reason: 'owned-creep-missing' };
  if (spawning) return { action: 'none', reason: 'creep-spawning' };
  if (!controllerExists || !controllerOwned) return { action: 'none', reason: 'owned-controller-missing' };
  if (!linkExists || !linkOwned) return { action: 'none', reason: 'owned-controller-link-missing' };
  if (!linkActive) return { action: 'none', reason: 'controller-link-inactive' };
  if (!anchorWalkable) return { action: 'none', reason: 'anchor-not-walkable' };
  if (anchorControllerRange > 3) return { action: 'none', reason: 'anchor-outside-controller-range' };
  if (anchorLinkRange > 1) return { action: 'none', reason: 'anchor-outside-link-range' };
  if (energyCapacity <= 0) return { action: 'none', reason: 'no-energy-capacity' };
  if (activeWork <= 0) return { action: 'none', reason: 'no-active-work' };
  if (!atAnchor) {
    return activeMove > 0
      ? { action: 'move', reason: 'move-to-anchor' }
      : { action: 'none', reason: 'no-active-move' };
  }
  if (energy <= 0) {
    return linkEnergy > 0
      ? { action: 'withdraw', reason: 'take-controller-link-energy' }
      : { action: 'none', reason: 'controller-link-empty' };
  }
  if (upgradeBlocked > 0) return { action: 'none', reason: 'controller-upgrade-blocked' };
  return { action: 'upgrade', reason: 'upgrade-ready' };
}

function getState(roomName) {
  Memory.fixedUpgraders ??= {};
  Memory.fixedUpgraders[roomName] ??= {
    enabled: false,
    creepName: null,
    controllerLinkId: null,
    anchor: null,
    pending: null,
    history: []
  };
  return Memory.fixedUpgraders[roomName];
}

function getOwnedLink(id, roomName) {
  if (typeof id !== 'string') return null;
  const link = Game.getObjectById(id);
  return link
    && link.structureType === STRUCTURE_LINK
    && link.my === true
    && link.room.name === roomName
    ? link
    : null;
}

function getAnchor(room, value) {
  if (
    !value
    || !Number.isInteger(value.x)
    || !Number.isInteger(value.y)
    || value.x < 0 || value.x > 49
    || value.y < 0 || value.y > 49
  ) return null;
  return new RoomPosition(value.x, value.y, room.name);
}

function isWalkable(room, position) {
  if (!position) return false;
  if (room.getTerrain().get(position.x, position.y) === TERRAIN_MASK_WALL) return false;

  const blockedStructure = room.lookForAt(
    LOOK_STRUCTURES,
    position.x,
    position.y
  ).some(item => OBSTACLE_OBJECT_TYPES.includes(item.structureType));

  const blockedSite = room.lookForAt(
    LOOK_CONSTRUCTION_SITES,
    position.x,
    position.y
  ).some(item => OBSTACLE_OBJECT_TYPES.includes(item.structureType));

  return !blockedStructure && !blockedSite;
}

function snapshot(creep, controller, link, anchor) {
  return {
    creepEnergy: creep?.store.getUsedCapacity(RESOURCE_ENERGY) ?? null,
    linkEnergy: link?.store.getUsedCapacity(RESOURCE_ENERGY) ?? null,
    controllerProgress: Number.isFinite(controller?.progress)
      ? controller.progress
      : null,
    ticksToDowngrade: controller?.ticksToDowngrade ?? null,
    atAnchor: Boolean(creep && anchor && creep.pos.isEqualTo(anchor))
  };
}

function allFinite(values) {
  return values.every(value => Number.isFinite(value));
}

function verifyPrevious(room, state, creep, controller, link, anchor) {
  const pending = state.pending;
  if (!pending || pending.tick >= Game.time) return null;

  const after = snapshot(creep, controller, link, anchor);
  const exactUpgradeEvent = pending.action === 'upgrade'
    && room.getEventLog().some(event =>
      event.event === EVENT_UPGRADE_CONTROLLER
      && event.objectId === pending.creepId
    );

  let status = 'not-observed';
  if (pending.action === 'move' && after.atAnchor) {
    status = 'anchor-arrival-observed';
  }
  if (pending.action === 'withdraw') {
    const canMeasureWithdraw = allFinite([
      after.creepEnergy,
      pending.before.creepEnergy,
      pending.before.linkEnergy,
      after.linkEnergy
    ]);

    if (!canMeasureWithdraw) {
      status = 'withdraw-evidence-unavailable';
    } else {
      const gain = after.creepEnergy - pending.before.creepEnergy;
      const loss = pending.before.linkEnergy - after.linkEnergy;
      status = gain > 0 && loss > 0
        ? 'withdraw-deltas-observed'
        : gain > 0 || loss > 0
          ? 'withdraw-partial-observation'
          : 'withdraw-not-observed';
    }
  }
  if (pending.action === 'upgrade') {
    const canMeasureSpent = allFinite([
      after.creepEnergy,
      pending.before.creepEnergy
    ]);
    const canMeasureProgress = allFinite([
      after.controllerProgress,
      pending.before.controllerProgress
    ]);
    const canMeasureDowngrade = allFinite([
      after.ticksToDowngrade,
      pending.before.ticksToDowngrade
    ]);

    if (exactUpgradeEvent) {
      status = 'upgrade-event-observed';
    } else if (!canMeasureSpent || (!canMeasureProgress && !canMeasureDowngrade)) {
      status = 'upgrade-evidence-unavailable';
    } else {
      const spent = pending.before.creepEnergy - after.creepEnergy;
      const progress = canMeasureProgress
        && after.controllerProgress > pending.before.controllerProgress;
      const downgrade = canMeasureDowngrade
        && after.ticksToDowngrade > pending.before.ticksToDowngrade;
      status = spent > 0 && (progress || downgrade)
        ? 'upgrade-deltas-observed'
        : spent > 0 || progress || downgrade
          ? 'upgrade-partial-observation'
          : 'upgrade-not-observed';
    }
  }

  const record = {
    ...pending,
    verifiedAt: Game.time,
    after: { ...after, exactUpgradeEvent },
    status
  };
  state.history ??= [];
  state.history.push(record);
  state.history = state.history.slice(-HISTORY_LIMIT);
  state.pending = null;
  state.lastVerification = record;
  return record;
}

function runFixedUpgrader(roomName) {
  const state = getState(roomName);
  const room = Game.rooms[roomName];
  if (!room) return { status: 'room-not-visible' };

  const controller = room.controller || null;
  const creep = typeof state.creepName === 'string'
    ? Game.creeps[state.creepName] || null
    : null;
  const link = getOwnedLink(state.controllerLinkId, roomName);
  const anchor = getAnchor(room, state.anchor);
  const verification = verifyPrevious(
    room, state, creep, controller, link, anchor
  );

  const decision = evaluateFixedUpgraderState({
    enabled: state.enabled,
    creepExists: Boolean(creep),
    creepOwned: creep?.my === true,
    spawning: creep?.spawning === true,
    activeWork: creep?.getActiveBodyparts(WORK) ?? 0,
    energyCapacity: creep?.store.getCapacity(RESOURCE_ENERGY) ?? 0,
    energy: creep?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0,
    activeMove: creep?.getActiveBodyparts(MOVE) ?? 0,
    atAnchor: Boolean(creep && anchor && creep.pos.isEqualTo(anchor)),
    anchorWalkable: isWalkable(room, anchor),
    anchorControllerRange: controller && anchor
      ? anchor.getRangeTo(controller)
      : Infinity,
    anchorLinkRange: link && anchor
      ? anchor.getRangeTo(link)
      : Infinity,
    controllerExists: Boolean(controller),
    controllerOwned: controller?.my === true,
    upgradeBlocked: controller?.upgradeBlocked ?? 0,
    linkExists: Boolean(link),
    linkOwned: link?.my === true,
    linkActive: link?.isActive() === true,
    linkEnergy: link?.store.getUsedCapacity(RESOURCE_ENERGY) ?? 0
  });

  state.lastReason = decision.reason;
  state.lastDecisionAt = Game.time;
  if (decision.action === 'none') {
    return { status: decision.reason, verification };
  }

  const before = snapshot(creep, controller, link, anchor);
  let result = ERR_INVALID_ARGS;
  if (decision.action === 'move') {
    result = creep.moveTo(anchor, { range: 0, reusePath: 10 });
  } else if (decision.action === 'withdraw') {
    result = creep.withdraw(link, RESOURCE_ENERGY);
  } else if (decision.action === 'upgrade') {
    result = creep.upgradeController(controller);
  }

  state.lastAction = decision.action;
  state.lastResult = result;
  state.lastResultAt = Game.time;
  if (result === OK) {
    state.pending = {
      tick: Game.time,
      action: decision.action,
      creepId: creep.id,
      creepName: creep.name,
      controllerId: controller.id,
      linkId: link.id,
      before
    };
  }

  return {
    status: result === OK ? 'command-accepted' : 'command-rejected',
    action: decision.action,
    reason: decision.reason,
    result,
    before,
    verification
  };
}

module.exports.loop = function () {
  const outcome = runFixedUpgrader('W1N1');
  if (outcome.status !== 'config-disabled' || outcome.verification) {
    console.log(JSON.stringify({
      type: 'fixed-upgrader',
      tick: Game.time,
      roomName: 'W1N1',
      ...outcome
    }));
  }
};

Verify the later result

  • Move: the Creep is on the exact anchor.
  • Withdraw: Creep Energy increased and Controller Link Energy decreased.
  • Upgrade: prefer an exact EVENT_UPGRADE_CONTROLLER whose objectId is the pending Creep ID.

If a Creep, Link, or Controller snapshot is missing, the code records withdraw-evidence-unavailable or upgrade-evidence-unavailable; it never lets null participate in arithmetic. When exact events are unavailable, Energy, Controller progress, and downgrade-timer changes are only bounded supporting observations. Concurrent Upgraders can confound pure deltas.

Evidence boundaries

Twenty-four offline cases passed, including missing objects, ownership, spawning, walkability, both action ranges, Energy capacity, active WORK and MOVE, empty Link, blocked Controller, move, withdraw, upgrade, missing Creep, Link and Controller snapshots, and later-observation classifications. The complete example passed a JavaScript syntax check.

These checks do not prove live shard traffic, anchor contention, competing Link senders, every Boost and Power Creep combination, RCL8 throughput, or that production IDs and coordinates are correct. Console and official-shard evidence remain pending.

Continue with Controller downgrade recovery, Storage Energy policy, or Room event logs.

SOURCE AND SCOPE

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.