MEMORY · PUBLIC FOREIGN SEGMENTS

Screeps RawMemory Foreign Segment: Publish and Read Public Segments Safely

Verification statusOfficial API: Checked — foreignSegment, setActiveForeignSegment(), setPublicSegments(), setDefaultPublicSegment(), and local segments · Timing and access model: Checked — next-tick availability, one foreign segment at a time, public-only access, and replacement configuration · JavaScript syntax: Passed by the article simulation gate

VERIFICATION

Evidence and test status

Official API
Checked — foreignSegment, setActiveForeignSegment(), setPublicSegments(), setDefaultPublicSegment(), and local segments
Timing and access model
Checked — next-tick availability, one foreign segment at a time, public-only access, and replacement configuration
JavaScript syntax
Passed by the article simulation gate
Offline protocol cases
Passed — IDs, public lists, defaults, matching, UTF-8 sizing, parsing, stream identity, regression, staleness, and rotation
Screeps Console test
Pending
Official-server foreign publication test
Pending
Evidence level
Official documentation review, repository integration, syntax checks, and deterministic offline simulation only

Quick answer

RawMemory.foreignSegment reads data that another player deliberately exposed on the current shard. It is not a way to inspect private Memory, and it is not the same system as InterShardMemory. The owner writes one of their local segments, marks selected IDs public, and may choose one default public ID. A reader requests one username and optional ID; the matching object becomes available on the next tick.

A reliable implementation keeps publication and subscription in separate coordinators. It validates IDs from 0 through 99, remembers the previous-tick request, accepts only a matching username and explicit ID, treats default-ID changes as a new stream, parses the returned string as untrusted data, and rotates multiple subscriptions because only one foreign segment can be active at a time.

Separate local, cross-shard, and foreign data

RawMemory.segments stores your own on-demand data on one shard. InterShardMemory exchanges your own shard-owned strings across shards. RawMemory.foreignSegment reads another player's public segment on the current shard. The foreign API never grants remote write access.

Activate the publisher's local segment first

Public visibility does not activate the owner's local segment for reading or writing. Request the local ID on tick N, then inspect and write it on tick N+1.

function requestPublisherSegment(segmentId) {
  if (
    !Number.isInteger(segmentId)
    || segmentId < 0
    || segmentId > 99
  ) {
    return {
      status: 'invalid-segment-id'
    };
  }

  RawMemory.setActiveSegments([segmentId]);

  return {
    status: 'activation-requested',
    segmentId,
    requestedAt: Game.time
  };
}

Publish a versioned envelope

A compact envelope gives readers stable identity and migration boundaries. Keep the public payload bounded and avoid serializing live game objects.

function createPublicEnvelope({
  publisher,
  segmentId,
  publisherEpoch,
  revision,
  updatedAt,
  payload
}) {
  return {
    schemaVersion: 1,
    publisher,
    segmentId,
    publisherEpoch,
    revision,
    updatedAt,
    payload
  };
}

Budget UTF-8 bytes, not JavaScript length

The official RawMemory documentation limits each segment to 100 KB, but the reviewed API text does not define the server-side byte-accounting algorithm. This guide uses UTF-8 counting only for its conservative 96 KiB project budget; 96 KiB is not presented as another official Screeps limit.

function utf8ByteLength(value) {
  const text = String(value);
  let bytes = 0;

  for (let index = 0; index < text.length; index += 1) {
    const code = text.charCodeAt(index);

    if (code < 0x80) {
      bytes += 1;
    } else if (code < 0x800) {
      bytes += 2;
    } else if (
      code >= 0xD800
      && code <= 0xDBFF
      && index + 1 < text.length
      && text.charCodeAt(index + 1) >= 0xDC00
      && text.charCodeAt(index + 1) <= 0xDFFF
    ) {
      bytes += 4;
      index += 1;
    } else {
      bytes += 3;
    }
  }

  return bytes;
}

Stage one complete local write

Only write after the segment appears in RawMemory.segments. The status below means the local assignment was staged; it does not claim that any reader already observed the revision.

const PUBLIC_SEGMENT_SAFE_LIMIT = 96 * 1024;

function writePublicSegment(
  segmentId,
  envelope
) {
  if (!Object.prototype.hasOwnProperty.call(
    RawMemory.segments,
    segmentId
  )) {
    return {
      status: 'segment-not-active',
      segmentId
    };
  }

  const serialized = JSON.stringify(envelope);
  const byteLength = utf8ByteLength(serialized);

  if (byteLength > PUBLIC_SEGMENT_SAFE_LIMIT) {
    return {
      status: 'payload-too-large',
      segmentId,
      byteLength
    };
  }

  RawMemory.segments[segmentId] = serialized;

  return {
    status: 'local-segment-write-staged',
    segmentId,
    byteLength,
    revision: envelope.revision
  };
}

Normalize the public ID list

Later setPublicSegments() calls replace earlier lists. The official documentation requires IDs from 0 through 99 but does not list duplicate IDs as a separate API error, so this coordinator validates every ID, deduplicates, sorts, and submits one complete desired list.

function normalizePublicSegmentIds(ids) {
  if (!Array.isArray(ids)) {
    return null;
  }

  if (!ids.every(id =>
    Number.isInteger(id)
    && id >= 0
    && id <= 99
  )) {
    return null;
  }

  return [...new Set(ids)]
    .sort((left, right) => left - right);
}

Submit one coherent public policy

This guide adds a project policy: when defaultId is non-null, require it to appear in the same desired public list. The official API documents setPublicSegments() and setDefaultPublicSegment() separately; this membership check is a local configuration invariant, not an extra Screeps return-code rule. Use an empty array to expose no segments and null to remove the default.

function applyPublicSegmentPolicy({
  publicIds,
  defaultId
}) {
  const normalized =
    normalizePublicSegmentIds(publicIds);

  if (!normalized) {
    return {
      status: 'invalid-public-list'
    };
  }

  if (
    defaultId !== null
    && (
      !Number.isInteger(defaultId)
      || !normalized.includes(defaultId)
    )
  ) {
    return {
      status: 'default-not-public'
    };
  }

  RawMemory.setPublicSegments(normalized);
  RawMemory.setDefaultPublicSegment(defaultId);

  return {
    status: 'public-policy-submitted',
    publicIds: normalized,
    defaultId
  };
}

Normalize an explicit or default request

Omitting the ID asks for the publisher's default public segment. Do not use a second null argument; clearing is performed by passing null as the username.

function normalizeForeignRequest(input) {
  if (
    !input
    || typeof input.username !== 'string'
    || input.username.trim() === ''
  ) {
    return {
      status: 'invalid-username'
    };
  }

  if (
    input.id !== undefined
    && (
      !Number.isInteger(input.id)
      || input.id < 0
      || input.id > 99
    )
  ) {
    return {
      status: 'invalid-segment-id'
    };
  }

  return {
    status: 'valid',
    request: {
      username: input.username.trim(),
      id: input.id,
      mode: input.id === undefined
        ? 'default'
        : 'explicit'
    }
  };
}

Request one foreign segment for the next tick

The call schedules the next-tick view. Preserve the request identity in Memory so the next tick can match the returned object.

function submitForeignRequest(request) {
  if (request.mode === 'default') {
    RawMemory.setActiveForeignSegment(
      request.username
    );
  } else {
    RawMemory.setActiveForeignSegment(
      request.username,
      request.id
    );
  }

  return {
    status: 'foreign-request-submitted',
    username: request.username,
    requestedId: request.id ?? null,
    mode: request.mode,
    requestedAt: Game.time
  };
}

Clear the next foreign request

Clear the reader when no subscription should remain active.

function clearForeignRequest() {
  RawMemory.setActiveForeignSegment(null);

  return {
    status: 'foreign-request-cleared',
    clearedAt: Game.time
  };
}

Match the previous-tick response

An explicit request must match both username and ID. A default request matches the username and records the actual returned ID.

function matchForeignSegment(
  pending,
  foreignSegment
) {
  if (!pending) {
    return {
      status: 'no-pending-request'
    };
  }

  if (
    !foreignSegment
    || typeof foreignSegment !== 'object'
  ) {
    return {
      status: 'foreign-segment-unavailable'
    };
  }

  if (
    foreignSegment.username
      !== pending.username
  ) {
    return {
      status: 'username-mismatch'
    };
  }

  if (
    pending.mode === 'explicit'
    && foreignSegment.id !== pending.id
  ) {
    return {
      status: 'segment-id-mismatch'
    };
  }

  return {
    status: 'foreign-segment-matched',
    username: foreignSegment.username,
    segmentId: foreignSegment.id,
    data: foreignSegment.data,
    mode: pending.mode
  };
}

Parse public strings as untrusted input

Validate JSON shape, schema, publisher identity, segment identity, epoch, and revision. Never execute foreign data with eval, Function, or dynamic module loading.

function parsePublicEnvelope(
  raw,
  expectedPublisher,
  expectedSegmentId
) {
  if (typeof raw !== 'string') {
    return {
      status: 'invalid-data-type',
      envelope: null
    };
  }

  let value;

  try {
    value = JSON.parse(raw);
  } catch {
    return {
      status: 'invalid-json',
      envelope: null
    };
  }

  if (
    !value
    || typeof value !== 'object'
    || Array.isArray(value)
  ) {
    return {
      status: 'invalid-envelope',
      envelope: null
    };
  }

  if (value.schemaVersion !== 1) {
    return {
      status: 'unsupported-schema',
      envelope: null
    };
  }

  if (value.publisher !== expectedPublisher) {
    return {
      status: 'publisher-mismatch',
      envelope: null
    };
  }

  if (value.segmentId !== expectedSegmentId) {
    return {
      status: 'envelope-segment-mismatch',
      envelope: null
    };
  }

  if (
    typeof value.publisherEpoch !== 'string'
    || value.publisherEpoch === ''
    || !Number.isInteger(value.revision)
    || value.revision < 0
  ) {
    return {
      status: 'invalid-version-fields',
      envelope: null
    };
  }

  return {
    status: 'valid',
    envelope: value
  };
}

Use a local observation window

A writer epoch or observed default-segment ID change starts a new stream. Within one stream, a lower revision is a regression. Staleness means the local reader has not seen progress; it does not prove the publisher stopped.

function observePublicStream({
  previous,
  envelope,
  observedSegmentId,
  now,
  maxSilentTicks = 100
}) {
  const streamChanged =
    !previous
    || previous.publisherEpoch
      !== envelope.publisherEpoch
    || previous.segmentId
      !== observedSegmentId;

  if (
    !streamChanged
    && envelope.revision
      < previous.revision
  ) {
    return {
      status: 'revision-regressed',
      state: previous
    };
  }

  const advanced =
    streamChanged
    || envelope.revision
      > previous.revision;
  const next = {
    publisherEpoch: envelope.publisherEpoch,
    segmentId: observedSegmentId,
    revision: envelope.revision,
    lastCheckedAt: now,
    lastAdvancedAt: advanced
      ? now
      : previous.lastAdvancedAt
  };

  if (
    !advanced
    && now - next.lastAdvancedAt
      > maxSilentTicks
  ) {
    return {
      status: 'public-stream-stale',
      state: next,
      silentTicks:
        now - next.lastAdvancedAt
    };
  }

  return {
    status: streamChanged
      ? 'public-stream-started'
      : advanced
        ? 'public-stream-advanced'
        : 'public-stream-unchanged',
    state: next,
    silentTicks:
      now - next.lastAdvancedAt
  };
}

Rotate multiple subscriptions

Only one foreign segment can be active at once. Round-robin selection makes the polling cadence explicit.

function rotateSubscriptionQueue(
  subscriptions,
  cursor
) {
  if (
    !Array.isArray(subscriptions)
    || subscriptions.length === 0
  ) {
    return {
      status: 'no-subscriptions',
      nextCursor: 0,
      subscription: null
    };
  }

  const safeCursor =
    Number.isInteger(cursor)
      ? Math.max(0, cursor)
      : 0;
  const index =
    safeCursor % subscriptions.length;

  return {
    status: 'subscription-selected',
    nextCursor:
      (index + 1) % subscriptions.length,
    subscription: subscriptions[index]
  };
}

Consume the old response before scheduling the next request

One coordinator owns the API. It first handles the object requested on the prior tick, then submits exactly one next request.

function finalizeForeignSegmentTick(
  subscriptions
) {
  Memory.foreignSegmentReader ??= {
    cursor: 0,
    pending: null,
    observations: {}
  };

  const state = Memory.foreignSegmentReader;
  const previousMatch = matchForeignSegment(
    state.pending,
    RawMemory.foreignSegment
  );
  const selected = rotateSubscriptionQueue(
    subscriptions,
    state.cursor
  );

  if (!selected.subscription) {
    clearForeignRequest();
    state.pending = null;

    return {
      previousMatch,
      nextRequest: {
        status: 'no-subscriptions'
      }
    };
  }

  const normalized = normalizeForeignRequest(
    selected.subscription
  );

  if (normalized.status !== 'valid') {
    clearForeignRequest();
    state.pending = null;
    state.cursor = selected.nextCursor;

    return {
      previousMatch,
      nextRequest: normalized
    };
  }

  const submitted = submitForeignRequest(
    normalized.request
  );

  state.cursor = selected.nextCursor;
  state.pending = {
    ...normalized.request,
    requestedAt: Game.time
  };

  return {
    previousMatch,
    nextRequest: submitted
  };
}

Common failure modes

  • Requesting several publishers in one tick: only the final one can be active for the next tick.
  • Reading immediately after the request: the data belongs to the previous activation state.
  • Scattered public-list calls: a later module silently replaces an earlier list.
  • Treating setPublicSegments as append: the method submits the complete replacement list.
  • Project-policy mismatch: this guide requires a non-null default ID to appear in the same desired public list.
  • Default request passes a null ID: the wrapper no longer represents the documented omitted-ID call.
  • No username or ID match: stale or unrelated data can be attributed to the wrong subscription.
  • Default ID changes but revision baseline remains: two different public streams are conflated.
  • Foreign JSON is trusted: malformed or hostile public data reaches business logic.
  • Secrets are published: public segments are intentionally readable by other players.

Evidence and production boundary

The repository simulation syntax-checks every Chinese and English JavaScript example and runs deterministic cases for ID validation, public-list replacement, project-policy default membership, next-tick request identity, explicit and default matching, UTF-8 sizing, envelope parsing, stream restarts, revision regression, stale observation, invalid-subscription cursor progress, queue rotation, and single-request coordination.

Screeps Console execution, official-server publication delay, live changes to another player's default segment, private-server differences, long-running CPU cost, and the truthfulness of third-party payloads remain Pending. The guide therefore distinguishes activation-requested, local-segment-write-staged, foreign-request-submitted, foreign-segment-matched, and later observed stream states.

Official references: RawMemory.foreignSegment, setActiveForeignSegment(), setPublicSegments(), setDefaultPublicSegment(), and RawMemory.segments.

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.