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

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 (44) hide show
  1. package/lib/shipload.d.ts +115 -27
  2. package/lib/shipload.js +806 -697
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +795 -694
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +21 -11
  7. package/lib/testing.js +272 -339
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +272 -339
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/contracts/server.ts +69 -26
  13. package/src/data/capability-formulas.ts +4 -4
  14. package/src/data/catalog.test.ts +81 -1
  15. package/src/data/catalog.ts +6 -4
  16. package/src/data/entities.json +84 -68
  17. package/src/data/item-ids.ts +5 -4
  18. package/src/data/items.json +30 -24
  19. package/src/data/kind-registry.json +75 -39
  20. package/src/data/kind-registry.ts +13 -0
  21. package/src/data/metadata.ts +203 -334
  22. package/src/data/recipes.json +194 -155
  23. package/src/data/task-types.json +184 -0
  24. package/src/derivation/build-methods.test.ts +4 -4
  25. package/src/derivation/capabilities.test.ts +14 -10
  26. package/src/derivation/capabilities.ts +8 -10
  27. package/src/derivation/capability-mappings.ts +4 -4
  28. package/src/derivation/capacity-fn-registry.test.ts +9 -0
  29. package/src/derivation/capacity-fn-registry.ts +18 -0
  30. package/src/derivation/recipe-usage.test.ts +7 -11
  31. package/src/entities/makers.ts +2 -2
  32. package/src/index-module.ts +15 -1
  33. package/src/influence/influence.test.ts +1 -1
  34. package/src/managers/actions.ts +17 -0
  35. package/src/managers/context.ts +9 -0
  36. package/src/managers/fund.ts +49 -0
  37. package/src/managers/index.ts +3 -0
  38. package/src/nft/description.ts +5 -10
  39. package/src/resolution/resolve-item.test.ts +15 -10
  40. package/src/resolution/resolve-item.ts +2 -2
  41. package/src/scheduling/unwrap.test.ts +60 -0
  42. package/src/scheduling/unwrap.ts +24 -8
  43. package/src/shipload.ts +5 -0
  44. package/src/types.ts +1 -0
@@ -4,7 +4,7 @@ import {getRecipe, type Recipe} from '../data/recipes-runtime'
4
4
  import {getItem} from '../data/catalog'
5
5
  import {
6
6
  ITEM_ENGINE_T1,
7
- ITEM_EXTRACTOR_T1_PACKED,
7
+ ITEM_EXTRACTOR_T2_PACKED,
8
8
  ITEM_GENERATOR_T1,
9
9
  ITEM_GATHERER_T1,
10
10
  ITEM_LOADER_T1,
@@ -16,7 +16,7 @@ import {
16
16
  ITEM_BATTERY_T1,
17
17
  ITEM_SHIP_T1_PACKED,
18
18
  ITEM_CONTAINER_T1_PACKED,
19
- ITEM_WAREHOUSE_T1_PACKED,
19
+ ITEM_WAREHOUSE_T2_PACKED,
20
20
  ITEM_CONTAINER_T2_PACKED,
21
21
  } from '../data/item-ids'
22
22
  import type {StatMapping, CapabilityAttributeRow} from '../data/capabilities'
@@ -35,8 +35,8 @@ export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
35
35
  battery: ITEM_BATTERY_T1,
36
36
  'ship-t1': ITEM_SHIP_T1_PACKED,
37
37
  'container-t1': ITEM_CONTAINER_T1_PACKED,
38
- 'warehouse-t1': ITEM_WAREHOUSE_T1_PACKED,
39
- 'extractor-t1': ITEM_EXTRACTOR_T1_PACKED,
38
+ 'warehouse-t2': ITEM_WAREHOUSE_T2_PACKED,
39
+ 'extractor-t2': ITEM_EXTRACTOR_T2_PACKED,
40
40
  'container-t2': ITEM_CONTAINER_T2_PACKED,
41
41
  }
42
42
 
@@ -0,0 +1,9 @@
1
+ import {expect, test} from 'bun:test'
2
+ import {resolveCapacityFnName} from './capacity-fn-registry'
3
+ import kindRegistryJson from '../data/kind-registry.json'
4
+
5
+ test('every template in the catalog resolves to a known capacity function', () => {
6
+ for (const t of kindRegistryJson.templates as {itemId: number}[]) {
7
+ expect(() => resolveCapacityFnName(t.itemId)).not.toThrow()
8
+ }
9
+ })
@@ -0,0 +1,18 @@
1
+ import {getBaseCapacityFnName} from '../data/kind-registry'
2
+
3
+ export const CAPACITY_FN_NAMES = ['ship', 'warehouse', 'depot', 'container', 'workshop'] as const
4
+
5
+ export type CapacityFnName = (typeof CAPACITY_FN_NAMES)[number]
6
+
7
+ function isCapacityFnName(name: string): name is CapacityFnName {
8
+ return (CAPACITY_FN_NAMES as readonly string[]).includes(name)
9
+ }
10
+
11
+ export function resolveCapacityFnName(itemId: number): CapacityFnName | undefined {
12
+ const name = getBaseCapacityFnName(itemId)
13
+ if (!name) return undefined
14
+ if (!isCapacityFnName(name)) {
15
+ throw new Error(`unrecognized base capacity function "${name}" for item ${itemId}`)
16
+ }
17
+ return name
18
+ }
@@ -7,6 +7,7 @@ import {
7
7
  } from './recipe-usage'
8
8
  import {
9
9
  ITEM_SENSOR,
10
+ ITEM_SENSOR_T2,
10
11
  ITEM_RESIN,
11
12
  ITEM_FRAME,
12
13
  ITEM_PLATE,
@@ -14,7 +15,7 @@ import {
14
15
  ITEM_GATHERER_T1,
15
16
  ITEM_CRAFTER_T1,
16
17
  ITEM_BUILDER_T1,
17
- ITEM_EXTRACTOR_T1_PACKED,
18
+ ITEM_EXTRACTOR_T2_PACKED,
18
19
  ITEM_ROUSTABOUT_T1A_PACKED,
19
20
  ITEM_PROSPECTOR_T1A_PACKED,
20
21
  ITEM_TENDER_T1A_PACKED,
@@ -32,12 +33,7 @@ test('getRecipeConsumers lists every recipe that consumes Sensor', () => {
32
33
  const consumers = getRecipeConsumers(ITEM_SENSOR)
33
34
  const ids = consumers.map((c) => c.outputItemId).sort((a, b) => a - b)
34
35
  expect(ids).toEqual(
35
- [
36
- ITEM_CRAFTER_T1,
37
- ITEM_BUILDER_T1,
38
- ITEM_EXTRACTOR_T1_PACKED,
39
- ITEM_ROUSTABOUT_T1A_PACKED,
40
- ].sort((a, b) => a - b)
36
+ [ITEM_CRAFTER_T1, ITEM_BUILDER_T1, ITEM_ROUSTABOUT_T1A_PACKED].sort((a, b) => a - b)
41
37
  )
42
38
  })
43
39
 
@@ -52,8 +48,8 @@ test('Frame feeds the gatherer drain stat', () => {
52
48
  })
53
49
 
54
50
  test('Sensor is a mass-only sink in the Extractor recipe', () => {
55
- const consumers = getRecipeConsumers(ITEM_SENSOR)
56
- const extractor = consumers.find((c) => c.outputItemId === ITEM_EXTRACTOR_T1_PACKED)
51
+ const consumers = getRecipeConsumers(ITEM_SENSOR_T2)
52
+ const extractor = consumers.find((c) => c.outputItemId === ITEM_EXTRACTOR_T2_PACKED)
57
53
  expect(extractor).toBeDefined()
58
54
  expect(extractor?.statFlows).toHaveLength(0)
59
55
  })
@@ -80,9 +76,9 @@ test('getResourceDemand scales by quantity', () => {
80
76
  expect(getResourceDemand(ITEM_PLATE, 3)).toEqual({ore: 30})
81
77
  })
82
78
 
83
- test('getComponentDemand reports Resin as consumed by eight recipes', () => {
79
+ test('getComponentDemand reports Resin as consumed by seven recipes', () => {
84
80
  const demand = getComponentDemand()
85
81
  const resin = demand.find((d) => d.itemId === ITEM_RESIN)
86
82
  expect(resin).toBeDefined()
87
- expect(resin?.consumerCount).toBe(8)
83
+ expect(resin?.consumerCount).toBe(7)
88
84
  })
@@ -6,7 +6,7 @@ import {getKindMeta, getTemplateMeta} from '../data/kind-registry'
6
6
  import type {EntityTypeName} from '../data/kind-registry'
7
7
  import {getEntityLayout} from '../data/recipes-runtime'
8
8
  import type {EntitySlot} from '../data/recipes-runtime'
9
- import {itemMetadata} from '../data/metadata'
9
+ import {getItemFamily} from '../data/metadata'
10
10
  import {getItem} from '../data/catalog'
11
11
  import {getModuleCapabilityType, moduleAccepts, moduleSlotTypeToCode} from '../capabilities/modules'
12
12
  import {computeEntityCapabilities} from '../derivation/capabilities'
@@ -52,7 +52,7 @@ function assignModulesToSlots(
52
52
  try {
53
53
  modName = getItem(mod.itemId).name
54
54
  } catch {
55
- modName = itemMetadata[mod.itemId]?.name ?? `item ${mod.itemId}`
55
+ modName = getItemFamily(mod.itemId)?.name ?? `item ${mod.itemId}`
56
56
  }
57
57
  throw new Error(
58
58
  `No compatible slot for module ${modName} (type ${modType}) on ${entityLabel}`
@@ -57,6 +57,7 @@ export {
57
57
  computeFreeCells,
58
58
  NftManager,
59
59
  BalancesManager,
60
+ FundManager,
60
61
  ConstructionManager,
61
62
  JobsManager,
62
63
  InfluenceManager,
@@ -80,6 +81,9 @@ export type {
80
81
  NftConfigForItem,
81
82
  WrapConfig,
82
83
  WrapGate,
84
+ FundBeneficiary,
85
+ FundToken,
86
+ FundAccrued,
83
87
  DepositConfig,
84
88
  PlatformBalance,
85
89
  TokenBalance,
@@ -120,6 +124,8 @@ export {
120
124
  categoryFromIndex,
121
125
  categoryLabelFromIndex,
122
126
  } from './data/catalog'
127
+ export {getItemFamily, itemFamilies, itemFamilyKey} from './data/metadata'
128
+ export type {ItemFamily} from './data/metadata'
123
129
  export {getCurrentEpoch, getEpochInfo} from './scheduling/epoch'
124
130
  export type {EpochInfo} from './scheduling/epoch'
125
131
  export {
@@ -370,12 +376,18 @@ export {
370
376
  derivedLoaders,
371
377
  estimateUnwrapDuration,
372
378
  incomingHoldMass,
379
+ projectCargomass,
373
380
  projectedPeakCargomass,
374
381
  receiveFits,
375
382
  unwrapLoadDuration,
376
383
  unwrapTransitDuration,
377
384
  } from './scheduling/unwrap'
378
- export type {DerivedLoaders, UnwrapDestination, UnwrapItem} from './scheduling/unwrap'
385
+ export type {
386
+ CargomassProjection,
387
+ DerivedLoaders,
388
+ UnwrapDestination,
389
+ UnwrapItem,
390
+ } from './scheduling/unwrap'
379
391
 
380
392
  export {
381
393
  projectedCargoAvailableAt,
@@ -413,6 +425,7 @@ export {
413
425
  ENTITY_NEXUS,
414
426
  ENTITY_HUB,
415
427
  ENTITY_DEPOT,
428
+ ENTITY_ASSEMBLY_YARD,
416
429
  getEntityClass,
417
430
  getPackedEntityType,
418
431
  getKindMeta,
@@ -438,6 +451,7 @@ export {
438
451
  isPlot,
439
452
  isHub,
440
453
  isDepot,
454
+ isAssemblyYard,
441
455
  } from './data/kind-registry'
442
456
  export type {EntityTypeName, KindMeta, TemplateMeta} from './data/kind-registry'
443
457
  export * from './capabilities'
@@ -136,7 +136,7 @@ describe('decomposition', () => {
136
136
  for (let i = 1; i < DECOMP_REGISTRY.length; i++) {
137
137
  expect(DECOMP_REGISTRY[i].itemId).toBeGreaterThan(DECOMP_REGISTRY[i - 1].itemId)
138
138
  }
139
- expect(DECOMP_REGISTRY.length).toBe(56)
139
+ expect(DECOMP_REGISTRY.length).toBe(57)
140
140
  })
141
141
 
142
142
  test('the retired Workshop has no decomposition entry', () => {
@@ -610,6 +610,18 @@ export class ActionsManager extends BaseManager {
610
610
  })
611
611
  }
612
612
 
613
+ blenddepot(
614
+ depotId: UInt64Type,
615
+ owner: NameType,
616
+ inputs: ServerContract.ActionParams.Type.cargo_item[]
617
+ ): Action {
618
+ return this.server.action('blenddepot', {
619
+ depot_id: UInt64.from(depotId),
620
+ owner: Name.from(owner),
621
+ inputs,
622
+ })
623
+ }
624
+
613
625
  deploy(
614
626
  entityId: UInt64Type,
615
627
  ref: ServerContract.ActionParams.Type.cargo_ref,
@@ -1038,6 +1050,11 @@ export class ActionsManager extends BaseManager {
1038
1050
  })
1039
1051
  }
1040
1052
 
1053
+ /** Pays every nonzero accrued token balance to the beneficiary. */
1054
+ claimFund(beneficiary: NameType): Action {
1055
+ return this.fund.action('claim', {beneficiary_account: Name.from(beneficiary)})
1056
+ }
1057
+
1041
1058
  collectFund(): Action {
1042
1059
  return this.fund.action('collect', {})
1043
1060
  }
@@ -13,6 +13,7 @@ import {ActionsManager} from './actions'
13
13
  import {ClusterManager} from './cluster'
14
14
  import {NftManager} from './nft'
15
15
  import {BalancesManager} from './balances'
16
+ import {FundManager} from './fund'
16
17
  import {JobsManager} from './jobs'
17
18
  import {InfluenceManager} from './influence'
18
19
  import {SubscriptionsManager} from '../subscriptions/manager'
@@ -27,6 +28,7 @@ export class GameContext {
27
28
  private _clusters?: ClusterManager
28
29
  private _nft?: NftManager
29
30
  private _balances?: BalancesManager
31
+ private _funds?: FundManager
30
32
  private _jobs?: JobsManager
31
33
  private _influence?: InfluenceManager
32
34
  private _subscriptions?: SubscriptionsManager
@@ -106,6 +108,13 @@ export class GameContext {
106
108
  return this._balances
107
109
  }
108
110
 
111
+ get funds(): FundManager {
112
+ if (!this._funds) {
113
+ this._funds = new FundManager(this)
114
+ }
115
+ return this._funds
116
+ }
117
+
109
118
  get jobs(): JobsManager {
110
119
  if (!this._jobs) {
111
120
  this._jobs = new JobsManager(this)
@@ -0,0 +1,49 @@
1
+ import {type Asset, Name, type NameType, type UInt16} from '@wharfkit/antelope'
2
+ import type {FundContract} from '../contracts'
3
+ import type {PlatformBalance} from './balances'
4
+ import {BaseManager} from './base'
5
+
6
+ export interface FundBeneficiary {
7
+ account: Name
8
+ bps: UInt16
9
+ }
10
+
11
+ export interface FundToken {
12
+ tokenContract: Name
13
+ symbol: Asset.Symbol
14
+ }
15
+
16
+ export interface FundAccrued {
17
+ tokenContract: Name
18
+ balance: Asset
19
+ }
20
+
21
+ /** Live fund ledger reads. Token custody can exceed the amounts accrued to beneficiaries. */
22
+ export class FundManager extends BaseManager {
23
+ async getBeneficiaries(): Promise<FundBeneficiary[]> {
24
+ const rows = (await this.fund
25
+ .table('benefs', this.fund.account)
26
+ .all()) as FundContract.Types.benef_row[]
27
+ return rows.map((row) => ({account: row.account, bps: row.bps}))
28
+ }
29
+
30
+ async getTokens(): Promise<FundToken[]> {
31
+ const rows = (await this.fund
32
+ .table('tokens', this.fund.account)
33
+ .all()) as FundContract.Types.token_row[]
34
+ return rows.map((row) => ({tokenContract: row.token_contract, symbol: row.token_symbol}))
35
+ }
36
+
37
+ /** Includes zero rows and claims for tokens no longer accepted; no rows returns []. */
38
+ async getAccrued(beneficiary: NameType): Promise<FundAccrued[]> {
39
+ const rows = (await this.fund
40
+ .table('accrued', Name.from(beneficiary))
41
+ .all()) as FundContract.Types.accrued_row[]
42
+ return rows.map((row) => ({tokenContract: row.token_contract, balance: row.balance}))
43
+ }
44
+
45
+ /** Platform balances that have not reached the fund's accrual ledger yet. */
46
+ async getUncollected(): Promise<PlatformBalance[]> {
47
+ return this.context.balances.getPlatformBalances(this.fund.account)
48
+ }
49
+ }
@@ -44,3 +44,6 @@ export type {
44
44
  ScheduledBuild,
45
45
  Reservation,
46
46
  } from './construction-types'
47
+
48
+ export {FundManager} from './fund'
49
+ export type {FundBeneficiary, FundToken, FundAccrued} from './fund'
@@ -32,6 +32,7 @@ import {MASS_UNITS_PER_TONNE, type ModuleType} from '../types'
32
32
  import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
33
33
  import {getBaseHullmassFor} from '../derivation/capabilities'
34
34
  import {computeEffectiveModuleStat} from '../derivation/stat-scaling'
35
+ import {resolveCapacityFnName, type CapacityFnName} from '../derivation/capacity-fn-registry'
35
36
 
36
37
  function idiv(a: number, b: number): number {
37
38
  return Math.floor(a / b)
@@ -89,24 +90,18 @@ export function computeBaseCapacityDepot(stats: bigint): number {
89
90
  return Math.floor(500_000 * 6 ** (s / 1998))
90
91
  }
91
92
 
92
- const CAPACITY_FN_BY_KIND: Record<string, (stats: bigint) => number> = {
93
+ const CAPACITY_FN_BY_NAME: Record<CapacityFnName, (stats: bigint) => number> = {
93
94
  ship: computeBaseCapacityShip,
94
95
  warehouse: computeBaseCapacityWarehouse,
95
96
  workshop: computeBaseCapacityWorkshop,
96
- extractor: computeBaseCapacityContainer,
97
- factory: computeBaseCapacityContainer,
98
- builddock: computeBaseCapacityContainer,
99
- mdriver: computeBaseCapacityContainer,
100
- mcatcher: computeBaseCapacityContainer,
101
97
  container: computeBaseCapacityContainer,
102
- nexus: computeBaseCapacityContainer,
103
98
  depot: computeBaseCapacityDepot,
104
99
  }
105
100
 
106
101
  export function computeBaseCapacityForEntity(itemId: number, stats: bigint): number {
107
- const kind = getTemplateMeta(itemId)?.kind
108
- if (!kind) return 0
109
- return CAPACITY_FN_BY_KIND[kind.toString()]?.(stats) ?? 0
102
+ const fnName = resolveCapacityFnName(itemId)
103
+ if (!fnName) return 0
104
+ return CAPACITY_FN_BY_NAME[fnName](stats)
110
105
  }
111
106
 
112
107
  export const computeEngineThrust = (vol: number, tier: number): number =>
@@ -2,11 +2,16 @@ import {expect, test} from 'bun:test'
2
2
  import {UInt16, UInt64} from '@wharfkit/antelope'
3
3
  import {resolveItem} from './resolve-item'
4
4
  import {encodeStats} from '../derivation/crafting'
5
- import {computeBaseHullmass, computeContainerCapabilities} from '../derivation/capabilities'
6
5
  import {
7
- ITEM_EXTRACTOR_T1_PACKED,
8
- ITEM_FACTORY_T1_PACKED,
9
- ITEM_HUB_T1_PACKED,
6
+ computeBaseHullmass,
7
+ computeContainerCapabilities,
8
+ applyCapacityTier,
9
+ } from '../derivation/capabilities'
10
+ import {getItem} from '../data/catalog'
11
+ import {
12
+ ITEM_EXTRACTOR_T2_PACKED,
13
+ ITEM_FACTORY_T2_PACKED,
14
+ ITEM_HUB_T2_PACKED,
10
15
  ITEM_MASS_DRIVER_T1_PACKED,
11
16
  ITEM_MASS_CATCHER_T1_PACKED,
12
17
  } from '../data/item-ids'
@@ -16,8 +21,8 @@ function hullStats(strength: number, density: number, hardness: number): UInt64
16
21
  }
17
22
 
18
23
  const CONTAINER_ENTITIES = [
19
- ['factory', ITEM_FACTORY_T1_PACKED],
20
- ['extractor', ITEM_EXTRACTOR_T1_PACKED],
24
+ ['factory', ITEM_FACTORY_T2_PACKED],
25
+ ['extractor', ITEM_EXTRACTOR_T2_PACKED],
21
26
  ['mass driver', ITEM_MASS_DRIVER_T1_PACKED],
22
27
  ['mass catcher', ITEM_MASS_CATCHER_T1_PACKED],
23
28
  ] as const
@@ -28,21 +33,21 @@ for (const [label, itemId] of CONTAINER_ENTITIES) {
28
33
  const resolved = resolveItem(UInt16.from(itemId), stats)
29
34
  const hull = resolved.attributes?.find((g) => g.capability === 'Hull')
30
35
  const capacity = hull?.attributes.find((a) => a.label === 'Capacity')?.value
31
- const expected = computeContainerCapabilities({
36
+ const base = computeContainerCapabilities({
32
37
  strength: 300,
33
38
  hardness: 400,
34
39
  density: 100,
35
40
  }).capacity
36
- expect(capacity).toBe(expected)
41
+ expect(capacity).toBe(applyCapacityTier(base, getItem(itemId).tier))
37
42
  })
38
43
  }
39
44
 
40
45
  test('resolveItem resolves the station hub with hullmass and zero cargo capacity', () => {
41
46
  const stats = hullStats(300, 100, 400)
42
- const resolved = resolveItem(UInt16.from(ITEM_HUB_T1_PACKED), stats)
47
+ const resolved = resolveItem(UInt16.from(ITEM_HUB_T2_PACKED), stats)
43
48
  const hull = resolved.attributes?.find((g) => g.capability === 'Hull')
44
49
  expect(hull?.attributes.find((a) => a.label === 'Mass')?.value).toBe(
45
- computeBaseHullmass(ITEM_HUB_T1_PACKED, {density: 100})
50
+ computeBaseHullmass(ITEM_HUB_T2_PACKED, {density: 100})
46
51
  )
47
52
  expect(hull?.attributes.find((a) => a.label === 'Capacity')?.value).toBe(0)
48
53
  })
@@ -4,7 +4,7 @@ import type {ResourceCategory} from '../types'
4
4
  import {getItem, getModules} from '../data/catalog'
5
5
  import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
6
6
  import {getEntityLayout} from '../data/recipes-runtime'
7
- import {entityMetadata, itemMetadata} from '../data/metadata'
7
+ import {entityMetadata, getItemFamily} from '../data/metadata'
8
8
  import {
9
9
  getModuleCapabilityType,
10
10
  isModuleItem,
@@ -370,7 +370,7 @@ function resolveEntity(
370
370
  modName = modItem.name
371
371
  modTier = modItem.tier
372
372
  } catch {
373
- modName = itemMetadata[modItemId]?.name ?? 'Module'
373
+ modName = getItemFamily(modItemId)?.name ?? 'Module'
374
374
  }
375
375
  const group = computeCapabilityGroup(modType, decodedStats, modTier, slot.outputPct)
376
376
  return {
@@ -5,8 +5,14 @@ import {
5
5
  unwrapLoadDuration,
6
6
  estimateUnwrapDuration,
7
7
  incomingHoldMass,
8
+ projectCargomass,
8
9
  projectedPeakCargomass,
9
10
  } from './unwrap'
11
+ import {TimePoint, UInt8, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
12
+ import {ServerContract} from '../contracts'
13
+ import {getItem} from '../data/catalog'
14
+ import {ITEM_BEAM} from '../data/item-ids'
15
+ import {TaskType} from '../types'
10
16
 
11
17
  describe('unwrap duration mirror', () => {
12
18
  test('derivedLoaders aggregates lanes like derived_loaders()', () => {
@@ -84,3 +90,57 @@ test('projectedPeakCargomass tracks the running peak from cargomass', () => {
84
90
  // No pending tasks: peak = base + candidate add.
85
91
  expect(projectedPeakCargomass(entity, new Date(0), 500)).toBe(1500)
86
92
  })
93
+
94
+ const DEPOSIT_START = new Date('2026-09-20T07:48:37.000Z')
95
+ const BEAM_QTY = 2_500
96
+ const BEAM_MASS = getItem(ITEM_BEAM).mass
97
+
98
+ function unloadingShip(projectedAtMs?: number) {
99
+ const cargo = ServerContract.Types.cargo_item.from({
100
+ item_id: UInt16.from(ITEM_BEAM),
101
+ quantity: UInt32.from(BEAM_QTY),
102
+ stats: UInt64.from(0),
103
+ modules: [],
104
+ })
105
+ const lane = ServerContract.Types.lane.from({
106
+ lane_key: UInt8.from(0),
107
+ schedule: ServerContract.Types.schedule.from({
108
+ started: TimePoint.fromMilliseconds(DEPOSIT_START.getTime()),
109
+ tasks: [
110
+ ServerContract.Types.task.from({
111
+ type: UInt8.from(TaskType.CIVIC_DEPOSIT),
112
+ duration: UInt32.from(1_309),
113
+ cancelable: UInt8.from(2),
114
+ cargo: [cargo],
115
+ couplings: [],
116
+ }),
117
+ ],
118
+ }),
119
+ })
120
+ return {
121
+ cargomass: BEAM_QTY * BEAM_MASS,
122
+ lanes: [lane],
123
+ holds: [],
124
+ projected_at:
125
+ projectedAtMs === undefined ? undefined : TimePoint.fromMilliseconds(projectedAtMs),
126
+ }
127
+ }
128
+
129
+ const UNLOAD_ENDS = new Date(DEPOSIT_START.getTime() + 1_309_000)
130
+ const AFTER_UNLOAD = new Date(UNLOAD_ENDS.getTime() + 60_000)
131
+
132
+ test('projectCargomass reports the mass held at the candidate and the peak around it', () => {
133
+ const plan = projectCargomass(unloadingShip(), AFTER_UNLOAD, 400 * BEAM_MASS)
134
+ // The queued unload empties the hold before the candidate lands.
135
+ expect(plan.used).toBe(400 * BEAM_MASS)
136
+ expect(plan.peak).toBe(BEAM_QTY * BEAM_MASS)
137
+ })
138
+
139
+ test('a task already applied to a projected row is not replayed', () => {
140
+ const applied = unloadingShip(UNLOAD_ENDS.getTime())
141
+ applied.cargomass = 0
142
+ const plan = projectCargomass(applied, AFTER_UNLOAD, 400 * BEAM_MASS)
143
+ expect(plan.used).toBe(400 * BEAM_MASS)
144
+ expect(plan.peak).toBe(400 * BEAM_MASS)
145
+ expect(projectedPeakCargomass(applied, AFTER_UNLOAD, 400 * BEAM_MASS)).toBe(plan.peak)
146
+ })
@@ -133,28 +133,44 @@ function cargoListMass(items: CargoItem[]): number {
133
133
  return m
134
134
  }
135
135
 
136
- export function projectedPeakCargomass(
137
- entity: sched.ScheduleData & {cargomass: number | {toNumber(): number}},
136
+ export interface CargomassProjection {
137
+ used: number
138
+ peak: number
139
+ }
140
+
141
+ export function projectCargomass(
142
+ entity: sched.AnchoredScheduleData & {cargomass: number | {toNumber(): number}},
138
143
  at: Date,
139
144
  addMass: number,
140
145
  removeMass = 0
141
- ): number {
142
- const events: {t: number; delta: number}[] = []
143
- for (const ordered of sched.orderedTasks(entity)) {
146
+ ): CargomassProjection {
147
+ const events: {t: number; delta: number; candidate: boolean}[] = []
148
+ for (const ordered of sched.unappliedTasks(entity)) {
144
149
  const eff = taskCargoEffect(ordered.task)
145
150
  const delta = cargoListMass(eff.added) - cargoListMass(eff.removed)
146
- events.push({t: ordered.completesAt.getTime(), delta})
151
+ events.push({t: ordered.completesAt.getTime(), delta, candidate: false})
147
152
  }
148
- events.push({t: at.getTime(), delta: addMass - removeMass})
153
+ events.push({t: at.getTime(), delta: addMass - removeMass, candidate: true})
149
154
  events.sort((a, b) => (a.t !== b.t ? a.t - b.t : b.delta - a.delta))
150
155
  let running = Number(entity.cargomass)
151
156
  let peak = running
157
+ let used = running
152
158
  for (const e of events) {
153
159
  running += e.delta
154
160
  if (running < 0) running = 0
155
161
  if (running > peak) peak = running
162
+ if (e.candidate) used = running
156
163
  }
157
- return Math.min(peak, 0xffff_ffff)
164
+ return {used: Math.min(used, 0xffff_ffff), peak: Math.min(peak, 0xffff_ffff)}
165
+ }
166
+
167
+ export function projectedPeakCargomass(
168
+ entity: sched.AnchoredScheduleData & {cargomass: number | {toNumber(): number}},
169
+ at: Date,
170
+ addMass: number,
171
+ removeMass = 0
172
+ ): number {
173
+ return projectCargomass(entity, at, addMass, removeMass).peak
158
174
  }
159
175
 
160
176
  export function receiveFits(
package/src/shipload.ts CHANGED
@@ -13,6 +13,7 @@ import type {ActionsManager} from './managers/actions'
13
13
  import type {ClusterManager} from './managers/cluster'
14
14
  import type {NftManager} from './managers/nft'
15
15
  import type {BalancesManager} from './managers/balances'
16
+ import type {FundManager} from './managers/fund'
16
17
  import type {JobsManager} from './managers/jobs'
17
18
  import type {InfluenceManager} from './managers/influence'
18
19
  import type {SubscriptionsManager} from './subscriptions/manager'
@@ -154,6 +155,10 @@ export class Shipload {
154
155
  return this._context.nft
155
156
  }
156
157
 
158
+ get funds(): FundManager {
159
+ return this._context.funds
160
+ }
161
+
157
162
  get balances(): BalancesManager {
158
163
  return this._context.balances
159
164
  }
package/src/types.ts CHANGED
@@ -208,6 +208,7 @@ export const CATEGORY_LABELS: Record<ResourceCategory, string> = {
208
208
 
209
209
  export interface Item {
210
210
  id: number
211
+ family: string
211
212
  name: string
212
213
  description: string
213
214
  color: string