@shipload/sdk 1.0.0-next.72 → 1.0.0-next.74

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 (47) hide show
  1. package/lib/shipload.d.ts +576 -133
  2. package/lib/shipload.js +6019 -5114
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +6002 -5108
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +194 -52
  7. package/lib/testing.js +767 -468
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +767 -468
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/contracts/server.ts +523 -143
  13. package/src/data/catalog.test.ts +81 -1
  14. package/src/data/catalog.ts +6 -4
  15. package/src/data/entities.json +2 -2
  16. package/src/data/metadata.ts +197 -363
  17. package/src/data/task-types.json +184 -0
  18. package/src/entities/makers.ts +2 -2
  19. package/src/index-module.ts +31 -2
  20. package/src/influence/civic-shuttle.test.ts +58 -0
  21. package/src/influence/civic-shuttle.ts +52 -6
  22. package/src/influence/duration.ts +0 -65
  23. package/src/influence/index.ts +4 -5
  24. package/src/influence/influence.test.ts +51 -136
  25. package/src/influence/valuation.ts +75 -0
  26. package/src/managers/actions.ts +49 -10
  27. package/src/managers/balances.test.ts +33 -0
  28. package/src/managers/balances.ts +15 -0
  29. package/src/managers/context.ts +50 -11
  30. package/src/managers/index.ts +14 -0
  31. package/src/managers/jobs.test.ts +82 -1
  32. package/src/managers/jobs.ts +41 -2
  33. package/src/managers/nft.test.ts +173 -1
  34. package/src/managers/nft.ts +35 -2
  35. package/src/managers/shuttle.test.ts +562 -0
  36. package/src/managers/shuttle.ts +397 -0
  37. package/src/resolution/resolve-item.ts +2 -2
  38. package/src/scheduling/availability.ts +10 -0
  39. package/src/scheduling/energy.ts +55 -1
  40. package/src/scheduling/jobs.test.ts +171 -1
  41. package/src/scheduling/jobs.ts +110 -13
  42. package/src/scheduling/projection.ts +2 -1
  43. package/src/scheduling/task-effects.ts +38 -0
  44. package/src/scheduling/unwrap.test.ts +60 -0
  45. package/src/scheduling/unwrap.ts +24 -8
  46. package/src/shipload.ts +23 -4
  47. package/src/types.ts +1 -0
@@ -1,6 +1,5 @@
1
1
  import {describe, expect, test} from 'bun:test'
2
- import {Checksum256, UInt8, UInt16, UInt64} from '@wharfkit/antelope'
3
- import type {ServerContract} from '../contracts'
2
+ import {Checksum256} from '@wharfkit/antelope'
4
3
  import {ITEM_ORE_T1, ITEM_PLATE, ITEM_CRYSTAL_T1} from '../data/item-ids'
5
4
  import {categoryIndex} from './categories'
6
5
  import {
@@ -20,9 +19,10 @@ import {
20
19
  popcount5,
21
20
  } from './demand'
22
21
  import {findDecomp, DECOMP_REGISTRY} from './decomp'
23
- import {civicDropoffDuration, depotTransferDuration, contributeDuration} from './duration'
22
+ import {contributeDuration} from './duration'
24
23
  import {getStatCount, statsSumSq} from './quality'
25
- import {pricingFromWeights, valueCargoItem, valueContribution} from './valuation'
24
+ import {explainCargoItem, pricingFromWeights, valueCargoItem, valueContribution} from './valuation'
25
+ import {getItems} from '../data/catalog'
26
26
  import {citizenryName, citizenryPatternCount} from './citizenry'
27
27
  import citizenryAdjectives from '../data/citizenry-adjectives.json'
28
28
  import citizenryNouns from '../data/citizenry-nouns.json'
@@ -155,6 +155,53 @@ describe('decomposition', () => {
155
155
  })
156
156
  })
157
157
 
158
+ describe('explainCargoItem', () => {
159
+ test('factors reproduce valueCargoItem for every valuable catalog item', () => {
160
+ const acute = buildDemand(0, categoryIndex('regolith'), DEMAND_TRIPLE_SEED)
161
+ for (const def of getItems()) {
162
+ if (def.type !== 'resource' && def.type !== 'component') continue
163
+ const item = {itemId: Number(def.id), quantity: 7, stats: packStats(363, 535, 246)}
164
+ const explained = explainCargoItem(item, acute, pricing)
165
+ expect(explained.totalAtomic).toBe(valueCargoItem(item, acute, pricing))
166
+ }
167
+ })
168
+
169
+ test('a resource explains mass, weight, quality and need', () => {
170
+ const acute = buildDemand(0, categoryIndex('crystal'), DEMAND_TRIPLE_SEED)
171
+ const explained = explainCargoItem(
172
+ {itemId: ITEM_CRYSTAL_T1, quantity: 5000, stats: packStats(PAR)},
173
+ acute,
174
+ pricing
175
+ )
176
+ if (explained.kind !== 'resource') throw new Error('expected resource')
177
+ expect(explained.mass).toBe(
178
+ BigInt(getItems().find((i) => Number(i.id) === ITEM_CRYSTAL_T1)!.mass) * 5000n
179
+ )
180
+ expect(explained.weight).toBe(1)
181
+ expect(explained.quality).toBeCloseTo(1, 6)
182
+ expect(explained.need).toBe(2)
183
+ expect(explained.points).toBeCloseTo(
184
+ (Number(explained.mass) / 10) * explained.weight * explained.quality * explained.need,
185
+ 3
186
+ )
187
+ })
188
+
189
+ test('a component explains raw mass, material weight, quality and effort', () => {
190
+ const explained = explainCargoItem(
191
+ {itemId: ITEM_PLATE, quantity: 3, stats: packStats(PAR)},
192
+ baseDemand,
193
+ pricing
194
+ )
195
+ if (explained.kind !== 'component') throw new Error('expected component')
196
+ const entry = findDecomp(ITEM_PLATE)!
197
+ const raw = entry.buckets.reduce((sum, b) => sum + b.rawMass, 0)
198
+ expect(explained.rawMass).toBe(raw * 3)
199
+ expect(explained.processedMass).toBe(entry.processedMass * 3)
200
+ expect(explained.weight).toBeGreaterThanOrEqual(1)
201
+ expect(explained.baseAtomic + explained.effortAtomic).toBe(explained.totalAtomic)
202
+ })
203
+ })
204
+
158
205
  describe('valuation', () => {
159
206
  test('a tonne of par T1 ore at base demand is worth exactly one point', () => {
160
207
  const value = valueCargoItem(
@@ -316,135 +363,3 @@ describe('scales', () => {
316
363
  expect(NEED_FP_SCALE % INFLUENCE_ATOMIC_PER_POINT).toBe(0)
317
364
  })
318
365
  })
319
-
320
- describe('depot transfer duration', () => {
321
- type ModuleEntry = ServerContract.Types.module_entry
322
- const DEPOT_ITEM = 10219
323
- const LOADER_T1 = 10103
324
-
325
- function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
326
- return {
327
- type: UInt8.from(0),
328
- installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
329
- } as unknown as ModuleEntry
330
- }
331
- function emptySlot(): ModuleEntry {
332
- return {type: UInt8.from(0)} as unknown as ModuleEntry
333
- }
334
-
335
- // A spawned depot: loader in slot 0, four empty bays after it.
336
- const depotWithLoader = (): ModuleEntry[] => [
337
- makeModuleEntry(LOADER_T1, packStats(500)),
338
- emptySlot(),
339
- emptySlot(),
340
- emptySlot(),
341
- emptySlot(),
342
- ]
343
- const params = (overrides: Record<string, unknown> = {}) => ({
344
- depotModules: depotWithLoader(),
345
- depotItemId: DEPOT_ITEM,
346
- depotKind: 'depot',
347
- depotZ: 0,
348
- shipKind: 'ship',
349
- shipZ: 0,
350
- cargoMass: 100,
351
- ...overrides,
352
- })
353
-
354
- test('drives off the depot loader, not the ship', () => {
355
- expect(depotTransferDuration(params())).toBeGreaterThan(0)
356
- })
357
-
358
- test('grows with cargo mass', () => {
359
- expect(depotTransferDuration(params({cargoMass: 10_000}))).toBeGreaterThan(
360
- depotTransferDuration(params({cargoMass: 100}))
361
- )
362
- })
363
-
364
- test('empty cargo costs nothing', () => {
365
- expect(depotTransferDuration(params({cargoMass: 0}))).toBe(0)
366
- })
367
-
368
- test('a depot with no loader installed cannot transfer', () => {
369
- expect(depotTransferDuration(params({depotModules: [emptySlot()]}))).toBe(0)
370
- })
371
-
372
- test('co-located transfers are floored at the orbital minimum distance', () => {
373
- // max(planetary 100, orbital 200) = 200, so anything under 200 reads as 200
374
- const touching = depotTransferDuration(params({shipZ: 0, depotZ: 0}))
375
- const under = depotTransferDuration(params({shipZ: 150, depotZ: 0}))
376
- const over = depotTransferDuration(params({shipZ: 400, depotZ: 0}))
377
- expect(under).toBe(touching)
378
- expect(over).toBeGreaterThan(touching)
379
- })
380
-
381
- test('never reports zero for real cargo', () => {
382
- expect(depotTransferDuration(params({cargoMass: 1}))).toBeGreaterThanOrEqual(1)
383
- })
384
- })
385
-
386
- describe('civic drop-off duration', () => {
387
- type ModuleEntry = ServerContract.Types.module_entry
388
- const DEPOT_ITEM = 10219
389
- const WORKSHOP_ITEM = 10208
390
- const LOADER_T1 = 10103
391
-
392
- function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
393
- return {
394
- type: UInt8.from(0),
395
- installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
396
- } as unknown as ModuleEntry
397
- }
398
- function emptySlot(): ModuleEntry {
399
- return {type: UInt8.from(0)} as unknown as ModuleEntry
400
- }
401
-
402
- const withLoader = () => ({
403
- buildingModules: [makeModuleEntry(LOADER_T1, packStats(500)), emptySlot()],
404
- buildingItemId: DEPOT_ITEM,
405
- buildingKind: 'depot',
406
- buildingZ: 0,
407
- shipKind: 'ship',
408
- shipZ: 400,
409
- cargoMass: 100,
410
- })
411
- const withoutLoader = () => ({
412
- buildingModules: [] as ModuleEntry[],
413
- buildingItemId: WORKSHOP_ITEM,
414
- buildingKind: 'workshop',
415
- buildingZ: 0,
416
- shipKind: 'ship',
417
- shipZ: 400,
418
- cargoMass: 100,
419
- })
420
-
421
- test('a building with a slot-0 loader prices off that loader', () => {
422
- const p = withLoader()
423
- expect(civicDropoffDuration(p)).toBe(
424
- depotTransferDuration({
425
- depotModules: p.buildingModules,
426
- depotItemId: p.buildingItemId,
427
- depotKind: p.buildingKind,
428
- depotZ: p.buildingZ,
429
- shipKind: p.shipKind,
430
- shipZ: p.shipZ,
431
- cargoMass: p.cargoMass,
432
- })
433
- )
434
- })
435
-
436
- test('a building with no loader falls back to the civic loader', () => {
437
- const p = withoutLoader()
438
- expect(civicDropoffDuration(p)).toBe(contributeDuration(p.cargoMass, p.shipZ))
439
- expect(civicDropoffDuration(p)).toBeGreaterThan(0)
440
- })
441
-
442
- test('grows with cargo mass on both paths', () => {
443
- expect(civicDropoffDuration({...withLoader(), cargoMass: 10_000})).toBeGreaterThan(
444
- civicDropoffDuration(withLoader())
445
- )
446
- expect(civicDropoffDuration({...withoutLoader(), cargoMass: 10_000})).toBeGreaterThan(
447
- civicDropoffDuration(withoutLoader())
448
- )
449
- })
450
- })
@@ -126,6 +126,81 @@ export function valueCargoItem(
126
126
  return withEffort
127
127
  }
128
128
 
129
+ export interface ResourceValuation {
130
+ kind: 'resource'
131
+ category: number
132
+ tier: number
133
+ mass: bigint
134
+ weight: number
135
+ quality: number
136
+ need: number
137
+ totalAtomic: bigint
138
+ points: number
139
+ }
140
+
141
+ export interface ComponentValuation {
142
+ kind: 'component'
143
+ rawMass: number
144
+ processedMass: number
145
+ weight: number
146
+ quality: number
147
+ baseAtomic: bigint
148
+ effortAtomic: bigint
149
+ totalAtomic: bigint
150
+ points: number
151
+ }
152
+
153
+ export type CargoValuation = ResourceValuation | ComponentValuation
154
+
155
+ export function explainCargoItem(
156
+ item: ValuedItem,
157
+ demand: DemandView,
158
+ pricing: InfluencePricing
159
+ ): CargoValuation {
160
+ const def = getItem(item.itemId)
161
+ const totalAtomic = valueCargoItem(item, demand, pricing)
162
+ const points = Number(totalAtomic) / INFLUENCE_ATOMIC_PER_POINT
163
+ const nStats = getStatCount(item.itemId)
164
+ const quality = statsSumSq(item.stats, nStats) / (nStats * pricing.d1)
165
+
166
+ if (def.type === 'resource') {
167
+ if (def.category === undefined) throw new Error('resource has no category')
168
+ const category = categoryIndex(def.category)
169
+ return {
170
+ kind: 'resource',
171
+ category,
172
+ tier: def.tier,
173
+ mass: BigInt(def.mass) * BigInt(item.quantity),
174
+ weight: Number(pricing.weight(category, def.tier)) / WEIGHT_FP_SCALE,
175
+ quality,
176
+ need: Number(needForCategory(demand, category)) / NEED_FP_SCALE,
177
+ totalAtomic,
178
+ points,
179
+ }
180
+ }
181
+
182
+ const entry = findDecomp(item.itemId)
183
+ if (!entry) throw new Error('item has no decomposition')
184
+ let rawMass = 0
185
+ let weighted = 0n
186
+ for (const bucket of entry.buckets) {
187
+ rawMass += bucket.rawMass
188
+ weighted += BigInt(bucket.rawMass) * pricing.weight(bucket.category, bucket.tier)
189
+ }
190
+ const effortAtomic = componentEffortAtomic(entry.processedMass, item.quantity)
191
+ return {
192
+ kind: 'component',
193
+ rawMass: rawMass * item.quantity,
194
+ processedMass: entry.processedMass * item.quantity,
195
+ weight: rawMass === 0 ? 0 : Number(weighted) / (rawMass * WEIGHT_FP_SCALE),
196
+ quality,
197
+ baseAtomic: totalAtomic - effortAtomic,
198
+ effortAtomic,
199
+ totalAtomic,
200
+ points,
201
+ }
202
+ }
203
+
129
204
  export function valueContribution(
130
205
  bundle: ValuedItem[],
131
206
  demand: DemandView,
@@ -332,25 +332,35 @@ export class ActionsManager extends BaseManager {
332
332
  depotstore(
333
333
  shipId: UInt64Type,
334
334
  depotId: UInt64Type,
335
- items: ServerContract.ActionParams.Type.cargo_item[]
335
+ items: ServerContract.ActionParams.Type.cargo_item[],
336
+ shuttledBy?: UInt64Type
336
337
  ): Action {
337
- return this.server.action('depotstore', {
338
+ const params: ServerContract.ActionParams.depotstore = {
338
339
  ship_id: UInt64.from(shipId),
339
340
  depot_id: UInt64.from(depotId),
340
341
  items,
341
- })
342
+ }
343
+ if (shuttledBy !== undefined) {
344
+ params.shuttled_by = UInt64.from(shuttledBy)
345
+ }
346
+ return this.server.action('depotstore', params)
342
347
  }
343
348
 
344
349
  depottake(
345
350
  shipId: UInt64Type,
346
351
  depotId: UInt64Type,
347
- items: ServerContract.ActionParams.Type.cargo_item[]
352
+ items: ServerContract.ActionParams.Type.cargo_item[],
353
+ shuttledBy?: UInt64Type
348
354
  ): Action {
349
- return this.server.action('depottake', {
355
+ const params: ServerContract.ActionParams.depottake = {
350
356
  ship_id: UInt64.from(shipId),
351
357
  depot_id: UInt64.from(depotId),
352
358
  items,
353
- })
359
+ }
360
+ if (shuttledBy !== undefined) {
361
+ params.shuttled_by = UInt64.from(shuttledBy)
362
+ }
363
+ return this.server.action('depottake', params)
354
364
  }
355
365
 
356
366
  shuttle(
@@ -518,7 +528,8 @@ export class ActionsManager extends BaseManager {
518
528
  workshopId: UInt64Type,
519
529
  recipeId: UInt16Type,
520
530
  quantity: UInt32Type,
521
- inputs: ServerContract.ActionParams.Type.cargo_item[]
531
+ inputs: ServerContract.ActionParams.Type.cargo_item[],
532
+ shuttledBy?: UInt64Type
522
533
  ): Action {
523
534
  const params: ServerContract.ActionParams.craftjob = {
524
535
  ship_id: UInt64.from(shipId),
@@ -527,6 +538,9 @@ export class ActionsManager extends BaseManager {
527
538
  quantity: UInt32.from(quantity),
528
539
  inputs,
529
540
  }
541
+ if (shuttledBy !== undefined) {
542
+ params.shuttled_by = UInt64.from(shuttledBy)
543
+ }
530
544
  return this.server.action('craftjob', params)
531
545
  }
532
546
 
@@ -540,6 +554,9 @@ export class ActionsManager extends BaseManager {
540
554
  canceljob(route: JobCancelRoute): Action {
541
555
  if (route.kind === 'dropoff') return this.cancel(route.shipId, route.laneKey, route.count)
542
556
  if (route.kind === 'build') return this.cancelbuild(route.jobId)
557
+ if (route.kind === 'civic') {
558
+ return this.cancelcivic(route.buildingId, route.laneKey, route.fromId)
559
+ }
543
560
  return this.cancelcraft(route.jobId)
544
561
  }
545
562
 
@@ -566,11 +583,14 @@ export class ActionsManager extends BaseManager {
566
583
  return this.server.action('clustercraft', params)
567
584
  }
568
585
 
569
- claimcraft(jobId: UInt64Type, shipId: UInt64Type): Action {
586
+ claimcraft(jobId: UInt64Type, shipId: UInt64Type, shuttledBy?: UInt64Type): Action {
570
587
  const params: ServerContract.ActionParams.claimcraft = {
571
588
  job_id: UInt64.from(jobId),
572
589
  ship_id: UInt64.from(shipId),
573
590
  }
591
+ if (shuttledBy !== undefined) {
592
+ params.shuttled_by = UInt64.from(shuttledBy)
593
+ }
574
594
  return this.server.action('claimcraft', params)
575
595
  }
576
596
 
@@ -578,7 +598,8 @@ export class ActionsManager extends BaseManager {
578
598
  targetId: UInt64Type,
579
599
  dockId: UInt64Type,
580
600
  targetItemId: UInt16Type,
581
- inputs: ServerContract.ActionParams.Type.cargo_item[]
601
+ inputs: ServerContract.ActionParams.Type.cargo_item[],
602
+ shuttledBy?: UInt64Type
582
603
  ): Action {
583
604
  const params: ServerContract.ActionParams.buildjob = {
584
605
  target_id: UInt64.from(targetId),
@@ -586,6 +607,9 @@ export class ActionsManager extends BaseManager {
586
607
  target_item_id: UInt16.from(targetItemId),
587
608
  inputs,
588
609
  }
610
+ if (shuttledBy !== undefined) {
611
+ params.shuttled_by = UInt64.from(shuttledBy)
612
+ }
589
613
  return this.server.action('buildjob', params)
590
614
  }
591
615
 
@@ -596,10 +620,13 @@ export class ActionsManager extends BaseManager {
596
620
  return this.server.action('claimbuild', params)
597
621
  }
598
622
 
599
- cancelbuild(jobId: UInt64Type): Action {
623
+ cancelbuild(jobId: UInt64Type, shuttledBy?: UInt64Type): Action {
600
624
  const params: ServerContract.ActionParams.cancelbuild = {
601
625
  job_id: UInt64.from(jobId),
602
626
  }
627
+ if (shuttledBy !== undefined) {
628
+ params.shuttled_by = UInt64.from(shuttledBy)
629
+ }
603
630
  return this.server.action('cancelbuild', params)
604
631
  }
605
632
 
@@ -610,6 +637,18 @@ export class ActionsManager extends BaseManager {
610
637
  })
611
638
  }
612
639
 
640
+ blenddepot(
641
+ depotId: UInt64Type,
642
+ owner: NameType,
643
+ inputs: ServerContract.ActionParams.Type.cargo_item[]
644
+ ): Action {
645
+ return this.server.action('blenddepot', {
646
+ depot_id: UInt64.from(depotId),
647
+ owner: Name.from(owner),
648
+ inputs,
649
+ })
650
+ }
651
+
613
652
  deploy(
614
653
  entityId: UInt64Type,
615
654
  ref: ServerContract.ActionParams.Type.cargo_ref,
@@ -78,6 +78,39 @@ describe('getDepositConfig', () => {
78
78
  })
79
79
  })
80
80
 
81
+ describe('setDepositConfig', () => {
82
+ test('a seeded config keeps every balance read off the chain', async () => {
83
+ const {manager, counts} = stubContext({
84
+ depositConfig: SCRAP,
85
+ balances: [{token_contract: 'scrap.gm', balance: '5 SCRAP'}],
86
+ wallet: ['7 SCRAP'],
87
+ })
88
+ manager.setDepositConfig({tokenContract: 'scrap.gm', symbol: '0,SCRAP'})
89
+
90
+ const config = await manager.getDepositConfig()
91
+ expect(String(config?.tokenContract)).toBe('scrap.gm')
92
+ expect(String(config?.symbol)).toBe('0,SCRAP')
93
+ await manager.getPlatformBalance('alice')
94
+ await manager.getWalletBalance('alice')
95
+
96
+ expect(counts.depositcfg).toBe(0)
97
+ })
98
+
99
+ test('a reload still reaches the chain', async () => {
100
+ const {manager, counts} = stubContext({depositConfig: SCRAP})
101
+ manager.setDepositConfig({tokenContract: 'scrap.gm', symbol: '0,SCRAP'})
102
+ await manager.getDepositConfig(true)
103
+ expect(counts.depositcfg).toBe(1)
104
+ })
105
+
106
+ test('seeding null records a chain with no deposit token', async () => {
107
+ const {manager, counts} = stubContext({depositConfig: SCRAP})
108
+ manager.setDepositConfig(null)
109
+ expect(await manager.getDepositConfig()).toBeNull()
110
+ expect(counts.depositcfg).toBe(0)
111
+ })
112
+ })
113
+
81
114
  describe('getPlatformBalance', () => {
82
115
  test('returns null when the owner has never opened a row', async () => {
83
116
  const {manager} = stubContext({depositConfig: SCRAP, balances: []})
@@ -7,6 +7,11 @@ export interface DepositConfig {
7
7
  symbol: Asset.Symbol
8
8
  }
9
9
 
10
+ export interface DepositConfigInput {
11
+ tokenContract: NameType
12
+ symbol: Asset.SymbolType
13
+ }
14
+
10
15
  export interface PlatformBalance {
11
16
  owner: Name
12
17
  tokenContract: Name
@@ -22,6 +27,16 @@ export interface TokenBalance {
22
27
  export class BalancesManager extends BaseManager {
23
28
  private depositConfig?: DepositConfig | null
24
29
 
30
+ // A caller holding the config already, from the indexer or its own cache, seeds it here so no balance read reaches the chain for it.
31
+ setDepositConfig(config: DepositConfigInput | null): void {
32
+ this.depositConfig = config
33
+ ? {
34
+ tokenContract: Name.from(config.tokenContract),
35
+ symbol: Asset.Symbol.from(config.symbol),
36
+ }
37
+ : null
38
+ }
39
+
25
40
  async getDepositConfig(reload = false): Promise<DepositConfig | null> {
26
41
  if (!reload && this.depositConfig !== undefined) {
27
42
  return this.depositConfig
@@ -15,6 +15,7 @@ import {NftManager} from './nft'
15
15
  import {BalancesManager} from './balances'
16
16
  import {FundManager} from './fund'
17
17
  import {JobsManager} from './jobs'
18
+ import {ShuttleManager} from './shuttle'
18
19
  import {InfluenceManager} from './influence'
19
20
  import {SubscriptionsManager} from '../subscriptions/manager'
20
21
 
@@ -30,12 +31,15 @@ export class GameContext {
30
31
  private _balances?: BalancesManager
31
32
  private _funds?: FundManager
32
33
  private _jobs?: JobsManager
34
+ private _shuttle?: ShuttleManager
33
35
  private _influence?: InfluenceManager
34
36
  private _subscriptions?: SubscriptionsManager
35
37
  private _subscriptionsUrl?: string
36
38
 
37
39
  private _gameCache?: PlatformContract.Types.game_row
40
+ private _gameInflight?: Promise<PlatformContract.Types.game_row>
38
41
  private _stateCache?: GameState
42
+ private _stateInflight?: Promise<GameState>
39
43
 
40
44
  constructor(
41
45
  public readonly client: APIClient,
@@ -122,6 +126,13 @@ export class GameContext {
122
126
  return this._jobs
123
127
  }
124
128
 
129
+ get shuttle(): ShuttleManager {
130
+ if (!this._shuttle) {
131
+ this._shuttle = new ShuttleManager(this)
132
+ }
133
+ return this._shuttle
134
+ }
135
+
125
136
  get influence(): InfluenceManager {
126
137
  if (!this._influence) {
127
138
  this._influence = new InfluenceManager(this)
@@ -149,25 +160,53 @@ export class GameContext {
149
160
  if (!reload && this._gameCache) {
150
161
  return this._gameCache
151
162
  }
152
- const game = await this.platform.table('games').get()
153
- if (!game) {
154
- throw new Error('Game not initialized')
163
+ if (!reload && this._gameInflight) {
164
+ return this._gameInflight
165
+ }
166
+ const inflight = (async () => {
167
+ const game = await this.platform.table('games').get()
168
+ if (!game) {
169
+ throw new Error('Game not initialized')
170
+ }
171
+ this._gameCache = game
172
+ return game
173
+ })()
174
+ this._gameInflight = inflight
175
+ try {
176
+ return await inflight
177
+ } finally {
178
+ if (this._gameInflight === inflight) {
179
+ this._gameInflight = undefined
180
+ }
155
181
  }
156
- this._gameCache = game
157
- return game
158
182
  }
159
183
 
160
184
  async getState(reload = false): Promise<GameState> {
161
185
  if (!reload && this._stateCache) {
162
186
  return this._stateCache
163
187
  }
164
- const state = await this.server.table('state').get()
165
- if (!state) {
166
- throw new Error('Game state not initialized')
188
+ if (!reload && this._stateInflight) {
189
+ return this._stateInflight
190
+ }
191
+ const inflight = (async () => {
192
+ const [state, game] = await Promise.all([
193
+ this.server.table('state').get(),
194
+ this.getGame(),
195
+ ])
196
+ if (!state) {
197
+ throw new Error('Game state not initialized')
198
+ }
199
+ this._stateCache = GameState.from(state, game)
200
+ return this._stateCache
201
+ })()
202
+ this._stateInflight = inflight
203
+ try {
204
+ return await inflight
205
+ } finally {
206
+ if (this._stateInflight === inflight) {
207
+ this._stateInflight = undefined
208
+ }
167
209
  }
168
- const game = this._gameCache
169
- this._stateCache = GameState.from(state, game)
170
- return this._stateCache
171
210
  }
172
211
 
173
212
  get cachedGame(): PlatformContract.Types.game_row | undefined {
@@ -15,6 +15,20 @@ export type {NftConfigForItem, WrapConfig, WrapGate} from './nft'
15
15
  export {BalancesManager} from './balances'
16
16
  export type {DepositConfig, PlatformBalance, TokenBalance} from './balances'
17
17
  export {JobsManager} from './jobs'
18
+ export {
19
+ ShuttleManager,
20
+ rankShuttleOptions,
21
+ shuttleReasonCode,
22
+ BOOKING_LEVEL_CODES,
23
+ OMITTED_REASONS,
24
+ } from './shuttle'
25
+ export type {
26
+ ShuttleMode,
27
+ ShuttleOption,
28
+ ShuttleOptions,
29
+ ShuttleReason,
30
+ ShuttleReasonCode,
31
+ } from './shuttle'
18
32
  export {InfluenceManager} from './influence'
19
33
  export type {
20
34
  CharterProgress,