KNOWLEDGE MODULE 05

Controller 与房间控制

从固定 Upgrader 与 Controller Link 供能诊断,继续处理 Safe Mode、Controller 降级、预订与占领边界。

专题内进度1 / 4查看完整学习顺序 →

Screeps 常见问题

Screeps Upgrader 站着不升级怎么排查:Controller Link、Energy、WORK 与固定站位

排查固定 Upgrader 到达 Controller 后不升级的问题:核对 Controller Link、站位距离、Energy、WORK/CARRY/MOVE、upgradeBlocked 和返回码,并用下一 tick 事件与 Store 变化验证动作。

难度
基础
适用阶段
固定升级链路
模块位置
1 / 4
前置知识
已完成新手路线即可开始

KNOWLEDGE GRAPH

把这篇文章连接到 API、错误码和工具

这里由现有 API Reference、Object Hub 与知识库顺序自动生成,用来继续排查和学习;它不替代正文中的具体前置条件与返回值说明。

RELATED API

相关 API 与对象入口

StructureLink API HubHub
POSSIBLE ERRORS

相关错误码

ERR_NOT_ENOUGH_RESOURCES-6ERR_INVALID_TARGET-7ERR_FULL-8ERR_TIRED-11ERR_RCL_NOT_ENOUGH-14
USEFUL TOOLS

相关诊断与规划工具

运输吞吐量规划器Tool房间运行诊断Tool
NEXT TOPIC

继续这个知识模块

当前属于“Controller 与房间控制”第 1 / 4 篇。

下一篇:StructureController.activateSafeMode() 怎么安全开启 Safe Mode
VERIFICATION文档已核对 · 语法已检查 · Console待测试 · 主循环待验证查看验证详情
官方文档
已核对
JavaScript 语法
已检查
Screeps Console
待测试
真实主循环
待验证
最后核对
2026年8月5日
测试环境
Node.js 离线模拟(固定站位、Controller/Link 距离、身体部件、Link 库存、upgradeBlocked、动作决策、缺失证据和下一 tick 验证分类;不是 Screeps 官方服务器)
测试日期
2026年8月5日
测试结果
24 个离线场景通过;完整示例通过 JavaScript 语法检查。真实房间交通、Link 网络竞争、Boost、Power Creep 效果和官方 shard 行为仍待验证。
本文目录15 个主要章节

Upgrader 已经走到 Controller 附近,却一直站着不升级,通常不是单一原因。

常见故障链包括:

  • Creep 虽然在 Controller 附近,却超过 upgradeController() 的范围 3;
  • 固定站位离 Controller Link 超过 1 格,无法 withdraw()
  • Controller Link 本身没有 Energy;
  • Creep 有 CARRY 容量,但没有有效 WORK
  • Creep 仍在生成、受伤后失去关键部件,或者站位被障碍占用;
  • Controller 处于 upgradeBlocked
  • 代码只调用了 moveTo(),却没有保存 upgradeController() 的返回值;
  • Link 传能逻辑与 Upgrader 逻辑分散在多处,双方都以为“另一边会补能”;
  • 同一 tick 只看到 OK,却没有在下一 tick 验证实际结果。

本文只解决一个明确问题:让一只指定 Upgrader 固定站在 Controller 与 Controller Link 都能触达的位置,空仓时从 Link 取 Energy,有 Energy 时升级 Controller,并把每一步失败原因保存下来。

这不是新手版“从 Source 往返 Controller”的改写。刚开始学习升级动作,请先看怎样让 Creep 自动升级 Controller。本文面向已经使用 Link、Storage 或专用运输者的房间。

先确认固定升级链路

Source / Storage
      ↓
运输者或 Source Link
      ↓
Controller Link
      ↓ withdraw(),范围 1
固定 Upgrader
      ↓ upgradeController(),范围 3
Controller
环节 负责什么 失败时最直接的信号
上游采集或储存 提供 Energy Storage、Container 或 Source Link 没有可用 Energy
Link 网络或运输者 给 Controller Link 补能 Controller Link 长期为 0
固定站位 同时触达 Link 与 Controller 到 Link 大于 1,或到 Controller 大于 3
Upgrader 身体 携带并消耗 Energy 没有容量、没有有效 WORK、仍在 spawning
Controller 状态 接受升级 非己方 Controller 或 upgradeBlocked > 0
验证记录 区分命令接受与结果发生 只有同 tick 的 OK,没有下一 tick 证据

Controller Link 为空时,不要让固定 Upgrader 临时跑回 Storage。那会把固定升级角色重新变成普通运输角色,并隐藏真正的上游供能故障。

与已有文章的明确区别

站内已有内容分别解决:

本文的搜索意图是“为什么固定 Upgrader 已经就位却不升级”,因此必须把站位、身体、Controller Link、上游供能、返回码和跨 tick 验证串成一条诊断链。

官方距离边界

固定站位必须同时满足:

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

upgradeController() 的范围是 3,withdraw() 的范围是 1。站位还必须可通行,且不能长期被另一只 Creep 占用。

先在 Console 中只读检查:

const room = Game.rooms.W1N1;
const controller = room?.controller;
const link = Game.getObjectById('CONTROLLER_LINK_ID');
const anchor = new RoomPosition(24, 25, 'W1N1');

({
  controllerRange: controller ? anchor.getRangeTo(controller) : null,
  linkRange: link ? anchor.getRangeTo(link) : null,
  terrain: room ? room.getTerrain().get(anchor.x, anchor.y) : null
});

一次性配置

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

不要依赖 room.find(FIND_MY_STRUCTURES)[0] 作为 Controller Link 身份。保存 ID 后,每个 tick 用 Game.getObjectById() 恢复并重新验证类型、所有权和房间。

不升级时按这个顺序排查

1. Creep 是否存在并已生成完成

Game.creeps[config.creepName] 不存在时记录 owned-creep-missingcreep.spawning === true 时记录 creep-spawning

2. 是否有 Energy 容量和有效 WORK

creep.store.getCapacity(RESOURCE_ENERGY);
creep.getActiveBodyparts(WORK);

容量为 0 时无法从 Link 取能;有效 WORK 为 0 时无法升级。受伤后应检查有效部件,而不是只看原始身体数组。

3. 是否在精确站位

使用 creep.pos.isEqualTo(anchor)。未到站位时只提交移动,不继续取能或升级。

controllerLink.store.getUsedCapacity(RESOURCE_ENERGY);

为 0 时,应向上排查 Source Link、cooldown、Link 协调器、Storage 运输者和吞吐量。站内工具中心提供运输吞吐量规划器,可用于估算运输者容量与往返路程。

5. Controller 是否被阻止升级

controller.upgradeBlocked;

大于 0 时,即使距离和 Energy 都正确,也不应继续把失败归因于 Link。

为什么取能和升级分开执行

命令会在当前 tick 排队并在之后结算;同 tick 读取到的对象属性仍是开始时状态。本文采用容易验证的状态机:

未到站位 → 只移动
已到站位且 Energy = 0 → 只 withdraw
已到站位且 Energy > 0 → 只 upgradeController

这不是唯一方案,但每个 tick 只有一个主要意图,更容易解释下一 tick 的位置、Store 和事件。

可离线测试的决策函数

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' };
}

完整固定 Upgrader 示例

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
    }));
  }
};

保存后,把房间名、Creep 名称、Link ID 和 anchor 坐标换成自己的数据。

三种动作的返回值

moveTo()

返回值 处理方向
OK 下一 tick 检查是否到达 anchor
ERR_BUSY 检查 spawning
ERR_NO_BODYPART 检查有效 MOVE
ERR_TIRED 检查 fatigue 与 MOVE 比例
ERR_NO_PATH 检查障碍、交通和 CostMatrix
ERR_INVALID_TARGET 检查 anchor 与视野

如果移动返回 OK 但连续多个 tick 位置不变,使用moveTo() 返回 OK 但不移动继续排查。

withdraw()

返回值 处理方向
OK 下一 tick 检查 Creep 增加、Link 减少
ERR_NOT_ENOUGH_RESOURCES 检查上游补能链
ERR_FULL 检查 Store 与 CARRY
ERR_NOT_IN_RANGE 修正 anchor 到 Link 范围 1
ERR_INVALID_TARGET 验证结构类型与所有权
ERR_INVALID_ARGS 检查资源类型和 amount

upgradeController()

返回值 处理方向
OK 下一 tick 查看事件与状态变化
ERR_NOT_OWNER 核对 Creep 身份
ERR_BUSY 检查 spawning
ERR_NOT_ENOUGH_RESOURCES 检查 Link 与 withdraw 分支
ERR_INVALID_TARGET 检查 Controller 与 upgradeBlocked
ERR_NOT_IN_RANGE 修正 anchor 到 Controller 范围 3
ERR_NO_BODYPART 检查有效 WORK
ERR_ACCESS_DENIED 保存房间安全与访问上下文

返回 OK 只说明命令被接受,不等于当前代码段已经能读到最终状态。

下一 tick 怎样验证

  • 移动:检查 creep.pos.isEqualTo(anchor)
  • 取能:比较 Creep Energy 增加与 Controller Link Energy 减少;
  • 升级:优先检查 EVENT_UPGRADE_CONTROLLER,并要求 event.objectId === pending.creepId

若 Creep、Link 或 Controller 的前后快照缺失,代码会记录 withdraw-evidence-unavailableupgrade-evidence-unavailable,不会让 null 参与减法。若事件不可用,再使用 Creep Energy、Controller progress 和 ticksToDowngrade 的有限变化作为辅助观察。并发 Upgrader 会使纯数值差值无法证明唯一因果。

固定 Upgrader 只负责“Link 有能量就取,Creep 有能量就升级”。controller-link-empty 应交给独立协调器。

最小排查顺序:

  1. Source Link 或 Storage 是否有 Energy;
  2. 源 Link 是否 cooldown === 0
  3. Controller Link 是否有空余容量;
  4. transferEnergy() 返回值是否保存;
  5. 下一 tick 是否观察到源 Link 与目标 Link 变化;
  6. 运输者往返距离和 CARRY 吞吐量是否足够;
  7. 是否有其他消费者抢占 Controller Link。

不要在多个模块中同时调用同一源 Link 的 transferEnergy();应由一个 Link 协调器决定最终目标。

固定 Upgrader 身体怎么理解

本文不提供万能最佳身体。身体取决于 RCL、补给频率、目标升级速度、Boost、RCL8、PWR_OPERATE_CONTROLLER、Spawn 时间和 anchor 是否需要移动。

至少保证有 Energy 容量、有有效 WORK,并在需要移动到 anchor 时有有效 MOVE。升级类 Boost 会提高升级效果而不增加 Energy 成本,但不能代替供能能力。

离线验证覆盖

24 个场景覆盖:配置、对象、所有权、spawning、站位、Controller/Link 距离、Energy 容量、WORK/MOVE、Link 空仓、upgradeBlocked、移动、取能、升级、缺失 Creep/Link/Controller 快照和下一 tick 分类。完整示例通过 node --check

离线测试不能证明官方 shard 交通、多 Creep 抢位、多 Link 竞争、Boost 与 Power Creep 的所有组合、RCL8 实际吞吐、真实 ID/坐标或最高效率。相关结论保持待验证。

推荐内链

官方资料

资料核对日期:2026-08-05。官方文档、完整示例语法和 24 个离线场景已核对;真实 Console 与官方 shard 行为仍待验证。

返回目录