Use this guide when
Use this workflow when a named Flag defines a mission location and its Memory may also name a game object. The reader must prove that the current object has the expected type and belongs to the Flag room. It must not silently accept a valid ID from another room.
Make the Flag contract explicit
const FLAG_CONTRACTS = {
remoteMine: {
flagName: 'RemoteMine_W2N2',
schemaVersion: 1,
allowedModes: new Set([
'observe',
'harvest',
'pause'
])
}
};
The Flag name, schema version, mode set, and fallback policy are project configuration. Screeps does not infer those meanings from color, position, or name.
Validate Flag Memory before resolving objects
function readRemoteMineMemory(flag, contract) {
if (!flag) {
return {
valid: false,
reason: 'flag-missing'
};
}
const value = flag.memory;
if (!value || typeof value !== 'object') {
return {
valid: false,
reason: 'memory-not-object'
};
}
if (value.version !== contract.schemaVersion) {
return {
valid: false,
reason: 'schema-version-mismatch'
};
}
if (!contract.allowedModes.has(value.mode)) {
return {
valid: false,
reason: 'invalid-mode'
};
}
const sourceId = value.sourceId;
if (
sourceId !== undefined
&& (
typeof sourceId !== 'string'
|| sourceId.length === 0
)
) {
return {
valid: false,
reason: 'invalid-source-id'
};
}
const fallbackPolicy =
value.fallbackPolicy ?? 'forbid';
if (
fallbackPolicy !== 'forbid'
&& fallbackPolicy !==
'nearest-visible-source'
) {
return {
valid: false,
reason: 'invalid-fallback-policy'
};
}
return {
valid: true,
reason: 'valid',
mode: value.mode,
sourceId: sourceId ?? null,
fallbackPolicy
};
}
Missing fallback configuration becomes forbid. That is a deliberate fail-closed default.
Validate type and room binding
function isSourceObject(object) {
return Boolean(
object
&& object.pos
&& typeof object.energy === 'number'
&& typeof object.energyCapacity === 'number'
);
}
function resolveBoundSource(flag, sourceId) {
const expectedRoomName = flag.pos.roomName;
const expectedRoomVisible = Boolean(
Game.rooms[expectedRoomName]
);
const object = Game.getObjectById(sourceId);
if (!object) {
return {
status: expectedRoomVisible
? 'target-missing-visible-room'
: 'target-unresolved-no-vision',
source: null,
expectedRoomName
};
}
if (!isSourceObject(object)) {
return {
status: 'target-wrong-type',
source: null
};
}
if (object.pos.roomName !== flag.pos.roomName) {
return {
status: 'target-room-mismatch',
source: null,
actualRoomName: object.pos.roomName
};
}
return {
status: 'configured-target-ready',
source: object
};
}
A globally valid object ID is not automatically valid configuration for this Flag. The room check prevents a stale copy-and-paste from binding the mission to another visible room.
Make fallback a reviewed mode, not an accident
function selectExplicitFallback(flag, config) {
if (config.sourceId !== null) {
return {
status: 'configured-target-required',
source: null
};
}
if (
config.fallbackPolicy !==
'nearest-visible-source'
) {
return {
status: 'fallback-forbidden',
source: null
};
}
if (!flag.room) {
return {
status: 'fallback-room-not-visible',
source: null
};
}
const source = flag.room
.find(FIND_SOURCES)
.map(candidate => ({
candidate,
range: flag.pos.getRangeTo(candidate)
}))
.sort((left, right) =>
left.range - right.range
|| left.candidate.id.localeCompare(
right.candidate.id
)
)[0]?.candidate ?? null;
return {
status: source
? 'explicit-fallback-ready'
: 'fallback-source-not-found',
source
};
}
Once a sourceId is configured, a missing, wrong-type, or wrong-room target blocks the mission. The fallback does not hide drift in reviewed identity.
Return one explainable configuration state
function readRemoteMineConfiguration() {
const contract = FLAG_CONTRACTS.remoteMine;
const flag = Game.flags[contract.flagName] ?? null;
const config = readRemoteMineMemory(
flag,
contract
);
if (!config.valid) {
return {
ready: false,
reason: config.reason,
flagName: contract.flagName
};
}
if (config.mode === 'pause') {
return {
ready: false,
reason: 'mission-paused',
flagName: flag.name,
roomName: flag.pos.roomName
};
}
const resolution = config.sourceId
? resolveBoundSource(flag, config.sourceId)
: selectExplicitFallback(flag, config);
if (!resolution.source) {
return {
ready: false,
reason: resolution.status,
flagName: flag.name,
roomName: flag.pos.roomName,
configuredSourceId: config.sourceId,
actualRoomName:
resolution.actualRoomName ?? null
};
}
return {
ready: true,
reason: resolution.status,
flagName: flag.name,
roomName: flag.pos.roomName,
mode: config.mode,
sourceId: resolution.source.id,
sourceSelection: config.sourceId
? 'configured-id'
: 'explicit-fallback'
};
}
The caller receives a reason it can log, visualize, or use to pause the mission. The reader does not create, move, recolor, remove, or recreate the Flag. The Flag API does not provide a rename operation.
Write diagnostics only when status changes
function recordConfigurationChange(
key,
status
) {
Memory.configurationStatus ??= {};
const previous =
Memory.configurationStatus[key] ?? null;
const comparable = {
ready: status.ready,
reason: status.reason,
flagName: status.flagName,
roomName: status.roomName ?? null,
sourceId: status.sourceId ?? null,
configuredSourceId:
status.configuredSourceId ?? null,
actualRoomName:
status.actualRoomName ?? null
};
const signature = JSON.stringify(comparable);
if (previous?.signature === signature) {
return {
changed: false,
status: previous
};
}
const next = {
...comparable,
signature,
changedAt: Game.time
};
Memory.configurationStatus[key] = next;
return {
changed: true,
status: next
};
}
This avoids rewriting the same persistent diagnostic every tick. It does not prove the configuration was tested by live mission behavior.
Distinguish missing vision from invalid identity
| Status | Meaning | Automatic fallback? |
|---|---|---|
target-unresolved-no-vision | The object cannot be resolved and the Flag room is not visible. | No |
target-missing-visible-room | The expected room is visible but the ID does not resolve. | No |
target-wrong-type | The ID resolves to another object kind. | No |
target-room-mismatch | The object exists in a different room. | No |
explicit-fallback-ready | No ID was configured and reviewed fallback is enabled. | Already explicit |
Treat the Flag name as immutable identity
A Flag's name is chosen when the Flag is created and cannot be changed later. If an operator needs a different configuration key, that is a remove-and-create operation, not a rename. Treat the replacement as a new identity boundary: re-check the new Flag's Memory, room, and configured target instead of assuming the old contract moved with the name.
This reader never removes and recreates a Flag to simulate renaming. A configuration migration should be explicit, observable, and separate from normal per-tick reads.
Keep the reader side-effect boundary narrow
The reader may write a separate diagnostic record. It does not alter flag.memory.sourceId after a fallback and does not move the Flag. Review the reason, then change configuration deliberately.
Keep target recovery and Flag policy separate
Use the saved-target guide for a general object-resolution state machine. Use the visibility guide when Game.rooms[roomName] is missing. This page owns the extra policy that a configured target must agree with the named Flag.