@shipload/sdk 1.0.0-next.6 → 1.0.0-next.60

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 (155) hide show
  1. package/lib/scan.d.ts +52 -0
  2. package/lib/scan.js +162 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +152 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +3589 -1363
  7. package/lib/shipload.js +15700 -5454
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +15423 -5402
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1173 -0
  12. package/lib/testing.js +4947 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +4941 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +35 -0
  20. package/src/capabilities/crafting.ts +34 -7
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +37 -19
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +42 -0
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.ts +16 -1
  28. package/src/contracts/platform.ts +231 -3
  29. package/src/contracts/server.ts +1518 -501
  30. package/src/coordinates/address.ts +88 -0
  31. package/src/coordinates/constants.test.ts +15 -0
  32. package/src/coordinates/constants.ts +23 -0
  33. package/src/coordinates/index.ts +15 -0
  34. package/src/coordinates/memo.test.ts +47 -0
  35. package/src/coordinates/memo.ts +20 -0
  36. package/src/coordinates/permutation.ts +77 -0
  37. package/src/coordinates/regions.ts +48 -0
  38. package/src/coordinates/sectors.ts +115 -0
  39. package/src/data/capabilities.ts +49 -18
  40. package/src/data/capability-formulas.ts +26 -14
  41. package/src/data/catalog.ts +0 -5
  42. package/src/data/colors.ts +13 -47
  43. package/src/data/entities.json +379 -14
  44. package/src/data/item-ids.ts +48 -12
  45. package/src/data/items.json +301 -74
  46. package/src/data/kind-registry.json +195 -0
  47. package/src/data/kind-registry.ts +182 -0
  48. package/src/data/metadata.ts +277 -48
  49. package/src/data/recipes-runtime.ts +6 -23
  50. package/src/data/recipes.json +2074 -173
  51. package/src/derivation/build-methods.test.ts +13 -0
  52. package/src/derivation/build-methods.ts +48 -0
  53. package/src/derivation/capabilities.test.ts +348 -0
  54. package/src/derivation/capabilities.ts +692 -0
  55. package/src/derivation/capability-mappings.ts +60 -16
  56. package/src/derivation/crafting.test.ts +17 -0
  57. package/src/derivation/crafting.ts +56 -33
  58. package/src/derivation/index.ts +27 -2
  59. package/src/derivation/recipe-usage.test.ts +88 -0
  60. package/src/derivation/recipe-usage.ts +141 -0
  61. package/src/derivation/reserve-regen.ts +34 -0
  62. package/src/derivation/resources.ts +9 -1
  63. package/src/derivation/rollups.test.ts +55 -0
  64. package/src/derivation/rollups.ts +72 -0
  65. package/src/derivation/stars.test.ts +67 -0
  66. package/src/derivation/stars.ts +25 -0
  67. package/src/derivation/stat-scaling.ts +12 -0
  68. package/src/derivation/stats.ts +6 -6
  69. package/src/derivation/stratum.ts +26 -22
  70. package/src/derivation/tiers.ts +40 -7
  71. package/src/derivation/upgrades.ts +14 -0
  72. package/src/derivation/wormhole.ts +154 -0
  73. package/src/entities/entity.ts +102 -0
  74. package/src/entities/gamestate.ts +3 -28
  75. package/src/entities/makers.ts +141 -135
  76. package/src/entities/slot-multiplier.ts +43 -0
  77. package/src/errors.ts +12 -16
  78. package/src/format.ts +26 -4
  79. package/src/index-module.ts +333 -35
  80. package/src/managers/actions.ts +668 -103
  81. package/src/managers/base.ts +6 -2
  82. package/src/managers/cluster.test.ts +39 -0
  83. package/src/managers/cluster.ts +53 -0
  84. package/src/managers/construction-types.ts +80 -0
  85. package/src/managers/construction.ts +443 -0
  86. package/src/managers/context.ts +29 -1
  87. package/src/managers/coordinates.ts +14 -0
  88. package/src/managers/entities.ts +27 -66
  89. package/src/managers/epochs.ts +40 -0
  90. package/src/managers/index.ts +20 -1
  91. package/src/managers/locations.ts +25 -29
  92. package/src/managers/nft.test.ts +14 -0
  93. package/src/managers/nft.ts +70 -0
  94. package/src/managers/players.ts +25 -0
  95. package/src/managers/plot.ts +122 -0
  96. package/src/nft/atomicassets.abi.json +1342 -0
  97. package/src/nft/atomicassets.ts +237 -0
  98. package/src/nft/atomicdata.ts +130 -0
  99. package/src/nft/buildImmutableData.ts +385 -0
  100. package/src/nft/description.ts +225 -52
  101. package/src/nft/index.ts +3 -0
  102. package/src/planner/index.ts +242 -0
  103. package/src/planner/planner.test.ts +334 -0
  104. package/src/resolution/describe-module.ts +43 -25
  105. package/src/resolution/display-name.ts +38 -10
  106. package/src/resolution/resolve-item.test.ts +48 -0
  107. package/src/resolution/resolve-item.ts +112 -58
  108. package/src/scan/index.ts +244 -0
  109. package/src/scan/scan-wasm.base64.ts +2 -0
  110. package/src/scheduling/accessor.ts +65 -23
  111. package/src/scheduling/availability.test.ts +204 -0
  112. package/src/scheduling/availability.ts +211 -0
  113. package/src/scheduling/cancel.test.ts +542 -0
  114. package/src/scheduling/cancel.ts +254 -0
  115. package/src/scheduling/cluster-stock.test.ts +31 -0
  116. package/src/scheduling/cluster-stock.ts +15 -0
  117. package/src/scheduling/energy.ts +47 -0
  118. package/src/scheduling/idle-resolve.ts +45 -0
  119. package/src/scheduling/jobs.ts +71 -0
  120. package/src/scheduling/lane-core.ts +128 -0
  121. package/src/scheduling/lanes.test.ts +250 -0
  122. package/src/scheduling/lanes.ts +227 -0
  123. package/src/scheduling/projection.ts +257 -133
  124. package/src/scheduling/schedule.test.ts +119 -0
  125. package/src/scheduling/schedule.ts +257 -117
  126. package/src/scheduling/task-cargo.test.ts +44 -0
  127. package/src/scheduling/task-cargo.ts +46 -0
  128. package/src/scheduling/unwrap.test.ts +86 -0
  129. package/src/scheduling/unwrap.ts +190 -0
  130. package/src/shipload.ts +26 -1
  131. package/src/subscriptions/manager.cluster.test.ts +51 -0
  132. package/src/subscriptions/manager.ts +245 -172
  133. package/src/subscriptions/mappers.ts +5 -8
  134. package/src/subscriptions/types.ts +28 -3
  135. package/src/testing/catalog-hash.ts +19 -0
  136. package/src/testing/index.ts +2 -0
  137. package/src/testing/projection-parity.ts +167 -0
  138. package/src/travel/reach.ts +21 -0
  139. package/src/travel/route-planner.ts +288 -0
  140. package/src/travel/route-simulator.ts +170 -0
  141. package/src/travel/travel.ts +188 -122
  142. package/src/types/capabilities.ts +37 -8
  143. package/src/types/entity.ts +3 -3
  144. package/src/types/index.ts +0 -1
  145. package/src/types.ts +51 -16
  146. package/src/utils/cargo.test.ts +14 -0
  147. package/src/utils/cargo.ts +29 -0
  148. package/src/utils/display-name.ts +70 -0
  149. package/src/utils/system.ts +15 -16
  150. package/src/capabilities/loading.ts +0 -8
  151. package/src/entities/container.ts +0 -123
  152. package/src/entities/ship-deploy.ts +0 -295
  153. package/src/entities/ship.ts +0 -221
  154. package/src/entities/warehouse.ts +0 -127
  155. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,167 @@
1
+ import type {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?: UInt32
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,21 @@
1
+ export interface ReachStats {
2
+ generator?: {capacity: bigint}
3
+ engines?: {drain: bigint}
4
+ }
5
+
6
+ export function computePerLegReach(s: ReachStats): number {
7
+ const capacity = s.generator?.capacity
8
+ const drain = s.engines?.drain
9
+ if (capacity === undefined || drain === undefined || drain === 0n) {
10
+ throw new Error('entity has no usable engine/generator (cannot compute per-leg reach)')
11
+ }
12
+ return Number(capacity) / Number(drain)
13
+ }
14
+
15
+ export function computeGroupPerLegReach(participants: ReachStats[]): number {
16
+ const movers = participants.filter((p) => p.engines !== undefined && p.engines.drain !== 0n)
17
+ if (movers.length === 0) {
18
+ throw new Error('group has no moving entity (cannot compute per-leg reach)')
19
+ }
20
+ return Math.min(...movers.map(computePerLegReach))
21
+ }
@@ -0,0 +1,288 @@
1
+ import {nearbyWormholes, wormholeAt} from '../derivation/wormhole'
2
+ import {Checksum256, type Checksum256Type} from '@wharfkit/antelope'
3
+
4
+ export interface Coord {
5
+ x: number
6
+ y: number
7
+ }
8
+
9
+ export interface Neighbor {
10
+ coord: Coord
11
+ dist: number
12
+ }
13
+
14
+ export interface SystemGraph {
15
+ hasSystem(c: Coord): boolean
16
+ nearby(c: Coord, reachTiles: number): Neighbor[]
17
+ }
18
+
19
+ export interface RoutePlan {
20
+ ok: true
21
+ waypoints: Coord[]
22
+ legs: number
23
+ totalDistance: number
24
+ }
25
+
26
+ export interface RouteLegInput {
27
+ from: Coord
28
+ to: Coord
29
+ distance: number
30
+ isDestination: boolean
31
+ }
32
+
33
+ /**
34
+ * Returns the elapsed-time search cost for a leg, or null when the leg is not contract-feasible.
35
+ */
36
+ export type RouteLegCost = (leg: RouteLegInput) => number | null
37
+ export type RouteHeuristicCost = (from: Coord, dest: Coord) => number
38
+
39
+ export type RouteFailureReason = 'empty-destination' | 'no-path' | 'max-legs'
40
+
41
+ export interface RouteFailure {
42
+ ok: false
43
+ reason: RouteFailureReason
44
+ furthest?: Coord
45
+ legsNeeded?: number
46
+ partialWaypoints?: Coord[]
47
+ }
48
+
49
+ export type RouteResult = RoutePlan | RouteFailure
50
+
51
+ export interface PlanRouteParams {
52
+ origin: Coord
53
+ dest: Coord
54
+ perLegReach: number
55
+ graph: SystemGraph
56
+ corridorSlack?: number
57
+ nodeBudget?: number
58
+ maxLegs?: number
59
+ legCost?: RouteLegCost
60
+ heuristicCost?: RouteHeuristicCost
61
+ }
62
+
63
+ const key = (c: Coord): string => `${c.x},${c.y}`
64
+ const sameCoord = (a: Coord, b: Coord): boolean => a.x === b.x && a.y === b.y
65
+ const dist = (a: Coord, b: Coord): number => Math.hypot(a.x - b.x, a.y - b.y)
66
+
67
+ export const MAX_LEGS = 12
68
+
69
+ export function planRoute(params: PlanRouteParams): RouteResult {
70
+ const {origin, dest, perLegReach, graph} = params
71
+ const corridorSlack = params.corridorSlack ?? perLegReach
72
+ const nodeBudget = params.nodeBudget ?? 5000
73
+ const maxLegs = params.maxLegs ?? MAX_LEGS
74
+
75
+ if (!graph.hasSystem(dest)) {
76
+ return {ok: false, reason: 'empty-destination'}
77
+ }
78
+
79
+ const straightLine = dist(origin, dest)
80
+ if (straightLine <= perLegReach) {
81
+ const directCost = params.legCost?.({
82
+ from: origin,
83
+ to: dest,
84
+ distance: straightLine,
85
+ isDestination: true,
86
+ })
87
+ if (directCost !== null) {
88
+ return {ok: true, waypoints: [dest], legs: 1, totalDistance: straightLine}
89
+ }
90
+ }
91
+ const heuristic = (c: Coord): number =>
92
+ params.heuristicCost?.(c, dest) ??
93
+ (params.legCost ? 0 : Math.ceil(dist(c, dest) / perLegReach))
94
+
95
+ const gScore = new Map<string, number>([[key(origin), 0]])
96
+ const cameFrom = new Map<string, Coord>()
97
+ const frontier: {coord: Coord; legs: number; cost: number; f: number; remaining: number}[] = [
98
+ {coord: origin, legs: 0, cost: 0, f: heuristic(origin), remaining: straightLine},
99
+ ]
100
+
101
+ let furthest = origin
102
+ let furthestRemaining = dist(origin, dest)
103
+ let expansions = 0
104
+ let cappedByMaxLegs = false
105
+
106
+ while (frontier.length > 0) {
107
+ let bestIdx = 0
108
+ for (let i = 1; i < frontier.length; i++) {
109
+ const a = frontier[i]
110
+ const b = frontier[bestIdx]
111
+ if (a.f < b.f || (a.f === b.f && a.remaining < b.remaining)) {
112
+ bestIdx = i
113
+ }
114
+ }
115
+ const current = frontier.splice(bestIdx, 1)[0]
116
+ if (current.cost !== gScore.get(key(current.coord))) continue
117
+
118
+ if (sameCoord(current.coord, dest)) {
119
+ return reconstruct(cameFrom, origin, dest)
120
+ }
121
+
122
+ if (current.remaining < furthestRemaining) {
123
+ furthestRemaining = current.remaining
124
+ furthest = current.coord
125
+ }
126
+
127
+ if (++expansions > nodeBudget) break
128
+
129
+ for (const n of graph.nearby(current.coord, perLegReach)) {
130
+ const inCorridor =
131
+ dist(origin, n.coord) + dist(n.coord, dest) <= straightLine + corridorSlack
132
+ if (!inCorridor) continue
133
+
134
+ const tentativeLegs = current.legs + 1
135
+ if (tentativeLegs > maxLegs) {
136
+ cappedByMaxLegs = true
137
+ continue
138
+ }
139
+ const evaluatedCost = params.legCost?.({
140
+ from: current.coord,
141
+ to: n.coord,
142
+ distance: n.dist,
143
+ isDestination: sameCoord(n.coord, dest),
144
+ })
145
+ if (evaluatedCost === null) continue
146
+ const legCost = evaluatedCost ?? 1
147
+ if (!(legCost >= 0) || !Number.isFinite(legCost)) continue
148
+ const nk = key(n.coord)
149
+ const nextCost = current.cost + legCost
150
+ if (nextCost < (gScore.get(nk) ?? Infinity)) {
151
+ gScore.set(nk, nextCost)
152
+ cameFrom.set(nk, current.coord)
153
+ const remaining = dist(n.coord, dest)
154
+ frontier.push({
155
+ coord: n.coord,
156
+ legs: tentativeLegs,
157
+ cost: nextCost,
158
+ f: nextCost + heuristic(n.coord),
159
+ remaining,
160
+ })
161
+ }
162
+ }
163
+ }
164
+
165
+ if (cappedByMaxLegs) {
166
+ return {
167
+ ok: false,
168
+ reason: 'max-legs',
169
+ furthest,
170
+ partialWaypoints: reconstructWaypoints(cameFrom, origin, furthest),
171
+ }
172
+ }
173
+ return {
174
+ ok: false,
175
+ reason: 'no-path',
176
+ furthest,
177
+ partialWaypoints: reconstructWaypoints(cameFrom, origin, furthest),
178
+ }
179
+ }
180
+
181
+ function reconstructWaypoints(cameFrom: Map<string, Coord>, origin: Coord, target: Coord): Coord[] {
182
+ if (sameCoord(target, origin)) return []
183
+ const path: Coord[] = [target]
184
+ let cur = target
185
+ while (!sameCoord(cur, origin)) {
186
+ const prev = cameFrom.get(key(cur))
187
+ if (!prev) break
188
+ path.unshift(prev)
189
+ cur = prev
190
+ }
191
+ return path.slice(1)
192
+ }
193
+
194
+ function reconstruct(cameFrom: Map<string, Coord>, origin: Coord, dest: Coord): RoutePlan {
195
+ const path: Coord[] = [dest]
196
+ let cur = dest
197
+ let totalDistance = 0
198
+ while (!sameCoord(cur, origin)) {
199
+ const prev = cameFrom.get(key(cur))
200
+ if (!prev) break
201
+ totalDistance += dist(prev, cur)
202
+ path.unshift(prev)
203
+ cur = prev
204
+ }
205
+ const waypoints = path.slice(1)
206
+ return {ok: true, waypoints, legs: waypoints.length, totalDistance}
207
+ }
208
+
209
+ export interface ScanProvider {
210
+ getLocationType(seedHex: string, x: number, y: number): number
211
+ systemsInBox(
212
+ seedHex: string,
213
+ xMin: number,
214
+ yMin: number,
215
+ xMax: number,
216
+ yMax: number
217
+ ): {x: number; y: number; locType: number}[]
218
+ }
219
+
220
+ // Keyed by provider identity, so pass a stable one (the `@shipload/sdk/scan` namespace).
221
+ const graphCache = new WeakMap<ScanProvider, Map<string, SystemGraph>>()
222
+
223
+ export function sdkSystemGraph(seed: Checksum256Type, scan: ScanProvider): SystemGraph {
224
+ const s = Checksum256.from(seed)
225
+ const seedHex = s.toString()
226
+ let bySeed = graphCache.get(scan)
227
+ if (!bySeed) {
228
+ bySeed = new Map()
229
+ graphCache.set(scan, bySeed)
230
+ }
231
+ const cached = bySeed.get(seedHex)
232
+ if (cached) return cached
233
+ const graph = wasmSystemGraph(s, seedHex, scan)
234
+ bySeed.set(seedHex, graph)
235
+ return graph
236
+ }
237
+
238
+ const SCAN_BUCKET = 48
239
+
240
+ function wasmSystemGraph(s: Checksum256, seedHex: string, scan: ScanProvider): SystemGraph {
241
+ // Per-bucket system cache: reused across the overlapping node queries A* makes (and across routes).
242
+ const bucketCache = new Map<string, {x: number; y: number}[]>()
243
+ const bucketSystems = (bx: number, by: number): {x: number; y: number}[] => {
244
+ const k = `${bx},${by}`
245
+ let v = bucketCache.get(k)
246
+ if (v === undefined) {
247
+ const xMin = bx * SCAN_BUCKET
248
+ const yMin = by * SCAN_BUCKET
249
+ v = scan
250
+ .systemsInBox(seedHex, xMin, yMin, xMin + SCAN_BUCKET - 1, yMin + SCAN_BUCKET - 1)
251
+ .map((cell) => ({x: cell.x, y: cell.y}))
252
+ bucketCache.set(k, v)
253
+ }
254
+ return v
255
+ }
256
+ return {
257
+ hasSystem: (c) =>
258
+ scan.getLocationType(seedHex, c.x, c.y) !== 0 || wormholeAt(s, c.x, c.y) !== null,
259
+ nearby: (c, reachTiles) => {
260
+ const r = Math.floor(reachTiles)
261
+ const seen = new Set<string>([`${c.x},${c.y}`])
262
+ const out: Neighbor[] = []
263
+ const bx0 = Math.floor((c.x - r) / SCAN_BUCKET)
264
+ const bx1 = Math.floor((c.x + r) / SCAN_BUCKET)
265
+ const by0 = Math.floor((c.y - r) / SCAN_BUCKET)
266
+ const by1 = Math.floor((c.y + r) / SCAN_BUCKET)
267
+ for (let bx = bx0; bx <= bx1; bx++) {
268
+ for (let by = by0; by <= by1; by++) {
269
+ for (const cell of bucketSystems(bx, by)) {
270
+ const dist = Math.hypot(cell.x - c.x, cell.y - c.y)
271
+ if (dist > reachTiles) continue
272
+ const k = `${cell.x},${cell.y}`
273
+ if (seen.has(k)) continue
274
+ seen.add(k)
275
+ out.push({coord: {x: cell.x, y: cell.y}, dist})
276
+ }
277
+ }
278
+ }
279
+ for (const coord of nearbyWormholes(s, c.x, c.y, reachTiles)) {
280
+ const k = `${coord.x},${coord.y}`
281
+ if (seen.has(k)) continue
282
+ seen.add(k)
283
+ out.push({coord, dist: Math.hypot(coord.x - c.x, coord.y - c.y)})
284
+ }
285
+ return out
286
+ },
287
+ }
288
+ }
@@ -0,0 +1,170 @@
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; 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 = 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
+ energyCostByMover[key] = Number(calc_energyusage(distance, m.engines.drain))
91
+ }
92
+
93
+ let rechargeBefore = false
94
+ let rechargeSeconds = 0
95
+
96
+ if (recharge) {
97
+ let maxDur = 0
98
+ for (const m of movers) {
99
+ if (!m.hasMovement || !m.generator) continue
100
+ const key = String(m.ref.entityId)
101
+ const curEnergy = energyByMover.get(key) ?? 0
102
+ const dur = Number(
103
+ calc_rechargetime(m.generator.capacity, curEnergy, m.generator.recharge)
104
+ )
105
+ if (dur > maxDur) maxDur = dur
106
+ }
107
+ rechargeSeconds = maxDur
108
+ rechargeBefore = rechargeSeconds > 0
109
+
110
+ if (rechargeBefore) {
111
+ for (const m of movers) {
112
+ if (!m.hasMovement || !m.generator) continue
113
+ const key = String(m.ref.entityId)
114
+ energyByMover.set(key, m.generator.capacity)
115
+ }
116
+ clock = Math.max(clock, rechargeFloor) + rechargeSeconds
117
+ }
118
+ }
119
+
120
+ for (const m of movers) {
121
+ if (!m.hasMovement || !m.engines) continue
122
+ const key = String(m.ref.entityId)
123
+ const cost = energyCostByMover[key] ?? 0
124
+ const curEnergy = energyByMover.get(key) ?? 0
125
+
126
+ if (recharge && m.generator) {
127
+ if (cost > m.generator.capacity) reachable = false
128
+ } else {
129
+ if (cost > curEnergy) reachable = false
130
+ }
131
+ }
132
+
133
+ const flightSeconds = Number(
134
+ calc_group_flighttime(
135
+ totalThrust,
136
+ haulCount,
137
+ pooledHaulCap,
138
+ weightedHaulEffNum,
139
+ totalMass,
140
+ distance
141
+ )
142
+ )
143
+
144
+ if (flightSeconds > TRAVEL_MAX_DURATION) reachable = false
145
+
146
+ for (const m of movers) {
147
+ if (!m.hasMovement || !m.engines) continue
148
+ const key = String(m.ref.entityId)
149
+ const cost = energyCostByMover[key] ?? 0
150
+ const curEnergy = energyByMover.get(key) ?? 0
151
+ energyByMover.set(key, Math.max(0, curEnergy - cost))
152
+ }
153
+
154
+ clock = Math.max(clock, mobilityBarrier) + flightSeconds
155
+
156
+ legs.push({
157
+ from,
158
+ to,
159
+ distanceCells,
160
+ energyCostByMover,
161
+ rechargeBefore,
162
+ rechargeSeconds,
163
+ flightSeconds,
164
+ })
165
+
166
+ from = to
167
+ }
168
+
169
+ return {legs, totalSeconds: clock, reachable}
170
+ }