@shipload/sdk 1.0.0-next.5 → 1.0.0-next.51
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 +52 -0
- package/lib/scan.js +162 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +152 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +3167 -1353
- package/lib/shipload.js +13319 -5640
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +13091 -5589
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1037 -0
- package/lib/testing.js +4363 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +4357 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.ts +51 -0
- package/src/capabilities/crafting.test.ts +7 -0
- package/src/capabilities/crafting.ts +6 -7
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +37 -19
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +22 -0
- package/src/capabilities/storage.ts +16 -1
- package/src/contracts/platform.ts +231 -3
- package/src/contracts/server.ts +1178 -511
- package/src/coordinates/address.ts +88 -0
- package/src/coordinates/constants.test.ts +15 -0
- package/src/coordinates/constants.ts +23 -0
- package/src/coordinates/index.ts +15 -0
- package/src/coordinates/memo.test.ts +47 -0
- package/src/coordinates/memo.ts +20 -0
- package/src/coordinates/permutation.ts +77 -0
- package/src/coordinates/regions.ts +48 -0
- package/src/coordinates/sectors.ts +115 -0
- package/src/data/capabilities.ts +31 -16
- package/src/data/capability-formulas.ts +21 -14
- package/src/data/catalog.ts +0 -5
- package/src/data/colors.ts +13 -47
- package/src/data/entities.json +154 -10
- package/src/data/item-ids.ts +31 -12
- package/src/data/items.json +192 -74
- package/src/data/kind-registry.json +131 -0
- package/src/data/kind-registry.ts +172 -0
- package/src/data/metadata.ts +166 -48
- package/src/data/recipes-runtime.ts +5 -23
- package/src/data/recipes.json +1003 -163
- package/src/derivation/build-methods.test.ts +13 -0
- package/src/derivation/build-methods.ts +48 -0
- package/src/derivation/capabilities.test.ts +151 -0
- package/src/derivation/capabilities.ts +527 -0
- package/src/derivation/capability-mappings.ts +58 -16
- package/src/derivation/crafting.ts +36 -33
- package/src/derivation/index.ts +28 -2
- package/src/derivation/recipe-usage.test.ts +81 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +98 -19
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +56 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/stratum.ts +26 -22
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/upgrades.ts +14 -0
- package/src/derivation/wormhole.ts +141 -0
- package/src/entities/entity.ts +98 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +131 -134
- package/src/entities/slot-multiplier.ts +43 -0
- package/src/errors.ts +12 -16
- package/src/format.ts +26 -4
- package/src/index-module.ts +294 -34
- package/src/managers/actions.ts +616 -105
- package/src/managers/base.ts +6 -2
- package/src/managers/cluster.test.ts +39 -0
- package/src/managers/cluster.ts +53 -0
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +412 -0
- package/src/managers/context.ts +29 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +27 -66
- package/src/managers/epochs.ts +40 -0
- package/src/managers/flatten-gather-plan.test.ts +80 -0
- package/src/managers/index.ts +20 -1
- package/src/managers/locations.ts +25 -29
- package/src/managers/nft.test.ts +14 -0
- package/src/managers/nft.ts +70 -0
- package/src/managers/players.ts +25 -0
- package/src/managers/plot.ts +122 -0
- package/src/nft/atomicassets.abi.json +1342 -0
- package/src/nft/atomicassets.ts +237 -0
- package/src/nft/atomicdata.ts +130 -0
- package/src/nft/buildImmutableData.ts +336 -0
- package/src/nft/description.ts +104 -36
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +242 -0
- package/src/planner/planner.test.ts +325 -0
- package/src/resolution/describe-module.ts +43 -25
- package/src/resolution/display-name.ts +38 -10
- package/src/resolution/resolve-item.test.ts +48 -0
- package/src/resolution/resolve-item.ts +74 -47
- package/src/scan/index.ts +244 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +65 -23
- package/src/scheduling/availability.ts +108 -0
- package/src/scheduling/cancel.test.ts +348 -0
- package/src/scheduling/cancel.ts +209 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +249 -0
- package/src/scheduling/lanes.ts +198 -0
- package/src/scheduling/projection.ts +210 -107
- package/src/scheduling/schedule.ts +240 -117
- package/src/scheduling/task-cargo.ts +46 -0
- package/src/scheduling/unwrap.test.ts +86 -0
- package/src/scheduling/unwrap.ts +190 -0
- package/src/shipload.ts +26 -1
- package/src/subscriptions/manager.cluster.test.ts +51 -0
- package/src/subscriptions/manager.ts +245 -172
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +28 -3
- package/src/testing/catalog-hash.ts +19 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/projection-parity.ts +167 -0
- package/src/travel/reach.ts +23 -0
- package/src/travel/route-planner.ts +277 -0
- package/src/travel/route-simulator.ts +174 -0
- package/src/travel/travel.ts +175 -118
- package/src/types/capabilities.ts +29 -6
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +39 -14
- package/src/utils/cargo.test.ts +14 -0
- package/src/utils/cargo.ts +29 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/system.ts +36 -24
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -123
- package/src/entities/ship-deploy.ts +0 -295
- package/src/entities/ship.ts +0 -221
- package/src/entities/warehouse.ts +0 -127
- package/src/types/entity-traits.ts +0 -69
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type {UInt16, UInt32} from '@wharfkit/antelope'
|
|
2
|
+
import type {ServerContract} from '../contracts'
|
|
3
|
+
import type {ProjectedEntity} from '../scheduling/projection'
|
|
4
|
+
import {type CargoStack, cargoItemToStack, mergeStacks} from '../capabilities/storage'
|
|
5
|
+
|
|
6
|
+
export interface ContractProjectedState {
|
|
7
|
+
owner: {toString(): string}
|
|
8
|
+
coordinates: ServerContract.Types.coordinates
|
|
9
|
+
energy?: UInt16
|
|
10
|
+
cargomass: UInt32
|
|
11
|
+
cargo: ServerContract.Types.cargo_view[]
|
|
12
|
+
hullmass?: UInt32
|
|
13
|
+
capacity?: UInt32
|
|
14
|
+
engines?: ServerContract.Types.movement_stats
|
|
15
|
+
generator?: ServerContract.Types.energy_stats
|
|
16
|
+
hauler?: ServerContract.Types.hauler_stats
|
|
17
|
+
gatherer_lanes: ServerContract.Types.gatherer_lane[]
|
|
18
|
+
crafter_lanes: ServerContract.Types.crafter_lane[]
|
|
19
|
+
loader_lanes: ServerContract.Types.loader_lane[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProjectionComparisonOptions {
|
|
23
|
+
step?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function assertProjectionEquals(
|
|
27
|
+
contract: ContractProjectedState,
|
|
28
|
+
sdk: ProjectedEntity,
|
|
29
|
+
options: ProjectionComparisonOptions = {}
|
|
30
|
+
): void {
|
|
31
|
+
const mismatches: string[] = []
|
|
32
|
+
|
|
33
|
+
const record = (name: string, c: unknown, s: unknown) => {
|
|
34
|
+
if (c !== s) mismatches.push(` ${name}: contract=${fmt(c)} sdk=${fmt(s)}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const recordStatBlock = (name: string, c: unknown, s: unknown) => {
|
|
38
|
+
const cPresent = c !== undefined && c !== null
|
|
39
|
+
const sPresent = s !== undefined && s !== null
|
|
40
|
+
if (cPresent !== sPresent) {
|
|
41
|
+
mismatches.push(
|
|
42
|
+
` ${name}: contract=${cPresent ? 'present' : 'absent'} sdk=${sPresent ? 'present' : 'absent'}`
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
if (!cPresent) return
|
|
47
|
+
const cn = JSON.stringify(normaliseStatBlock(c))
|
|
48
|
+
const sn = JSON.stringify(normaliseStatBlock(s))
|
|
49
|
+
if (cn !== sn) mismatches.push(` ${name}: contract=${cn} sdk=${sn}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
record('coordinates.x', toNum(contract.coordinates.x), Number(sdk.location.x))
|
|
53
|
+
record('coordinates.y', toNum(contract.coordinates.y), Number(sdk.location.y))
|
|
54
|
+
record('energy', toNum(contract.energy), Number(sdk.energy))
|
|
55
|
+
record('cargomass', toNum(contract.cargomass), Number(sdk.cargoMass))
|
|
56
|
+
record('hullmass', toNum(contract.hullmass), Number(sdk.shipMass))
|
|
57
|
+
record('capacity', toNum(contract.capacity), sdk.capacity ? Number(sdk.capacity) : undefined)
|
|
58
|
+
|
|
59
|
+
recordStatBlock('engines', contract.engines, sdk.engines)
|
|
60
|
+
recordStatBlock('generator', contract.generator, sdk.generator)
|
|
61
|
+
recordStatBlock('hauler', contract.hauler, sdk.hauler)
|
|
62
|
+
|
|
63
|
+
const normLane = (l: {slot_index?: unknown; [k: string]: unknown}) =>
|
|
64
|
+
Object.fromEntries(
|
|
65
|
+
Object.entries(l)
|
|
66
|
+
.filter(([k]) => k !== 'slot_index')
|
|
67
|
+
.map(([k, v]) => [k, toNum(v) ?? 0])
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const compareLanes = (name: string, cLanes: unknown[], sLanes: unknown[]) => {
|
|
71
|
+
if (cLanes.length !== sLanes.length) {
|
|
72
|
+
mismatches.push(` ${name}.length: contract=${cLanes.length} sdk=${sLanes.length}`)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
for (let i = 0; i < cLanes.length; i++) {
|
|
76
|
+
const cn = JSON.stringify(normLane(cLanes[i] as Record<string, unknown>))
|
|
77
|
+
const sn = JSON.stringify(normLane(sLanes[i] as Record<string, unknown>))
|
|
78
|
+
if (cn !== sn) mismatches.push(` ${name}[${i}]: contract=${cn} sdk=${sn}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
compareLanes('gatherer_lanes', contract.gatherer_lanes as unknown[], sdk.gathererLanes ?? [])
|
|
83
|
+
compareLanes('crafter_lanes', contract.crafter_lanes as unknown[], sdk.crafterLanes ?? [])
|
|
84
|
+
compareLanes('loader_lanes', contract.loader_lanes as unknown[], sdk.loaderLanes ?? [])
|
|
85
|
+
|
|
86
|
+
if (contract.cargo.length > 0 || sdk.cargo.length > 0) {
|
|
87
|
+
const contractCargo = normaliseCargo(mergeContractCargo(contract.cargo))
|
|
88
|
+
const sdkCargo = normaliseCargo(sdk.cargo)
|
|
89
|
+
if (contractCargo.length !== sdkCargo.length) {
|
|
90
|
+
mismatches.push(
|
|
91
|
+
` cargo.length: contract=${contractCargo.length} sdk=${sdkCargo.length}`
|
|
92
|
+
)
|
|
93
|
+
} else {
|
|
94
|
+
for (let i = 0; i < contractCargo.length; i++) {
|
|
95
|
+
const c = contractCargo[i]
|
|
96
|
+
const s = sdkCargo[i]
|
|
97
|
+
if (c.itemId !== s.itemId || c.stats !== s.stats || c.quantity !== s.quantity) {
|
|
98
|
+
mismatches.push(
|
|
99
|
+
` cargo[${i}]: contract={item:${c.itemId},stats:${c.stats},qty:${c.quantity}} sdk={item:${s.itemId},stats:${s.stats},qty:${s.quantity}}`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (mismatches.length > 0) {
|
|
107
|
+
const header =
|
|
108
|
+
options.step !== undefined
|
|
109
|
+
? `projection divergence at step ${options.step}:`
|
|
110
|
+
: 'projection divergence:'
|
|
111
|
+
throw new Error([header, ...mismatches].join('\n'))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface NormalisedStack {
|
|
116
|
+
itemId: number
|
|
117
|
+
stats: string
|
|
118
|
+
quantity: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function mergeContractCargo(cargo: ServerContract.Types.cargo_view[]): CargoStack[] {
|
|
122
|
+
return cargo.reduce<CargoStack[]>(
|
|
123
|
+
(acc, row) =>
|
|
124
|
+
mergeStacks(acc, cargoItemToStack(row as unknown as ServerContract.Types.cargo_item)),
|
|
125
|
+
[]
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normaliseCargo(cargo: CargoStack[]): NormalisedStack[] {
|
|
130
|
+
return cargo
|
|
131
|
+
.map((s) => ({
|
|
132
|
+
itemId: Number(s.item_id),
|
|
133
|
+
stats: BigInt(s.stats.toString()).toString(),
|
|
134
|
+
quantity: BigInt(s.quantity.toString()).toString(),
|
|
135
|
+
}))
|
|
136
|
+
.sort(stackSort)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function stackSort(a: NormalisedStack, b: NormalisedStack): number {
|
|
140
|
+
if (a.itemId !== b.itemId) return a.itemId - b.itemId
|
|
141
|
+
return a.stats < b.stats ? -1 : a.stats > b.stats ? 1 : 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function toNum(v: unknown): number | undefined {
|
|
145
|
+
if (v === undefined || v === null) return undefined
|
|
146
|
+
if (typeof v === 'number') return v
|
|
147
|
+
if (typeof v === 'bigint') return Number(v)
|
|
148
|
+
if (typeof (v as {toNumber?: unknown}).toNumber === 'function') {
|
|
149
|
+
return (v as {toNumber(): number}).toNumber()
|
|
150
|
+
}
|
|
151
|
+
return Number(v as number)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fmt(v: unknown): string {
|
|
155
|
+
if (v === undefined) return 'undefined'
|
|
156
|
+
if (v === null) return 'null'
|
|
157
|
+
return String(v)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normaliseStatBlock(block: unknown): Record<string, number> {
|
|
161
|
+
const out: Record<string, number> = {}
|
|
162
|
+
const obj = block as Record<string, unknown>
|
|
163
|
+
for (const k of Object.keys(obj).sort()) {
|
|
164
|
+
out[k] = toNum(obj[k]) ?? 0
|
|
165
|
+
}
|
|
166
|
+
return out
|
|
167
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ReachStats {
|
|
2
|
+
generator?: {capacity: bigint}
|
|
3
|
+
engines?: {drain: bigint}
|
|
4
|
+
hauler?: {drain: bigint}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function computePerLegReach(s: ReachStats, haulCount = 0): number {
|
|
8
|
+
const capacity = s.generator?.capacity
|
|
9
|
+
const drain = s.engines?.drain
|
|
10
|
+
if (capacity === undefined || drain === undefined || drain === 0n) {
|
|
11
|
+
throw new Error('entity has no usable engine/generator (cannot compute per-leg reach)')
|
|
12
|
+
}
|
|
13
|
+
const haulDrain = s.hauler && haulCount > 0 ? s.hauler.drain * BigInt(haulCount) : 0n
|
|
14
|
+
return Number(capacity) / Number(drain + haulDrain)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function computeGroupPerLegReach(participants: ReachStats[], haulCount: number): number {
|
|
18
|
+
const movers = participants.filter((p) => p.engines !== undefined && p.engines.drain !== 0n)
|
|
19
|
+
if (movers.length === 0) {
|
|
20
|
+
throw new Error('group has no moving entity (cannot compute per-leg reach)')
|
|
21
|
+
}
|
|
22
|
+
return Math.min(...movers.map((p) => computePerLegReach(p, haulCount)))
|
|
23
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {distanceBetweenPoints, findNearbyPlanets} from './travel'
|
|
2
|
+
import {hasSystem} from '../utils/system'
|
|
3
|
+
import {nearbyWormholes, wormholeAt} from '../derivation/wormhole'
|
|
4
|
+
import {PRECISION} from '../types'
|
|
5
|
+
import {Checksum256, type Checksum256Type} from '@wharfkit/antelope'
|
|
6
|
+
|
|
7
|
+
export interface Coord {
|
|
8
|
+
x: number
|
|
9
|
+
y: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Neighbor {
|
|
13
|
+
coord: Coord
|
|
14
|
+
dist: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SystemGraph {
|
|
18
|
+
hasSystem(c: Coord): boolean
|
|
19
|
+
nearby(c: Coord, reachTiles: number): Neighbor[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RoutePlan {
|
|
23
|
+
ok: true
|
|
24
|
+
waypoints: Coord[]
|
|
25
|
+
legs: number
|
|
26
|
+
totalDistance: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type RouteFailureReason = 'empty-destination' | 'no-path' | 'max-legs'
|
|
30
|
+
|
|
31
|
+
export interface RouteFailure {
|
|
32
|
+
ok: false
|
|
33
|
+
reason: RouteFailureReason
|
|
34
|
+
furthest?: Coord
|
|
35
|
+
legsNeeded?: number
|
|
36
|
+
partialWaypoints?: Coord[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type RouteResult = RoutePlan | RouteFailure
|
|
40
|
+
|
|
41
|
+
export interface PlanRouteParams {
|
|
42
|
+
origin: Coord
|
|
43
|
+
dest: Coord
|
|
44
|
+
perLegReach: number
|
|
45
|
+
graph: SystemGraph
|
|
46
|
+
corridorSlack?: number
|
|
47
|
+
nodeBudget?: number
|
|
48
|
+
maxLegs?: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const key = (c: Coord): string => `${c.x},${c.y}`
|
|
52
|
+
const sameCoord = (a: Coord, b: Coord): boolean => a.x === b.x && a.y === b.y
|
|
53
|
+
const dist = (a: Coord, b: Coord): number => Math.hypot(a.x - b.x, a.y - b.y)
|
|
54
|
+
|
|
55
|
+
export const MAX_LEGS = 12
|
|
56
|
+
|
|
57
|
+
export function planRoute(params: PlanRouteParams): RouteResult {
|
|
58
|
+
const {origin, dest, perLegReach, graph} = params
|
|
59
|
+
const corridorSlack = params.corridorSlack ?? perLegReach
|
|
60
|
+
const nodeBudget = params.nodeBudget ?? 5000
|
|
61
|
+
const maxLegs = params.maxLegs ?? MAX_LEGS
|
|
62
|
+
|
|
63
|
+
if (!graph.hasSystem(dest)) {
|
|
64
|
+
return {ok: false, reason: 'empty-destination'}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const straightLine = dist(origin, dest)
|
|
68
|
+
const heuristic = (c: Coord): number => Math.ceil(dist(c, dest) / perLegReach)
|
|
69
|
+
|
|
70
|
+
const gScore = new Map<string, number>([[key(origin), 0]])
|
|
71
|
+
const cameFrom = new Map<string, Coord>()
|
|
72
|
+
const frontier: {coord: Coord; g: number; f: number; remaining: number}[] = [
|
|
73
|
+
{coord: origin, g: 0, f: heuristic(origin), remaining: straightLine},
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
let furthest = origin
|
|
77
|
+
let furthestRemaining = dist(origin, dest)
|
|
78
|
+
let expansions = 0
|
|
79
|
+
let cappedByMaxLegs = false
|
|
80
|
+
|
|
81
|
+
while (frontier.length > 0) {
|
|
82
|
+
let bestIdx = 0
|
|
83
|
+
for (let i = 1; i < frontier.length; i++) {
|
|
84
|
+
const a = frontier[i]
|
|
85
|
+
const b = frontier[bestIdx]
|
|
86
|
+
if (a.f < b.f || (a.f === b.f && a.remaining < b.remaining)) {
|
|
87
|
+
bestIdx = i
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const current = frontier.splice(bestIdx, 1)[0]
|
|
91
|
+
|
|
92
|
+
if (sameCoord(current.coord, dest)) {
|
|
93
|
+
return reconstruct(cameFrom, origin, dest)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (current.remaining < furthestRemaining) {
|
|
97
|
+
furthestRemaining = current.remaining
|
|
98
|
+
furthest = current.coord
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (++expansions > nodeBudget) break
|
|
102
|
+
|
|
103
|
+
for (const n of graph.nearby(current.coord, perLegReach)) {
|
|
104
|
+
const inCorridor =
|
|
105
|
+
dist(origin, n.coord) + dist(n.coord, dest) <= straightLine + corridorSlack
|
|
106
|
+
if (!inCorridor) continue
|
|
107
|
+
|
|
108
|
+
const tentativeG = current.g + 1
|
|
109
|
+
if (tentativeG > maxLegs) {
|
|
110
|
+
cappedByMaxLegs = true
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
const nk = key(n.coord)
|
|
114
|
+
if (tentativeG < (gScore.get(nk) ?? Infinity)) {
|
|
115
|
+
gScore.set(nk, tentativeG)
|
|
116
|
+
cameFrom.set(nk, current.coord)
|
|
117
|
+
const remaining = dist(n.coord, dest)
|
|
118
|
+
frontier.push({
|
|
119
|
+
coord: n.coord,
|
|
120
|
+
g: tentativeG,
|
|
121
|
+
f: tentativeG + Math.ceil(remaining / perLegReach),
|
|
122
|
+
remaining,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (cappedByMaxLegs) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
reason: 'max-legs',
|
|
132
|
+
furthest,
|
|
133
|
+
partialWaypoints: reconstructWaypoints(cameFrom, origin, furthest),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
reason: 'no-path',
|
|
139
|
+
furthest,
|
|
140
|
+
partialWaypoints: reconstructWaypoints(cameFrom, origin, furthest),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function reconstructWaypoints(cameFrom: Map<string, Coord>, origin: Coord, target: Coord): Coord[] {
|
|
145
|
+
if (sameCoord(target, origin)) return []
|
|
146
|
+
const path: Coord[] = [target]
|
|
147
|
+
let cur = target
|
|
148
|
+
while (!sameCoord(cur, origin)) {
|
|
149
|
+
const prev = cameFrom.get(key(cur))
|
|
150
|
+
if (!prev) break
|
|
151
|
+
path.unshift(prev)
|
|
152
|
+
cur = prev
|
|
153
|
+
}
|
|
154
|
+
return path.slice(1)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function reconstruct(cameFrom: Map<string, Coord>, origin: Coord, dest: Coord): RoutePlan {
|
|
158
|
+
const path: Coord[] = [dest]
|
|
159
|
+
let cur = dest
|
|
160
|
+
let totalDistance = 0
|
|
161
|
+
while (!sameCoord(cur, origin)) {
|
|
162
|
+
const prev = cameFrom.get(key(cur))
|
|
163
|
+
if (!prev) break
|
|
164
|
+
totalDistance += dist(prev, cur)
|
|
165
|
+
path.unshift(prev)
|
|
166
|
+
cur = prev
|
|
167
|
+
}
|
|
168
|
+
const waypoints = path.slice(1)
|
|
169
|
+
return {ok: true, waypoints, legs: waypoints.length, totalDistance}
|
|
170
|
+
}
|
|
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
|
+
|
|
192
|
+
export function sdkSystemGraph(seed: Checksum256Type): SystemGraph {
|
|
193
|
+
const s = Checksum256.from(seed)
|
|
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 {
|
|
204
|
+
return {
|
|
205
|
+
hasSystem: (c) => hasSystem(s, {x: c.x, y: c.y}) || wormholeAt(s, c.x, c.y) !== null,
|
|
206
|
+
nearby: (c, reachTiles) => {
|
|
207
|
+
const seen = new Set<string>([`${c.x},${c.y}`])
|
|
208
|
+
const out: Neighbor[] = []
|
|
209
|
+
for (const d of findNearbyPlanets(s, {x: c.x, y: c.y}, reachTiles * PRECISION)) {
|
|
210
|
+
const coord = {x: Number(d.destination.x), y: Number(d.destination.y)}
|
|
211
|
+
const k = `${coord.x},${coord.y}`
|
|
212
|
+
if (seen.has(k)) continue
|
|
213
|
+
seen.add(k)
|
|
214
|
+
out.push({coord, dist: Number(d.distance) / PRECISION})
|
|
215
|
+
}
|
|
216
|
+
for (const coord of nearbyWormholes(s, c.x, c.y, reachTiles)) {
|
|
217
|
+
const k = `${coord.x},${coord.y}`
|
|
218
|
+
if (seen.has(k)) continue
|
|
219
|
+
seen.add(k)
|
|
220
|
+
out.push({coord, dist: Math.hypot(coord.x - c.x, coord.y - c.y)})
|
|
221
|
+
}
|
|
222
|
+
return out
|
|
223
|
+
},
|
|
224
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type {UInt64Type} from '@wharfkit/antelope'
|
|
2
|
+
import {PRECISION, TRAVEL_MAX_DURATION} from '../types'
|
|
3
|
+
import {
|
|
4
|
+
calc_energyusage,
|
|
5
|
+
calc_group_flighttime,
|
|
6
|
+
calc_rechargetime,
|
|
7
|
+
distanceBetweenPoints,
|
|
8
|
+
} from './travel'
|
|
9
|
+
|
|
10
|
+
export interface RouteMoverInput {
|
|
11
|
+
ref: {entityType: string; entityId: UInt64Type}
|
|
12
|
+
hasMovement: boolean
|
|
13
|
+
engines?: {thrust: number; drain: number}
|
|
14
|
+
generator?: {capacity: number; recharge: number}
|
|
15
|
+
hauler?: {capacity: number; drain: number; efficiency: number}
|
|
16
|
+
mass: number
|
|
17
|
+
energy: number
|
|
18
|
+
priorMobilityEnd: number
|
|
19
|
+
narrowBarrierEnd: number
|
|
20
|
+
/** Max end (seconds-from-now) over all this entity's lanes incl. worker/craft; gates recharges like the contract's all_lanes_end. Default 0. */
|
|
21
|
+
allLanesEnd?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RouteLegSim {
|
|
25
|
+
from: {x: number; y: number}
|
|
26
|
+
to: {x: number; y: number}
|
|
27
|
+
distanceCells: number
|
|
28
|
+
energyCostByMover: Record<string, number>
|
|
29
|
+
rechargeBefore: boolean
|
|
30
|
+
rechargeSeconds: number
|
|
31
|
+
flightSeconds: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RouteSim {
|
|
35
|
+
legs: RouteLegSim[]
|
|
36
|
+
totalSeconds: number
|
|
37
|
+
reachable: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function simulateRoute(
|
|
41
|
+
movers: RouteMoverInput[],
|
|
42
|
+
waypoints: {x: number; y: number}[],
|
|
43
|
+
origin: {x: number; y: number},
|
|
44
|
+
recharge: boolean
|
|
45
|
+
): RouteSim {
|
|
46
|
+
const totalThrust = movers
|
|
47
|
+
.filter((m) => m.hasMovement && m.engines)
|
|
48
|
+
.reduce((sum, m) => sum + m.engines!.thrust, 0)
|
|
49
|
+
|
|
50
|
+
const totalMass = movers.reduce((sum, m) => sum + m.mass, 0)
|
|
51
|
+
|
|
52
|
+
const haulCount = movers.filter((m) => !m.hasMovement).length
|
|
53
|
+
|
|
54
|
+
const pooledHaulCap = movers
|
|
55
|
+
.filter((m) => m.hasMovement && m.hauler)
|
|
56
|
+
.reduce((sum, m) => sum + m.hauler!.capacity, 0)
|
|
57
|
+
|
|
58
|
+
const weightedHaulEffNum = movers
|
|
59
|
+
.filter((m) => m.hasMovement && m.hauler)
|
|
60
|
+
.reduce((sum, m) => sum + m.hauler!.efficiency * m.hauler!.capacity, 0)
|
|
61
|
+
|
|
62
|
+
const energyByMover: Map<string, number> = new Map(
|
|
63
|
+
movers.map((m) => [String(m.ref.entityId), m.energy])
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
let reachable = true
|
|
67
|
+
const legs: RouteLegSim[] = []
|
|
68
|
+
|
|
69
|
+
const mobilityBarrier = movers
|
|
70
|
+
.filter((m) => m.hasMovement)
|
|
71
|
+
.reduce((mx, m) => Math.max(mx, m.priorMobilityEnd, m.narrowBarrierEnd), 0)
|
|
72
|
+
|
|
73
|
+
const rechargeFloor = movers
|
|
74
|
+
.filter((m) => m.hasMovement && m.generator)
|
|
75
|
+
.reduce((mx, m) => Math.max(mx, m.allLanesEnd ?? 0), 0)
|
|
76
|
+
|
|
77
|
+
let clock = 0
|
|
78
|
+
|
|
79
|
+
let from = origin
|
|
80
|
+
for (const to of waypoints) {
|
|
81
|
+
const distance = distanceBetweenPoints(from.x, from.y, to.x, to.y)
|
|
82
|
+
const distanceNum = Number(distance)
|
|
83
|
+
const distanceCells = Math.trunc(distanceNum / PRECISION)
|
|
84
|
+
|
|
85
|
+
const energyCostByMover: Record<string, number> = {}
|
|
86
|
+
|
|
87
|
+
for (const m of movers) {
|
|
88
|
+
if (!m.hasMovement || !m.engines) continue
|
|
89
|
+
const key = String(m.ref.entityId)
|
|
90
|
+
let cost = Number(calc_energyusage(distance, m.engines.drain))
|
|
91
|
+
if (m.hauler) {
|
|
92
|
+
cost += Math.trunc(distanceCells) * m.hauler.drain * haulCount
|
|
93
|
+
}
|
|
94
|
+
energyCostByMover[key] = cost
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let rechargeBefore = false
|
|
98
|
+
let rechargeSeconds = 0
|
|
99
|
+
|
|
100
|
+
if (recharge) {
|
|
101
|
+
let maxDur = 0
|
|
102
|
+
for (const m of movers) {
|
|
103
|
+
if (!m.hasMovement || !m.generator) continue
|
|
104
|
+
const key = String(m.ref.entityId)
|
|
105
|
+
const curEnergy = energyByMover.get(key) ?? 0
|
|
106
|
+
const dur = Number(
|
|
107
|
+
calc_rechargetime(m.generator.capacity, curEnergy, m.generator.recharge)
|
|
108
|
+
)
|
|
109
|
+
if (dur > maxDur) maxDur = dur
|
|
110
|
+
}
|
|
111
|
+
rechargeSeconds = maxDur
|
|
112
|
+
rechargeBefore = rechargeSeconds > 0
|
|
113
|
+
|
|
114
|
+
if (rechargeBefore) {
|
|
115
|
+
for (const m of movers) {
|
|
116
|
+
if (!m.hasMovement || !m.generator) continue
|
|
117
|
+
const key = String(m.ref.entityId)
|
|
118
|
+
energyByMover.set(key, m.generator.capacity)
|
|
119
|
+
}
|
|
120
|
+
clock = Math.max(clock, rechargeFloor) + rechargeSeconds
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const m of movers) {
|
|
125
|
+
if (!m.hasMovement || !m.engines) continue
|
|
126
|
+
const key = String(m.ref.entityId)
|
|
127
|
+
const cost = energyCostByMover[key] ?? 0
|
|
128
|
+
const curEnergy = energyByMover.get(key) ?? 0
|
|
129
|
+
|
|
130
|
+
if (recharge && m.generator) {
|
|
131
|
+
if (cost > m.generator.capacity) reachable = false
|
|
132
|
+
} else {
|
|
133
|
+
if (cost > curEnergy) reachable = false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const flightSeconds = Number(
|
|
138
|
+
calc_group_flighttime(
|
|
139
|
+
totalThrust,
|
|
140
|
+
haulCount,
|
|
141
|
+
pooledHaulCap,
|
|
142
|
+
weightedHaulEffNum,
|
|
143
|
+
totalMass,
|
|
144
|
+
distance
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if (flightSeconds > TRAVEL_MAX_DURATION) reachable = false
|
|
149
|
+
|
|
150
|
+
for (const m of movers) {
|
|
151
|
+
if (!m.hasMovement || !m.engines) continue
|
|
152
|
+
const key = String(m.ref.entityId)
|
|
153
|
+
const cost = energyCostByMover[key] ?? 0
|
|
154
|
+
const curEnergy = energyByMover.get(key) ?? 0
|
|
155
|
+
energyByMover.set(key, Math.max(0, curEnergy - cost))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
clock = Math.max(clock, mobilityBarrier) + flightSeconds
|
|
159
|
+
|
|
160
|
+
legs.push({
|
|
161
|
+
from,
|
|
162
|
+
to,
|
|
163
|
+
distanceCells,
|
|
164
|
+
energyCostByMover,
|
|
165
|
+
rechargeBefore,
|
|
166
|
+
rechargeSeconds,
|
|
167
|
+
flightSeconds,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
from = to
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {legs, totalSeconds: clock, reachable}
|
|
174
|
+
}
|