Use this guide when
Use this workflow when one scheduler must submit one Factory batch and distinguish production from ordinary hauling. Output growth alone is not enough: a hauler can add the product, remove components, or change free capacity. Exact verification records every recipe component and requires an exclusive Store window.
Bind one request to one Factory and commodity
function validateFactoryRequest(request) {
if (!request || request.enabled !== true) {
return { valid: false, reason: 'disabled' };
}
if (
typeof request.requestId !== 'string'
|| request.requestId.length === 0
|| typeof request.factoryId !== 'string'
|| typeof request.resourceType !== 'string'
) {
return {
valid: false,
reason: 'request-invalid'
};
}
return { valid: true, reason: 'valid' };
}
function getOwnedActiveFactory(id) {
const factory = Game.getObjectById(id);
return factory
&& factory.structureType ===
STRUCTURE_FACTORY
&& factory.my === true
&& factory.isActive() === true
? factory
: null;
}
The exact Factory ID prevents a room search from selecting another structure. The request ID identifies the approved batch across ticks.
Read the current recipe from COMMODITIES
function getCommodityRecipe(resourceType) {
const recipe =
typeof resourceType === 'string'
? COMMODITIES[resourceType]
: null;
if (
!recipe
|| !Number.isInteger(recipe.amount)
|| recipe.amount <= 0
|| !recipe.components
) {
return null;
}
return recipe;
}
function readComponentAmounts(factory, recipe) {
return Object.fromEntries(
Object.keys(recipe.components).map(
resourceType => [
resourceType,
factory.store.getUsedCapacity(
resourceType
)
]
)
);
}
Use the runtime constant rather than a copied recipe table. The component map is also the identity of the batch that must be reconciled later.
Separate permanent Factory level from active operation
function getOperateFactoryEffect(factory) {
return factory.effects?.find(effect =>
effect.effect === PWR_OPERATE_FACTORY
&& effect.ticksRemaining > 0
) ?? null;
}
function checkFactoryLevel(factory, recipe) {
if (!Number.isInteger(recipe.level)) {
return {
ready: true,
reason: 'level-not-required'
};
}
if (factory.level !== recipe.level) {
return {
ready: false,
reason: 'permanent-level-mismatch'
};
}
const effect = getOperateFactoryEffect(factory);
return effect?.level === recipe.level
? {
ready: true,
reason: 'operated-at-required-level',
effect
}
: {
ready: false,
reason: 'operate-effect-missing'
};
}
factory.level is the permanent level assigned by the first successful PWR_OPERATE_FACTORY use and cannot later be changed. A leveled recipe also requires the matching active operation effect; a missing effect may produce ERR_BUSY. Level-less recipes do not need that effect.
Check cooldown, every component, and output capacity
function buildFactoryPlan(request) {
const validation =
validateFactoryRequest(request);
if (!validation.valid) {
return {
ready: false,
reason: validation.reason
};
}
const factory = getOwnedActiveFactory(
request.factoryId
);
const recipe = getCommodityRecipe(
request.resourceType
);
if (!factory || !recipe) {
return {
ready: false,
reason: factory
? 'recipe-invalid'
: 'factory-unavailable'
};
}
if (factory.cooldown > 0) {
return {
ready: false,
reason: 'factory-cooling-down'
};
}
const levelCheck =
checkFactoryLevel(factory, recipe);
if (!levelCheck.ready) return levelCheck;
const componentsBefore =
readComponentAmounts(factory, recipe);
const missing = Object.entries(
recipe.components
).filter(([resourceType, required]) =>
componentsBefore[resourceType] < required
);
if (missing.length > 0) {
return {
ready: false,
reason: 'components-insufficient',
missing
};
}
if (
factory.store.getFreeCapacity(
request.resourceType
) < recipe.amount
) {
return {
ready: false,
reason: 'output-capacity-insufficient'
};
}
return {
ready: true,
reason: 'ready',
factory,
recipe,
componentsBefore,
effect: levelCheck.effect ?? null
};
}
Preflight is a current-tick decision. The API return code remains authoritative if another owner changes the Factory before the call.
Reserve one exclusive Factory Store window
Between snapshot and verification, block component hauling, product hauling, and a second produce() call for this Factory. Without that scheduler contract, Store deltas cannot distinguish one production batch from unrelated logistics. Record exclusiveWindow: true only when the caller actually enforces it.
Snapshot the full recipe and submit once
function submitFactoryRequest(request) {
const plan = buildFactoryPlan(request);
request.lastCheckedAt = Game.time;
request.lastStatus = plan.reason;
if (!plan.ready) {
return {
status: 'not-submitted',
reason: plan.reason,
result: null
};
}
request.enabled = false;
request.status = 'submitted';
request.submittedAt = Game.time;
request.snapshot = {
requestId: request.requestId,
exclusiveWindow:
request.exclusiveWindow === true,
factoryId: plan.factory.id,
resourceType: request.resourceType,
outputAmount: plan.recipe.amount,
outputBefore:
plan.factory.store.getUsedCapacity(
request.resourceType
),
components: {
...plan.recipe.components
},
componentsBefore:
plan.componentsBefore,
recipeCooldown:
plan.recipe.cooldown ?? null,
cooldownBefore: plan.factory.cooldown,
factoryLevel: plan.factory.level,
operateEffect: plan.effect
? {
level: plan.effect.level,
ticksRemaining:
plan.effect.ticksRemaining
}
: null
};
const result = plan.factory.produce(
request.resourceType
);
request.result = result;
request.resultAt = Game.time;
request.status = result === OK
? 'accepted-pending-batch'
: 'failed-review-required';
return {
status: request.status,
result
};
}
A non-OK call stays disabled. Review current components, capacity, cooldown, and Power state before authorizing another batch.
Verify one output increase and every component decrease
function verifyFactoryRequest(request) {
const snapshot = request?.snapshot;
if (!snapshot || request.result !== OK) {
return {
status: 'submission-not-accepted'
};
}
if (snapshot.exclusiveWindow !== true) {
return { status: 'not-exclusive' };
}
const factory = getOwnedActiveFactory(
snapshot.factoryId
);
if (!factory) {
return { status: 'factory-unavailable' };
}
const outputDelta =
factory.store.getUsedCapacity(
snapshot.resourceType
) - snapshot.outputBefore;
const componentDeltas = Object.fromEntries(
Object.keys(snapshot.components).map(
resourceType => [
resourceType,
factory.store.getUsedCapacity(
resourceType
)
- snapshot.componentsBefore[
resourceType
]
]
)
);
const componentsExact = Object.entries(
snapshot.components
).every(([resourceType, required]) =>
componentDeltas[resourceType] === -required
);
const exact =
outputDelta === snapshot.outputAmount
&& componentsExact;
if (exact) {
return {
status: 'verified-exact-batch',
outputDelta,
componentDeltas,
cooldownNow: factory.cooldown
};
}
const unchanged =
outputDelta === 0
&& Object.values(componentDeltas)
.every(delta => delta === 0);
return {
status: unchanged
? 'pending-no-store-delta'
: 'state-changed-ambiguous',
outputDelta,
componentDeltas,
cooldownNow: factory.cooldown
};
}
Cooldown greater than zero is supporting evidence, not a unique batch identity. The exact component and output signature is stronger, and it still depends on the exclusive Store window.
Distinguish permanent level, active effect, and settlement
| Status | Meaning |
|---|---|
permanent-level-mismatch | The Factory was permanently assigned a different level. |
operate-effect-missing | The correct permanent level exists, but the active operation effect is absent. |
accepted-pending-batch | produce() returned OK; Store evidence is pending. |
state-changed-ambiguous | Hauling or another batch may have altered the Store signature. |
verified-exact-batch | The output and every component changed by exactly one recipe batch. |
Preserve all documented return codes
Documented results include ERR_NOT_OWNER, ERR_BUSY, ERR_NOT_ENOUGH_RESOURCES, ERR_INVALID_TARGET, ERR_FULL, ERR_INVALID_ARGS, ERR_TIRED, and ERR_RCL_NOT_ENOUGH. ERR_BUSY specifically covers a Factory that is not currently operated when the recipe requires it.
Keep hauling, Power scheduling, and profitability separate
This page owns one reviewed production batch. Component delivery, output evacuation, Power Creep scheduling, commodity chains, market profitability, and multi-Factory allocation require separate policies. Use the market deal guide only after production identity is settled.