RESOURCES · POWER PROCESSING EVIDENCE

Verify Power Processing Without Inventing an Event

CURATED PATH

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

Market / Industry

Recommended next steps

Protect the room Energy reserve firstResource budgetSupply Power and Energy across RoomsLogistics pathUse the market when an immediate supply is neededProcurement path
Verification statusChinese source article: Reviewed in full · Official engine: Checked — processPower consumes local Power and Energy but does not emit a Room event · Attribution boundary: Exact local resource signatures are supporting evidence; account GPL can be changed by another Power Spawn

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official engine
Checked — processPower consumes local Power and Energy but does not emit a Room event
Attribution boundary
Exact local resource signatures are supporting evidence; account GPL can be changed by another Power Spawn
Static code review
Passed — exact structure ID, same-tick guard, reserve check, pending signature and transfer-confound detection
JavaScript syntax
Passed
Screeps Console test
Pending
Live multi-tick verification
Pending
Genuine room or Console screenshots
Pending
Last verified
August 3, 2026

Start with the missing event

StructurePowerSpawn.processPower() does not currently create a Room event in the official engine. Do not invent an EVENT_PROCESS_POWER, and do not claim that an account-level GPL increase uniquely identifies one Power Spawn.

The strongest practical workflow available from normal game objects is:

  • one dispatcher owns all Power Spawn processing calls;
  • each exact Power Spawn can be submitted at most once per tick;
  • the dispatcher stores the planned Power and Energy signature only after OK;
  • the next tick checks the same structure and labels conflicting transfers.

Calculate the exact local plan

function getProcessPowerPlan(powerSpawn) {
  const effect = (powerSpawn.effects || []).find(item =>
    item.effect === PWR_OPERATE_POWER
  );

  let powerAmount = 1;
  if (effect && Number.isInteger(effect.level)) {
    const values = POWER_INFO[PWR_OPERATE_POWER]?.effect;
    const extra = Array.isArray(values)
      ? values[effect.level - 1]
      : null;

    if (Number.isFinite(extra)) {
      powerAmount += extra;
    }
  }

  powerAmount = Math.min(
    powerAmount,
    powerSpawn.store.getUsedCapacity(RESOURCE_POWER)
  );

  return {
    powerAmount,
    energyAmount:
      powerAmount * POWER_SPAWN_ENERGY_RATIO,
    effectLevel: effect?.level ?? null
  };
}

Snapshot the plan before submission. An effect may expire later, but the pending record must describe what the dispatcher expected for the submitted tick.

Protect the room budget

function getBudgetedRoomEnergy(room, powerSpawn) {
  const storage = room.storage?.store.getUsedCapacity(
    RESOURCE_ENERGY
  ) || 0;
  const terminal = room.terminal?.store.getUsedCapacity(
    RESOURCE_ENERGY
  ) || 0;
  const local = powerSpawn.store.getUsedCapacity(
    RESOURCE_ENERGY
  );

  return storage + terminal + local;
}

function canAffordPowerProcessing(
  room,
  powerSpawn,
  plan,
  reserve
) {
  if (
    !Number.isFinite(reserve)
    || reserve < 0
    || plan.powerAmount <= 0
  ) {
    return false;
  }

  return powerSpawn.store.getUsedCapacity(
    RESOURCE_POWER
  ) >= plan.powerAmount
    && powerSpawn.store.getUsedCapacity(
      RESOURCE_ENERGY
    ) >= plan.energyAmount
    && getBudgetedRoomEnergy(room, powerSpawn)
      - plan.energyAmount
      >= reserve;
}

The reserve is a room policy, not an official constant. Keep spawn recovery, active defense and Terminal obligations outside this simple stock total when your economy requires stricter commitments.

Dispatch once per structure and tick

function createPowerProcessingDispatcher() {
  const submittedIds = new Set();

  return function submit(config) {
    if (!config || config.enabled !== true) {
      return { status: 'disabled' };
    }

    const powerSpawn = Game.getObjectById(
      config.powerSpawnId
    );
    if (
      !powerSpawn
      || powerSpawn.structureType
        !== STRUCTURE_POWER_SPAWN
      || powerSpawn.my !== true
      || powerSpawn.isActive() !== true
    ) {
      return { status: 'power-spawn-unavailable' };
    }

    if (submittedIds.has(powerSpawn.id)) {
      return { status: 'already-submitted-this-tick' };
    }
    submittedIds.add(powerSpawn.id);

    const plan = getProcessPowerPlan(powerSpawn);
    if (
      !canAffordPowerProcessing(
        powerSpawn.room,
        powerSpawn,
        plan,
        config.energyReserve
      )
    ) {
      return { status: 'budget-or-resource-blocked', plan };
    }

    const before = {
      power: powerSpawn.store.getUsedCapacity(
        RESOURCE_POWER
      ),
      energy: powerSpawn.store.getUsedCapacity(
        RESOURCE_ENERGY
      ),
      gpl: Game.gpl.progress
    };
    const result = powerSpawn.processPower();

    if (result !== OK) {
      return {
        status: 'processing-rejected',
        result,
        powerSpawnId: powerSpawn.id,
        plan
      };
    }

    Memory.pendingPowerProcessing ??= {};
    Memory.pendingPowerProcessing[powerSpawn.id] = {
      submittedAt: Game.time,
      roomName: powerSpawn.room.name,
      powerSpawnId: powerSpawn.id,
      powerAmount: plan.powerAmount,
      energyAmount: plan.energyAmount,
      effectLevel: plan.effectLevel,
      before
    };

    return {
      status: 'processing-accepted',
      result,
      powerSpawnId: powerSpawn.id,
      plan
    };
  };
}

The in-memory Set protects one script execution. A production empire should instantiate the dispatcher once near the top of the main loop and route every module through it.

Verify the next-tick resource signature

function verifyPowerProcessing(pending) {
  if (!pending) return { status: 'no-pending-process' };
  if (Game.time <= pending.submittedAt) {
    return { status: 'wait-for-next-tick' };
  }
  if (Game.time > pending.submittedAt + 1) {
    return { status: 'process-window-missed' };
  }

  const powerSpawn = Game.getObjectById(
    pending.powerSpawnId
  );
  if (!powerSpawn) {
    return { status: 'power-spawn-missing' };
  }

  const powerNow = powerSpawn.store.getUsedCapacity(
    RESOURCE_POWER
  );
  const energyNow = powerSpawn.store.getUsedCapacity(
    RESOURCE_ENERGY
  );

  return {
    status:
      powerNow === pending.before.power
        - pending.powerAmount
      && energyNow === pending.before.energy
        - pending.energyAmount
        ? 'local-signature-matches'
        : 'local-signature-does-not-match',
    powerBefore: pending.before.power,
    powerNow,
    expectedPowerSpent: pending.powerAmount,
    energyBefore: pending.before.energy,
    energyNow,
    expectedEnergySpent: pending.energyAmount,
    gplBefore: pending.before.gpl,
    gplNow: Game.gpl.progress
  };
}

A matching signature is supporting evidence for this exact structure. It is not the same as an event record, because other operations can change the Power Spawn Store during the same processed tick.

Detect transfer confounds

function findPowerSpawnTransferConfounds(pending) {
  const room = Game.rooms[pending.roomName];
  if (!room) return { status: 'room-not-visible' };

  const events = room.getEventLog().filter(event =>
    event.event === EVENT_TRANSFER
    && (
      event.objectId === pending.powerSpawnId
      || event.data?.targetId === pending.powerSpawnId
    )
    && (
      event.data?.resourceType === RESOURCE_ENERGY
      || event.data?.resourceType === RESOURCE_POWER
    )
  );

  return events.length === 0
    ? { status: 'no-transfer-confound-observed' }
    : {
        status: 'transfer-confounded',
        count: events.length
      };
}

Run this alongside the local signature check. A transfer-confounded result is not a failure of processPower(); it means the net Store deltas cannot isolate the process call. The cleanest production design prevents Power or Energy delivery to that structure during the sampled verification tick.

Use GPL only as corroboration

Game.gpl.progress is account-wide. Another owned Power Spawn can increase it during the same tick, so GPL cannot identify this structure. Report GPL as corroborating context and use the exact Power Spawn Store signature as the local observation. When the signature does not match or transfers are present, keep the result inconclusive rather than manufacturing certainty.

Production integration boundary

Verify the previous pending sample before submitting another call for the same Power Spawn. Coordinate logistics so a deliberately sampled tick is not confounded, and keep Power delivery, effect scheduling and reserve policy in separate modules. Console execution, real multi-Power-Spawn behavior, CPU cost and screenshots remain pending.

Official documentation

Review the official Screeps API for StructurePowerSpawn.processPower(), POWER_SPAWN_ENERGY_RATIO, PWR_OPERATE_POWER, POWER_INFO, Game.gpl, Store and Room events.

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.