Quick answer
Store explicit source and target Link IDs in room configuration. Recover both with Game.getObjectById(), require owned STRUCTURE_LINK objects, different IDs, the same room, active structures, and source cooldown zero. Calculate the send amount from current source Energy and target free capacity, optionally subtract a configured target-capacity reserve, require a documented minimum send, call transferEnergy(), save the result, and re-read both Links later.
Use fixed Link IDs
Memory.linkNetwork ??= {};
Memory.linkNetwork.W1N1 = {
enabled: true,
sourceLinkId: 'replace-with-source-link-id',
controllerLinkId: 'replace-with-target-link-id',
minimumSend: 200,
targetReserve: 0
};
function getOwnedLink(id) {
if (typeof id !== 'string') {
return null;
}
const structure = Game.getObjectById(id);
if (
!structure
|| structure.structureType !== STRUCTURE_LINK
|| structure.my !== true
) {
return null;
}
return structure;
}
Do not assign meaning to room.find(...)[0]. Link order is not a stable Source-Link or Controller-Link contract.
Keep both Links in one room
function linksShareRoom(sourceLink, targetLink) {
return Boolean(
sourceLink
&& targetLink
&& sourceLink.room.name === targetLink.room.name
);
}
Remote does not mean cross-room. Preserve ERR_NOT_IN_RANGE as evidence that the configured Links do not satisfy the same-room rule.
Calculate a conservative amount
function calculateLinkSendAmount(input) {
if (
!Number.isFinite(input.sourceEnergy)
|| !Number.isFinite(input.targetFree)
|| !Number.isFinite(input.targetReserve)
) {
return 0;
}
return Math.max(
0,
Math.min(
input.sourceEnergy,
Math.max(0, input.targetFree - input.targetReserve)
)
);
}
The conservative amount may leave the target short of full after transfer loss, but it avoids exceeding visible capacity through a fragile inverse-loss calculation.
Estimate loss only for diagnostics
function estimateLinkTransfer(amount) {
if (!Number.isInteger(amount) || amount <= 0) {
return {
valid: false,
sent: 0,
estimatedLoss: 0,
estimatedReceived: 0
};
}
const estimatedLoss = Math.ceil(
amount * LINK_LOSS_RATIO
);
return {
valid: true,
sent: amount,
estimatedLoss,
estimatedReceived: Math.max(
0,
amount - estimatedLoss
)
};
}
This uses the official loss constant for a local estimate. It does not replace later Store observation or claim exact attribution when other transfers occur.
Build a testable transfer plan
function evaluateLinkTransfer(input) {
if (!input.sourceExists || !input.targetExists) {
return { ready: false, reason: 'link-missing' };
}
if (input.sameObject) {
return { ready: false, reason: 'same-link' };
}
if (!input.sameRoom) {
return { ready: false, reason: 'different-room' };
}
if (!input.sourceActive || !input.targetActive) {
return { ready: false, reason: 'link-inactive' };
}
if (
!Number.isInteger(input.sourceCooldown)
|| input.sourceCooldown > 0
) {
return { ready: false, reason: 'source-not-ready' };
}
const amount = calculateLinkSendAmount({
sourceEnergy: input.sourceEnergy,
targetFree: input.targetFree,
targetReserve: input.targetReserve
});
const minimumSend = Number.isFinite(input.minimumSend)
? Math.max(1, input.minimumSend)
: 1;
if (amount < minimumSend) {
return {
ready: false,
reason: 'amount-below-threshold',
amount,
minimumSend
};
}
return {
ready: true,
reason: 'ready',
amount,
minimumSend
};
}
minimumSend is a scheduling policy used to avoid frequent tiny transfers. It is not an official Link minimum.
Complete Link transfer example
function runLinkTransfer(roomName) {
const config = Memory.linkNetwork?.[roomName];
if (!config || config.enabled !== true) {
return { status: 'config-disabled' };
}
const sourceLink = getOwnedLink(config.sourceLinkId);
const targetLink = getOwnedLink(config.controllerLinkId);
if (!sourceLink || !targetLink) {
return { status: 'link-missing' };
}
const sourceEnergy = sourceLink.store.getUsedCapacity(
RESOURCE_ENERGY
);
const targetEnergy = targetLink.store.getUsedCapacity(
RESOURCE_ENERGY
);
const targetFree = targetLink.store.getFreeCapacity(
RESOURCE_ENERGY
);
const plan = evaluateLinkTransfer({
sourceExists: true,
targetExists: true,
sameObject: sourceLink.id === targetLink.id,
sameRoom:
sourceLink.room.name === targetLink.room.name
&& sourceLink.room.name === roomName,
sourceActive: sourceLink.isActive(),
targetActive: targetLink.isActive(),
sourceCooldown: sourceLink.cooldown,
sourceEnergy,
targetFree,
targetReserve: Number.isFinite(config.targetReserve)
? Math.max(0, config.targetReserve)
: 0,
minimumSend: config.minimumSend
});
if (!plan.ready) {
return {
status: plan.reason,
...plan
};
}
const before = {
gameTick: Game.time,
sourceLinkId: sourceLink.id,
targetLinkId: targetLink.id,
sourceEnergy,
targetEnergy,
targetFree,
sourceCooldown: sourceLink.cooldown,
estimate: estimateLinkTransfer(plan.amount)
};
const result = sourceLink.transferEnergy(
targetLink,
plan.amount
);
return {
status: result === OK
? 'link-transfer-scheduled'
: 'link-transfer-rejected',
result,
amount: plan.amount,
before
};
}
module.exports.loop = function () {
const outcome = runLinkTransfer('W1N1');
if (
outcome.status === 'link-transfer-rejected'
|| Game.time % 100 === 0
) {
console.log(JSON.stringify({
type: 'link-transfer-status',
...outcome
}));
}
};
Leave target capacity through a policy reserve
const targetReserve = 100;
A target-capacity reserve can leave space for another source Link or a temporary delivery. It is room configuration, not a StructureLink property or universal recommendation.
Use one dispatcher for multiple source Links
Several modules can read the same target free capacity before actions settle. Use one Link-network dispatcher, stable source priority, and at most the intended number of target assignments per tick. A local reservation map is coordination, not a Store lock.
Verify later Store and cooldown state
function inspectLinkTransfer(before) {
const source = getOwnedLink(before.sourceLinkId);
const target = getOwnedLink(before.targetLinkId);
return {
sourceFound: Boolean(source),
targetFound: Boolean(target),
sourceEnergyBefore: before.sourceEnergy,
sourceEnergyNow: source
? source.store.getUsedCapacity(RESOURCE_ENERGY)
: null,
targetEnergyBefore: before.targetEnergy,
targetEnergyNow: target
? target.store.getUsedCapacity(RESOURCE_ENERGY)
: null,
sourceCooldownNow: source?.cooldown ?? null
};
}
Other same-tick logistics can change both Stores, so report net state rather than claiming one perfect transfer delta.
Handle return codes
| Code | Typical cause | Review |
|---|---|---|
OK | Transfer scheduled | Both Stores and cooldown later |
ERR_NOT_OWNER | Source Link not yours | ID and ownership |
ERR_NOT_ENOUGH_RESOURCES | Source stock changed | Current source Store |
ERR_INVALID_TARGET | Target is not a valid Link | Type and current object |
ERR_FULL | Target cannot receive amount | Capacity and concurrent actions |
ERR_NOT_IN_RANGE | Links are not in one room | Room identities |
ERR_INVALID_ARGS | Invalid amount | Positive finite amount |
ERR_TIRED | Source cooldown active | sourceLink.cooldown |
ERR_RCL_NOT_ENOUGH | Link inactive | RCL and isActive() |
Debugging checklist
- Store explicit source and target IDs.
- Validate owned Link types every tick.
- Reject the same object and different rooms.
- Check both active states.
- Check source cooldown.
- Read current source stock and target capacity.
- Apply target reserve and minimum send as labeled policies.
- Use loss estimates only for logs.
- Coordinate multiple source Links centrally.
- Save the return code and verify later state.
Scope and next steps
This guide does not discover Link roles automatically, optimize cooldown distance, fill a Controller Link from multiple rooms, coordinate Creep hauling, or guarantee exact received Energy. Continue with reachable Source selection.
Frequently asked questions
Why can the target remain partly empty?
The conservative send amount does not reverse-calculate loss. A later transfer can use current Store state.
Should minimumSend always be 200?
No. It is an example scheduling threshold. Set it from your room's traffic and urgency.
Can two source Links send in one tick?
The API and current target capacity determine accepted actions. A dispatcher should coordinate visible capacity and preserve every return code.