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
| Status | Meaning |
|---|---|
queued-in-plan | A producer requested vision; no API call happened. |
no-request | The final coordinator had nothing valid to submit. |
plan-mismatch | The plan does not belong to this Observer or tick. |
rejected | The one final API call returned a non-OK code. |
submitted | The 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.