@shipload/sdk 1.0.0-next.65 → 1.0.0-next.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/lib/shipload.d.ts +786 -321
  2. package/lib/shipload.js +2731 -1305
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +2644 -1286
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +223 -146
  7. package/lib/testing.js +907 -563
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +907 -563
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/capabilities/crafting.test.ts +51 -6
  13. package/src/capabilities/crafting.ts +19 -4
  14. package/src/capabilities/gathering.ts +2 -2
  15. package/src/capabilities/storage.test.ts +26 -0
  16. package/src/capabilities/storage.ts +6 -6
  17. package/src/contracts/fund.ts +79 -2
  18. package/src/contracts/platform.ts +10 -1
  19. package/src/contracts/server.ts +521 -330
  20. package/src/data/charters.json +325 -127
  21. package/src/data/entities.json +29 -24
  22. package/src/data/items.json +114 -114
  23. package/src/data/kind-registry.json +3 -3
  24. package/src/data/kind-registry.ts +5 -0
  25. package/src/data/metadata.ts +9 -2
  26. package/src/data/recipes.json +56 -95
  27. package/src/derivation/build-methods.test.ts +59 -1
  28. package/src/derivation/build-methods.ts +2 -0
  29. package/src/derivation/capabilities.ts +7 -7
  30. package/src/derivation/strata.ts +5 -3
  31. package/src/derivation/tiers.ts +5 -5
  32. package/src/entity/hydrate.test.ts +156 -0
  33. package/src/entity/hydrate.ts +86 -0
  34. package/src/errors.ts +9 -0
  35. package/src/format.ts +15 -13
  36. package/src/index-module.ts +32 -5
  37. package/src/influence/ballot.test.ts +103 -0
  38. package/src/influence/ballot.ts +98 -0
  39. package/src/influence/charter-metadata.test.ts +136 -0
  40. package/src/influence/charter-metadata.ts +236 -0
  41. package/src/influence/charters.test.ts +51 -65
  42. package/src/influence/charters.ts +70 -40
  43. package/src/influence/civic-shuttle.test.ts +159 -0
  44. package/src/influence/civic-shuttle.ts +115 -0
  45. package/src/influence/civic.test.ts +287 -0
  46. package/src/influence/civic.ts +269 -0
  47. package/src/influence/constants.ts +30 -9
  48. package/src/influence/decomp.ts +15 -15
  49. package/src/influence/duration.ts +42 -6
  50. package/src/influence/index.ts +59 -6
  51. package/src/influence/influence.test.ts +76 -9
  52. package/src/influence/valuation.ts +17 -17
  53. package/src/managers/actions.ts +99 -37
  54. package/src/managers/base.ts +4 -0
  55. package/src/managers/context.ts +3 -1
  56. package/src/managers/epochs.ts +32 -1
  57. package/src/managers/index.ts +4 -2
  58. package/src/managers/influence.test.ts +18 -20
  59. package/src/managers/influence.ts +137 -84
  60. package/src/managers/jobs.test.ts +2 -2
  61. package/src/managers/jobs.ts +4 -4
  62. package/src/nft/description.ts +10 -11
  63. package/src/planner/index.ts +54 -0
  64. package/src/planner/planner.test.ts +127 -2
  65. package/src/resolution/display-name.ts +1 -1
  66. package/src/scheduling/availability.test.ts +113 -0
  67. package/src/scheduling/availability.ts +48 -8
  68. package/src/scheduling/epoch.ts +1 -0
  69. package/src/scheduling/jobs.ts +1 -1
  70. package/src/scheduling/projection.ts +2 -16
  71. package/src/scheduling/task-cargo.test.ts +62 -1
  72. package/src/scheduling/task-cargo.ts +25 -10
  73. package/src/scheduling/task-effects.test.ts +35 -0
  74. package/src/scheduling/task-effects.ts +34 -0
  75. package/src/scheduling/unwrap.test.ts +3 -3
  76. package/src/scheduling/unwrap.ts +3 -3
  77. package/src/shipload.ts +22 -3
  78. package/src/travel/reach.test.ts +86 -0
  79. package/src/travel/reach.ts +23 -10
  80. package/src/travel/route-build.test.ts +63 -0
  81. package/src/travel/route-build.ts +60 -0
  82. package/src/travel/travel.ts +2 -1
  83. package/src/types.ts +6 -2
  84. package/src/utils/hash.ts +18 -2
  85. package/src/utils/system.ts +6 -1
@@ -1,26 +1,34 @@
1
1
  import chartersJson from '../data/charters.json'
2
- import {CHARTER_EFFECT_REFIT_MODULES, CHARTER_EFFECT_SPAWN_ENTITY, CHARTER_NONE} from './constants'
3
-
4
- export interface CharterEffect {
2
+ import {
3
+ CHARTER_NONE,
4
+ CIVIC_GRANT_LEVEL_GATE,
5
+ CIVIC_GRANT_MODULE,
6
+ CIVIC_GRANT_RUNG,
7
+ } from './constants'
8
+
9
+ export interface CharterGrant {
5
10
  kind: number
6
- itemId: number
7
- targetItemId: number
8
- slotMask: number
11
+ building: number
9
12
  stat: number
13
+ value: number
10
14
  }
11
15
 
12
16
  export interface CharterNode {
13
17
  nodeId: number
14
18
  cost: bigint
15
19
  prereqs: number[]
16
- effect: CharterEffect
20
+ grants: CharterGrant[]
21
+ exclusionGroup: number
22
+ repeatCap: number
17
23
  }
18
24
 
19
25
  export const CHARTER_REGISTRY: CharterNode[] = chartersJson.nodes.map((node) => ({
20
26
  nodeId: node.nodeId,
21
27
  cost: BigInt(node.cost),
22
28
  prereqs: [...node.prereqs],
23
- effect: {...node.effect},
29
+ grants: node.grants.map((grant) => ({...grant})),
30
+ exclusionGroup: node.exclusionGroup,
31
+ repeatCap: node.repeatCap,
24
32
  }))
25
33
 
26
34
  export function charterNode(nodeId: number): CharterNode | undefined {
@@ -28,58 +36,77 @@ export function charterNode(nodeId: number): CharterNode | undefined {
28
36
  return CHARTER_REGISTRY.find((n) => n.nodeId === nodeId)
29
37
  }
30
38
 
31
- export type CharterIneligibility = 'already-taken' | 'prereq-missing' | 'refit-target-missing'
39
+ export type CharterIneligibility = 'already-taken' | 'prereq-missing'
32
40
 
33
41
  export const CHARTER_INELIGIBILITY_MESSAGES: Record<CharterIneligibility, string> = {
34
42
  'already-taken': 'charter is already completed at this world',
35
43
  'prereq-missing': 'charter prerequisites are not completed',
36
- 'refit-target-missing': 'charter refit target not found at this world',
37
44
  }
38
45
 
39
46
  export interface BuiltCharter {
40
47
  nodeId: number
41
- entityId: bigint
48
+ repeats: number
42
49
  }
43
50
 
44
51
  export interface CharterWorld {
45
52
  built: BuiltCharter[]
46
- entityExists?: (entityId: bigint) => boolean
47
53
  }
48
54
 
49
- export interface ChosenCharter {
50
- chosen: number
51
- chosenEpoch: number
55
+ export interface WorldBuilding {
56
+ entityId: bigint
57
+ building: number
52
58
  }
53
59
 
54
60
  function builtCharter(world: CharterWorld, nodeId: number): BuiltCharter | undefined {
55
61
  return world.built.find((row) => row.nodeId === nodeId)
56
62
  }
57
63
 
58
- export function charterSpawnNodeFor(targetItemId: number): CharterNode | undefined {
59
- return CHARTER_REGISTRY.find(
60
- (node) =>
61
- node.effect.kind === CHARTER_EFFECT_SPAWN_ENTITY && node.effect.itemId === targetItemId
64
+ export function charterGateNodeFor(building: number, level: number = 1): CharterNode | undefined {
65
+ return CHARTER_REGISTRY.find((node) =>
66
+ node.grants.some(
67
+ (grant) =>
68
+ grant.kind === CIVIC_GRANT_LEVEL_GATE &&
69
+ grant.building === building &&
70
+ grant.value === level
71
+ )
62
72
  )
63
73
  }
64
74
 
65
- export function charterPrereqsMet(world: CharterWorld, node: CharterNode): boolean {
66
- return node.prereqs.every(
67
- (prereq) => prereq === CHARTER_NONE || builtCharter(world, prereq) !== undefined
68
- )
75
+ export function charterBuildingEntity(buildings: WorldBuilding[], building: number): bigint {
76
+ const record = buildings.find((row) => row.building === building)
77
+ return record ? record.entityId : 0n
78
+ }
79
+
80
+ export function charterRungValue(world: CharterWorld, building: number, stat: number): number {
81
+ let total = 0
82
+ for (const node of CHARTER_REGISTRY) {
83
+ if (builtCharter(world, node.nodeId) === undefined) continue
84
+ for (const grant of node.grants) {
85
+ if (grant.kind !== CIVIC_GRANT_RUNG) continue
86
+ if (grant.building !== building || grant.stat !== stat) continue
87
+ total += grant.value
88
+ }
89
+ }
90
+ return total
69
91
  }
70
92
 
71
- export function charterEffectTargetEntity(world: CharterWorld, node: CharterNode): bigint {
72
- const spawn = charterSpawnNodeFor(node.effect.targetItemId)
73
- if (!spawn) return 0n
74
- const record = builtCharter(world, spawn.nodeId)
75
- if (!record) return 0n
76
- if (world.entityExists && !world.entityExists(record.entityId)) return 0n
77
- return record.entityId
93
+ export function charterModuleCount(world: CharterWorld, building: number): number {
94
+ let total = 0
95
+ for (const node of CHARTER_REGISTRY) {
96
+ if (builtCharter(world, node.nodeId) === undefined) continue
97
+ for (const grant of node.grants) {
98
+ if (grant.kind !== CIVIC_GRANT_MODULE) continue
99
+ if (grant.building !== building) continue
100
+ total += grant.value
101
+ }
102
+ }
103
+ return total
78
104
  }
79
105
 
80
- export function charterEffectTargetPresent(world: CharterWorld, node: CharterNode): boolean {
81
- if (node.effect.kind !== CHARTER_EFFECT_REFIT_MODULES) return true
82
- return charterEffectTargetEntity(world, node) !== 0n
106
+ export function charterPrereqsMet(world: CharterWorld, node: CharterNode): boolean {
107
+ return node.prereqs.every(
108
+ (prereq) => prereq === CHARTER_NONE || builtCharter(world, prereq) !== undefined
109
+ )
83
110
  }
84
111
 
85
112
  export function charterIneligible(
@@ -88,7 +115,6 @@ export function charterIneligible(
88
115
  ): CharterIneligibility | undefined {
89
116
  if (builtCharter(world, node.nodeId) !== undefined) return 'already-taken'
90
117
  if (!charterPrereqsMet(world, node)) return 'prereq-missing'
91
- if (!charterEffectTargetPresent(world, node)) return 'refit-target-missing'
92
118
  return undefined
93
119
  }
94
120
 
@@ -110,11 +136,15 @@ export function charterSingletonMandate(world: CharterWorld): number {
110
136
  return only
111
137
  }
112
138
 
113
- export function effectiveMandate(
114
- stored: ChosenCharter,
139
+ export function charterEligibleChained(
115
140
  world: CharterWorld,
116
- epoch: number
117
- ): number {
118
- if (stored.chosen !== CHARTER_NONE && stored.chosenEpoch === epoch) return stored.chosen
119
- return charterSingletonMandate(world)
141
+ node: CharterNode,
142
+ seated: number[]
143
+ ): boolean {
144
+ if (builtCharter(world, node.nodeId) !== undefined || seated.includes(node.nodeId)) return false
145
+ for (const prereq of node.prereqs) {
146
+ if (prereq === CHARTER_NONE) continue
147
+ if (builtCharter(world, prereq) === undefined && !seated.includes(prereq)) return false
148
+ }
149
+ return true
120
150
  }
@@ -0,0 +1,159 @@
1
+ import {UInt8, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
2
+ import {expect, test} from 'bun:test'
3
+ import type {ServerContract} from '../contracts'
4
+ import {encodeStats} from '../derivation/crafting'
5
+ import {ITEM_DEPOT_T1_PACKED, ITEM_LOADER_T1, ITEM_STORAGE_T1} from '../data/item-ids'
6
+ import {JOB_QUEUE_CAP} from '../scheduling/jobs'
7
+ import {HoldKind, TaskType} from '../types'
8
+ import {
9
+ civicShuttleBays,
10
+ civicTransfersAtCap,
11
+ civicTransfersFrom,
12
+ pendingCivicTransfers,
13
+ selectCivicShuttleBay,
14
+ CIVIC_TRANSFER_PER_PLAYER_CAP,
15
+ } from './civic-shuttle'
16
+
17
+ type Lane = ServerContract.Types.lane
18
+ type ModuleEntry = ServerContract.Types.module_entry
19
+ type Task = ServerContract.Types.task
20
+
21
+ const T0 = new Date('2026-01-01T00:00:00.000Z')
22
+ const loaderStats = encodeStats([300, 500])
23
+ const storageStats = encodeStats([300, 300])
24
+
25
+ function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
26
+ return {
27
+ type: UInt8.from(0),
28
+ installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
29
+ } as unknown as ModuleEntry
30
+ }
31
+
32
+ // The live Depot layout: two loader bays then four storage.
33
+ function depotModules(): ModuleEntry[] {
34
+ return [
35
+ makeModuleEntry(ITEM_LOADER_T1, loaderStats),
36
+ makeModuleEntry(ITEM_LOADER_T1, loaderStats),
37
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
38
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
39
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
40
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
41
+ ]
42
+ }
43
+
44
+ function shuttleTask(senderId: number, receiverId: number, group?: number): Task {
45
+ return {
46
+ type: UInt8.from(TaskType.SHUTTLE),
47
+ duration: UInt32.from(60),
48
+ cancelable: UInt8.from(2),
49
+ cargo: [],
50
+ couplings: [
51
+ {
52
+ counterpart: {entity_id: UInt64.from(senderId), entity_type: UInt8.from(1)},
53
+ hold: UInt64.from(1),
54
+ kind: UInt8.from(HoldKind.PULL),
55
+ },
56
+ {
57
+ counterpart: {entity_id: UInt64.from(receiverId), entity_type: UInt8.from(1)},
58
+ hold: UInt64.from(1),
59
+ kind: UInt8.from(HoldKind.PUSH),
60
+ },
61
+ ],
62
+ entitygroup: group === undefined ? undefined : UInt64.from(group),
63
+ } as unknown as Task
64
+ }
65
+
66
+ function makeLane(laneKey: number, tasks: Task[]): Lane {
67
+ return {
68
+ lane_key: UInt8.from(laneKey),
69
+ schedule: {started: {toDate: () => T0}, tasks},
70
+ } as unknown as Lane
71
+ }
72
+
73
+ test('civicShuttleBays lists both Depot bays with their queue depth', () => {
74
+ const lanes = [makeLane(1, [shuttleTask(20, 21)])]
75
+ const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, lanes)
76
+ expect(bays.map((b) => b.laneKey)).toEqual([1, 2])
77
+ expect(bays.map((b) => b.slotIndex)).toEqual([0, 1])
78
+ expect(bays[0].queued).toBe(1)
79
+ expect(bays[1].queued).toBe(0)
80
+ expect(bays.every((b) => !b.full)).toBe(true)
81
+ expect(bays.every((b) => b.thrust > 0 && b.mass > 0)).toBe(true)
82
+ })
83
+
84
+ test('a bay is full at the job queue cap', () => {
85
+ const filled = Array.from({length: JOB_QUEUE_CAP}, () => shuttleTask(20, 21))
86
+ const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, [makeLane(1, filled)])
87
+ expect(bays[0].full).toBe(true)
88
+ expect(bays[1].full).toBe(false)
89
+ })
90
+
91
+ test('selectCivicShuttleBay takes the first free bay, then the lowest occupied one', () => {
92
+ const modules = depotModules()
93
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, [])?.laneKey).toBe(1)
94
+
95
+ const firstBusy = [makeLane(1, [shuttleTask(20, 21)])]
96
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, firstBusy)?.laneKey).toBe(2)
97
+
98
+ const bothBusy = [makeLane(1, [shuttleTask(20, 21)]), makeLane(2, [shuttleTask(22, 23)])]
99
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, bothBusy)?.laneKey).toBe(1)
100
+ })
101
+
102
+ test('selectCivicShuttleBay returns nothing when the building has no loaders', () => {
103
+ const modules = [makeModuleEntry(ITEM_STORAGE_T1, storageStats)]
104
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, [])).toBeUndefined()
105
+ })
106
+
107
+ test('pendingCivicTransfers reports the bay, both counterparties, and the tail', () => {
108
+ const lanes = [
109
+ makeLane(1, [shuttleTask(20, 21), shuttleTask(22, 23)]),
110
+ makeLane(2, [shuttleTask(24, 25)]),
111
+ ]
112
+ const transfers = pendingCivicTransfers({lanes} as never)
113
+ expect(transfers.length).toBe(3)
114
+
115
+ const first = transfers.find((t) => String(t.senderId) === '20')!
116
+ expect(first.laneKey).toBe(1)
117
+ expect(String(first.receiverId)).toBe('21')
118
+ expect(first.isTail).toBe(false)
119
+ expect(first.callable).toBe(false)
120
+ expect(first.startsAt).toEqual(T0)
121
+ expect(first.completesAt).toEqual(new Date(T0.getTime() + 60_000))
122
+
123
+ const second = transfers.find((t) => String(t.senderId) === '22')!
124
+ expect(second.isTail).toBe(true)
125
+ expect(second.callable).toBe(true)
126
+
127
+ const onBayTwo = transfers.find((t) => String(t.senderId) === '24')!
128
+ expect(onBayTwo.laneKey).toBe(2)
129
+ expect(onBayTwo.callable).toBe(true)
130
+ })
131
+
132
+ test('a grouped tail transfer is not callable', () => {
133
+ const lanes = [makeLane(1, [shuttleTask(20, 21, 7)])]
134
+ const [transfer] = pendingCivicTransfers({lanes} as never)
135
+ expect(transfer.isTail).toBe(true)
136
+ expect(transfer.callable).toBe(false)
137
+ })
138
+
139
+ test('civicTransfersFrom counts only the ships a player sends from', () => {
140
+ const lanes = [
141
+ makeLane(1, [shuttleTask(20, 21), shuttleTask(30, 31)]),
142
+ makeLane(2, [shuttleTask(22, 23)]),
143
+ ]
144
+ const entity = {lanes} as never
145
+ expect(civicTransfersFrom(entity, [20, 22]).length).toBe(2)
146
+ expect(civicTransfersFrom(entity, [30]).length).toBe(1)
147
+ expect(civicTransfersFrom(entity, [99]).length).toBe(0)
148
+ })
149
+
150
+ test('civicTransfersAtCap trips at the per-player cap', () => {
151
+ const mine = Array.from({length: CIVIC_TRANSFER_PER_PLAYER_CAP}, (_, i) =>
152
+ shuttleTask(20 + i, 50)
153
+ )
154
+ const senderIds = mine.map((_, i) => 20 + i)
155
+ const under = {lanes: [makeLane(1, mine.slice(0, -1))]} as never
156
+ const at = {lanes: [makeLane(1, mine)]} as never
157
+ expect(civicTransfersAtCap(under, senderIds)).toBe(false)
158
+ expect(civicTransfersAtCap(at, senderIds)).toBe(true)
159
+ })
@@ -0,0 +1,115 @@
1
+ import type {UInt64} from '@wharfkit/antelope'
2
+ import type {ServerContract} from '../contracts'
3
+ import {getItem} from '../data/catalog'
4
+ import {JOB_QUEUE_CAP} from '../scheduling/jobs'
5
+ import {laneKeyForModule, resolveLaneLoader} from '../scheduling/lanes'
6
+ import {orderedTasks, type ScheduleData} from '../scheduling/schedule'
7
+ import {HoldKind, TaskType} from '../types'
8
+
9
+ type CargoItem = ServerContract.Types.cargo_item
10
+ type Lane = ServerContract.Types.lane
11
+ type ModuleEntry = ServerContract.Types.module_entry
12
+
13
+ export const CIVIC_TRANSFER_PER_PLAYER_CAP = 4
14
+
15
+ export interface CivicShuttleBay {
16
+ laneKey: number
17
+ slotIndex: number
18
+ thrust: number
19
+ mass: number
20
+ outputPct: number
21
+ queued: number
22
+ full: boolean
23
+ }
24
+
25
+ export function civicShuttleBays(
26
+ modules: ModuleEntry[],
27
+ entityItemId: number,
28
+ lanes: Lane[]
29
+ ): CivicShuttleBay[] {
30
+ const bays: CivicShuttleBay[] = []
31
+ for (let slotIndex = 0; slotIndex < modules.length; slotIndex++) {
32
+ const installed = modules[slotIndex].installed
33
+ if (!installed) continue
34
+ if (getItem(installed.item_id).moduleType !== 'loader') continue
35
+ const laneKey = laneKeyForModule(slotIndex)
36
+ const loader = resolveLaneLoader(modules, entityItemId, laneKey)
37
+ const lane = lanes.find((l) => l.lane_key.toNumber() === laneKey)
38
+ const queued = lane ? lane.schedule.tasks.length : 0
39
+ bays.push({
40
+ laneKey,
41
+ slotIndex,
42
+ thrust: loader.thrust,
43
+ mass: loader.mass,
44
+ outputPct: loader.outputPct,
45
+ queued,
46
+ full: queued >= JOB_QUEUE_CAP,
47
+ })
48
+ }
49
+ return bays
50
+ }
51
+
52
+ // Mirrors worker_lane_key_or_mobility for loaders: first free bay, else the lowest bay.
53
+ export function selectCivicShuttleBay(
54
+ modules: ModuleEntry[],
55
+ entityItemId: number,
56
+ lanes: Lane[]
57
+ ): CivicShuttleBay | undefined {
58
+ const bays = civicShuttleBays(modules, entityItemId, lanes)
59
+ return bays.find((bay) => bay.queued === 0) ?? bays[0]
60
+ }
61
+
62
+ export interface PendingCivicTransfer {
63
+ laneKey: number
64
+ taskIndex: number
65
+ senderId: UInt64
66
+ receiverId: UInt64
67
+ cargo: CargoItem[]
68
+ startsAt: Date
69
+ completesAt: Date
70
+ isTail: boolean
71
+ callable: boolean
72
+ }
73
+
74
+ export function pendingCivicTransfers(entity: ScheduleData): PendingCivicTransfer[] {
75
+ const tailIndex = new Map<number, number>()
76
+ for (const l of entity.lanes ?? []) {
77
+ tailIndex.set(l.lane_key.toNumber(), l.schedule.tasks.length - 1)
78
+ }
79
+ const out: PendingCivicTransfer[] = []
80
+ for (const entry of orderedTasks(entity)) {
81
+ if (entry.task.type.toNumber() !== TaskType.SHUTTLE) continue
82
+ const pull = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PULL)
83
+ const push = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PUSH)
84
+ if (!pull || !push) continue
85
+ const isTail = tailIndex.get(entry.laneKey) === entry.taskIndex
86
+ out.push({
87
+ laneKey: entry.laneKey,
88
+ taskIndex: entry.taskIndex,
89
+ senderId: pull.counterpart.entity_id,
90
+ receiverId: push.counterpart.entity_id,
91
+ cargo: entry.task.cargo,
92
+ startsAt: entry.startsAt,
93
+ completesAt: entry.completesAt,
94
+ isTail,
95
+ callable: isTail && !entry.task.entitygroup,
96
+ })
97
+ }
98
+ return out
99
+ }
100
+
101
+ // Mirrors civic_transfers_for: the sender side of every shuttle the building holds, by owner.
102
+ export function civicTransfersFrom(
103
+ entity: ScheduleData,
104
+ senderIds: Array<UInt64 | string | number>
105
+ ): PendingCivicTransfer[] {
106
+ const owned = new Set(senderIds.map((id) => String(id)))
107
+ return pendingCivicTransfers(entity).filter((t) => owned.has(String(t.senderId)))
108
+ }
109
+
110
+ export function civicTransfersAtCap(
111
+ entity: ScheduleData,
112
+ senderIds: Array<UInt64 | string | number>
113
+ ): boolean {
114
+ return civicTransfersFrom(entity, senderIds).length >= CIVIC_TRANSFER_PER_PLAYER_CAP
115
+ }