@shipload/sdk 1.0.0-next.41 → 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.
@@ -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
+ }
@@ -169,9 +169,38 @@ function reconstruct(cameFrom: Map<string, Coord>, origin: Coord, dest: Coord):
169
169
  return {ok: true, waypoints, legs: waypoints.length, totalDistance}
170
170
  }
171
171
 
172
+ export interface ScanProvider {
173
+ getLocationType(seedHex: string, x: number, y: number): number
174
+ systemsInBox(
175
+ seedHex: string,
176
+ xMin: number,
177
+ yMin: number,
178
+ xMax: number,
179
+ yMax: number
180
+ ): {x: number; y: number; locType: number}[]
181
+ }
182
+
183
+ let scanProvider: ScanProvider | null = null
184
+ const graphCache = new Map<string, SystemGraph>()
185
+
186
+ // Inject a fast (e.g. wasm) location-type backend; null restores the pure-JS path. Clears the graph cache.
187
+ export function setScanProvider(provider: ScanProvider | null): void {
188
+ scanProvider = provider
189
+ graphCache.clear()
190
+ }
191
+
172
192
  export function sdkSystemGraph(seed: Checksum256Type): SystemGraph {
173
193
  const s = Checksum256.from(seed)
174
- // Travelable nodes mirror the contract's is_travelable: systems plus wormhole mouths.
194
+ const seedHex = s.toString()
195
+ const cached = graphCache.get(seedHex)
196
+ if (cached) return cached
197
+ const graph = scanProvider ? wasmSystemGraph(s, seedHex, scanProvider) : jsSystemGraph(s)
198
+ graphCache.set(seedHex, graph)
199
+ return graph
200
+ }
201
+
202
+ // Travelable nodes mirror the contract's is_travelable: systems plus wormhole mouths.
203
+ function jsSystemGraph(s: Checksum256): SystemGraph {
175
204
  return {
176
205
  hasSystem: (c) => hasSystem(s, {x: c.x, y: c.y}) || wormholeAt(s, c.x, c.y) !== null,
177
206
  nearby: (c, reachTiles) => {
@@ -194,3 +223,55 @@ export function sdkSystemGraph(seed: Checksum256Type): SystemGraph {
194
223
  },
195
224
  }
196
225
  }
226
+
227
+ const SCAN_BUCKET = 48
228
+
229
+ function wasmSystemGraph(s: Checksum256, seedHex: string, scan: ScanProvider): SystemGraph {
230
+ // Per-bucket system cache: reused across the overlapping node queries A* makes (and across routes).
231
+ const bucketCache = new Map<string, {x: number; y: number}[]>()
232
+ const bucketSystems = (bx: number, by: number): {x: number; y: number}[] => {
233
+ const k = `${bx},${by}`
234
+ let v = bucketCache.get(k)
235
+ if (v === undefined) {
236
+ const xMin = bx * SCAN_BUCKET
237
+ const yMin = by * SCAN_BUCKET
238
+ v = scan
239
+ .systemsInBox(seedHex, xMin, yMin, xMin + SCAN_BUCKET - 1, yMin + SCAN_BUCKET - 1)
240
+ .map((cell) => ({x: cell.x, y: cell.y}))
241
+ bucketCache.set(k, v)
242
+ }
243
+ return v
244
+ }
245
+ return {
246
+ hasSystem: (c) =>
247
+ scan.getLocationType(seedHex, c.x, c.y) !== 0 || wormholeAt(s, c.x, c.y) !== null,
248
+ nearby: (c, reachTiles) => {
249
+ const r = Math.floor(reachTiles)
250
+ const seen = new Set<string>([`${c.x},${c.y}`])
251
+ const out: Neighbor[] = []
252
+ const bx0 = Math.floor((c.x - r) / SCAN_BUCKET)
253
+ const bx1 = Math.floor((c.x + r) / SCAN_BUCKET)
254
+ const by0 = Math.floor((c.y - r) / SCAN_BUCKET)
255
+ const by1 = Math.floor((c.y + r) / SCAN_BUCKET)
256
+ for (let bx = bx0; bx <= bx1; bx++) {
257
+ for (let by = by0; by <= by1; by++) {
258
+ for (const cell of bucketSystems(bx, by)) {
259
+ const dist = Math.hypot(cell.x - c.x, cell.y - c.y)
260
+ if (dist > reachTiles) continue
261
+ const k = `${cell.x},${cell.y}`
262
+ if (seen.has(k)) continue
263
+ seen.add(k)
264
+ out.push({coord: {x: cell.x, y: cell.y}, dist})
265
+ }
266
+ }
267
+ }
268
+ for (const coord of nearbyWormholes(s, c.x, c.y, reachTiles)) {
269
+ const k = `${coord.x},${coord.y}`
270
+ if (seen.has(k)) continue
271
+ seen.add(k)
272
+ out.push({coord, dist: Math.hypot(coord.x - c.x, coord.y - c.y)})
273
+ }
274
+ return out
275
+ },
276
+ }
277
+ }