RESOURCES · MINERAL HARVEST EVENT IDENTITY

Verify Mineral Harvesting Without Trusting Store Deltas Alone

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

Feed harvested minerals into Lab reactionsDownstream productionFeed resources into Factory productionDownstream productionMove minerals across Rooms with a TerminalLogistics path
Verification statusChinese source article: Reviewed in full · Official engine: Checked — Mineral harvest emits EVENT_HARVEST with Miner objectId and Mineral targetId · Amount boundary: The engine event can report body harvest power while remaining Mineral and Store overflow can limit retained resources

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official engine
Checked — Mineral harvest emits EVENT_HARVEST with Miner objectId and Mineral targetId
Amount boundary
The engine event can report body harvest power while remaining Mineral and Store overflow can limit retained resources
Static code review
Passed — exact Miner, Mineral and Extractor identity with one-tick verification
JavaScript syntax
Passed
Screeps Console test
Pending
Live multi-tick verification
Pending
Genuine room or Console screenshots
Pending
Official engine source
Checked screeps/engine 80977824199a596d174d392fd0cf8c458c21fcbd: Mineral harvest has no Store-capacity ERR_FULL preflight; the processor can drop overflow after applying the WORK harvest batch
Mineral batch policy
Project policy — the example rejects boosted WORK and, for unboosted WORK, requires current free Store capacity to fit min(mineralAmount, active WORK × HARVEST_MINERAL_POWER)
Final-depletion event boundary
Checked — the current Mineral processor records EVENT_HARVEST amount from harvest power while actual Mineral removal is capped by remaining mineralAmount
Screeps Console test
Pending — no live near-full or final-depletion Mineral trace is claimed
Live Mineral overflow/event comparison
Pending — no live multi-WORK near-full Store or final-depletion event comparison was collected
Last editorial review
August 17, 2026
Publication status
Published

Define the Mineral harvest contract

A valid Mineral operation binds three objects: one Miner, the room's Mineral, and the active Extractor occupying that Mineral's tile. A room-level Extractor lookup is not enough, and a later Store increase does not identify which action produced it.

The workflow should therefore separate current-tick submission from next-tick verification:

  • resolve the current objects and validate their relationship;
  • move when the Miner is outside range 1;
  • save IDs only after harvest() returns OK;
  • on the next tick, match EVENT_HARVEST by exact Miner and Mineral IDs.

Resolve the exact Mineral and Extractor

function resolveMineralStation(room) {
  const mineral = room?.find(FIND_MINERALS)[0] || null;
  if (!mineral) {
    return { ready: false, status: 'mineral-missing' };
  }

  const extractor = mineral.pos
    .lookFor(LOOK_STRUCTURES)
    .find(structure =>
      structure.structureType === STRUCTURE_EXTRACTOR
    ) || null;

  if (!extractor) {
    return { ready: false, status: 'extractor-missing', mineral };
  }
  if (extractor.my !== true || extractor.isActive() !== true) {
    return { ready: false, status: 'extractor-inactive', mineral, extractor };
  }

  return { ready: true, status: 'station-ready', mineral, extractor };
}

When mineralAmount is zero, keep the Mineral object as the source of truth. Its regeneration state belongs to the object, not to a Memory countdown copied on an earlier tick.

Check the Miner without inventing success

function inspectMinerForStation(creep, station) {
  if (!creep || creep.my !== true || creep.spawning === true) {
    return { ready: false, status: 'miner-unavailable' };
  }
  if (creep.room.name !== station.mineral.pos.roomName) {
    return { ready: false, status: 'miner-in-wrong-room' };
  }
  if (station.mineral.mineralAmount <= 0) {
    return {
      ready: false,
      status: 'mineral-regenerating',
      ticksToRegeneration:
        station.mineral.ticksToRegeneration ?? null
    };
  }
  if (station.extractor.cooldown > 0) {
    return {
      ready: false,
      status: 'extractor-cooldown',
      cooldown: station.extractor.cooldown
    };
  }
  if (creep.getActiveBodyparts(WORK) <= 0) {
    return { ready: false, status: 'no-active-work' };
  }
  if (
    creep.store.getFreeCapacity(
      station.mineral.mineralType
    ) <= 0
  ) {
    return { ready: false, status: 'no-mineral-capacity' };
  }

  return { ready: true, status: 'miner-ready' };
}

The free-capacity check above is only a coarse gate. The current Mineral harvest() submission path does not return ERR_FULL because the Creep has too little Store space. The checked processor can harvest a full WORK batch and drop the overflow. The submission function below therefore adds a conservative project policy for an unboosted Miner: do not submit the next harvest when the visible remaining Mineral output for that batch is larger than the current free Store capacity.

Save only accepted harvest identity

function submitMineralHarvest(creep) {
  const station = resolveMineralStation(creep?.room);
  if (!station.ready) return station;

  const state = inspectMinerForStation(creep, station);
  if (!state.ready) return state;

  const { mineral, extractor } = station;
  const hasBoostedWork = creep.body.some(part =>
    part.type === WORK
    && part.hits > 0
    && Boolean(part.boost)
  );

  if (hasBoostedWork) {
    return {
      status: 'boosted-work-batch-out-of-scope',
      mineralId: mineral.id
    };
  }

  const activeWork = creep.getActiveBodyparts(WORK);
  const harvestBatch =
    activeWork * HARVEST_MINERAL_POWER;
  const plannedOutput = Math.min(
    mineral.mineralAmount,
    harvestBatch
  );
  const freeCapacity =
    creep.store.getFreeCapacity(mineral.mineralType) ?? 0;

  if (freeCapacity < plannedOutput) {
    return {
      status: 'deliver-before-next-harvest',
      mineralId: mineral.id,
      plannedOutput,
      freeCapacity
    };
  }

  if (!creep.pos.isNearTo(mineral)) {
    return {
      status: 'moving-to-mineral',
      moveResult: creep.moveTo(mineral, {
        range: 1,
        reusePath: 10
      })
    };
  }

  const result = creep.harvest(mineral);
  if (result !== OK) {
    return {
      status: 'harvest-rejected',
      result,
      mineralId: mineral.id,
      extractorId: extractor.id
    };
  }

  Memory.pendingMineralHarvests ??= {};
  Memory.pendingMineralHarvests[creep.id] = {
    submittedAt: Game.time,
    roomName: creep.room.name,
    creepId: creep.id,
    creepName: creep.name,
    mineralId: mineral.id,
    mineralType: mineral.mineralType,
    extractorId: extractor.id,
    mineralBefore: mineral.mineralAmount,
    storeBefore:
      creep.store.getUsedCapacity(mineral.mineralType),
    plannedOutput
  };

  return {
    status: 'harvest-accepted',
    result,
    mineralId: mineral.id,
    extractorId: extractor.id
  };
}

ERR_NOT_IN_RANGE belongs to the attempted harvest action. This example checks range first so movement and harvesting remain separate intents with separate statuses.

Match the exact harvest event

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

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

  const matches = room.getEventLog().filter(event =>
    event.event === EVENT_HARVEST
    && event.objectId === pending.creepId
    && event.data?.targetId === pending.mineralId
  );

  if (matches.length === 0) {
    return { status: 'accepted-harvest-event-missing' };
  }
  if (matches.length > 1) {
    return { status: 'harvest-event-ambiguous', count: matches.length };
  }

  const mineral = Game.getObjectById(pending.mineralId);
  const creep = Game.getObjectById(pending.creepId);
  return {
    status: 'harvest-event-observed',
    eventAmount: matches[0].data?.amount ?? null,
    mineralBefore: pending.mineralBefore,
    mineralNow: mineral?.mineralAmount ?? null,
    storeBefore: pending.storeBefore,
    storeNow: creep
      ? creep.store.getUsedCapacity(pending.mineralType)
      : null
  };
}

Run this verifier before replacing the pending record with a new action. Room events are available for a narrow window; a later net comparison cannot reconstruct exact actor-target identity.

Separate event amount from retained Store gain

The official engine records the Miner as objectId and the Mineral as targetId. In the checked engine snapshot 80977824199a596d174d392fd0cf8c458c21fcbd, the Mineral event amount is based on body harvest power, while the actual Mineral removal is capped by remaining mineralAmount and Store overflow can be dropped. On the final depletion harvest, event.data.amount can therefore be larger than the Mineral actually removed even when Store capacity is not the limiting factor. Therefore:

  • use the event to prove actor-target identity;
  • use current mineralAmount to describe depletion;
  • use the Creep Store to describe retained resources;
  • do not require all three numeric deltas to equal the event amount at the final harvest.

Treat depletion as a state transition

function describeMineralAvailability(mineral) {
  if (!mineral) return { status: 'mineral-missing' };

  if (mineral.mineralAmount <= 0) {
    return {
      status: 'mineral-regenerating',
      ticksToRegeneration:
        mineral.ticksToRegeneration ?? null
    };
  }

  return {
    status: 'mineral-available',
    mineralAmount: mineral.mineralAmount,
    mineralType: mineral.mineralType
  };
}

Stop generating harvest intents while the Mineral is depleted. Keep logistics and replacement decisions separate from the harvest verifier so a delivery action cannot overwrite pending harvest evidence.

Production integration boundary

Use one role runner for each fixed Mineral station, verify the prior accepted action before submitting the next one, and keep hauling in another state. The next-batch guard above is intentionally scoped to unboosted WORK parts; a boosted Miner needs an explicit boost-aware batch calculation or a deliberate drop-mining policy. Console execution, near-full Store overflow, final-depletion event amount, real cooldown timing, regeneration and screenshots remain pending.

Official documentation

Review the official Screeps API for Minerals, Extractors, Creep.harvest(), Store capacity, Room.getEventLog(), EVENT_HARVEST and action return codes.

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.