CONTROLLER · RESERVE AND CLAIM IDENTITY

Reserve or Claim a Controller Without Losing Mission Identity

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

Check current remote-room visibilityVisibility prerequisiteAcquire remote vision with an ObserverObservation pathPlan a cross-room Claimer routeMovement pathVerify reserveController eventsVerification
Verification statusChinese source article: Reviewed in full · Official engine: Checked — reserveController emits EVENT_RESERVE_CONTROLLER for the acting Creep and amount, without a Controller targetId · Claim boundary: Checked — claimController changes the exact Controller owner and room ownership but does not emit a Room event

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official engine
Checked — reserveController emits EVENT_RESERVE_CONTROLLER for the acting Creep and amount, without a Controller targetId
Claim boundary
Checked — claimController changes the exact Controller owner and room ownership but does not emit a Room event
Static code review
Passed — exact Claimer, Controller, room, action, confirmation, accepted-call and next-tick verification states
JavaScript syntax
Passed
Screeps Console test
Pending
Live reserve event, claim ownership, hostile reservation and missed-window verification
Pending
Genuine room or Console screenshots
Pending
Last verified
August 3, 2026

Separate mission choice from operation proof

reserveController() and claimController() solve different strategic goals, but both need the same operational discipline. The mission must identify one Creep, one Controller and one action before submission. An OK return value means the current intent was accepted; it does not by itself prove the next processed state.

ActionGame-state resultStrongest next-tick evidence
reserveController()The Controller remains neutral and its reservation time increasesThe exact Claimer's EVENT_RESERVE_CONTROLLER plus the exact room Controller's reservation state
claimController()The exact neutral Controller becomes owned at level 1The exact Controller ID reports your username and my === true

The official engine records a reserve event with the acting Creep as objectId and the added reservation amount. That event does not include a Controller targetId. A claim produces no Room event, so do not invent one.

Resolve the exact Claimer and Controller

function resolveControllerMission(creep, mission) {
  if (!creep || creep.my !== true || creep.spawning === true) {
    return { ready: false, status: 'claimer-unavailable' };
  }

  if (creep.getActiveBodyparts(CLAIM) <= 0) {
    return { ready: false, status: 'no-active-claim-part' };
  }

  const controller = creep.room.controller || null;
  if (!controller) {
    return { ready: false, status: 'controller-missing' };
  }

  if (
    mission.roomName !== creep.room.name
    || mission.controllerId !== controller.id
  ) {
    return {
      ready: false,
      status: 'mission-identity-mismatch',
      observedControllerId: controller.id
    };
  }

  return { ready: true, status: 'identity-ready', controller };
}

Store the Controller ID after the room is scouted and reviewed. Do not accept whichever Controller happens to be visible when a mission runs. The room name is useful for routing, while the ID prevents a stale or corrupted mission from silently targeting another object.

Evaluate reserve and claim rules

function countOwnedRooms() {
  return Object.values(Game.rooms)
    .filter(room => room.controller?.my === true)
    .length;
}

function evaluateControllerAction(creep, controller, mission) {
  if (!['reserve', 'claim'].includes(mission.action)) {
    return { ready: false, status: 'invalid-action' };
  }

  if (controller.owner) {
    return { ready: false, status: 'controller-already-owned' };
  }

  const username = creep.owner.username;
  const reservationUsername =
    controller.reservation?.username ?? null;

  if (
    reservationUsername
    && reservationUsername !== username
  ) {
    return { ready: false, status: 'hostile-reservation' };
  }

  if (mission.action === 'claim') {
    const expectedConfirmation =
      'CLAIM_' + controller.pos.roomName + '_' + controller.id;

    if (mission.confirmation !== expectedConfirmation) {
      return { ready: false, status: 'claim-confirmation-mismatch' };
    }

    if (countOwnedRooms() >= Game.gcl.level) {
      return { ready: false, status: 'gcl-capacity-unavailable' };
    }
  }

  if (!creep.pos.inRangeTo(controller, 1)) {
    return { ready: false, status: 'move-to-controller' };
  }

  return { ready: true, status: 'action-ready' };
}

The confirmation string is a project safeguard, not an API requirement. It binds a permanent claim to the reviewed room and Controller ID. Reservation remains renewable; claiming is a one-time ownership transition.

Record only an accepted Controller action

function submitControllerMission(creep) {
  const mission = Memory.controllerMissions?.[creep?.name];
  if (!mission || mission.enabled !== true) {
    return { status: 'mission-disabled' };
  }

  const resolved = resolveControllerMission(creep, mission);
  if (!resolved.ready) return resolved;

  const controller = resolved.controller;
  const decision = evaluateControllerAction(
    creep,
    controller,
    mission
  );

  if (decision.status === 'move-to-controller') {
    return {
      status: 'moving',
      moveResult: creep.moveTo(controller, {
        range: 1,
        reusePath: 10
      })
    };
  }
  if (!decision.ready) return decision;

  Memory.pendingControllerOperations ??= {};
  const existing =
    Memory.pendingControllerOperations[creep.name];

  if (existing && existing.submittedAt >= Game.time - 1) {
    return { status: 'pending-verification' };
  }

  const before = {
    ownerUsername: controller.owner?.username ?? null,
    reservationUsername:
      controller.reservation?.username ?? null,
    reservationTicks:
      controller.reservation?.ticksToEnd ?? 0,
    ownedRoomCount: countOwnedRooms()
  };

  const result = mission.action === 'reserve'
    ? creep.reserveController(controller)
    : creep.claimController(controller);

  mission.lastResult = result;
  mission.lastResultAt = Game.time;

  if (result !== OK) {
    return {
      status: 'controller-action-rejected',
      action: mission.action,
      result
    };
  }

  Memory.pendingControllerOperations[creep.name] = {
    submittedAt: Game.time,
    action: mission.action,
    creepId: creep.id,
    creepName: creep.name,
    username: creep.owner.username,
    roomName: creep.room.name,
    controllerId: controller.id,
    before
  };

  mission.lastStatus = 'accepted-awaiting-verification';

  if (mission.action === 'claim') {
    mission.enabled = false;
    mission.completion = 'pending-owner-verification';
  }

  return {
    status: 'controller-action-accepted',
    action: mission.action,
    result,
    controllerId: controller.id
  };
}

Disabling an accepted claim prevents another call, but it must not be labeled completed until ownership is observed. A reserve mission can remain enabled, although the pending record blocks another submission until the one-tick evidence window is consumed.

Verify reservation through the exact event

function verifyReserveOperation(pending) {
  if (Game.time <= pending.submittedAt) {
    return { status: 'wait-for-next-tick' };
  }
  if (Game.time > pending.submittedAt + 1) {
    return { status: 'reserve-event-window-missed' };
  }

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

  const controller = room.controller;
  if (!controller || controller.id !== pending.controllerId) {
    return { status: 'controller-identity-mismatch' };
  }

  const matches = room.getEventLog().filter(event =>
    event.event === EVENT_RESERVE_CONTROLLER
    && event.objectId === pending.creepId
  );

  if (matches.length === 0) {
    return { status: 'accepted-reserve-event-missing' };
  }
  if (matches.length > 1) {
    return {
      status: 'reserve-event-ambiguous',
      count: matches.length
    };
  }

  const reservation = controller.reservation;
  const usernameMatches =
    reservation?.username === pending.username;

  return {
    status: usernameMatches
      ? 'reserve-event-and-state-observed'
      : 'reserve-event-state-mismatch',
    amount: matches[0].data?.amount ?? null,
    controllerId: controller.id,
    reservationUsername:
      reservation?.username ?? null,
    reservationTicks:
      reservation?.ticksToEnd ?? 0,
    beforeTicks: pending.before.reservationTicks
  };
}

The event identifies the Claimer, not the Controller target. The verifier therefore combines three facts: it reads the event from the pending room, confirms that room's Controller ID, and checks the reservation username. The event amount is operation output; a current ticksToEnd value is processed state and should not be forced to equal a simple before-plus-amount calculation.

Verify claiming through exact ownership

function verifyClaimOperation(pending) {
  if (Game.time <= pending.submittedAt) {
    return { status: 'wait-for-next-tick' };
  }

  const room = Game.rooms[pending.roomName];
  if (!room) return { status: 'claimed-room-not-visible' };

  const controller = room.controller;
  if (!controller || controller.id !== pending.controllerId) {
    return { status: 'controller-identity-mismatch' };
  }

  const ownerMatches =
    controller.owner?.username === pending.username;

  return {
    status: ownerMatches && controller.my === true
      ? 'claim-owner-observed'
      : 'accepted-claim-owner-not-observed',
    controllerId: controller.id,
    ownerUsername: controller.owner?.username ?? null,
    controllerMy: controller.my === true,
    controllerLevel: controller.level,
    ownedRoomCountNow: countOwnedRooms(),
    ownedRoomCountBefore:
      pending.before.ownedRoomCount
  };
}

function verifyPendingControllerOperation(pending) {
  if (!pending) return { status: 'no-pending-operation' };

  return pending.action === 'reserve'
    ? verifyReserveOperation(pending)
    : verifyClaimOperation(pending);
}

claimController() has no Room event in the official engine. Exact ownership on the saved Controller is therefore the primary evidence. The account's owned-room count is corroboration only; another room could be claimed or lost during the same period.

Keep incomplete evidence visible

StatusMeaningResponse
mission-identity-mismatchThe visible Controller is not the reviewed objectStop and repair mission data
hostile-reservationAnother username owns the reservationDo not auto-attack or auto-claim
accepted-reserve-event-missingAn accepted reserve lacks the exact Claimer eventPreserve the discrepancy
reserve-event-window-missedThe one-tick Room event window was not readRecord the missed sample
accepted-claim-owner-not-observedThe accepted claim did not produce exact ownership evidenceKeep the mission disabled for review
claimed-room-not-visibleThe expected owned room is not currently visibleDo not infer completion from GCL alone

Production integration boundary

Run one Controller mission dispatcher before role code can submit another Controller action. Consume pending verification before allowing a new reserve call, and keep accepted claims disabled until exact ownership is observed. Console execution, live hostile-reservation behavior, shard-specific timing and genuine screenshots remain pending.

Official documentation

Review the official Screeps API for Creep.reserveController(), Creep.claimController(), Room.getEventLog(), EVENT_RESERVE_CONTROLLER, Controller ownership and reservation fields, GCL, range and return codes before adapting the examples.

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.