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.