VISION · SINGLE-CALL OBSERVER COORDINATION

Stop Multiple Observer Calls From Overwriting the Request You Track

CURATED PATH

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

Debugging / Observability

Recommended next steps

Understand next-tick Observer visibilityPrerequisite conceptInspect Room events after visibility arrivesContinue observationConnect remote vision to route planningMovement branch
Verification statusChinese source article: Reviewed in full · Official docs: Checked — next-tick vision, Game.rooms, OBSERVER_RANGE and OPERATE_OBSERVER · Contributed docs: Checked — later same-tick observeRoom calls can replace earlier accepted calls

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — next-tick vision, Game.rooms, OBSERVER_RANGE and OPERATE_OBSERVER
Contributed docs
Checked — later same-tick observeRoom calls can replace earlier accepted calls
Submission boundary
Planning requests are not game commands; only the final OK call is stored
Attribution boundary
Requested last tick and visible now does not prove exclusive Observer causation
JavaScript syntax
Passed
Offline coordinator review
Passed — duplicate producer, priority tie, plan mismatch, rejected call and next-tick result states
Screeps Console test
Pending
Live same-tick overwrite, final-call, next-tick visibility and multi-Observer test
Pending
Live multi-tick verification
Pending
Last verified
July 31, 2026

Use this guide when

Use this workflow when several modules can ask the same owned Observer for vision. Collect requests first, choose one target, and call observeRoom() once. The accepted target is the only request that may be written as the expected next-tick result.

An accepted call can still be overwritten later in the tick

The API return value belongs to the call you made. Screeps contributed documentation also warns that a later observeRoom() call on the same Observer in the same tick can replace an earlier request even when both calls returned OK. A per-module success log is therefore not enough.

function createObservationPlan(observerId) {
  return {
    observerId,
    createdAt: Game.time,
    requests: []
  };
}

One coordinator must own the final call. Keep this overwrite behavior in the live-test checklist because the page does not claim a fresh server experiment.

Collect requests without touching the Observer

function requestObservation(plan, request) {
  if (
    !plan
    || plan.createdAt !== Game.time
    || !request
    || typeof request.roomName !== 'string'
    || request.roomName.length === 0
  ) {
    return {
      accepted: false,
      reason: 'invalid-request'
    };
  }

  plan.requests.push({
    roomName: request.roomName,
    priority: Number.isFinite(request.priority)
      ? request.priority
      : 0,
    reason: typeof request.reason === 'string'
      ? request.reason
      : 'unspecified'
  });

  return {
    accepted: true,
    reason: 'queued-in-plan'
  };
}

This function accepts a planning request, not a game command. It must never write Memory.observerState or report that vision was scheduled.

Choose one deterministic target

function selectObservationRequest(plan) {
  if (
    !plan
    || plan.createdAt !== Game.time
    || !Array.isArray(plan.requests)
  ) {
    return null;
  }

  const unique = new Map();

  for (const request of plan.requests) {
    const existing = unique.get(request.roomName);
    if (
      !existing
      || request.priority > existing.priority
    ) {
      unique.set(request.roomName, request);
    }
  }

  return [...unique.values()]
    .sort((left, right) =>
      right.priority - left.priority
      || left.roomName.localeCompare(right.roomName)
    )[0] ?? null;
}

Deduplication prevents two producers from turning one room into two apparent requests. Priority and room name make the final choice explainable.

Read the previous accepted request before replacing it

function readPreviousObservation(state) {
  if (
    !state
    || typeof state.roomName !== 'string'
    || !Number.isInteger(state.submittedAt)
  ) {
    return {
      status: 'none',
      room: null
    };
  }

  if (state.submittedAt === Game.time) {
    return {
      status: 'waiting-current-tick',
      room: null
    };
  }

  if (state.submittedAt !== Game.time - 1) {
    return {
      status: 'expired',
      room: null
    };
  }

  const room = Game.rooms[state.roomName];
  return {
    status: room
      ? 'requested-last-tick-visible-now'
      : 'requested-last-tick-missing-now',
    room: room ?? null
  };
}

Visibility now does not prove exclusive Observer causation. A Creep, owned structure, another Observer, or another vision source may also expose the Room.

Make one final API call and save only that result

function submitObservationPlan(observer, plan) {
  if (
    !observer
    || observer.structureType !== STRUCTURE_OBSERVER
    || observer.my !== true
    || observer.isActive() !== true
  ) {
    return {
      status: 'observer-unavailable',
      result: null
    };
  }

  if (
    !plan
    || plan.observerId !== observer.id
    || plan.createdAt !== Game.time
  ) {
    return {
      status: 'plan-mismatch',
      result: null
    };
  }

  const selected = selectObservationRequest(plan);
  if (!selected) {
    return {
      status: 'no-request',
      result: null
    };
  }

  const result = observer.observeRoom(
    selected.roomName
  );

  if (result === OK) {
    Memory.observerState = {
      observerId: observer.id,
      observerRoom: observer.room.name,
      roomName: selected.roomName,
      reason: selected.reason,
      submittedAt: Game.time
    };
  }

  return {
    status: result === OK
      ? 'submitted'
      : 'rejected',
    selected,
    result
  };
}

Do not call observeRoom() again for this Observer after the coordinator runs. A project with multiple Observers should keep one plan and one final call per Observer ID.

Process, plan, then submit

module.exports.loop = function () {
  const previous = readPreviousObservation(
    Memory.observerState
  );

  if (previous.status ===
      'requested-last-tick-visible-now') {
    Memory.roomIntel ??= {};
    Memory.roomIntel[previous.room.name] = {
      observedAt: Game.time,
      sourceCount:
        previous.room.find(FIND_SOURCES).length,
      hostileCount:
        previous.room.find(
          FIND_HOSTILE_CREEPS
        ).length
    };
  }

  const observer = Game.getObjectById(
    Memory.observerConfig?.observerId
  );
  const plan = createObservationPlan(
    observer?.id ?? null
  );

  requestObservation(plan, {
    roomName:
      Memory.observerConfig?.primaryRoom,
    priority: 100,
    reason: 'primary-intel'
  });

  for (const roomName of
      Memory.observerConfig?.backgroundRooms ?? []) {
    requestObservation(plan, {
      roomName,
      priority: 10,
      reason: 'background-refresh'
    });
  }

  return submitObservationPlan(observer, plan);
};

The example intentionally performs the previous-result read before a new accepted request can replace the stored state.

Treat range as a diagnostic, not a second source of truth

The normal Observer range is controlled by OBSERVER_RANGE. OPERATE_OBSERVER can grant unlimited range while its effect is active. Calculate linear distance for diagnostics, then trust the actual method return code for the submitted request.

function describeObserverRequest(
  observer,
  roomName
) {
  const linearDistance =
    Game.map.getRoomLinearDistance(
      observer.room.name,
      roomName
    );

  return {
    observerRoom: observer.room.name,
    roomName,
    linearDistance,
    insideBaseRange:
      linearDistance <= OBSERVER_RANGE
  };
}

Expose planning and submission states separately

StatusMeaning
queued-in-planA producer requested vision; no API call happened.
no-requestThe final coordinator had nothing valid to submit.
plan-mismatchThe plan does not belong to this Observer or tick.
rejectedThe one final API call returned a non-OK code.
submittedThe final API call returned OK; next-tick visibility is still pending.

Keep configuration, visibility, and intel separate

Use the Flag configuration guide to define reviewed target rooms. Use the visibility guide when the problem is simply a missing Game.rooms entry. This page owns only multi-producer coordination around the final Observer request.

Official documentation

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.