SPAWNING · COLLAPSE RECOVERY STATE

Recover a Room with No Harvesters Without Spawning Duplicates

Verification statusExisting English route: Preserved · Official docs: Checked — spawnCreep(), Spawning state, room Energy, body parts, and respawn boundary · Static API review: Passed — current-tick acceptance, exact Spawn/name identity, later-tick creation states

VERIFICATION

Evidence and test status

Existing English route
Preserved
Official docs
Checked — spawnCreep(), Spawning state, room Energy, body parts, and respawn boundary
Static API review
Passed — current-tick acceptance, exact Spawn/name identity, later-tick creation states
Offline decision review
Passed — capable, spawning, accepted, below-minimum, rejected, missing, and overdue states
Human editorial pass
Passed
Screeps Console test
Pending
Live multi-tick verification
Pending
Genuine room or Console screenshots
Pending
Last verified
August 3, 2026

Use this guide when

Use this recovery branch when an owned visible room has no live general harvester and no matching recovery Creep is already being spawned. The example assumes a general harvester needs active WORK, CARRY, and MOVE. A fixed miner-and-hauler economy should define a different minimum contract instead of copying that assumption.

Choose the dynamic body guide when the room still has working Energy income and can wait for a normal target body. Choose the spawn return-code guide when one specific request is already failing.

Define what counts as recovered

A role label alone is not enough. A Creep whose active harvesting or delivery parts have been destroyed may still be present in Game.creeps but unable to restore the room's Energy loop.

const RECOVERY_ROLE = 'harvester';
const EMERGENCY_BODY = [WORK, CARRY, MOVE];

function isCapableGeneralHarvester(creep, roomName) {
  return Boolean(
    creep
    && creep.my === true
    && creep.spawning !== true
    && creep.memory?.role === RECOVERY_ROLE
    && creep.memory?.homeRoom === roomName
    && creep.getActiveBodyparts(WORK) > 0
    && creep.getActiveBodyparts(CARRY) > 0
    && creep.getActiveBodyparts(MOVE) > 0
  );
}

This contract is project policy. It is intentionally explicit so that a damaged or differently assigned Creep does not silently block emergency recovery.

Use four recovery states

StateMeaningNext action
capable-harvester-existsThe room already has a live Creep that satisfies the recovery contract.Run normal spawning.
recovery-spawningThe exact accepted recovery name is still present in spawn.spawning.Do not submit another recovery request.
recovery-creep-readyThe accepted name now exists in Game.creeps and satisfies the contract.Clear the pending record and resume normal policy.
recovery-neededNo capable live or in-production recovery Creep exists.Choose one Spawn and submit one minimum request.

Verify an accepted request before planning another

Save the exact Spawn ID and requested Creep name only after the real spawnCreep() call returns OK. On later ticks, recover current objects rather than retaining live references.

function verifyEmergencyRecovery(room) {
  const pending = room?.memory?.emergencyRecovery;

  if (!pending) {
    return { status: 'no-pending-recovery' };
  }

  if (Game.time <= pending.submittedAt) {
    return {
      status: 'accepted-this-tick',
      creepName: pending.creepName
    };
  }

  const creep = Game.creeps[pending.creepName];

  if (isCapableGeneralHarvester(creep, room.name)) {
    delete room.memory.emergencyRecovery;
    return {
      status: 'recovery-creep-ready',
      creepName: creep.name,
      creepId: creep.id
    };
  }

  const spawn = Game.getObjectById(pending.spawnId);

  if (spawn?.spawning?.name === pending.creepName) {
    return {
      status: 'recovery-spawning',
      creepName: pending.creepName,
      remainingTime: spawn.spawning.remainingTime,
      needTime: spawn.spawning.needTime
    };
  }

  const latestExpectedTick =
    pending.submittedAt
    + pending.bodyLength * CREEP_SPAWN_TIME
    + 1;

  return {
    status: Game.time > latestExpectedTick
      ? 'recovery-overdue'
      : 'accepted-request-not-observed',
    creepName: pending.creepName,
    submittedAt: pending.submittedAt,
    latestExpectedTick
  };
}

bodyLength * CREEP_SPAWN_TIME is a base planning boundary. The current spawn.spawning.needTime and remainingTime are better live observations once the Spawn exposes them. A missing match must remain a failure state; do not erase it by submitting a second request immediately.

Submit one minimum recovery request

function getBodyCost(body) {
  return body.reduce((total, part) => {
    const cost = BODYPART_COST[part];

    if (!Number.isFinite(cost)) {
      throw new TypeError(
        'Unknown body part: ' + String(part)
      );
    }

    return total + cost;
  }, 0);
}

function selectEmergencySpawn(room) {
  return room.find(FIND_MY_SPAWNS)
    .filter(spawn =>
      spawn.my === true
      && spawn.isActive()
      && !spawn.spawning
    )
    .sort((left, right) =>
      left.name.localeCompare(right.name)
    )[0] ?? null;
}

function createEmergencyName(room, spawn) {
  return [
    'EmergencyHarvester',
    room.name,
    spawn.name,
    Game.time
  ].join('-');
}

function submitEmergencyRecovery(room) {
  if (!room?.controller?.my) {
    return { status: 'owned-room-unavailable' };
  }

  const capable = Object.values(Game.creeps)
    .filter(creep =>
      isCapableGeneralHarvester(creep, room.name)
    );

  if (capable.length > 0) {
    return {
      status: 'capable-harvester-exists',
      count: capable.length
    };
  }

  const pendingState = verifyEmergencyRecovery(room);

  if (
    pendingState.status !== 'no-pending-recovery'
    && pendingState.status !== 'recovery-overdue'
  ) {
    return pendingState;
  }

  const spawn = selectEmergencySpawn(room);

  if (!spawn) {
    return { status: 'spawn-unavailable' };
  }

  const minimumCost = getBodyCost(EMERGENCY_BODY);

  if (room.energyAvailable < minimumCost) {
    return {
      status: 'energy-below-minimum',
      energyAvailable: room.energyAvailable,
      minimumCost
    };
  }

  const creepName = createEmergencyName(room, spawn);
  const memory = {
    role: RECOVERY_ROLE,
    homeRoom: room.name,
    emergencyRecovery: true,
    memoryVersion: 1
  };

  const dryRunResult = spawn.spawnCreep(
    EMERGENCY_BODY,
    creepName,
    {
      memory,
      dryRun: true
    }
  );

  if (dryRunResult !== OK) {
    return {
      status: 'dry-run-rejected',
      spawnId: spawn.id,
      creepName,
      dryRunResult
    };
  }

  const result = spawn.spawnCreep(
    EMERGENCY_BODY,
    creepName,
    { memory }
  );

  if (result !== OK) {
    return {
      status: 'spawn-rejected-after-dry-run',
      spawnId: spawn.id,
      creepName,
      dryRunResult,
      result
    };
  }

  room.memory.emergencyRecovery = {
    spawnId: spawn.id,
    creepName,
    submittedAt: Game.time,
    bodyLength: EMERGENCY_BODY.length
  };

  return {
    status: 'emergency-spawn-accepted',
    spawnId: spawn.id,
    creepName,
    submittedAt: Game.time,
    minimumCost,
    dryRunResult,
    result
  };
}

The pending record is written after the final return code, not after the dry run. A dry run checks current conditions but does not reserve the Spawn, the name, or the room's Energy.

Put recovery ahead of normal Spawn policy

module.exports.loop = function () {
  const room = Game.rooms.W1N1;

  if (!room) {
    return;
  }

  const outcome = submitEmergencyRecovery(room);

  if (
    outcome.status === 'emergency-spawn-accepted'
    || outcome.status === 'recovery-spawning'
    || outcome.status === 'accepted-this-tick'
    || outcome.status === 'accepted-request-not-observed'
  ) {
    return;
  }

  if (
    outcome.status === 'dry-run-rejected'
    || outcome.status === 'spawn-rejected-after-dry-run'
    || outcome.status === 'recovery-overdue'
  ) {
    console.log(JSON.stringify({
      type: 'emergency-recovery',
      roomName: room.name,
      ...outcome
    }));
    return;
  }

  // Continue ordinary quotas only when recovery
  // neither used nor owns a Spawn request.
};

Normal role quotas should not overwrite the diagnostic state of an accepted emergency request. Centralize Spawn submission if more than one module can reach this branch.

When the room is below the minimum cost

The current standard constants make [WORK, CARRY, MOVE] cost 200 Energy. An established room with no capable workforce and less than the configured minimum cannot create that body from nothing. Recovery may require a surviving hauler, a different valid role contract, Energy support from another room, manual reassignment, or a respawn decision.

The initial-Spawn safety refill is a special respawn protection. Do not generalize it to every established or rebuilt Spawn.

Interpret the current-tick result correctly

ResultMeaning hereWhat to inspect next
OKThe Spawn accepted this request.The exact name in spawn.spawning and later Game.creeps.
ERR_BUSYAnother request owns the Spawn.Your centralized Spawn coordinator.
ERR_NAME_EXISTSThe requested name already exists.The pending record and naming policy.
ERR_NOT_ENOUGH_ENERGYCurrent Spawn-and-Extension Energy changed or was insufficient.room.energyAvailable and competing calls.
ERR_INVALID_ARGSThe body, name, or options are malformed.The captured request snapshot.
ERR_RCL_NOT_ENOUGHThe selected Spawn is inactive at this Controller level.spawn.isActive() and ownership.

Production adaptation notes

A mature colony should count capability groups rather than one role string, include remote miners and haulers only when they can actually restore this room, and let one Spawn scheduler own all final calls. Keep pending records bounded, retain a terminal failure long enough to diagnose it, and do not report recovery complete until the new Creep exists and satisfies the declared contract.

Verification status and evidence boundary

The code and API boundaries were checked statically and the pure decision branches were reviewed offline. No real room collapse, Spawn contention, powered spawning, Console output, or live multi-tick recovery sequence was available. Those checks remain pending.

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.