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.levelis current RCL.controller.ticksToDowngradeis 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
| Code | Typical cause | Response |
|---|---|---|
OK | Upgrade scheduled | Timer and progress later |
ERR_NOT_OWNER | Creep or Controller ownership | Current objects |
ERR_BUSY | Creep spawning | Wait |
ERR_NOT_ENOUGH_RESOURCES | No Creep Energy | Supply chain |
ERR_INVALID_TARGET | Invalid Controller | Current room state |
ERR_NOT_IN_RANGE | Beyond range 3 | Move to range 3 |
ERR_NO_BODYPART | No active WORK | Body 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.