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

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipload/sdk",
3
3
  "description": "SDKs for Shipload",
4
- "version": "1.0.0-next.42",
4
+ "version": "1.0.0-next.43",
5
5
  "homepage": "https://github.com/shipload/toolkit/tree/master/packages/sdk",
6
6
  "repository": {
7
7
  "type": "git",
@@ -8,6 +8,14 @@ export interface StatMapping {
8
8
  stat: string
9
9
  capability: string
10
10
  attribute: string
11
+ source: string // producing module/role, always present
12
+ }
13
+
14
+ export interface CapabilityAttributeRow {
15
+ capability: string
16
+ attribute: string
17
+ description: string
18
+ source?: string // producing module/role; absent when no formula-derived producer exists
11
19
  }
12
20
 
13
21
  export const capabilityNames: string[] = [
@@ -875,147 +875,5 @@
875
875
  }
876
876
  ],
877
877
  "blendWeights": []
878
- },
879
- {
880
- "outputItemId": 20001,
881
- "outputMass": 500,
882
- "inputs": [
883
- {
884
- "itemId": 10001,
885
- "quantity": 200
886
- },
887
- {
888
- "itemId": 102,
889
- "quantity": 15
890
- }
891
- ],
892
- "statSlots": [
893
- {
894
- "sources": [
895
- {
896
- "inputIndex": 0,
897
- "statIndex": 0
898
- },
899
- {
900
- "inputIndex": 1,
901
- "statIndex": 0
902
- }
903
- ]
904
- },
905
- {
906
- "sources": [
907
- {
908
- "inputIndex": 0,
909
- "statIndex": 1
910
- },
911
- {
912
- "inputIndex": 1,
913
- "statIndex": 2
914
- }
915
- ]
916
- }
917
- ],
918
- "blendWeights": [
919
- 1,
920
- 1
921
- ]
922
- },
923
- {
924
- "outputItemId": 20002,
925
- "outputMass": 300,
926
- "inputs": [
927
- {
928
- "itemId": 10002,
929
- "quantity": 200
930
- },
931
- {
932
- "itemId": 402,
933
- "quantity": 10
934
- },
935
- {
936
- "itemId": 502,
937
- "quantity": 20
938
- }
939
- ],
940
- "statSlots": [
941
- {
942
- "sources": [
943
- {
944
- "inputIndex": 0,
945
- "statIndex": 0
946
- },
947
- {
948
- "inputIndex": 1,
949
- "statIndex": 1
950
- }
951
- ]
952
- },
953
- {
954
- "sources": [
955
- {
956
- "inputIndex": 0,
957
- "statIndex": 1
958
- },
959
- {
960
- "inputIndex": 2,
961
- "statIndex": 2
962
- }
963
- ]
964
- }
965
- ],
966
- "blendWeights": [
967
- 1,
968
- 1,
969
- 1
970
- ]
971
- },
972
- {
973
- "outputItemId": 20200,
974
- "outputMass": 80000,
975
- "inputs": [
976
- {
977
- "itemId": 20001,
978
- "quantity": 600
979
- },
980
- {
981
- "itemId": 20002,
982
- "quantity": 200
983
- }
984
- ],
985
- "statSlots": [
986
- {
987
- "sources": [
988
- {
989
- "inputIndex": 0,
990
- "statIndex": 0
991
- }
992
- ]
993
- },
994
- {
995
- "sources": [
996
- {
997
- "inputIndex": 0,
998
- "statIndex": 1
999
- }
1000
- ]
1001
- },
1002
- {
1003
- "sources": [
1004
- {
1005
- "inputIndex": 1,
1006
- "statIndex": 0
1007
- }
1008
- ]
1009
- },
1010
- {
1011
- "sources": [
1012
- {
1013
- "inputIndex": 1,
1014
- "statIndex": 1
1015
- }
1016
- ]
1017
- }
1018
- ],
1019
- "blendWeights": []
1020
878
  }
1021
879
  ]
@@ -18,7 +18,8 @@ import {
18
18
  ITEM_WAREHOUSE_T1_PACKED,
19
19
  ITEM_CONTAINER_T2_PACKED,
20
20
  } from '../data/item-ids'
21
- import type {StatMapping} from '../data/capabilities'
21
+ import type {StatMapping, CapabilityAttributeRow} from '../data/capabilities'
22
+ import {capabilityAttributes} from '../data/capabilities'
22
23
 
23
24
  export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
24
25
  engine: ITEM_ENGINE_T1,
@@ -69,6 +70,12 @@ function traceToRawCategoryStat(
69
70
  return traceToRawCategoryStat(subRecipe, subSource, nextVisited)
70
71
  }
71
72
 
73
+ // Producing role for a capability·attribute: entity hull slots all roll up to "Hull"; modules use their own name.
74
+ export function sourceLabelForOutput(itemId: number): string {
75
+ const item = getItem(itemId)
76
+ return item.type === 'entity' ? 'Hull' : item.name
77
+ }
78
+
72
79
  let cached: StatMapping[] | undefined
73
80
 
74
81
  export function deriveStatMappings(): StatMapping[] {
@@ -82,20 +89,22 @@ export function deriveStatMappings(): StatMapping[] {
82
89
  const itemId = KIND_TO_ITEM_ID[kind]
83
90
  const recipe = getRecipe(itemId)
84
91
  if (!recipe) continue
92
+ const source = sourceLabelForOutput(itemId)
85
93
  for (const [slotIdxStr, consumer] of Object.entries(slots)) {
86
94
  const slotIdx = Number(slotIdxStr)
87
95
  const slot = recipe.statSlots[slotIdx]
88
96
  if (!slot) continue
89
- for (const source of slot.sources) {
90
- const stat = traceToRawCategoryStat(recipe, source)
97
+ for (const src of slot.sources) {
98
+ const stat = traceToRawCategoryStat(recipe, src)
91
99
  if (!stat) continue
92
- const key = `${stat.label}|${consumer.capability}|${consumer.attribute}`
100
+ const key = `${stat.label}|${consumer.capability}|${consumer.attribute}|${source}`
93
101
  if (seen.has(key)) continue
94
102
  seen.add(key)
95
103
  out.push({
96
104
  stat: stat.label,
97
105
  capability: consumer.capability,
98
106
  attribute: consumer.attribute,
107
+ source,
99
108
  })
100
109
  }
101
110
  }
@@ -115,3 +124,39 @@ export function getStatMappingsForStat(stat: string): StatMapping[] {
115
124
  export function getStatMappingsForCapability(capability: string): StatMapping[] {
116
125
  return deriveStatMappings().filter((m) => m.capability === capability)
117
126
  }
127
+
128
+ export function getProducersForAttribute(capability: string, attribute: string): string[] {
129
+ const seen = new Set<string>()
130
+ const out: string[] = []
131
+ for (const m of deriveStatMappings()) {
132
+ if (m.capability !== capability || m.attribute !== attribute) continue
133
+ if (seen.has(m.source)) continue
134
+ seen.add(m.source)
135
+ out.push(m.source)
136
+ }
137
+ return out
138
+ }
139
+
140
+ export function getCapabilityAttributeRows(): CapabilityAttributeRow[] {
141
+ const rows: CapabilityAttributeRow[] = []
142
+ for (const ca of capabilityAttributes) {
143
+ const producers = getProducersForAttribute(ca.capability, ca.attribute)
144
+ if (producers.length === 0) {
145
+ rows.push({
146
+ capability: ca.capability,
147
+ attribute: ca.attribute,
148
+ description: ca.description,
149
+ })
150
+ continue
151
+ }
152
+ for (const source of producers) {
153
+ rows.push({
154
+ capability: ca.capability,
155
+ attribute: ca.attribute,
156
+ description: ca.description,
157
+ source,
158
+ })
159
+ }
160
+ }
161
+ return rows
162
+ }
@@ -283,6 +283,17 @@ export type {
283
283
  CancelEligibilityInput,
284
284
  } from './scheduling/cancel'
285
285
 
286
+ export {
287
+ derivedLoaders,
288
+ estimateUnwrapDuration,
289
+ incomingHoldMass,
290
+ projectedPeakCargomass,
291
+ receiveFits,
292
+ unwrapLoadDuration,
293
+ unwrapTransitDuration,
294
+ } from './scheduling/unwrap'
295
+ export type {DerivedLoaders, UnwrapDestination, UnwrapItem} from './scheduling/unwrap'
296
+
286
297
  export {
287
298
  projectedCargoAvailableAt,
288
299
  availableForItem,
@@ -348,13 +359,16 @@ export {
348
359
  isInvertedAttribute,
349
360
  getCapabilityAttributes,
350
361
  } from './data/capabilities'
351
- export type {CapabilityAttribute, StatMapping} from './data/capabilities'
362
+ export type {CapabilityAttribute, StatMapping, CapabilityAttributeRow} from './data/capabilities'
352
363
 
353
364
  export {
354
365
  deriveStatMappings,
355
366
  getStatMappings,
356
367
  getStatMappingsForStat,
357
368
  getStatMappingsForCapability,
369
+ getProducersForAttribute,
370
+ getCapabilityAttributeRows,
371
+ sourceLabelForOutput,
358
372
  } from './derivation/capability-mappings'
359
373
  export {SLOT_FORMULAS} from './data/capability-formulas'
360
374
  export type {SlotConsumer, SlotConsumerKind} from './data/capability-formulas'
@@ -422,6 +436,7 @@ export {
422
436
  feistelInv,
423
437
  regionOf,
424
438
  partnerRegion,
439
+ nearbyWormholes,
425
440
  wormholeAt,
426
441
  wormholeAtRegionEndpoint,
427
442
  isValidWormholePair,
@@ -593,6 +593,23 @@ export class ActionsManager extends BaseManager {
593
593
  )
594
594
  }
595
595
 
596
+ sendAsset(owner: NameType, recipient: NameType, assetId: UInt64Type, memo = ''): Action {
597
+ return Action.from(
598
+ {
599
+ account: this.atomicAssetsAccount,
600
+ name: 'transfer',
601
+ authorization: [{actor: Name.from(owner), permission: 'active'}],
602
+ data: {
603
+ from: Name.from(owner),
604
+ to: Name.from(recipient),
605
+ asset_ids: [UInt64.from(assetId)],
606
+ memo,
607
+ },
608
+ },
609
+ ATOMICASSETS_ABI
610
+ )
611
+ }
612
+
596
613
  // Two top-level actions the wallet signs to unwrap an NFT into a host's cargo.
597
614
  unwrapCargoTx(owner: NameType, assetId: UInt64Type, hostId: UInt64Type): Action[] {
598
615
  return [this.transferForUnwrap(owner, assetId), this.placecargo(owner, hostId, assetId)]
@@ -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
+ }