LOGISTICS · LINK ENERGY NETWORK

How to Transfer Link Energy Without Depending on Structure Array Order

Recover source and target Links by fixed IDs, require ownership, different objects, the same room, active structures and zero source cooldown, calculate a conservative amount from source stock and target free capacity, estimate LINK_LOSS_RATIO only for logs, and verify Store changes later.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — StructureLink.transferEnergy(), same-room restriction, cooldown, Store, LINK_LOSS_RATIO and return codes · Policy boundary: Link identities, minimumSend and targetReserve are room logistics policies, not official StructureLink fields

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — StructureLink.transferEnergy(), same-room restriction, cooldown, Store, LINK_LOSS_RATIO and return codes
Policy boundary
Link identities, minimumSend and targetReserve are room logistics policies, not official StructureLink fields
Execution boundary
OK schedules a transfer; both Link Stores and source cooldown must be observed later
JavaScript syntax
Passed
Offline transfer review
Passed — missing Link, same object, cross-room, inactive, cooldown, stock, capacity, threshold and ready states
Screeps Console test
Pending
Live loss rounding, cooldown distance, concurrent send, Store and target-reserve test
Pending
Last verified
July 26, 2026

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

CodeTypical causeReview
OKTransfer scheduledBoth Stores and cooldown later
ERR_NOT_OWNERSource Link not yoursID and ownership
ERR_NOT_ENOUGH_RESOURCESSource stock changedCurrent source Store
ERR_INVALID_TARGETTarget is not a valid LinkType and current object
ERR_FULLTarget cannot receive amountCapacity and concurrent actions
ERR_NOT_IN_RANGELinks are not in one roomRoom identities
ERR_INVALID_ARGSInvalid amountPositive finite amount
ERR_TIREDSource cooldown activesourceLink.cooldown
ERR_RCL_NOT_ENOUGHLink inactiveRCL 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.

Official documentation

SOURCE AND SCOPE

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.