@shipload/sdk 1.0.0-next.42 → 1.0.0-next.44

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 +222 -172
  2. package/lib/shipload.js +658 -421
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +644 -423
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +50 -67
  7. package/lib/testing.js +168 -208
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +169 -209
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/contracts/server.ts +126 -214
  13. package/src/data/capabilities.ts +22 -14
  14. package/src/data/capability-formulas.ts +7 -7
  15. package/src/data/entities.json +4 -0
  16. package/src/data/item-ids.ts +1 -0
  17. package/src/data/items.json +6 -0
  18. package/src/data/kind-registry.json +18 -6
  19. package/src/data/kind-registry.ts +7 -0
  20. package/src/data/metadata.ts +22 -15
  21. package/src/data/recipes.json +147 -138
  22. package/src/derivation/build-methods.test.ts +13 -0
  23. package/src/derivation/build-methods.ts +5 -2
  24. package/src/derivation/capabilities.ts +8 -8
  25. package/src/derivation/capability-mappings.ts +49 -4
  26. package/src/index-module.ts +21 -1
  27. package/src/managers/actions.ts +42 -8
  28. package/src/managers/cluster.test.ts +39 -0
  29. package/src/managers/cluster.ts +53 -0
  30. package/src/managers/context.ts +9 -0
  31. package/src/managers/index.ts +2 -0
  32. package/src/nft/buildImmutableData.ts +15 -17
  33. package/src/nft/description.ts +8 -8
  34. package/src/resolution/describe-module.ts +6 -6
  35. package/src/resolution/resolve-item.ts +3 -3
  36. package/src/scheduling/unwrap.test.ts +60 -0
  37. package/src/scheduling/unwrap.ts +187 -0
  38. package/src/shipload.ts +5 -0
  39. package/src/subscriptions/manager.cluster.test.ts +46 -0
  40. package/src/subscriptions/manager.ts +22 -0
  41. package/src/subscriptions/types.ts +16 -0
  42. package/src/types.ts +7 -0
@@ -17,7 +17,7 @@ import {
17
17
  type UInt64Type,
18
18
  } from '@wharfkit/antelope'
19
19
  import {BaseManager} from './base'
20
- import {Coordinates, PRECISION, type CoordinatesType} from '../types'
20
+ import {Coordinates, PRECISION, type ClusterSlotType, type CoordinatesType} from '../types'
21
21
  import {ServerContract} from '../contracts'
22
22
  import {ATOMICASSETS_ABI, SHIPLOAD_COLLECTION} from '../nft/atomicassets'
23
23
  import {getItem} from '../data/catalog'
@@ -435,22 +435,39 @@ export class ActionsManager extends BaseManager {
435
435
  })
436
436
  }
437
437
 
438
- deploy(entityId: UInt64Type, ref: ServerContract.ActionParams.Type.cargo_ref): Action {
438
+ deploy(
439
+ entityId: UInt64Type,
440
+ ref: ServerContract.ActionParams.Type.cargo_ref,
441
+ slot?: ClusterSlotType
442
+ ): Action {
439
443
  return this.server.action('deploy', {
440
444
  id: UInt64.from(entityId),
441
445
  ref,
446
+ slot: slot ? {hub: UInt64.from(slot.hub), gx: slot.gx, gy: slot.gy} : undefined,
442
447
  })
443
448
  }
444
449
 
445
- claimplot(
446
- entityId: UInt64Type,
447
- targetItemId: UInt16Type,
448
- coords: ServerContract.ActionParams.Type.coordinates
449
- ): Action {
450
+ claimplot(entityId: UInt64Type, targetItemId: UInt16Type, slot: ClusterSlotType): Action {
450
451
  return this.server.action('claimplot', {
451
452
  builder_id: UInt64.from(entityId),
452
453
  target_item_id: UInt16.from(targetItemId),
453
- coords,
454
+ slot: {hub: UInt64.from(slot.hub), gx: slot.gx, gy: slot.gy},
455
+ })
456
+ }
457
+
458
+ movetile(
459
+ hubId: UInt64Type,
460
+ fromGx: number,
461
+ fromGy: number,
462
+ toGx: number,
463
+ toGy: number
464
+ ): Action {
465
+ return this.server.action('movetile', {
466
+ hub_id: UInt64.from(hubId),
467
+ from_gx: fromGx,
468
+ from_gy: fromGy,
469
+ to_gx: toGx,
470
+ to_gy: toGy,
454
471
  })
455
472
  }
456
473
 
@@ -593,6 +610,23 @@ export class ActionsManager extends BaseManager {
593
610
  )
594
611
  }
595
612
 
613
+ sendAsset(owner: NameType, recipient: NameType, assetId: UInt64Type, memo = ''): Action {
614
+ return Action.from(
615
+ {
616
+ account: this.atomicAssetsAccount,
617
+ name: 'transfer',
618
+ authorization: [{actor: Name.from(owner), permission: 'active'}],
619
+ data: {
620
+ from: Name.from(owner),
621
+ to: Name.from(recipient),
622
+ asset_ids: [UInt64.from(assetId)],
623
+ memo,
624
+ },
625
+ },
626
+ ATOMICASSETS_ABI
627
+ )
628
+ }
629
+
596
630
  // Two top-level actions the wallet signs to unwrap an NFT into a host's cargo.
597
631
  unwrapCargoTx(owner: NameType, assetId: UInt64Type, hostId: UInt64Type): Action[] {
598
632
  return [this.transferForUnwrap(owner, assetId), this.placecargo(owner, hostId, assetId)]
@@ -0,0 +1,39 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {computeFreeCells} from './cluster'
3
+
4
+ describe('computeFreeCells', () => {
5
+ test('returns footprint cells that are not occupied', () => {
6
+ const footprint = [
7
+ {gx: 2, gy: 0},
8
+ {gx: 0, gy: 2},
9
+ {gx: 1, gy: 1},
10
+ ]
11
+ const occupied = [{gx: 2, gy: 0, entity: 7}]
12
+ expect(computeFreeCells(footprint, occupied)).toEqual([
13
+ {gx: 0, gy: 2},
14
+ {gx: 1, gy: 1},
15
+ ])
16
+ })
17
+
18
+ test('matches occupancy on signed coordinates', () => {
19
+ const footprint = [
20
+ {gx: -2, gy: 0},
21
+ {gx: 0, gy: -2},
22
+ {gx: -1, gy: 1},
23
+ ]
24
+ const occupied = [{gx: 0, gy: -2, entity: 9}]
25
+ expect(computeFreeCells(footprint, occupied)).toEqual([
26
+ {gx: -2, gy: 0},
27
+ {gx: -1, gy: 1},
28
+ ])
29
+ })
30
+
31
+ test('empty footprint yields no free cells', () => {
32
+ expect(computeFreeCells([], [])).toEqual([])
33
+ })
34
+
35
+ test('fully occupied footprint yields no free cells', () => {
36
+ const footprint = [{gx: 1, gy: 1}]
37
+ expect(computeFreeCells(footprint, [{gx: 1, gy: 1, entity: 3}])).toEqual([])
38
+ })
39
+ })
@@ -0,0 +1,53 @@
1
+ import {UInt16, type UInt16Type, UInt64, type UInt64Type} from '@wharfkit/antelope'
2
+ import {BaseManager} from './base'
3
+ import type {ServerContract} from '../contracts'
4
+
5
+ export interface GridCell {
6
+ gx: number
7
+ gy: number
8
+ }
9
+
10
+ export interface ClusterCell extends GridCell {
11
+ entity: number
12
+ }
13
+
14
+ export interface Cluster {
15
+ root: number
16
+ cells: ClusterCell[]
17
+ }
18
+
19
+ export function computeFreeCells(footprint: GridCell[], occupied: ClusterCell[]): GridCell[] {
20
+ const taken = new Set(occupied.map((c) => `${c.gx},${c.gy}`))
21
+ return footprint.filter((c) => !taken.has(`${c.gx},${c.gy}`))
22
+ }
23
+
24
+ export class ClusterManager extends BaseManager {
25
+ async getFootprint(itemId: UInt16Type): Promise<GridCell[]> {
26
+ const res = (await this.server.readonly('getfootprint', {
27
+ item_id: UInt16.from(itemId),
28
+ })) as ServerContract.Types.footprint_result
29
+ return res.cells.map((c) => ({gx: Number(c.gx), gy: Number(c.gy)}))
30
+ }
31
+
32
+ async getCluster(hubId: UInt64Type): Promise<Cluster> {
33
+ const res = (await this.server.readonly('getcluster', {
34
+ hub_id: UInt64.from(hubId),
35
+ })) as ServerContract.Types.cluster_row
36
+ return {
37
+ root: Number(res.root),
38
+ cells: res.cells.map((c) => ({
39
+ gx: Number(c.gx),
40
+ gy: Number(c.gy),
41
+ entity: Number(c.entity),
42
+ })),
43
+ }
44
+ }
45
+
46
+ async freeCells(hubId: UInt64Type, hubItemId: UInt16Type): Promise<GridCell[]> {
47
+ const [footprint, cluster] = await Promise.all([
48
+ this.getFootprint(hubItemId),
49
+ this.getCluster(hubId),
50
+ ])
51
+ return computeFreeCells(footprint, cluster.cells)
52
+ }
53
+ }
@@ -9,6 +9,7 @@ import {LocationsManager} from './locations'
9
9
  import {CoordinatesManager} from './coordinates'
10
10
  import {EpochsManager} from './epochs'
11
11
  import {ActionsManager} from './actions'
12
+ import {ClusterManager} from './cluster'
12
13
  import {NftManager} from './nft'
13
14
  import {SubscriptionsManager} from '../subscriptions/manager'
14
15
 
@@ -19,6 +20,7 @@ export class GameContext {
19
20
  private _coordinates?: CoordinatesManager
20
21
  private _epochs?: EpochsManager
21
22
  private _actions?: ActionsManager
23
+ private _clusters?: ClusterManager
22
24
  private _nft?: NftManager
23
25
  private _subscriptions?: SubscriptionsManager
24
26
  private _subscriptionsUrl?: string
@@ -75,6 +77,13 @@ export class GameContext {
75
77
  return this._actions
76
78
  }
77
79
 
80
+ get clusters(): ClusterManager {
81
+ if (!this._clusters) {
82
+ this._clusters = new ClusterManager(this)
83
+ }
84
+ return this._clusters
85
+ }
86
+
78
87
  get nft(): NftManager {
79
88
  if (!this._nft) {
80
89
  this._nft = new NftManager(this)
@@ -8,6 +8,8 @@ export {LocationsManager} from './locations'
8
8
  export type {LocationStratum} from './locations'
9
9
  export {EpochsManager} from './epochs'
10
10
  export {ActionsManager} from './actions'
11
+ export {ClusterManager, computeFreeCells} from './cluster'
12
+ export type {GridCell, ClusterCell, Cluster} from './cluster'
11
13
  export {NftManager} from './nft'
12
14
  export type {NftConfigForItem} from './nft'
13
15
  export {PlotManager} from './plot'
@@ -198,16 +198,14 @@ export function buildModuleImmutable(
198
198
  }
199
199
  case MODULE_GATHERER: {
200
200
  const str = decodeStat(stats, 0)
201
- const tol = decodeStat(stats, 1)
202
- const con = decodeStat(stats, 2)
203
- const ref = decodeStat(stats, 3)
201
+ const hrd = decodeStat(stats, 1)
202
+ const sat = decodeStat(stats, 2)
204
203
  base.push({first: 'strength', second: ['uint16', str]})
205
- base.push({first: 'tolerance', second: ['uint16', tol]})
206
- base.push({first: 'saturation', second: ['uint16', con]})
207
- base.push({first: 'plasticity', second: ['uint16', ref]})
204
+ base.push({first: 'hardness', second: ['uint16', hrd]})
205
+ base.push({first: 'saturation', second: ['uint16', sat]})
208
206
  base.push({first: 'yield', second: ['uint16', computeGathererYield(str)]})
209
- base.push({first: 'drain', second: ['uint16', computeGathererDrain(con)]})
210
- base.push({first: 'depth', second: ['uint16', computeGathererDepth(tol, item.tier)]})
207
+ base.push({first: 'drain', second: ['uint16', computeGathererDrain(sat)]})
208
+ base.push({first: 'depth', second: ['uint16', computeGathererDepth(hrd, item.tier)]})
211
209
  break
212
210
  }
213
211
  case MODULE_LOADER: {
@@ -220,17 +218,17 @@ export function buildModuleImmutable(
220
218
  break
221
219
  }
222
220
  case MODULE_WARP: {
223
- const res = decodeStat(stats, 0)
224
- base.push({first: 'resonance', second: ['uint16', res]})
225
- base.push({first: 'range', second: ['uint32', computeWarpRange(res)]})
221
+ const ref = decodeStat(stats, 0)
222
+ base.push({first: 'reflectivity', second: ['uint16', ref]})
223
+ base.push({first: 'range', second: ['uint32', computeWarpRange(ref)]})
226
224
  break
227
225
  }
228
226
  case MODULE_CRAFTER: {
229
- const rea = decodeStat(stats, 0)
227
+ const fin = decodeStat(stats, 0)
230
228
  const con = decodeStat(stats, 1)
231
- base.push({first: 'reactivity', second: ['uint16', rea]})
229
+ base.push({first: 'fineness', second: ['uint16', fin]})
232
230
  base.push({first: 'conductivity', second: ['uint16', con]})
233
- base.push({first: 'speed', second: ['uint16', computeCrafterSpeed(rea)]})
231
+ base.push({first: 'speed', second: ['uint16', computeCrafterSpeed(fin)]})
234
232
  base.push({first: 'drain', second: ['uint16', computeCrafterDrain(con)]})
235
233
  break
236
234
  }
@@ -267,13 +265,13 @@ export function buildModuleImmutable(
267
265
  case MODULE_HAULER: {
268
266
  const res = decodeStat(stats, 0)
269
267
  const pla = decodeStat(stats, 1)
270
- const ref = decodeStat(stats, 2)
268
+ const con = decodeStat(stats, 2)
271
269
  base.push({first: 'resonance', second: ['uint16', res]})
272
270
  base.push({first: 'plasticity', second: ['uint16', pla]})
273
- base.push({first: 'reflectivity', second: ['uint16', ref]})
271
+ base.push({first: 'conductivity', second: ['uint16', con]})
274
272
  base.push({first: 'capacity', second: ['uint8', computeHaulerCapacity(res)]})
275
273
  base.push({first: 'efficiency', second: ['uint16', computeHaulerEfficiency(pla)]})
276
- base.push({first: 'drain', second: ['uint16', computeHaulerDrain(ref)]})
274
+ base.push({first: 'drain', second: ['uint16', computeHaulerDrain(con)]})
277
275
  break
278
276
  }
279
277
  }
@@ -107,7 +107,7 @@ export function entityDisplayName(itemId: number): string {
107
107
  case ITEM_WAREHOUSE_T1_PACKED:
108
108
  return 'Warehouse'
109
109
  case ITEM_EXTRACTOR_T1_PACKED:
110
- return 'Extractor'
110
+ return 'Mining Rig'
111
111
  case ITEM_FACTORY_T1_PACKED:
112
112
  return 'Factory'
113
113
  case ITEM_CONTAINER_T1_PACKED:
@@ -124,19 +124,19 @@ export function moduleDisplayName(itemId: number): string {
124
124
  case ITEM_ENGINE_T1:
125
125
  return 'Engine'
126
126
  case ITEM_GENERATOR_T1:
127
- return 'Generator'
127
+ return 'Reactor'
128
128
  case ITEM_GATHERER_T1:
129
- return 'Gatherer'
129
+ return 'Limpet Bay'
130
130
  case ITEM_LOADER_T1:
131
- return 'Loader'
131
+ return 'Shuttle Bay'
132
132
  case ITEM_CRAFTER_T1:
133
- return 'Crafter'
133
+ return 'Fabricator'
134
134
  case ITEM_STORAGE_T1:
135
- return 'Cargo Bay'
135
+ return 'Cargo Hold'
136
136
  case ITEM_HAULER_T1:
137
- return 'Hauler'
137
+ return 'Tractor Beam'
138
138
  case ITEM_WARP_T1:
139
- return 'Warp'
139
+ return 'Warp Drive'
140
140
  case ITEM_BATTERY_T1:
141
141
  return 'Battery Bank'
142
142
  default:
@@ -61,8 +61,8 @@ const TEMPLATES: Record<string, TemplateSpec> = {
61
61
  ],
62
62
  highlightKeys: ['yield', 'depth', 'drain'],
63
63
  },
64
- loader: {
65
- id: 'module.loader.description',
64
+ loading: {
65
+ id: 'module.loading.description',
66
66
  template: 'generates {thrust} thrust with a weight of {mass} per unit',
67
67
  params: [
68
68
  ['thrust', 'Thrust'],
@@ -70,8 +70,8 @@ const TEMPLATES: Record<string, TemplateSpec> = {
70
70
  ],
71
71
  highlightKeys: ['thrust', 'mass'],
72
72
  },
73
- crafter: {
74
- id: 'module.crafter.description',
73
+ crafting: {
74
+ id: 'module.crafting.description',
75
75
  template: 'manufactures items at {speed} speed while draining {drain} energy per second',
76
76
  params: [
77
77
  ['speed', 'Speed'],
@@ -91,8 +91,8 @@ const TEMPLATES: Record<string, TemplateSpec> = {
91
91
  params: [['capacity', 'Energy Capacity']],
92
92
  highlightKeys: ['capacity'],
93
93
  },
94
- hauler: {
95
- id: 'module.hauler.description',
94
+ hauling: {
95
+ id: 'module.hauling.description',
96
96
  template:
97
97
  'locks onto up to {capacity} targets at {efficiency} efficiency while draining {drain} energy per distance travelled per target',
98
98
  params: [
@@ -198,7 +198,7 @@ function computeCapabilityGroup(
198
198
  case MODULE_LOADER: {
199
199
  const caps = computeLoaderCapabilities(stats)
200
200
  return {
201
- capability: 'Loader',
201
+ capability: 'Loading',
202
202
  attributes: [
203
203
  {label: 'Mass', value: caps.mass},
204
204
  {label: 'Thrust', value: caps.thrust},
@@ -209,7 +209,7 @@ function computeCapabilityGroup(
209
209
  case MODULE_CRAFTER: {
210
210
  const caps = computeCrafterCapabilities(stats)
211
211
  return {
212
- capability: 'Crafter',
212
+ capability: 'Crafting',
213
213
  attributes: [
214
214
  {label: 'Speed', value: caps.speed},
215
215
  {label: 'Drain', value: caps.drain},
@@ -219,7 +219,7 @@ function computeCapabilityGroup(
219
219
  case MODULE_HAULER: {
220
220
  const caps = computeHaulerCapabilities(stats)
221
221
  return {
222
- capability: 'Hauler',
222
+ capability: 'Hauling',
223
223
  attributes: [
224
224
  {label: 'Capacity', value: caps.capacity},
225
225
  {label: 'Efficiency', value: caps.efficiency},
@@ -0,0 +1,60 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {
3
+ derivedLoaders,
4
+ unwrapTransitDuration,
5
+ unwrapLoadDuration,
6
+ estimateUnwrapDuration,
7
+ incomingHoldMass,
8
+ projectedPeakCargomass,
9
+ } from './unwrap'
10
+
11
+ describe('unwrap duration mirror', () => {
12
+ test('derivedLoaders aggregates lanes like derived_loaders()', () => {
13
+ expect(derivedLoaders([])).toBeNull()
14
+ expect(
15
+ derivedLoaders([
16
+ {mass: 1000, thrust: 10},
17
+ {mass: 1400, thrust: 20},
18
+ ])
19
+ ).toEqual({mass: 1200, thrust: 30, quantity: 2}) // floor(2400/2)=1200, sum thrust, count
20
+ })
21
+
22
+ test('transit floors distance then flight time', () => {
23
+ // distance = floor(sqrt(3^2+4^2)*10000)=50000; accel=400/mass*10000; flight=floor(2*sqrt(d/accel))
24
+ const mass = 1000
25
+ const accel = (400 / mass) * 10000
26
+ const expected = Math.floor(2 * Math.sqrt(50000 / accel))
27
+ expect(unwrapTransitDuration(mass, {x: 0, y: 0}, {x: 3, y: 4})).toBe(expected)
28
+ })
29
+
30
+ test('load uses altitude z, adds loader mass, divides by quantity', () => {
31
+ const loaders = {mass: 1200, thrust: 30, quantity: 2}
32
+ const itemMass = 800
33
+ const accel = (30 / (itemMass + 1200)) * 10000
34
+ const flight = Math.floor(2 * Math.sqrt(3000 / accel))
35
+ expect(unwrapLoadDuration(loaders, itemMass, 3000)).toBe(Math.floor(flight / 2))
36
+ })
37
+
38
+ test('zero item mass and no loaders are safe', () => {
39
+ expect(unwrapTransitDuration(0, {x: 0, y: 0}, {x: 9, y: 9})).toBe(0)
40
+ expect(unwrapLoadDuration(null, 500, 3000)).toBe(0)
41
+ })
42
+ })
43
+
44
+ test('incomingHoldMass sums incoming-kind hold mass', () => {
45
+ expect(incomingHoldMass([])).toBe(0)
46
+ // PUSH(2) + FLIGHT(5) count; BUILD(4) does not
47
+ expect(
48
+ incomingHoldMass([
49
+ {kind: 2, incoming_mass: 100},
50
+ {kind: 4, incoming_mass: 999},
51
+ {kind: 5, incoming_mass: 50},
52
+ ])
53
+ ).toBe(150)
54
+ })
55
+
56
+ test('projectedPeakCargomass tracks the running peak from cargomass', () => {
57
+ const entity = {cargomass: 1000, lanes: [], cargo: [], schedule: undefined} as never
58
+ // No pending tasks: peak = base + candidate add.
59
+ expect(projectedPeakCargomass(entity, new Date(0), 500)).toBe(1500)
60
+ })
@@ -0,0 +1,187 @@
1
+ import type {UInt16Type, UInt32Type} from '@wharfkit/antelope'
2
+ import {calcCargoItemMass} from '../capabilities/storage'
3
+ import type {ServerContract} from '../contracts'
4
+ import {PRECISION} from '../types'
5
+ import * as sched from './schedule'
6
+ import {taskCargoEffect} from './availability'
7
+ import {candidateLaneCompletesAt} from './lanes'
8
+
9
+ const NFT_TRANSIT_THRUST = 400
10
+
11
+ export interface DerivedLoaders {
12
+ mass: number
13
+ thrust: number
14
+ quantity: number
15
+ }
16
+
17
+ export function derivedLoaders(
18
+ lanes: {mass: UInt32Type | number; thrust: UInt16Type | number}[] | undefined
19
+ ): DerivedLoaders | null {
20
+ if (!lanes || lanes.length === 0) return null
21
+ let totalMass = 0
22
+ let totalThrust = 0
23
+ for (const l of lanes) {
24
+ totalMass += Number(l.mass)
25
+ totalThrust += Number(l.thrust)
26
+ }
27
+ const count = lanes.length
28
+ return {
29
+ mass: Math.floor(totalMass / count),
30
+ thrust: Math.min(totalThrust, 65_535),
31
+ quantity: count,
32
+ }
33
+ }
34
+
35
+ function acceleration(thrust: number, mass: number): number {
36
+ if (mass <= 0) return 0
37
+ return (thrust / mass) * PRECISION
38
+ }
39
+
40
+ function flightTime(distance: number, accel: number): number {
41
+ if (accel <= 0 || distance <= 0) return 0
42
+ return Math.floor(2 * Math.sqrt(distance / accel))
43
+ }
44
+
45
+ function distance2d(ax: number, ay: number, bx: number, by: number): number {
46
+ const dx = ax - bx
47
+ const dy = ay - by
48
+ return Math.floor(Math.sqrt(dx * dx + dy * dy) * PRECISION)
49
+ }
50
+
51
+ export function unwrapTransitDuration(
52
+ itemMass: number,
53
+ origin: {x: number; y: number},
54
+ dest: {x: number; y: number}
55
+ ): number {
56
+ if (itemMass <= 0) return 0
57
+ return flightTime(
58
+ distance2d(origin.x, origin.y, dest.x, dest.y),
59
+ acceleration(NFT_TRANSIT_THRUST, itemMass)
60
+ )
61
+ }
62
+
63
+ export function unwrapLoadDuration(
64
+ loaders: DerivedLoaders | null,
65
+ itemMass: number,
66
+ destZ: number
67
+ ): number {
68
+ if (!loaders || itemMass <= 0) return 0
69
+ const total = itemMass + loaders.mass
70
+ const flight = flightTime(destZ, acceleration(loaders.thrust, total))
71
+ return Math.floor(flight / loaders.quantity)
72
+ }
73
+
74
+ export interface UnwrapItem {
75
+ itemId: number
76
+ quantity: number
77
+ modules: ServerContract.Types.module_entry[]
78
+ originX: number
79
+ originY: number
80
+ }
81
+
82
+ export interface UnwrapDestination {
83
+ loader_lanes?: {mass: UInt32Type | number; thrust: UInt16Type | number}[]
84
+ coordinates: {x: UInt32Type | number; y: UInt32Type | number; z?: UInt32Type | number}
85
+ }
86
+
87
+ export function estimateUnwrapDuration(dest: UnwrapDestination, item: UnwrapItem): number {
88
+ const itemMass = Number(
89
+ calcCargoItemMass({
90
+ item_id: item.itemId as never,
91
+ quantity: item.quantity as never,
92
+ modules: item.modules,
93
+ })
94
+ )
95
+ const loaders = derivedLoaders(dest.loader_lanes)
96
+ const dz = Number(dest.coordinates.z ?? 0)
97
+ const load = unwrapLoadDuration(loaders, itemMass, dz)
98
+ const transit = unwrapTransitDuration(
99
+ itemMass,
100
+ {x: item.originX, y: item.originY},
101
+ {
102
+ x: Number(dest.coordinates.x),
103
+ y: Number(dest.coordinates.y),
104
+ }
105
+ )
106
+ return load + transit
107
+ }
108
+
109
+ // Hold kinds that count as incoming (mirror is_incoming_hold_kind in holds.hpp).
110
+ const INCOMING_HOLD_KINDS = new Set<number>([2, 3, 5])
111
+
112
+ export function incomingHoldMass(
113
+ holds:
114
+ | {kind: number | {toNumber(): number}; incoming_mass: number | {toNumber(): number}}[]
115
+ | undefined
116
+ ): number {
117
+ if (!holds) return 0
118
+ let total = 0
119
+ for (const h of holds) {
120
+ if (INCOMING_HOLD_KINDS.has(Number(h.kind))) total += Number(h.incoming_mass)
121
+ }
122
+ return total
123
+ }
124
+
125
+ type CargoItem = ServerContract.Types.cargo_item
126
+
127
+ function cargoListMass(items: CargoItem[]): number {
128
+ let m = 0
129
+ for (const it of items) m += Number(calcCargoItemMass(it))
130
+ return m
131
+ }
132
+
133
+ export function projectedPeakCargomass(
134
+ entity: sched.ScheduleData & {cargomass: number | {toNumber(): number}},
135
+ at: Date,
136
+ addMass: number,
137
+ removeMass = 0
138
+ ): number {
139
+ const events: {t: number; delta: number}[] = []
140
+ for (const ordered of sched.orderedTasks(entity)) {
141
+ const eff = taskCargoEffect(ordered.task)
142
+ const delta = cargoListMass(eff.added) - cargoListMass(eff.removed)
143
+ events.push({t: ordered.completesAt.getTime(), delta})
144
+ }
145
+ events.push({t: at.getTime(), delta: addMass - removeMass})
146
+ events.sort((a, b) => (a.t !== b.t ? a.t - b.t : b.delta - a.delta))
147
+ let running = Number(entity.cargomass)
148
+ let peak = running
149
+ for (const e of events) {
150
+ running += e.delta
151
+ if (running < 0) running = 0
152
+ if (running > peak) peak = running
153
+ }
154
+ return Math.min(peak, 0xffff_ffff)
155
+ }
156
+
157
+ export function receiveFits(
158
+ dest: UnwrapDestination &
159
+ sched.ScheduleData & {
160
+ cargomass: number | {toNumber(): number}
161
+ capacity?: number | {toNumber(): number}
162
+ holds?: {
163
+ kind: number | {toNumber(): number}
164
+ incoming_mass: number | {toNumber(): number}
165
+ }[]
166
+ },
167
+ item: UnwrapItem,
168
+ now: Date
169
+ ): boolean {
170
+ const capacity = Number(dest.capacity ?? 0)
171
+ if (capacity <= 0) return false
172
+ const itemMass = Number(
173
+ calcCargoItemMass({
174
+ item_id: item.itemId as never,
175
+ quantity: item.quantity as never,
176
+ modules: item.modules,
177
+ })
178
+ )
179
+ const duration = estimateUnwrapDuration(dest, item)
180
+ const candidateCompletes = candidateLaneCompletesAt(dest, sched.LANE_MOBILITY, duration, now)
181
+ const peak = projectedPeakCargomass(
182
+ dest,
183
+ candidateCompletes,
184
+ itemMass + incomingHoldMass(dest.holds)
185
+ )
186
+ return peak <= capacity
187
+ }
package/src/shipload.ts CHANGED
@@ -10,6 +10,7 @@ import type {LocationsManager} from './managers/locations'
10
10
  import type {CoordinatesManager} from './managers/coordinates'
11
11
  import type {EpochsManager} from './managers/epochs'
12
12
  import type {ActionsManager} from './managers/actions'
13
+ import type {ClusterManager} from './managers/cluster'
13
14
  import type {NftManager} from './managers/nft'
14
15
  import type {SubscriptionsManager} from './subscriptions/manager'
15
16
  import type {GameState} from './entities/gamestate'
@@ -123,6 +124,10 @@ export class Shipload {
123
124
  return this._context.actions
124
125
  }
125
126
 
127
+ get clusters(): ClusterManager {
128
+ return this._context.clusters
129
+ }
130
+
126
131
  get nft(): NftManager {
127
132
  return this._context.nft
128
133
  }