@shipload/sdk 1.0.0-next.33 → 1.0.0-next.35

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.
@@ -3,6 +3,7 @@ import type {
3
3
  BoundingBox,
4
4
  BoundsDeltaMessage,
5
5
  ClientMessage,
6
+ EntityDeletedMessage,
6
7
  ServerMessage,
7
8
  SnapshotMessage,
8
9
  SubscribeEntityMessage,
@@ -25,50 +26,73 @@ export interface SubscriptionsOptions {
25
26
  pongTimeoutMs?: number
26
27
  }
27
28
 
28
- export interface BoundsSubscriptionHandle {
29
- readonly subId: string
30
- unsubscribe(): void
31
- updateBounds(bounds: BoundingBox): void
32
- current: Map<number, EntityInstance>
29
+ export type ExactEntitySubscriptionFilter = {
30
+ id: string | number
31
+ owner?: never
32
+ bounds?: never
33
+ prioritizeOwner?: never
34
+ }
35
+
36
+ export type BroadEntitySubscriptionFilter = {
37
+ id?: undefined
38
+ owner?: string
39
+ bounds?: BoundingBox
40
+ prioritizeOwner?: string
41
+ }
42
+
43
+ export type EntitySubscriptionFilter = ExactEntitySubscriptionFilter | BroadEntitySubscriptionFilter
44
+
45
+ export interface EntitySubscriptionMeta {
46
+ seq?: number
47
+ truncated?: boolean
48
+ }
49
+
50
+ export interface EntitySubscriptionHandlers {
51
+ onSnapshot?: (entities: EntityInstance[], meta: EntitySubscriptionMeta) => void
52
+ onUpdate?: (entity: EntityInstance, meta: EntitySubscriptionMeta) => void
53
+ onBoundsDelta?: (
54
+ entered: EntityInstance[],
55
+ exited: number[],
56
+ meta: EntitySubscriptionMeta
57
+ ) => void
58
+ onDeleted?: (id: string, meta: EntitySubscriptionMeta) => void
59
+ onError?: (error: Error) => void
33
60
  }
34
61
 
35
- export interface OwnerSubscriptionHandle {
62
+ export interface EntitiesSubscriptionHandle {
36
63
  readonly subId: string
64
+ readonly filter: EntitySubscriptionFilter
37
65
  unsubscribe(): void
38
66
  current: Map<number, EntityInstance>
39
67
  }
40
68
 
41
- export interface EntitySubscriptionHandle {
42
- readonly subId: string
43
- readonly entityType: SubscriptionEntityType
44
- readonly entityId: string
45
- unsubscribe(): void
46
- current: EntityInstance | null
69
+ export type BoundsSubscriptionHandle = EntitiesSubscriptionHandle & {
70
+ readonly filter: BroadEntitySubscriptionFilter & {bounds: BoundingBox}
71
+ updateBounds(bounds: BoundingBox): void
72
+ }
73
+ export type OwnerSubscriptionHandle = EntitiesSubscriptionHandle & {
74
+ readonly filter: BroadEntitySubscriptionFilter
75
+ }
76
+ export type EntitySubscriptionHandle = EntitiesSubscriptionHandle & {
77
+ readonly filter: ExactEntitySubscriptionFilter
78
+ }
79
+
80
+ type EntitiesSubscriptionEntry = {
81
+ filter: InternalEntitySubscriptionFilter
82
+ handlers: EntitySubscriptionHandlers
83
+ handle: EntitiesSubscriptionHandle
84
+ }
85
+
86
+ type InternalEntitySubscriptionFilter = {
87
+ id?: string | number
88
+ owner?: string
89
+ bounds?: BoundingBox
90
+ prioritizeOwner?: string
47
91
  }
48
92
 
49
93
  export class SubscriptionsManager {
50
94
  private readonly conn: WebSocketConnection
51
- private readonly entitySubs = new Map<
52
- string,
53
- {
54
- type: SubscriptionEntityType
55
- id: string
56
- onUpdate: (e: EntityInstance) => void
57
- handle: EntitySubscriptionHandle
58
- }
59
- >()
60
- private readonly boundsSubs = new Map<
61
- string,
62
- {
63
- bounds?: BoundingBox
64
- owner?: string
65
- prioritizeOwner?: string
66
- onSnapshot?: (entities: EntityInstance[]) => void
67
- onUpdate?: (entity: EntityInstance) => void
68
- onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
69
- handle: BoundsSubscriptionHandle | OwnerSubscriptionHandle
70
- }
71
- >()
95
+ private readonly entitySubs = new Map<string, EntitiesSubscriptionEntry>()
72
96
  private subCounter = 0
73
97
  private hasConnected = false
74
98
 
@@ -97,113 +121,162 @@ export class SubscriptionsManager {
97
121
  this.conn.send(msg)
98
122
  }
99
123
 
100
- subscribeEntity(
101
- type: SubscriptionEntityType,
102
- id: string,
103
- onUpdate: (e: EntityInstance) => void
104
- ): EntitySubscriptionHandle {
105
- const subId = this.generateSubID('ent')
106
- const msg: SubscribeEntityMessage = {
107
- type: 'subscribe_entity',
108
- sub_id: subId,
109
- entity_type: type,
110
- entity_id: id,
111
- }
112
- const handle: EntitySubscriptionHandle = {
124
+ subscribeEntities(
125
+ filter: BroadEntitySubscriptionFilter & {bounds: BoundingBox},
126
+ handlers?: EntitySubscriptionHandlers
127
+ ): BoundsSubscriptionHandle
128
+ subscribeEntities(
129
+ filter: ExactEntitySubscriptionFilter,
130
+ handlers?: EntitySubscriptionHandlers
131
+ ): EntitySubscriptionHandle
132
+ subscribeEntities(
133
+ filter: BroadEntitySubscriptionFilter,
134
+ handlers?: EntitySubscriptionHandlers
135
+ ): EntitiesSubscriptionHandle
136
+ subscribeEntities(
137
+ filter: EntitySubscriptionFilter,
138
+ handlers?: EntitySubscriptionHandlers
139
+ ): EntitiesSubscriptionHandle
140
+ subscribeEntities(
141
+ filter: EntitySubscriptionFilter,
142
+ handlers: EntitySubscriptionHandlers = {}
143
+ ): EntitiesSubscriptionHandle {
144
+ const storedFilter = this.normalizeFilter(filter)
145
+ const subId = this.generateSubID(this.subscriptionPrefix(storedFilter))
146
+ const handle: EntitiesSubscriptionHandle = {
113
147
  subId,
114
- entityType: type,
115
- entityId: id,
116
- unsubscribe: () => this.unsubscribeEntity(subId),
117
- current: null,
148
+ get filter() {
149
+ return SubscriptionsManager.publicFilter(storedFilter)
150
+ },
151
+ unsubscribe: () => this.unsubscribeEntities(subId),
152
+ current: new Map(),
118
153
  }
119
- this.entitySubs.set(subId, {type, id, onUpdate, handle})
120
- this.sendMessage(msg)
154
+ if (storedFilter.id === undefined && storedFilter.bounds) {
155
+ ;(handle as BoundsSubscriptionHandle).updateBounds = (bounds) =>
156
+ this.updateBounds(subId, bounds)
157
+ }
158
+
159
+ this.entitySubs.set(subId, {filter: storedFilter, handlers, handle})
160
+ this.sendMessage(this.subscribeMessage(subId, storedFilter))
121
161
  return handle
122
162
  }
123
163
 
124
- private unsubscribeEntity(subId: string) {
125
- const entry = this.entitySubs.get(subId)
126
- if (!entry) return
127
- this.entitySubs.delete(subId)
128
- const msg: UnsubscribeEntityMessage = {type: 'unsubscribe_entity', sub_id: subId}
129
- this.sendMessage(msg)
164
+ subscribeEntity(
165
+ id: string | number,
166
+ handlers: EntitySubscriptionHandlers
167
+ ): EntitySubscriptionHandle {
168
+ return this.subscribeEntities({id}, handlers)
169
+ }
170
+
171
+ subscribeOwner(
172
+ owner: string,
173
+ handlers: EntitySubscriptionHandlers = {}
174
+ ): OwnerSubscriptionHandle {
175
+ return this.subscribeEntities({owner}, handlers) as OwnerSubscriptionHandle
130
176
  }
131
177
 
132
178
  subscribeBounds(
133
179
  bounds: BoundingBox,
134
- handlers: {
135
- onSnapshot?: (entities: EntityInstance[]) => void
136
- onUpdate?: (entity: EntityInstance) => void
137
- onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
180
+ handlers: EntitySubscriptionHandlers & {
138
181
  owner?: string
139
182
  prioritizeOwner?: string
140
- }
183
+ } = {}
141
184
  ): BoundsSubscriptionHandle {
142
- const subId = this.generateSubID('bnd')
143
- const msg: SubscribeMessage = {
144
- type: 'subscribe',
145
- sub_id: subId,
146
- bounds,
147
- owner: handlers.owner,
148
- prioritize_owner: handlers.prioritizeOwner,
185
+ return this.subscribeEntities(
186
+ {bounds, owner: handlers.owner, prioritizeOwner: handlers.prioritizeOwner},
187
+ handlers
188
+ )
189
+ }
190
+
191
+ subscribeAllEntities(handlers: EntitySubscriptionHandlers = {}): EntitiesSubscriptionHandle {
192
+ return this.subscribeEntities({}, handlers)
193
+ }
194
+
195
+ private normalizeFilter(filter: EntitySubscriptionFilter): InternalEntitySubscriptionFilter {
196
+ const raw = filter as InternalEntitySubscriptionFilter
197
+ if (
198
+ raw.id !== undefined &&
199
+ (raw.owner !== undefined ||
200
+ raw.bounds !== undefined ||
201
+ raw.prioritizeOwner !== undefined)
202
+ ) {
203
+ throw new Error(
204
+ 'Exact entity subscription filters cannot include owner, bounds, or prioritizeOwner'
205
+ )
149
206
  }
150
- const handle: BoundsSubscriptionHandle = {
151
- subId,
152
- unsubscribe: () => this.unsubscribeBounds(subId),
153
- updateBounds: (b) => this.updateBounds(subId, b),
154
- current: new Map(),
207
+ if (raw.id !== undefined) {
208
+ return {id: raw.id}
209
+ }
210
+ return {
211
+ owner: raw.owner,
212
+ bounds: raw.bounds ? this.cloneBounds(raw.bounds) : undefined,
213
+ prioritizeOwner: raw.prioritizeOwner,
155
214
  }
156
- this.boundsSubs.set(subId, {
157
- bounds,
158
- owner: handlers.owner,
159
- prioritizeOwner: handlers.prioritizeOwner,
160
- onSnapshot: handlers.onSnapshot,
161
- onUpdate: handlers.onUpdate,
162
- onBoundsDelta: handlers.onBoundsDelta,
163
- handle,
164
- })
165
- this.sendMessage(msg)
166
- return handle
167
215
  }
168
216
 
169
- subscribeOwner(
170
- owner: string,
171
- handlers: {
172
- onSnapshot?: (entities: EntityInstance[]) => void
173
- onUpdate?: (entity: EntityInstance) => void
174
- } = {}
175
- ): OwnerSubscriptionHandle {
176
- const subId = this.generateSubID('own')
177
- const msg: SubscribeMessage = {
217
+ private static publicFilter(
218
+ filter: InternalEntitySubscriptionFilter
219
+ ): EntitySubscriptionFilter {
220
+ if (filter.id !== undefined) {
221
+ return Object.freeze({id: filter.id}) as ExactEntitySubscriptionFilter
222
+ }
223
+
224
+ return Object.freeze({
225
+ owner: filter.owner,
226
+ bounds: filter.bounds ? (Object.freeze({...filter.bounds}) as BoundingBox) : undefined,
227
+ prioritizeOwner: filter.prioritizeOwner,
228
+ }) as BroadEntitySubscriptionFilter
229
+ }
230
+
231
+ private cloneBounds(bounds: BoundingBox): BoundingBox {
232
+ return {...bounds}
233
+ }
234
+
235
+ private subscriptionPrefix(filter: InternalEntitySubscriptionFilter): string {
236
+ if (filter.id !== undefined) return 'ent'
237
+ if (filter.bounds) return 'bnd'
238
+ if (filter.owner) return 'own'
239
+ return 'all'
240
+ }
241
+
242
+ private subscribeMessage(
243
+ subId: string,
244
+ filter: InternalEntitySubscriptionFilter
245
+ ): SubscribeEntityMessage | SubscribeMessage {
246
+ if (filter.id !== undefined) {
247
+ return {
248
+ type: 'subscribe_entity',
249
+ sub_id: subId,
250
+ entity_id: String(filter.id),
251
+ }
252
+ }
253
+
254
+ return {
178
255
  type: 'subscribe',
179
256
  sub_id: subId,
180
- owner,
257
+ owner: filter.owner,
258
+ bounds: filter.bounds,
259
+ prioritize_owner: filter.prioritizeOwner,
181
260
  }
182
- const handle: OwnerSubscriptionHandle = {
183
- subId,
184
- unsubscribe: () => this.unsubscribeBounds(subId),
185
- current: new Map(),
186
- }
187
- this.boundsSubs.set(subId, {
188
- bounds: undefined,
189
- owner,
190
- prioritizeOwner: undefined,
191
- onSnapshot: handlers.onSnapshot,
192
- onUpdate: handlers.onUpdate,
193
- handle,
194
- })
195
- this.sendMessage(msg)
196
- return handle
197
261
  }
198
262
 
199
- private unsubscribeBounds(subId: string) {
200
- this.boundsSubs.delete(subId)
263
+ private unsubscribeEntities(subId: string) {
264
+ const entry = this.entitySubs.get(subId)
265
+ if (!entry) return
266
+ this.entitySubs.delete(subId)
267
+ if (entry.filter.id !== undefined) {
268
+ const msg: UnsubscribeEntityMessage = {type: 'unsubscribe_entity', sub_id: subId}
269
+ this.sendMessage(msg)
270
+ return
271
+ }
201
272
  this.sendMessage({type: 'unsubscribe', sub_id: subId})
202
273
  }
203
274
 
204
275
  private updateBounds(subId: string, bounds: BoundingBox) {
205
- const entry = this.boundsSubs.get(subId)
206
- if (entry) entry.bounds = bounds
276
+ const entry = this.entitySubs.get(subId)
277
+ if (!entry) return
278
+ if (entry.filter.id !== undefined || !entry.filter.bounds) return
279
+ entry.filter.bounds = this.cloneBounds(bounds)
207
280
  const msg: UpdateBoundsMessage = {type: 'update_bounds', sub_id: subId, bounds}
208
281
  this.sendMessage(msg)
209
282
  }
@@ -215,23 +288,7 @@ export class SubscriptionsManager {
215
288
  return
216
289
  }
217
290
  for (const [subId, entry] of this.entitySubs) {
218
- const msg: SubscribeEntityMessage = {
219
- type: 'subscribe_entity',
220
- sub_id: subId,
221
- entity_type: entry.type,
222
- entity_id: entry.id,
223
- }
224
- this.sendMessage(msg)
225
- }
226
- for (const [subId, entry] of this.boundsSubs) {
227
- const msg: SubscribeMessage = {
228
- type: 'subscribe',
229
- sub_id: subId,
230
- bounds: entry.bounds,
231
- owner: entry.owner,
232
- prioritize_owner: entry.prioritizeOwner,
233
- }
234
- this.sendMessage(msg)
291
+ this.sendMessage(this.subscribeMessage(subId, entry.filter))
235
292
  }
236
293
  }
237
294
 
@@ -246,6 +303,9 @@ export class SubscriptionsManager {
246
303
  case 'bounds_delta':
247
304
  this.handleBoundsDelta(msg)
248
305
  break
306
+ case 'entity_deleted':
307
+ this.handleEntityDeleted(msg)
308
+ break
249
309
  case 'error':
250
310
  this.handleError(msg)
251
311
  break
@@ -258,60 +318,53 @@ export class SubscriptionsManager {
258
318
  }
259
319
 
260
320
  private handleSnapshot(msg: SnapshotMessage) {
261
- const entSub = this.entitySubs.get(msg.sub_id)
262
- if (entSub) {
263
- if (msg.entities.length > 0) {
264
- const ent = this.parseEntity(msg.entities[0])
265
- entSub.handle.current = ent
266
- entSub.onUpdate(ent)
267
- }
268
- return
269
- }
270
- const boundsSub = this.boundsSubs.get(msg.sub_id)
271
- if (boundsSub) {
272
- const ents = msg.entities.map((e) => this.parseEntity(e))
273
- boundsSub.handle.current.clear()
274
- for (const e of ents) boundsSub.handle.current.set(Number(e.id), e)
275
- boundsSub.onSnapshot?.(ents)
321
+ const sub = this.entitySubs.get(msg.sub_id)
322
+ if (!sub) return
323
+ const meta = {seq: msg.seq, truncated: msg.truncated === true}
324
+ const ents = msg.entities.map((e) => this.parseEntity(e))
325
+ sub.handle.current.clear()
326
+ for (const e of ents) sub.handle.current.set(Number(e.id), e)
327
+ sub.handlers.onSnapshot?.(ents, meta)
328
+ if (sub.filter.id !== undefined && ents[0]) {
329
+ sub.handlers.onUpdate?.(ents[0], {seq: msg.seq})
276
330
  }
277
331
  }
278
332
 
279
333
  private handleUpdate(msg: UpdateMessage) {
280
334
  const ent = this.parseEntity(msg.entity)
281
335
  for (const subId of msg.sub_ids) {
282
- const entSub = this.entitySubs.get(subId)
283
- if (entSub) {
284
- entSub.handle.current = ent
285
- entSub.onUpdate(ent)
286
- continue
287
- }
288
- const boundsSub = this.boundsSubs.get(subId)
289
- if (boundsSub) {
290
- boundsSub.handle.current.set(msg.entity_id, ent)
291
- boundsSub.onUpdate?.(ent)
292
- }
336
+ const sub = this.entitySubs.get(subId)
337
+ if (!sub) continue
338
+ sub.handle.current.set(msg.entity_id, ent)
339
+ sub.handlers.onUpdate?.(ent, {seq: msg.seq})
293
340
  }
294
341
  }
295
342
 
296
343
  private handleBoundsDelta(msg: BoundsDeltaMessage) {
297
- const sub = this.boundsSubs.get(msg.sub_id)
344
+ const sub = this.entitySubs.get(msg.sub_id)
298
345
  if (!sub) return
346
+ const meta = {seq: msg.seq, truncated: msg.truncated === true}
299
347
  const entered = msg.entered.map((e) => this.parseEntity(e))
300
348
  for (const e of entered) sub.handle.current.set(Number(e.id), e)
301
349
  for (const id of msg.exited) sub.handle.current.delete(id)
302
- sub.onBoundsDelta?.(entered, msg.exited)
350
+ sub.handlers.onBoundsDelta?.(entered, msg.exited, meta)
303
351
  }
304
352
 
305
353
  private handleError(msg: {sub_id?: string; error: string}) {
306
354
  if (!msg.sub_id) return
307
- const entSub = this.entitySubs.get(msg.sub_id)
308
- if (entSub) {
355
+ const sub = this.entitySubs.get(msg.sub_id)
356
+ if (!sub) return
357
+ this.entitySubs.delete(msg.sub_id)
358
+ sub.handlers.onError?.(new Error(msg.error))
359
+ }
360
+
361
+ private handleEntityDeleted(msg: EntityDeletedMessage) {
362
+ const sub = this.entitySubs.get(msg.sub_id)
363
+ if (!sub) return
364
+ sub.handle.current.delete(msg.entity_id)
365
+ if (sub.filter.id !== undefined) {
309
366
  this.entitySubs.delete(msg.sub_id)
310
- return
311
- }
312
- const boundsSub = this.boundsSubs.get(msg.sub_id)
313
- if (boundsSub) {
314
- this.boundsSubs.delete(msg.sub_id)
315
367
  }
368
+ sub.handlers.onDeleted?.(String(msg.entity_id), {seq: msg.seq})
316
369
  }
317
370
  }
@@ -39,7 +39,6 @@ export type UnsubscribeMessage = {
39
39
  export type SubscribeEntityMessage = {
40
40
  type: 'subscribe_entity'
41
41
  sub_id: string
42
- entity_type: 'ship' | 'warehouse' | 'container' | 'nexus'
43
42
  entity_id: string
44
43
  }
45
44
 
@@ -79,8 +78,8 @@ export type AckMessage = {
79
78
  }
80
79
 
81
80
  export type WireEntity = Record<string, unknown> & {
82
- type: number
83
- type_name: 'ship' | 'warehouse' | 'container' | 'nexus'
81
+ type: number | string
82
+ type_name?: string
84
83
  id: string | number
85
84
  owner: string
86
85
  coordinates: WireCoordinates
@@ -112,6 +111,13 @@ export type BoundsDeltaMessage = {
112
111
  truncated?: boolean
113
112
  }
114
113
 
114
+ export type EntityDeletedMessage = {
115
+ type: 'entity_deleted'
116
+ sub_id: string
117
+ entity_id: number
118
+ seq: number
119
+ }
120
+
115
121
  export type EventMessage = {
116
122
  type: 'event'
117
123
  sub_id: string
@@ -138,6 +144,7 @@ export type ServerMessage =
138
144
  | SnapshotMessage
139
145
  | UpdateMessage
140
146
  | BoundsDeltaMessage
147
+ | EntityDeletedMessage
141
148
  | EventMessage
142
149
  | EventCatchupCompleteMessage
143
150
  | PongMessage
@@ -37,9 +37,14 @@ import {
37
37
  import {EntityClass} from '../data/kind-registry'
38
38
  import {getItem} from '../data/catalog'
39
39
  import {hasSystem} from '../utils/system'
40
+ import {WH} from '../derivation/wormhole'
40
41
  import * as scheduleModel from '../scheduling/schedule'
41
42
  import type {ScheduleData} from '../scheduling/schedule'
42
43
 
44
+ function isPositionalTask(task: ServerContract.Types.task): boolean {
45
+ return task.type.equals(TaskType.TRAVEL) || task.type.equals(TaskType.TRANSIT)
46
+ }
47
+
43
48
  export function calc_orbital_altitude(mass: number): number {
44
49
  if (mass <= BASE_ORBITAL_MASS) {
45
50
  return MIN_ORBITAL_ALTITUDE
@@ -125,7 +130,7 @@ export function getInterpolatedPosition(
125
130
  return {x: Number(settled.x), y: Number(settled.y)}
126
131
  }
127
132
  const task = tasks[taskIndex]
128
- if (!task.type.equals(TaskType.TRAVEL) || !task.coordinates) {
133
+ if (!isPositionalTask(task) || !task.coordinates) {
129
134
  const origin = getFlightOrigin(entity, taskIndex)
130
135
  return {x: Number(origin.x), y: Number(origin.y)}
131
136
  }
@@ -198,6 +203,11 @@ export function calc_flighttime(distance: UInt64Type, acceleration: number): UIn
198
203
  return UInt32.from(2 * Math.sqrt(Number(distance) / acceleration))
199
204
  }
200
205
 
206
+ export function calc_transit_duration(ax: number, ay: number, bx: number, by: number): UInt32 {
207
+ const distance = distanceBetweenPoints(ax, ay, bx, by)
208
+ return UInt32.from(Math.floor(distance.toNumber() / (PRECISION * WH.TRANSIT_SPEED)))
209
+ }
210
+
201
211
  export function calc_loader_flighttime(ship: ShipLike, mass: UInt64, altitude?: number): UInt32 {
202
212
  const z = altitude ?? ship.coordinates.z?.toNumber() ?? calc_orbital_altitude(Number(mass))
203
213
  return calc_flighttime(z, calc_loader_acceleration(ship, mass))
@@ -449,7 +459,7 @@ export function getFlightOrigin(
449
459
  let origin = entity.coordinates
450
460
  for (let i = 0; i < flightTaskIndex && i < tasks.length; i++) {
451
461
  const task = tasks[i]
452
- if (task.type.equals(TaskType.TRAVEL) && task.coordinates) {
462
+ if (isPositionalTask(task) && task.coordinates) {
453
463
  origin = task.coordinates
454
464
  }
455
465
  }
@@ -462,7 +472,7 @@ export function getDestinationLocation(
462
472
  const tasks = mobilityTasks(entity)
463
473
  for (let i = tasks.length - 1; i >= 0; i--) {
464
474
  const task = tasks[i]
465
- if (task.type.equals(TaskType.TRAVEL) && task.coordinates) {
475
+ if (isPositionalTask(task) && task.coordinates) {
466
476
  return task.coordinates
467
477
  }
468
478
  }
@@ -485,7 +495,7 @@ export function getPositionAt(
485
495
 
486
496
  const task = tasks[taskIndex]
487
497
 
488
- if (!task.type.equals(TaskType.TRAVEL) || !task.coordinates) {
498
+ if (!isPositionalTask(task) || !task.coordinates) {
489
499
  return getFlightOrigin(entity, taskIndex)
490
500
  }
491
501
 
package/src/types.ts CHANGED
@@ -51,6 +51,7 @@ export enum TaskType {
51
51
  WARP = 6,
52
52
  CRAFT = 7,
53
53
  DEPLOY = 8,
54
+ TRANSIT = 9,
54
55
  UNWRAP = 10,
55
56
  UNDEPLOY = 11,
56
57
  DEMOLISH = 13,
@@ -3,6 +3,7 @@ import {hash512} from './hash'
3
3
  import {Coordinates, type CoordinatesType, LocationType} from '../types'
4
4
  import {ServerContract} from '../contracts'
5
5
  import {deriveLocationSize} from '../derivation/location-size'
6
+ import {wormholeAt} from '../derivation/wormhole'
6
7
  import syllables from '../data/syllables.json'
7
8
  import nebulaAdjectives from '../data/nebula-adjectives.json'
8
9
  import nebulaNouns from '../data/nebula-nouns.json'
@@ -110,6 +111,16 @@ export function hasSystem(gameSeed: Checksum256Type, coordinates: CoordinatesTyp
110
111
  return getLocationType(gameSeed, coordinates) !== LocationType.EMPTY
111
112
  }
112
113
 
114
+ export function getLocationKind(
115
+ gameSeed: Checksum256Type,
116
+ x: number,
117
+ y: number
118
+ ): 'wormhole' | 'system' | 'empty' {
119
+ if (wormholeAt(gameSeed, x, y)) return 'wormhole'
120
+ if (hasSystem(gameSeed, {x, y})) return 'system'
121
+ return 'empty'
122
+ }
123
+
113
124
  export function deriveLocationStatic(
114
125
  gameSeed: Checksum256Type,
115
126
  coordinates: CoordinatesType