@shipload/sdk 1.0.0-next.73 → 1.0.0-next.75

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 (42) hide show
  1. package/lib/shipload.d.ts +514 -127
  2. package/lib/shipload.js +5952 -4948
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +5936 -4942
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +174 -47
  7. package/lib/testing.js +539 -147
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +539 -147
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/contracts/server.ts +457 -134
  13. package/src/data/component-process.test.ts +119 -0
  14. package/src/data/component-process.ts +33 -0
  15. package/src/data/entities.json +2 -2
  16. package/src/data/metadata.ts +18 -5
  17. package/src/index-module.ts +29 -2
  18. package/src/influence/civic-shuttle.test.ts +58 -0
  19. package/src/influence/civic-shuttle.ts +52 -6
  20. package/src/influence/duration.ts +0 -65
  21. package/src/influence/index.ts +4 -5
  22. package/src/influence/influence.test.ts +51 -136
  23. package/src/influence/valuation.ts +75 -0
  24. package/src/managers/actions.ts +37 -10
  25. package/src/managers/balances.test.ts +33 -0
  26. package/src/managers/balances.ts +15 -0
  27. package/src/managers/context.ts +50 -11
  28. package/src/managers/index.ts +14 -0
  29. package/src/managers/jobs.test.ts +82 -1
  30. package/src/managers/jobs.ts +41 -2
  31. package/src/managers/nft.test.ts +173 -1
  32. package/src/managers/nft.ts +35 -2
  33. package/src/managers/shuttle.test.ts +562 -0
  34. package/src/managers/shuttle.ts +397 -0
  35. package/src/resolution/resolve-item.ts +4 -0
  36. package/src/scheduling/availability.ts +10 -0
  37. package/src/scheduling/energy.ts +55 -1
  38. package/src/scheduling/jobs.test.ts +171 -1
  39. package/src/scheduling/jobs.ts +110 -13
  40. package/src/scheduling/projection.ts +2 -1
  41. package/src/scheduling/task-effects.ts +38 -0
  42. package/src/shipload.ts +23 -4
@@ -0,0 +1,119 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {
3
+ ITEM_BEAM,
4
+ ITEM_ENGINE_T1,
5
+ ITEM_ORE_T1,
6
+ ITEM_PLATE,
7
+ ITEM_PLATE_T2,
8
+ ITEM_PROSPECTOR_T1A_PACKED,
9
+ ITEM_PROSPECTOR_T2A_PACKED,
10
+ ITEM_PROSPECTOR_T2B_PACKED,
11
+ ITEM_ROUSTABOUT_T1A_PACKED,
12
+ ITEM_STORAGE_T1,
13
+ } from './item-ids'
14
+ import {getComponents} from './catalog'
15
+ import {getRecipe} from './recipes-runtime'
16
+ import {completeComponentSet, getComponentProcess} from './component-process'
17
+ import {resolveItem} from '../resolution/resolve-item'
18
+
19
+ function inputsOf(itemId: number) {
20
+ const recipe = getRecipe(itemId)
21
+ if (!recipe) throw new Error(`no recipe for ${itemId}`)
22
+ return recipe.inputs
23
+ }
24
+
25
+ describe('component process', () => {
26
+ test('every component at every tier is refined or machined, five of each per tier', () => {
27
+ const tiers = new Set(getComponents().map((item) => item.tier))
28
+ for (const tier of tiers) {
29
+ const kinds = getComponents({tier}).map((item) => getComponentProcess(item.id))
30
+ expect(kinds.filter((k) => k === 'refined').length).toBe(5)
31
+ expect(kinds.filter((k) => k === 'machined').length).toBe(5)
32
+ }
33
+ })
34
+
35
+ test('the refined five are the single-resource components', () => {
36
+ const refined = getComponents({tier: 1})
37
+ .filter((item) => getComponentProcess(item.id) === 'refined')
38
+ .map((item) => item.name)
39
+ .sort()
40
+ expect(refined).toEqual(['Ceramic', 'Plasma Cell', 'Plate', 'Polymer', 'Sensor'])
41
+ })
42
+
43
+ test('the kind holds across tiers', () => {
44
+ expect(getComponentProcess(ITEM_PLATE)).toBe('refined')
45
+ expect(getComponentProcess(ITEM_PLATE_T2)).toBe('refined')
46
+ expect(getComponentProcess(ITEM_BEAM)).toBe('machined')
47
+ })
48
+
49
+ test('non-components and unknown ids have no kind', () => {
50
+ expect(getComponentProcess(ITEM_ORE_T1)).toBeUndefined()
51
+ expect(getComponentProcess(ITEM_ENGINE_T1)).toBeUndefined()
52
+ expect(getComponentProcess(ITEM_ROUSTABOUT_T1A_PACKED)).toBeUndefined()
53
+ expect(getComponentProcess(65535)).toBeUndefined()
54
+ })
55
+
56
+ test('resolveItem carries the kind for components only', () => {
57
+ expect(resolveItem(ITEM_BEAM).process).toBe('machined')
58
+ expect(resolveItem(ITEM_PLATE_T2).process).toBe('refined')
59
+ expect(resolveItem(ITEM_ORE_T1).process).toBeUndefined()
60
+ })
61
+ })
62
+
63
+ describe('completeComponentSet', () => {
64
+ test('recognises the hull First and Second step recipes', () => {
65
+ expect(completeComponentSet(inputsOf(ITEM_ROUSTABOUT_T1A_PACKED))).toEqual({
66
+ process: 'refined',
67
+ tier: 1,
68
+ quantity: 200,
69
+ })
70
+ expect(completeComponentSet(inputsOf(ITEM_PROSPECTOR_T1A_PACKED))).toEqual({
71
+ process: 'machined',
72
+ tier: 1,
73
+ quantity: 100,
74
+ })
75
+ expect(completeComponentSet(inputsOf(ITEM_PROSPECTOR_T2A_PACKED))).toEqual({
76
+ process: 'refined',
77
+ tier: 2,
78
+ quantity: 200,
79
+ })
80
+ expect(completeComponentSet(inputsOf(ITEM_PROSPECTOR_T2B_PACKED))).toEqual({
81
+ process: 'machined',
82
+ tier: 2,
83
+ quantity: 150,
84
+ })
85
+ })
86
+
87
+ test('a mixed module recipe is not a complete set', () => {
88
+ expect(completeComponentSet(inputsOf(ITEM_STORAGE_T1))).toBeNull()
89
+ })
90
+
91
+ test('unequal quantities keep the set but drop the shared quantity', () => {
92
+ const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input, index) =>
93
+ index === 0 ? {...input, quantity: 50} : input
94
+ )
95
+ expect(completeComponentSet(inputs)).toEqual({process: 'refined', tier: 1, quantity: null})
96
+ })
97
+
98
+ test('one machined part among four refined is not a set', () => {
99
+ const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input, index) =>
100
+ index === 0 ? {...input, itemId: ITEM_BEAM} : input
101
+ )
102
+ expect(completeComponentSet(inputs)).toBeNull()
103
+ })
104
+
105
+ test('a repeated family is not a set', () => {
106
+ const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input) => ({
107
+ ...input,
108
+ itemId: ITEM_PLATE,
109
+ }))
110
+ expect(completeComponentSet(inputs)).toBeNull()
111
+ })
112
+
113
+ test('mixed tiers are not a set', () => {
114
+ const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input, index) =>
115
+ index === 0 ? {...input, itemId: ITEM_PLATE_T2} : input
116
+ )
117
+ expect(completeComponentSet(inputs)).toBeNull()
118
+ })
119
+ })
@@ -0,0 +1,33 @@
1
+ import {getItem} from './catalog'
2
+ import {getItemFamily, type ComponentProcess} from './metadata'
3
+ import type {RecipeInput} from './recipes-runtime'
4
+
5
+ export const COMPONENT_PROCESS_ORDER: readonly ComponentProcess[] = ['refined', 'machined']
6
+
7
+ export function getComponentProcess(itemId: number): ComponentProcess | undefined {
8
+ return getItemFamily(itemId)?.process
9
+ }
10
+
11
+ export interface CompleteComponentSet {
12
+ process: ComponentProcess
13
+ tier: number
14
+ quantity: number | null
15
+ }
16
+
17
+ export function completeComponentSet(inputs: readonly RecipeInput[]): CompleteComponentSet | null {
18
+ const parts = inputs.filter((input) => getComponentProcess(input.itemId) !== undefined)
19
+ if (parts.length !== 5) return null
20
+ const process = getComponentProcess(parts[0].itemId)
21
+ const tier = getItem(parts[0].itemId).tier
22
+ const families = new Set<string>()
23
+ for (const part of parts) {
24
+ const item = getItem(part.itemId)
25
+ if (getComponentProcess(part.itemId) !== process || item.tier !== tier) return null
26
+ families.add(item.family)
27
+ }
28
+ if (!process || families.size !== 5) return null
29
+ const quantity = parts.every((part) => part.quantity === parts[0].quantity)
30
+ ? parts[0].quantity
31
+ : null
32
+ return {process, tier, quantity}
33
+ }
@@ -108,12 +108,12 @@
108
108
  "slots": [
109
109
  {
110
110
  "type": "builder",
111
- "outputPct": 200,
111
+ "outputPct": 75,
112
112
  "maxTier": 1
113
113
  },
114
114
  {
115
115
  "type": "builder",
116
- "outputPct": 200,
116
+ "outputPct": 75,
117
117
  "maxTier": 1
118
118
  }
119
119
  ],
@@ -1,9 +1,12 @@
1
1
  import items from './items.json'
2
2
 
3
+ export type ComponentProcess = 'refined' | 'machined'
4
+
3
5
  export interface ItemFamily {
4
6
  name: string
5
7
  description: string
6
8
  color: string
9
+ process?: ComponentProcess
7
10
  }
8
11
 
9
12
  export interface EntityMetadata {
@@ -14,27 +17,27 @@ export const itemFamilies: Record<string, ItemFamily> = {
14
17
  // === Resources ===
15
18
  ore: {
16
19
  name: 'Ore',
17
- description: 'Refines into Plate and Beam.',
20
+ description: 'Goes into Plate and Beam.',
18
21
  color: '#C26D3F',
19
22
  },
20
23
  crystal: {
21
24
  name: 'Crystal',
22
- description: 'Refines into Sensors and Resonators.',
25
+ description: 'Goes into Sensors and Resonators.',
23
26
  color: '#4ADBFF',
24
27
  },
25
28
  gas: {
26
29
  name: 'Gas',
27
- description: 'Refines into Plasma Cells and Reactors.',
30
+ description: 'Goes into Plasma Cells and Reactors.',
28
31
  color: '#B877FF',
29
32
  },
30
33
  regolith: {
31
34
  name: 'Regolith',
32
- description: 'Refines into Ceramic and Frame.',
35
+ description: 'Goes into Ceramic and Frame.',
33
36
  color: '#C4A57B',
34
37
  },
35
38
  biomass: {
36
39
  name: 'Biomass',
37
- description: 'Refines into Polymer and Resin.',
40
+ description: 'Goes into Polymer and Resin.',
38
41
  color: '#5A8B3E',
39
42
  },
40
43
 
@@ -43,51 +46,61 @@ export const itemFamilies: Record<string, ItemFamily> = {
43
46
  name: 'Plate',
44
47
  description: 'Goes into Cargo Holds, Containers, and hulls.',
45
48
  color: '#7B8D9E',
49
+ process: 'refined',
46
50
  },
47
51
  frame: {
48
52
  name: 'Frame',
49
53
  description: 'Goes into Limpet Bays, Cargo Holds, and hulls.',
50
54
  color: '#C4A57B',
55
+ process: 'machined',
51
56
  },
52
57
  'plasma-cell': {
53
58
  name: 'Plasma Cell',
54
59
  description: 'Goes into Engines, Battery Banks, and hulls.',
55
60
  color: '#E86344',
61
+ process: 'refined',
56
62
  },
57
63
  resonator: {
58
64
  name: 'Resonator',
59
65
  description: 'Goes into Power Cores, Tractor Beams, Warp Drives, and hulls.',
60
66
  color: '#4ADBFF',
67
+ process: 'machined',
61
68
  },
62
69
  beam: {
63
70
  name: 'Beam',
64
71
  description: 'Goes into Limpet Bays and hulls.',
65
72
  color: '#7B8D9E',
73
+ process: 'machined',
66
74
  },
67
75
  sensor: {
68
76
  name: 'Sensor',
69
77
  description: 'Goes into Fabricators, Assembly Arms, and hulls.',
70
78
  color: '#4ADBFF',
79
+ process: 'refined',
71
80
  },
72
81
  polymer: {
73
82
  name: 'Polymer',
74
83
  description: 'Goes into Shuttle Bays, Battery Banks, and hulls.',
75
84
  color: '#5A8B3E',
85
+ process: 'refined',
76
86
  },
77
87
  ceramic: {
78
88
  name: 'Ceramic',
79
89
  description: 'Goes into Assembly Arms, Containers, and hulls.',
80
90
  color: '#C4A57B',
91
+ process: 'refined',
81
92
  },
82
93
  reactor: {
83
94
  name: 'Reactor',
84
95
  description: 'Goes into Fabricators, Warp Drives, and hulls.',
85
96
  color: '#B877FF',
97
+ process: 'machined',
86
98
  },
87
99
  resin: {
88
100
  name: 'Resin',
89
101
  description: 'Goes into Tractor Beams and hulls.',
90
102
  color: '#5A8B3E',
103
+ process: 'machined',
91
104
  },
92
105
 
93
106
  // === Modules ===
@@ -62,6 +62,22 @@ export {
62
62
  JobsManager,
63
63
  InfluenceManager,
64
64
  } from './managers'
65
+ export {
66
+ ShuttleManager,
67
+ rankShuttleOptions,
68
+ shuttleReasonCode,
69
+ shuttleCandidates,
70
+ BOOKING_LEVEL_CODES,
71
+ OMITTED_REASONS,
72
+ } from './managers/shuttle'
73
+ export type {
74
+ ShuttleMode,
75
+ ShuttleOption,
76
+ ShuttleOptions,
77
+ ShuttleReason,
78
+ ShuttleReasonCode,
79
+ CandidateEntity,
80
+ } from './managers/shuttle'
65
81
  export type {GridCell, ClusterCell, Cluster} from './managers'
66
82
  export type {
67
83
  CharterProgress,
@@ -125,7 +141,13 @@ export {
125
141
  categoryLabelFromIndex,
126
142
  } from './data/catalog'
127
143
  export {getItemFamily, itemFamilies, itemFamilyKey} from './data/metadata'
128
- export type {ItemFamily} from './data/metadata'
144
+ export type {ComponentProcess, ItemFamily} from './data/metadata'
145
+ export {
146
+ COMPONENT_PROCESS_ORDER,
147
+ completeComponentSet,
148
+ getComponentProcess,
149
+ } from './data/component-process'
150
+ export type {CompleteComponentSet} from './data/component-process'
129
151
  export {getCurrentEpoch, getEpochInfo} from './scheduling/epoch'
130
152
  export type {EpochInfo} from './scheduling/epoch'
131
153
  export {
@@ -341,6 +363,7 @@ export {
341
363
  jobDropoffTask,
342
364
  jobCancelRoute,
343
365
  buildJobCancelRoute,
366
+ hostedDropoff,
344
367
  jobCancellable,
345
368
  jobCancellationBlockReason,
346
369
  jobDeposited,
@@ -356,7 +379,10 @@ export type {
356
379
  JobStatusInput,
357
380
  JobCancelRoute,
358
381
  JobCancellationBlockReason,
382
+ JobRouteOptions,
383
+ HostedLeg,
359
384
  OwnedJob,
385
+ BuildJob,
360
386
  PickupInFlight,
361
387
  } from './scheduling/jobs'
362
388
 
@@ -400,6 +426,7 @@ export {
400
426
  cargoInputKey,
401
427
  hasSourceCoupling,
402
428
  hasIncomingCoupling,
429
+ isHostedCivicLeg,
403
430
  } from './scheduling/availability'
404
431
  export type {CargoInput, IncomingHoldEntity, IncomingSource} from './scheduling/availability'
405
432
 
@@ -409,7 +436,7 @@ export {calcClusterIntake, calcClustercraftDuration, INTAKE_RATE} from './capabi
409
436
 
410
437
  export {maxCraftable} from './capabilities/craftable'
411
438
 
412
- export {energyAtTime} from './scheduling/energy'
439
+ export {energyAtTime, energyDrawsFunded} from './scheduling/energy'
413
440
 
414
441
  export * from './types/capabilities'
415
442
  export * from './types/entity'
@@ -12,6 +12,7 @@ import {
12
12
  pendingCivicTransfers,
13
13
  selectCivicShuttleBay,
14
14
  CIVIC_TRANSFER_PER_PLAYER_CAP,
15
+ type CivicShuttleBay,
15
16
  } from './civic-shuttle'
16
17
 
17
18
  type Lane = ServerContract.Types.lane
@@ -63,6 +64,32 @@ function shuttleTask(senderId: number, receiverId: number, group?: number): Task
63
64
  } as unknown as Task
64
65
  }
65
66
 
67
+ function civicTask(type: number, couplings: unknown[]): Task {
68
+ return {
69
+ type: UInt8.from(type),
70
+ duration: UInt32.from(60),
71
+ cancelable: UInt8.from(2),
72
+ cargo: [],
73
+ couplings,
74
+ } as unknown as Task
75
+ }
76
+
77
+ function pullCoupling(id: number) {
78
+ return {
79
+ counterpart: {entity_id: UInt64.from(id), entity_type: UInt8.from(1)},
80
+ hold: UInt64.from(1),
81
+ kind: UInt8.from(HoldKind.PULL),
82
+ }
83
+ }
84
+
85
+ function pushCoupling(id: number) {
86
+ return {
87
+ counterpart: {entity_id: UInt64.from(id), entity_type: UInt8.from(1)},
88
+ hold: UInt64.from(1),
89
+ kind: UInt8.from(HoldKind.PUSH),
90
+ }
91
+ }
92
+
66
93
  function makeLane(laneKey: number, tasks: Task[]): Lane {
67
94
  return {
68
95
  lane_key: UInt8.from(laneKey),
@@ -142,6 +169,13 @@ test('selectCivicShuttleBay takes the first free bay, then the lowest occupied o
142
169
  expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, bothBusy, T0)?.laneKey).toBe(1)
143
170
  })
144
171
 
172
+ test('selectCivicShuttleBay picks the earliest finish, not the first free bay', () => {
173
+ const modules = depotModules()
174
+ const lanes = [makeLane(1, [shuttleTask(20, 21)])]
175
+ const fast = (bay: CivicShuttleBay) => (bay.slotIndex === 0 ? 10 : 500)
176
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, lanes, T0, fast)?.slotIndex).toBe(0)
177
+ })
178
+
145
179
  test('selectCivicShuttleBay returns nothing when the building has no loaders', () => {
146
180
  const modules = [makeModuleEntry(ITEM_STORAGE_T1, storageStats)]
147
181
  expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, [], T0)).toBeUndefined()
@@ -190,6 +224,30 @@ test('civicTransfersFrom counts only the ships a player sends from', () => {
190
224
  expect(civicTransfersFrom(entity, [99], T0).length).toBe(0)
191
225
  })
192
226
 
227
+ test('civic legs on bays count toward the player cap', () => {
228
+ const depot = {
229
+ lanes: [makeLane(1, [civicTask(TaskType.CIVIC_WITHDRAW, [pushCoupling(7)])])],
230
+ } as never
231
+ expect(civicTransfersFrom(depot, [7]).length).toBe(1)
232
+ })
233
+
234
+ test('a civic deposit leg reports the same player id as sender and receiver', () => {
235
+ const depot = {
236
+ lanes: [makeLane(1, [civicTask(TaskType.CIVIC_DEPOSIT, [pullCoupling(7)])])],
237
+ } as never
238
+ const [transfer] = pendingCivicTransfers(depot, T0)
239
+ expect(String(transfer.senderId)).toBe('7')
240
+ expect(String(transfer.receiverId)).toBe('7')
241
+ })
242
+
243
+ test('a civic pickup leg is not callable while CANCEL_NEVER', () => {
244
+ const task = civicTask(TaskType.CIVIC_WITHDRAW, [pushCoupling(7)])
245
+ ;(task as unknown as {cancelable: unknown}).cancelable = UInt8.from(0)
246
+ const depot = {lanes: [makeLane(1, [task])]} as never
247
+ const [transfer] = pendingCivicTransfers(depot, T0)
248
+ expect(transfer.callable).toBe(false)
249
+ })
250
+
193
251
  test('civicTransfersAtCap trips at the per-player cap', () => {
194
252
  const mine = Array.from({length: CIVIC_TRANSFER_PER_PLAYER_CAP}, (_, i) =>
195
253
  shuttleTask(20 + i, 50)
@@ -4,7 +4,7 @@ import {getItem} from '../data/catalog'
4
4
  import {JOB_QUEUE_CAP} from '../scheduling/jobs'
5
5
  import {laneKeyForModule, resolveLaneLoader} from '../scheduling/lanes'
6
6
  import {orderedTasks, type ScheduleData} from '../scheduling/schedule'
7
- import {HoldKind, TaskType} from '../types'
7
+ import {HoldKind, TaskCancelable, TaskType} from '../types'
8
8
 
9
9
  type CargoItem = ServerContract.Types.cargo_item
10
10
  type Lane = ServerContract.Types.lane
@@ -22,7 +22,6 @@ export interface CivicShuttleBay {
22
22
  full: boolean
23
23
  }
24
24
 
25
- // a landed booking clears during the next civicshuttle, so only unfinished tasks occupy a bay
26
25
  function unfinishedTasks(lane: Lane | undefined, now: Date): number {
27
26
  if (!lane) return 0
28
27
  const startedMs = lane.schedule.started.toDate().getTime()
@@ -64,15 +63,38 @@ export function civicShuttleBays(
64
63
  return bays
65
64
  }
66
65
 
67
- // Mirrors worker_lane_key_or_mobility for loaders: first free bay, else the lowest bay.
66
+ function laneEndMs(lane: Lane | undefined, nowMs: number): number {
67
+ if (!lane) return nowMs
68
+ let endSec = 0
69
+ for (const task of lane.schedule.tasks) endSec += task.duration.toNumber()
70
+ return Math.max(nowMs, lane.schedule.started.toDate().getTime() + endSec * 1000)
71
+ }
72
+
73
+ // Mirrors cargo.cpp pick_civic_bay: earliest finish among bays below the queue cap.
68
74
  export function selectCivicShuttleBay(
69
75
  modules: ModuleEntry[],
70
76
  entityItemId: number,
71
77
  lanes: Lane[],
72
- now: Date = new Date()
78
+ now: Date = new Date(),
79
+ durationFor: (bay: CivicShuttleBay) => number = () => 0,
80
+ minStart?: Date
73
81
  ): CivicShuttleBay | undefined {
74
82
  const bays = civicShuttleBays(modules, entityItemId, lanes, now)
75
- return bays.find((bay) => bay.queued === 0) ?? bays[0]
83
+ const nowMs = now.getTime()
84
+ const floorMs = Math.max(nowMs, minStart?.getTime() ?? nowMs)
85
+ let best: CivicShuttleBay | undefined
86
+ let bestEnd = Infinity
87
+ for (const bay of bays) {
88
+ if (bay.full) continue
89
+ const lane = lanes.find((l) => l.lane_key.toNumber() === bay.laneKey)
90
+ const start = Math.max(laneEndMs(lane, nowMs), floorMs)
91
+ const end = start + durationFor(bay) * 1000
92
+ if (end < bestEnd) {
93
+ best = bay
94
+ bestEnd = end
95
+ }
96
+ }
97
+ return best ?? bays[0]
76
98
  }
77
99
 
78
100
  export interface PendingCivicTransfer {
@@ -98,7 +120,31 @@ export function pendingCivicTransfers(
98
120
  }
99
121
  const out: PendingCivicTransfer[] = []
100
122
  for (const entry of orderedTasks(entity)) {
101
- if (entry.task.type.toNumber() !== TaskType.SHUTTLE) continue
123
+ const type = entry.task.type.toNumber()
124
+ if (type === TaskType.CIVIC_DEPOSIT || type === TaskType.CIVIC_WITHDRAW) {
125
+ const side = entry.task.couplings[0]
126
+ if (!side) continue
127
+ const isTail = tailIndex.get(entry.laneKey) === entry.taskIndex
128
+ const complete = entry.completesAt.getTime() <= now.getTime()
129
+ out.push({
130
+ laneKey: entry.laneKey,
131
+ taskIndex: entry.taskIndex,
132
+ senderId: side.counterpart.entity_id,
133
+ receiverId: side.counterpart.entity_id,
134
+ cargo: entry.task.cargo,
135
+ startsAt: entry.startsAt,
136
+ completesAt: entry.completesAt,
137
+ isTail,
138
+ complete,
139
+ callable:
140
+ isTail &&
141
+ !complete &&
142
+ !entry.task.entitygroup &&
143
+ entry.task.cancelable.toNumber() !== TaskCancelable.NEVER,
144
+ })
145
+ continue
146
+ }
147
+ if (type !== TaskType.SHUTTLE) continue
102
148
  const pull = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PULL)
103
149
  const push = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PUSH)
104
150
  if (!pull || !push) continue
@@ -1,9 +1,4 @@
1
- import type {NameType} from '@wharfkit/antelope'
2
- import type {ServerContract} from '../contracts'
3
- import {getEntityClass} from '../data/kind-registry'
4
1
  import {computeLoaderMass, computeLoaderThrust} from '../nft/description'
5
- import {laneKeyForModule, resolveLaneLoader} from '../scheduling/lanes'
6
- import {calc_onesided_duration} from '../travel/travel'
7
2
  import {MASS_STAT_SCALE, MASS_UNITS_PER_TONNE, PRECISION} from '../types'
8
3
  import {CIVIC_LOADER_STAT, CIVIC_LOADER_TIER, DEFAULT_ORBITAL_Z} from './constants'
9
4
 
@@ -39,63 +34,3 @@ export function contributeDuration(totalMass: number, altitudeZ = 0): number {
39
34
  export function contributeDurationForTonnes(tonnes: number, altitudeZ = 0): number {
40
35
  return contributeDuration(Math.floor(tonnes * MASS_UNITS_PER_TONNE), altitudeZ)
41
36
  }
42
-
43
- export const DEPOT_LOADER_SLOT = 0
44
-
45
- export interface DepotTransferParams {
46
- depotModules: ServerContract.Types.module_entry[]
47
- depotItemId: number
48
- depotKind: NameType
49
- depotZ: number
50
- shipKind: NameType
51
- shipZ: number
52
- cargoMass: number
53
- }
54
-
55
- // Mirrors depot.cpp: the depot's slot-0 loader drives the transfer; 0 when no loader is installed.
56
- export function depotTransferDuration(params: DepotTransferParams): number {
57
- const loader = resolveLaneLoader(
58
- params.depotModules,
59
- params.depotItemId,
60
- laneKeyForModule(DEPOT_LOADER_SLOT)
61
- )
62
- if (!loader.valid) return 0
63
- return calc_onesided_duration(
64
- loader.thrust,
65
- loader.mass,
66
- params.shipZ,
67
- params.depotZ,
68
- getEntityClass(params.shipKind),
69
- getEntityClass(params.depotKind),
70
- params.cargoMass
71
- )
72
- }
73
-
74
- export interface CivicDropoffParams {
75
- buildingModules: ServerContract.Types.module_entry[]
76
- buildingItemId: number
77
- buildingKind: NameType
78
- buildingZ: number
79
- shipKind: NameType
80
- shipZ: number
81
- cargoMass: number
82
- }
83
-
84
- // Mirrors jobs.cpp craftjob: the building's slot-0 loader when one is installed, the civic loader when none is.
85
- export function civicDropoffDuration(params: CivicDropoffParams): number {
86
- const loader = resolveLaneLoader(
87
- params.buildingModules,
88
- params.buildingItemId,
89
- laneKeyForModule(DEPOT_LOADER_SLOT)
90
- )
91
- if (!loader.valid) return contributeDuration(params.cargoMass, params.shipZ)
92
- return calc_onesided_duration(
93
- loader.thrust,
94
- loader.mass,
95
- params.shipZ,
96
- params.buildingZ,
97
- getEntityClass(params.shipKind),
98
- getEntityClass(params.buildingKind),
99
- params.cargoMass
100
- )
101
- }
@@ -26,24 +26,23 @@ export {
26
26
  export {
27
27
  componentBaseAtomic,
28
28
  componentEffortAtomic,
29
+ explainCargoItem,
29
30
  pricingFromWeights,
30
31
  resourceValueAtomic,
31
32
  valueCargoItem,
32
33
  valueContribution,
33
34
  weightedQualityAtomic,
35
+ type CargoValuation,
36
+ type ComponentValuation,
34
37
  type InfluencePricing,
38
+ type ResourceValuation,
35
39
  type ValuedItem,
36
40
  } from './valuation'
37
41
  export {
38
- civicDropoffDuration,
39
42
  civicLoader,
40
43
  contributeDuration,
41
44
  contributeDurationForTonnes,
42
- depotTransferDuration,
43
- DEPOT_LOADER_SLOT,
44
- type CivicDropoffParams,
45
45
  type CivicLoaderStats,
46
- type DepotTransferParams,
47
46
  } from './duration'
48
47
  export {
49
48
  civicShuttleBays,