@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.
- package/lib/scan.d.ts +19 -1
- package/lib/scan.js +54 -28
- package/lib/scan.js.map +1 -1
- package/lib/scan.m.js +52 -29
- package/lib/scan.m.js.map +1 -1
- package/lib/shipload.d.ts +98 -1
- package/lib/shipload.js +297 -165
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +286 -166
- package/lib/shipload.m.js.map +1 -1
- package/package.json +1 -1
- package/src/data/capabilities.ts +8 -0
- package/src/data/recipes.json +1 -143
- package/src/derivation/capabilities.test.ts +4 -4
- package/src/derivation/capabilities.ts +1 -1
- package/src/derivation/capability-mappings.ts +49 -4
- package/src/derivation/recipe-usage.test.ts +8 -9
- package/src/derivation/wormhole.ts +5 -0
- package/src/index-module.ts +19 -2
- package/src/managers/actions.ts +17 -0
- package/src/managers/entities.ts +9 -0
- package/src/managers/index.ts +1 -0
- package/src/managers/players.ts +25 -0
- package/src/nft/buildImmutableData.ts +2 -2
- package/src/scan/index.ts +93 -29
- package/src/scan/scan-wasm.base64.ts +1 -1
- package/src/scheduling/unwrap.test.ts +60 -0
- package/src/scheduling/unwrap.ts +187 -0
- package/src/travel/route-planner.ts +82 -1
|
@@ -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
|
-
|
|
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
|
+
}
|