CONTROLLER · DOWNGRADE RECOVERY

How to Detect Controller Downgrade Risk and Recover Safely

Compare ticksToDowngrade with configurable enter and recovery thresholds, use CONTROLLER_DOWNGRADE for context, select an owned ready Upgrader with Energy and active WORK, move to range 3, save upgradeController() results, and exit emergency mode only after recovery.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — ticksToDowngrade, CONTROLLER_DOWNGRADE, upgradeController(), range and return codes · Threshold boundary: Emergency and recovery thresholds plus the upgrader role name are project policies, not official safety values

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — ticksToDowngrade, CONTROLLER_DOWNGRADE, upgradeController(), range and return codes
Threshold boundary
Emergency and recovery thresholds plus the upgrader role name are project policies, not official safety values
Execution boundary
OK schedules upgrading; progress and downgrade timer recovery require later observation
JavaScript syntax
Passed
Offline recovery review
Passed — ownership, timer, threshold validity, hysteresis, Upgrader Energy, active WORK and range states
Screeps Console test
Pending
Live downgrade timer, recovery hysteresis, upgrader supply and Controller progress test
Pending
Last verified
July 26, 2026

Quick answer

Read the owned Controller's current ticksToDowngrade, enter emergency mode only below a configured threshold, remain in that mode until a higher recovery threshold is reached, select a non-spawning Upgrader with Energy and active WORK, call upgradeController() or move to range 3, save every result, and re-read the Controller timer later.

Separate level, current timer, and maximum timer

  • controller.level is current RCL.
  • controller.ticksToDowngrade is current remaining time.
  • CONTROLLER_DOWNGRADE[controller.level] is the level's maximum timer context.
function getControllerDowngradeRatio(controller) {
  const maximum = controller
    ? CONTROLLER_DOWNGRADE[controller.level]
    : null;

  if (
    !Number.isFinite(maximum)
    || maximum <= 0
    || !Number.isFinite(controller.ticksToDowngrade)
  ) {
    return null;
  }

  return controller.ticksToDowngrade / maximum;
}

The constant table is not current state. Use the live timer for the decision and the table only for context or a ratio.

Use enter and recovery thresholds

Memory.controllerSafety ??= {};
Memory.controllerSafety.W1N1 = {
  enabled: true,
  emergencyThreshold: 5000,
  recoveryThreshold: 10000,
  emergencyActive: false
};

Both values are examples. The recovery value must be greater than the entry value so the system does not repeatedly enter and leave emergency mode near one number.

Build a testable risk state

function evaluateDowngradeRisk(input) {
  if (
    input.owned !== true
    || !Number.isFinite(input.ticksToDowngrade)
  ) {
    return {
      active: false,
      reason: 'controller-unavailable'
    };
  }

  if (
    !Number.isFinite(input.emergencyThreshold)
    || !Number.isFinite(input.recoveryThreshold)
    || input.emergencyThreshold <= 0
    || input.recoveryThreshold
      <= input.emergencyThreshold
  ) {
    return {
      active: false,
      reason: 'invalid-thresholds'
    };
  }

  if (input.emergencyActive === true) {
    return input.ticksToDowngrade
      >= input.recoveryThreshold
      ? { active: false, reason: 'recovered' }
      : { active: true, reason: 'risk-continues' };
  }

  return input.ticksToDowngrade
    < input.emergencyThreshold
    ? { active: true, reason: 'risk-entered' }
    : { active: false, reason: 'normal' };
}

Select a ready emergency Upgrader

function selectEmergencyUpgrader(room, controller) {
  const candidates = room.find(FIND_MY_CREEPS, {
    filter: creep =>
      creep.memory?.role === 'upgrader'
      && creep.spawning !== true
      && creep.store.getUsedCapacity(
        RESOURCE_ENERGY
      ) > 0
      && creep.getActiveBodyparts(WORK) > 0
  });

  return candidates.sort((left, right) => {
    const energyDifference =
      right.store.getUsedCapacity(RESOURCE_ENERGY)
      - left.store.getUsedCapacity(RESOURCE_ENERGY);

    if (energyDifference !== 0) {
      return energyDifference;
    }

    const rangeDifference =
      left.pos.getRangeTo(controller)
      - right.pos.getRangeTo(controller);

    if (rangeDifference !== 0) {
      return rangeDifference;
    }

    return left.name.localeCompare(right.name);
  })[0] || null;
}

The role name is custom Memory. A room using different role identifiers must replace it or select from an explicit task queue.

Complete Controller safety example

function runControllerSafety(room) {
  const controller = room?.controller || null;
  const config = room
    ? Memory.controllerSafety?.[room.name]
    : null;

  if (
    !controller
    || controller.my !== true
    || !config
    || config.enabled !== true
  ) {
    return { status: 'disabled-or-unavailable' };
  }

  config.emergencyActive ??= false;
  const decision = evaluateDowngradeRisk({
    owned: controller.my,
    ticksToDowngrade: controller.ticksToDowngrade,
    emergencyThreshold: config.emergencyThreshold,
    recoveryThreshold: config.recoveryThreshold,
    emergencyActive: config.emergencyActive
  });

  config.emergencyActive = decision.active;
  config.lastReason = decision.reason;
  config.lastCheckedAt = Game.time;
  config.lastTicksToDowngrade =
    controller.ticksToDowngrade;
  config.lastRatio = getControllerDowngradeRatio(
    controller
  );

  if (!decision.active) {
    return { status: decision.reason };
  }

  const upgrader = selectEmergencyUpgrader(
    room,
    controller
  );
  if (!upgrader) {
    config.lastAction = 'no-ready-upgrader';
    return { status: 'no-ready-upgrader' };
  }

  const before = {
    gameTick: Game.time,
    creepName: upgrader.name,
    energy: upgrader.store.getUsedCapacity(
      RESOURCE_ENERGY
    ),
    activeWork: upgrader.getActiveBodyparts(WORK),
    range: upgrader.pos.getRangeTo(controller),
    ticksToDowngrade: controller.ticksToDowngrade,
    progress: controller.progress
  };
  const result = upgrader.upgradeController(
    controller
  );

  if (result === ERR_NOT_IN_RANGE) {
    const moveResult = upgrader.moveTo(controller, {
      range: 3,
      reusePath: 5
    });
    config.lastAction = 'moving-to-controller';
    config.lastMoveResult = moveResult;
    config.lastBefore = before;
    return {
      status: 'moving-to-controller',
      result,
      moveResult,
      before
    };
  }

  config.lastBefore = before;
  config.lastResult = result;
  config.lastResultAt = Game.time;
  config.lastAction = result === OK
    ? 'upgrade-scheduled'
    : 'upgrade-rejected';

  return {
    status: config.lastAction,
    result,
    before
  };
}
module.exports.loop = function () {
  const room = Game.rooms.W1N1;
  if (!room) {
    return;
  }

  const outcome = runControllerSafety(room);
  if (
    outcome.status === 'upgrade-rejected'
    || outcome.status === 'no-ready-upgrader'
    || Game.time % 100 === 0
  ) {
    console.log(JSON.stringify({
      type: 'controller-downgrade-safety',
      roomName: room.name,
      ...outcome
    }));
  }
};

Avoid threshold flapping

With one threshold, a small upgrade can end emergency mode and the next timer decrease can immediately restart it. A higher recovery threshold keeps the priority active until more safety margin is restored.

function thresholdsAreValid(enter, recover) {
  return Number.isFinite(enter)
    && Number.isFinite(recover)
    && enter > 0
    && recover > enter;
}

Record missing recovery capacity

no-ready-upgrader is a diagnosis, not a fix. The room may need spawning, Energy delivery, body recovery, Controller Link supply or task reassignment. This guide does not silently spawn a new Creep because that requires a separate room survival policy.

Verify timer recovery later

Read controller.ticksToDowngrade and controller.progress again. Do not write a predicted timer into Memory. Other Upgraders or power effects can also change the observed state.

Handle return codes

CodeTypical causeResponse
OKUpgrade scheduledTimer and progress later
ERR_NOT_OWNERCreep or Controller ownershipCurrent objects
ERR_BUSYCreep spawningWait
ERR_NOT_ENOUGH_RESOURCESNo Creep EnergySupply chain
ERR_INVALID_TARGETInvalid ControllerCurrent room state
ERR_NOT_IN_RANGEBeyond range 3Move to range 3
ERR_NO_BODYPARTNo active WORKBody damage

Debugging checklist

  • Require a visible owned Controller.
  • Read current timer and level.
  • Validate enter and recovery thresholds.
  • Keep emergency state in Memory.
  • Select a ready Upgrader with Energy.
  • Count active WORK parts.
  • Move to range 3 only on the range error.
  • Save before state and return code.
  • Record missing Upgrader capacity.
  • Verify timer and progress later.

Scope and next steps

This guide does not spawn emergency workers, repair an Energy economy, prioritize multiple rooms, operate Controller powers or predict exact timer restoration. Continue with Reserve versus Claim missions.

Frequently asked questions

Why select more Energy before shorter range?

It favors the Creep able to sustain more emergency upgrades. Your room can reverse those priorities if travel time is more important.

Why use an enabled switch?

It lets rooms opt into the policy explicitly and preserves diagnostics without forcing every owned Controller into the same thresholds.

Can RCL 8 still need upgrading?

Yes. Upgrading continues to maintain the Controller timer and contributes to GCL even after room level progress is complete.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.