@shipload/sdk 1.0.0-next.2 → 1.0.0-next.21

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 (85) hide show
  1. package/lib/shipload.d.ts +1731 -1044
  2. package/lib/shipload.js +6758 -4523
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +6649 -4479
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +833 -0
  7. package/lib/testing.js +3647 -0
  8. package/lib/testing.js.map +1 -0
  9. package/lib/testing.m.js +3641 -0
  10. package/lib/testing.m.js.map +1 -0
  11. package/package.json +15 -2
  12. package/src/capabilities/gathering.ts +17 -7
  13. package/src/capabilities/modules.ts +9 -0
  14. package/src/capabilities/storage.ts +1 -1
  15. package/src/contracts/platform.ts +211 -3
  16. package/src/contracts/server.ts +723 -438
  17. package/src/data/capabilities.ts +9 -329
  18. package/src/data/capability-formulas.ts +76 -0
  19. package/src/data/catalog.ts +0 -5
  20. package/src/data/colors.ts +14 -28
  21. package/src/data/entities.json +46 -10
  22. package/src/data/item-ids.ts +17 -13
  23. package/src/data/items.json +308 -37
  24. package/src/data/kind-registry.json +85 -0
  25. package/src/data/kind-registry.ts +150 -0
  26. package/src/data/metadata.ts +99 -24
  27. package/src/data/recipes-runtime.ts +3 -23
  28. package/src/data/recipes.json +265 -96
  29. package/src/derivation/build-methods.ts +45 -0
  30. package/src/derivation/capabilities.ts +414 -0
  31. package/src/derivation/capability-mappings.ts +117 -0
  32. package/src/derivation/crafting.ts +23 -24
  33. package/src/derivation/index.ts +8 -2
  34. package/src/derivation/reserve-regen.ts +34 -0
  35. package/src/derivation/resources.ts +125 -38
  36. package/src/derivation/stats.ts +1 -2
  37. package/src/derivation/stratum.ts +15 -19
  38. package/src/derivation/tiers.ts +28 -7
  39. package/src/entities/entity.ts +98 -0
  40. package/src/entities/gamestate.ts +3 -28
  41. package/src/entities/makers.ts +75 -129
  42. package/src/entities/slot-multiplier.ts +37 -0
  43. package/src/errors.ts +10 -15
  44. package/src/format.ts +26 -4
  45. package/src/index-module.ts +151 -40
  46. package/src/managers/actions.ts +184 -82
  47. package/src/managers/base.ts +2 -2
  48. package/src/managers/construction-types.ts +68 -0
  49. package/src/managers/construction.ts +292 -0
  50. package/src/managers/context.ts +9 -0
  51. package/src/managers/entities.ts +18 -66
  52. package/src/managers/epochs.ts +40 -0
  53. package/src/managers/index.ts +16 -1
  54. package/src/managers/locations.ts +2 -20
  55. package/src/managers/nft.ts +28 -0
  56. package/src/managers/plot.ts +123 -0
  57. package/src/nft/atomicassets.ts +231 -0
  58. package/src/nft/atomicdata.ts +130 -0
  59. package/src/nft/buildImmutableData.ts +319 -0
  60. package/src/nft/description.ts +45 -13
  61. package/src/nft/index.ts +3 -0
  62. package/src/resolution/describe-module.ts +5 -8
  63. package/src/resolution/display-name.ts +38 -10
  64. package/src/resolution/resolve-item.ts +20 -12
  65. package/src/scheduling/accessor.ts +4 -0
  66. package/src/scheduling/projection.ts +79 -27
  67. package/src/scheduling/schedule.ts +15 -1
  68. package/src/scheduling/task-cargo.ts +46 -0
  69. package/src/shipload.ts +5 -0
  70. package/src/subscriptions/manager.ts +40 -6
  71. package/src/subscriptions/mappers.ts +3 -8
  72. package/src/subscriptions/types.ts +3 -2
  73. package/src/testing/catalog-hash.ts +19 -0
  74. package/src/testing/index.ts +2 -0
  75. package/src/testing/projection-parity.ts +143 -0
  76. package/src/travel/travel.ts +61 -2
  77. package/src/types/index.ts +0 -1
  78. package/src/types.ts +17 -12
  79. package/src/utils/cargo.ts +27 -0
  80. package/src/utils/system.ts +25 -24
  81. package/src/entities/container.ts +0 -108
  82. package/src/entities/ship-deploy.ts +0 -258
  83. package/src/entities/ship.ts +0 -204
  84. package/src/entities/warehouse.ts +0 -119
  85. package/src/types/entity-traits.ts +0 -69
@@ -14,10 +14,12 @@ import {
14
14
  RECIPE_INPUTS_INSUFFICIENT,
15
15
  RECIPE_INPUTS_INVALID,
16
16
  RECIPE_NOT_FOUND,
17
- SHIP_CARGO_NOT_LOADED,
17
+ ENTITY_CARGO_NOT_LOADED,
18
18
  } from '../errors'
19
- import {getRecipe, type RecipeInput} from '../data/recipes-runtime'
20
- import {getItem} from '../data/catalog'
19
+ import {getEntityLayout, getRecipe, type RecipeInput} from '../data/recipes-runtime'
20
+ import {computeEntityCapabilities} from '../derivation/capabilities'
21
+ import {decodeCraftedItemStats} from '../derivation/crafting'
22
+ import {packedModulesToInstalled, type InstalledModule} from '../entities/slot-multiplier'
21
23
  import {distanceBetweenCoordinates, lerp} from '../travel/travel'
22
24
  import {
23
25
  calcStacksMass,
@@ -63,19 +65,74 @@ export interface Projectable extends ScheduleData {
63
65
  cargo: ServerContract.Types.cargo_item[]
64
66
  cargomass: UInt32
65
67
  owner?: Name
68
+ stats?: bigint
69
+ item_id?: number | UInt16
70
+ modules?: ServerContract.Types.module_entry[] | InstalledModule[]
66
71
  }
67
72
 
68
- function getHullMass(entity: Projectable): UInt32 {
69
- return UInt32.from(entity.hullmass ?? 0)
73
+ function toInstalledModules(
74
+ modules: ServerContract.Types.module_entry[] | InstalledModule[]
75
+ ): InstalledModule[] {
76
+ if (modules.length > 0 && 'itemId' in modules[0]) {
77
+ return modules as InstalledModule[]
78
+ }
79
+ return packedModulesToInstalled(modules as ServerContract.Types.module_entry[])
80
+ }
81
+
82
+ interface ProjectedCaps {
83
+ hullmass?: UInt32
84
+ capacity?: UInt32
85
+ engines?: ServerContract.Types.movement_stats
86
+ generator?: ServerContract.Types.energy_stats
87
+ loaders?: ServerContract.Types.loader_stats
88
+ hauler?: ServerContract.Types.hauler_stats
89
+ }
90
+
91
+ function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
92
+ if (
93
+ entity.item_id === undefined ||
94
+ entity.modules === undefined ||
95
+ entity.stats === undefined
96
+ ) {
97
+ return undefined
98
+ }
99
+
100
+ const itemId = Number(
101
+ typeof entity.item_id === 'number' ? entity.item_id : entity.item_id.value
102
+ )
103
+ const hullStats = decodeCraftedItemStats(itemId, entity.stats)
104
+ const layout = getEntityLayout(itemId)?.slots ?? []
105
+ const installed = toInstalledModules(entity.modules)
106
+ const caps = computeEntityCapabilities(hullStats, itemId, installed, layout)
107
+
108
+ return {
109
+ hullmass: UInt32.from(caps.hullmass),
110
+ capacity: UInt32.from(caps.capacity),
111
+ engines: caps.engines ? ServerContract.Types.movement_stats.from(caps.engines) : undefined,
112
+ generator: caps.generator
113
+ ? ServerContract.Types.energy_stats.from(caps.generator)
114
+ : undefined,
115
+ loaders: caps.loaders ? ServerContract.Types.loader_stats.from(caps.loaders) : undefined,
116
+ hauler: caps.hauler ? ServerContract.Types.hauler_stats.from(caps.hauler) : undefined,
117
+ }
70
118
  }
71
119
 
72
120
  export function createProjectedEntity(entity: Projectable): ProjectedEntity {
73
- const shipMass = getHullMass(entity)
74
- const loaders = entity.loaders
75
- const engines = entity.engines
76
- const generator = entity.generator
77
- const hauler = entity.hauler
78
- const capacity = entity.capacity
121
+ const needsRecompute =
122
+ entity.hullmass === undefined ||
123
+ entity.loaders === undefined ||
124
+ entity.engines === undefined ||
125
+ entity.generator === undefined ||
126
+ entity.hauler === undefined ||
127
+ entity.capacity === undefined
128
+ const caps = needsRecompute ? recomputeCaps(entity) : undefined
129
+
130
+ const shipMass = UInt32.from(entity.hullmass ?? caps?.hullmass ?? 0)
131
+ const loaders = entity.loaders ?? caps?.loaders
132
+ const engines = entity.engines ?? caps?.engines
133
+ const generator = entity.generator ?? caps?.generator
134
+ const hauler = entity.hauler ?? caps?.hauler
135
+ const capacity = entity.capacity ?? caps?.capacity
79
136
 
80
137
  const cargo: CargoStack[] = entity.cargo.map(cargoItemToStack)
81
138
 
@@ -255,7 +312,6 @@ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task):
255
312
  applyAddCargoTask(projected, task)
256
313
  break
257
314
  case TaskType.UNLOAD:
258
- case TaskType.WRAP:
259
315
  applyRemoveCargoTask(projected, task)
260
316
  break
261
317
  case TaskType.GATHER:
@@ -267,6 +323,9 @@ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task):
267
323
  case TaskType.DEPLOY:
268
324
  applyDeployTask(projected, task)
269
325
  break
326
+ case TaskType.UNDEPLOY:
327
+ case TaskType.DEMOLISH:
328
+ break
270
329
  }
271
330
  }
272
331
 
@@ -342,19 +401,10 @@ function validateCraftTask(task: ServerContract.Types.task, projected: Projected
342
401
  let matched = false
343
402
  for (let ri = 0; ri < recipe.length; ri++) {
344
403
  const req = recipe[ri]
345
- if ('itemId' in req) {
346
- if (input.item_id.toNumber() === req.itemId) {
347
- groupedInputs[ri].push(input)
348
- matched = true
349
- break
350
- }
351
- } else {
352
- const item = getItem(input.item_id)
353
- if (item.category === req.category && item.tier === req.tier) {
354
- groupedInputs[ri].push(input)
355
- matched = true
356
- break
357
- }
404
+ if (input.item_id.toNumber() === req.itemId) {
405
+ groupedInputs[ri].push(input)
406
+ matched = true
407
+ break
358
408
  }
359
409
  }
360
410
  if (!matched) throw new Error(RECIPE_INPUTS_INVALID)
@@ -385,7 +435,7 @@ function validateCraftTask(task: ServerContract.Types.task, projected: Projected
385
435
  break
386
436
  }
387
437
  }
388
- if (!found) throw new Error(SHIP_CARGO_NOT_LOADED)
438
+ if (!found) throw new Error(ENTITY_CARGO_NOT_LOADED)
389
439
  }
390
440
  }
391
441
 
@@ -436,7 +486,6 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
436
486
  if (taskComplete) applyAddCargoTask(projected, task)
437
487
  break
438
488
  case TaskType.UNLOAD:
439
- case TaskType.WRAP:
440
489
  if (taskComplete) applyRemoveCargoTask(projected, task)
441
490
  break
442
491
  case TaskType.GATHER:
@@ -448,6 +497,9 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
448
497
  case TaskType.DEPLOY:
449
498
  if (taskComplete) applyDeployTask(projected, task)
450
499
  break
500
+ case TaskType.UNDEPLOY:
501
+ case TaskType.DEMOLISH:
502
+ break
451
503
  }
452
504
  }
453
505
 
@@ -77,7 +77,7 @@ export function currentTaskIndex(entity: ScheduleData, now: Date): number {
77
77
  timeAccum += taskDuration
78
78
  }
79
79
 
80
- return entity.schedule.tasks.length - 1
80
+ return -1
81
81
  }
82
82
 
83
83
  export function currentTask(entity: ScheduleData, now: Date): Task | undefined {
@@ -147,6 +147,20 @@ export function currentTaskProgress(entity: ScheduleData, now: Date): number {
147
147
  return Math.min(1, elapsed / duration)
148
148
  }
149
149
 
150
+ export function currentTaskProgressFloat(entity: ScheduleData, now: Date): number {
151
+ if (!entity.schedule || entity.schedule.tasks.length === 0) return 0
152
+ const index = currentTaskIndex(entity, now)
153
+ if (index < 0) return 0
154
+ const task = entity.schedule.tasks[index]
155
+ const durationMs = task.duration.toNumber() * 1000
156
+ if (durationMs === 0) return 1
157
+ const startedMs = entity.schedule.started.toDate().getTime()
158
+ const taskStartMs = startedMs + getTaskStartTime(entity, index) * 1000
159
+ const elapsedMs = now.getTime() - taskStartMs
160
+ if (elapsedMs <= 0) return 0
161
+ return Math.min(1, elapsedMs / durationMs)
162
+ }
163
+
150
164
  export function scheduleProgress(entity: ScheduleData, now: Date): number {
151
165
  const duration = scheduleDuration(entity)
152
166
  if (duration === 0) return hasSchedule(entity) ? 1 : 0
@@ -0,0 +1,46 @@
1
+ import type {ServerContract} from '../contracts'
2
+ import {TaskType} from '../types'
3
+
4
+ export type TaskCargoDirection = 'in' | 'out'
5
+
6
+ export interface TaskCargoChange {
7
+ direction: TaskCargoDirection
8
+ item_id: number
9
+ stats: bigint
10
+ modules: ServerContract.Types.module_entry[]
11
+ quantity: number
12
+ }
13
+
14
+ function toChange(
15
+ item: ServerContract.Types.cargo_item,
16
+ direction: TaskCargoDirection
17
+ ): TaskCargoChange {
18
+ return {
19
+ direction,
20
+ item_id: Number(item.item_id),
21
+ stats: BigInt(item.stats.toString()),
22
+ modules: item.modules ?? [],
23
+ quantity: Number(item.quantity),
24
+ }
25
+ }
26
+
27
+ export function taskCargoChanges(task: ServerContract.Types.task): TaskCargoChange[] {
28
+ const items = task.cargo ?? []
29
+ if (items.length === 0) return []
30
+ switch (Number(task.type)) {
31
+ case TaskType.LOAD:
32
+ case TaskType.UNWRAP:
33
+ return items.map((i) => toChange(i, 'in'))
34
+ case TaskType.GATHER:
35
+ return task.entitytarget ? [] : items.map((i) => toChange(i, 'in'))
36
+ case TaskType.UNLOAD:
37
+ return items.map((i) => toChange(i, 'out'))
38
+ case TaskType.CRAFT:
39
+ return [
40
+ ...items.slice(0, -1).map((i) => toChange(i, 'out')),
41
+ toChange(items[items.length - 1], 'in'),
42
+ ]
43
+ default:
44
+ return []
45
+ }
46
+ }
package/src/shipload.ts CHANGED
@@ -9,6 +9,7 @@ import type {PlayersManager} from './managers/players'
9
9
  import type {LocationsManager} from './managers/locations'
10
10
  import type {EpochsManager} from './managers/epochs'
11
11
  import type {ActionsManager} from './managers/actions'
12
+ import type {NftManager} from './managers/nft'
12
13
  import type {SubscriptionsManager} from './subscriptions/manager'
13
14
  import type {GameState} from './entities/gamestate'
14
15
 
@@ -107,6 +108,10 @@ export class Shipload {
107
108
  return this._context.actions
108
109
  }
109
110
 
111
+ get nft(): NftManager {
112
+ return this._context.nft
113
+ }
114
+
110
115
  get subscriptions(): SubscriptionsManager {
111
116
  return this._context.subscriptions
112
117
  }
@@ -13,12 +13,10 @@ import type {
13
13
  WireEntity,
14
14
  } from './types'
15
15
  import {mapEntity, parseWireEntity} from './mappers'
16
- import type {Ship} from '../entities/ship'
17
- import type {Warehouse} from '../entities/warehouse'
18
- import type {Container} from '../entities/container'
16
+ import type {Entity} from '../entities/entity'
19
17
 
20
- export type SubscriptionEntityType = 'ship' | 'warehouse' | 'container'
21
- export type EntityInstance = Ship | Warehouse | Container
18
+ export type SubscriptionEntityType = 'ship' | 'warehouse' | 'container' | 'nexus'
19
+ export type EntityInstance = Entity
22
20
 
23
21
  export interface SubscriptionsOptions {
24
22
  url: string
@@ -34,6 +32,12 @@ export interface BoundsSubscriptionHandle {
34
32
  current: Map<number, EntityInstance>
35
33
  }
36
34
 
35
+ export interface OwnerSubscriptionHandle {
36
+ readonly subId: string
37
+ unsubscribe(): void
38
+ current: Map<number, EntityInstance>
39
+ }
40
+
37
41
  export interface EntitySubscriptionHandle {
38
42
  readonly subId: string
39
43
  readonly entityType: SubscriptionEntityType
@@ -62,7 +66,7 @@ export class SubscriptionsManager {
62
66
  onSnapshot?: (entities: EntityInstance[]) => void
63
67
  onUpdate?: (entity: EntityInstance) => void
64
68
  onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
65
- handle: BoundsSubscriptionHandle
69
+ handle: BoundsSubscriptionHandle | OwnerSubscriptionHandle
66
70
  }
67
71
  >()
68
72
  private subCounter = 0
@@ -162,6 +166,36 @@ export class SubscriptionsManager {
162
166
  return handle
163
167
  }
164
168
 
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 = {
178
+ type: 'subscribe',
179
+ sub_id: subId,
180
+ owner,
181
+ }
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
+ }
198
+
165
199
  private unsubscribeBounds(subId: string) {
166
200
  this.boundsSubs.delete(subId)
167
201
  this.sendMessage({type: 'unsubscribe', sub_id: subId})
@@ -1,14 +1,9 @@
1
1
  import {ServerContract} from '../contracts'
2
- import {Ship} from '../entities/ship'
3
- import {Warehouse} from '../entities/warehouse'
4
- import {Container} from '../entities/container'
2
+ import {Entity} from '../entities/entity'
5
3
  import type {WireEntity} from './types'
6
4
 
7
- export function mapEntity(ei: ServerContract.Types.entity_info): Ship | Warehouse | Container {
8
- if (ei.type.equals('ship')) return new Ship(ei)
9
- if (ei.type.equals('warehouse')) return new Warehouse(ei)
10
- if (ei.type.equals('container')) return new Container(ei)
11
- throw new Error(`mapEntity: unknown entity type ${ei.type.toString()}`)
5
+ export function mapEntity(ei: ServerContract.Types.entity_info): Entity {
6
+ return new Entity(ei)
12
7
  }
13
8
 
14
9
  export function parseWireEntity(raw: WireEntity): ServerContract.Types.entity_info {
@@ -39,7 +39,7 @@ export type UnsubscribeMessage = {
39
39
  export type SubscribeEntityMessage = {
40
40
  type: 'subscribe_entity'
41
41
  sub_id: string
42
- entity_type: 'ship' | 'warehouse' | 'container'
42
+ entity_type: 'ship' | 'warehouse' | 'container' | 'nexus'
43
43
  entity_id: string
44
44
  }
45
45
 
@@ -80,10 +80,11 @@ export type AckMessage = {
80
80
 
81
81
  export type WireEntity = Record<string, unknown> & {
82
82
  type: number
83
- type_name: 'ship' | 'warehouse' | 'container'
83
+ type_name: 'ship' | 'warehouse' | 'container' | 'nexus'
84
84
  id: string | number
85
85
  owner: string
86
86
  coordinates: WireCoordinates
87
+ item_id: number
87
88
  }
88
89
 
89
90
  export type SnapshotMessage = {
@@ -0,0 +1,19 @@
1
+ import {createHash} from 'node:crypto'
2
+ import {readFileSync} from 'node:fs'
3
+
4
+ export const CATALOG_FILES_REL = [
5
+ 'items.json',
6
+ 'recipes.json',
7
+ 'entities.json',
8
+ 'kind-registry.json',
9
+ 'item-ids.ts',
10
+ ] as const
11
+
12
+ export function computeCatalogHash(filePaths: ReadonlyArray<string>): string {
13
+ const hash = createHash('sha256')
14
+ for (const p of filePaths) {
15
+ hash.update(readFileSync(p))
16
+ hash.update('\0')
17
+ }
18
+ return hash.digest('hex')
19
+ }
@@ -0,0 +1,2 @@
1
+ export * from './catalog-hash'
2
+ export * from './projection-parity'
@@ -0,0 +1,143 @@
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
+ loaders?: ServerContract.Types.loader_stats
16
+ generator?: ServerContract.Types.energy_stats
17
+ hauler?: ServerContract.Types.hauler_stats
18
+ }
19
+
20
+ export interface ProjectionComparisonOptions {
21
+ step?: number
22
+ }
23
+
24
+ export function assertProjectionEquals(
25
+ contract: ContractProjectedState,
26
+ sdk: ProjectedEntity,
27
+ options: ProjectionComparisonOptions = {}
28
+ ): void {
29
+ const mismatches: string[] = []
30
+
31
+ const record = (name: string, c: unknown, s: unknown) => {
32
+ if (c !== s) mismatches.push(` ${name}: contract=${fmt(c)} sdk=${fmt(s)}`)
33
+ }
34
+
35
+ const recordStatBlock = (name: string, c: unknown, s: unknown) => {
36
+ const cPresent = c !== undefined && c !== null
37
+ const sPresent = s !== undefined && s !== null
38
+ if (cPresent !== sPresent) {
39
+ mismatches.push(
40
+ ` ${name}: contract=${cPresent ? 'present' : 'absent'} sdk=${sPresent ? 'present' : 'absent'}`
41
+ )
42
+ return
43
+ }
44
+ if (!cPresent) return
45
+ const cn = JSON.stringify(normaliseStatBlock(c))
46
+ const sn = JSON.stringify(normaliseStatBlock(s))
47
+ if (cn !== sn) mismatches.push(` ${name}: contract=${cn} sdk=${sn}`)
48
+ }
49
+
50
+ record('coordinates.x', toNum(contract.coordinates.x), Number(sdk.location.x))
51
+ record('coordinates.y', toNum(contract.coordinates.y), Number(sdk.location.y))
52
+ record('energy', toNum(contract.energy), Number(sdk.energy))
53
+ record('cargomass', toNum(contract.cargomass), Number(sdk.cargoMass))
54
+ record('hullmass', toNum(contract.hullmass), Number(sdk.shipMass))
55
+ record('capacity', toNum(contract.capacity), sdk.capacity ? Number(sdk.capacity) : undefined)
56
+
57
+ recordStatBlock('engines', contract.engines, sdk.engines)
58
+ recordStatBlock('loaders', contract.loaders, sdk.loaders)
59
+ recordStatBlock('generator', contract.generator, sdk.generator)
60
+ recordStatBlock('hauler', contract.hauler, sdk.hauler)
61
+
62
+ if (contract.cargo.length > 0 || sdk.cargo.length > 0) {
63
+ const contractCargo = normaliseCargo(mergeContractCargo(contract.cargo))
64
+ const sdkCargo = normaliseCargo(sdk.cargo)
65
+ if (contractCargo.length !== sdkCargo.length) {
66
+ mismatches.push(
67
+ ` cargo.length: contract=${contractCargo.length} sdk=${sdkCargo.length}`
68
+ )
69
+ } else {
70
+ for (let i = 0; i < contractCargo.length; i++) {
71
+ const c = contractCargo[i]
72
+ const s = sdkCargo[i]
73
+ if (c.itemId !== s.itemId || c.stats !== s.stats || c.quantity !== s.quantity) {
74
+ mismatches.push(
75
+ ` cargo[${i}]: contract={item:${c.itemId},stats:${c.stats},qty:${c.quantity}} sdk={item:${s.itemId},stats:${s.stats},qty:${s.quantity}}`
76
+ )
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ if (mismatches.length > 0) {
83
+ const header =
84
+ options.step !== undefined
85
+ ? `projection divergence at step ${options.step}:`
86
+ : 'projection divergence:'
87
+ throw new Error([header, ...mismatches].join('\n'))
88
+ }
89
+ }
90
+
91
+ interface NormalisedStack {
92
+ itemId: number
93
+ stats: string
94
+ quantity: string
95
+ }
96
+
97
+ function mergeContractCargo(cargo: ServerContract.Types.cargo_view[]): CargoStack[] {
98
+ return cargo.reduce<CargoStack[]>(
99
+ (acc, row) =>
100
+ mergeStacks(acc, cargoItemToStack(row as unknown as ServerContract.Types.cargo_item)),
101
+ []
102
+ )
103
+ }
104
+
105
+ function normaliseCargo(cargo: CargoStack[]): NormalisedStack[] {
106
+ return cargo
107
+ .map((s) => ({
108
+ itemId: Number(s.item_id),
109
+ stats: BigInt(s.stats.toString()).toString(),
110
+ quantity: BigInt(s.quantity.toString()).toString(),
111
+ }))
112
+ .sort(stackSort)
113
+ }
114
+
115
+ function stackSort(a: NormalisedStack, b: NormalisedStack): number {
116
+ if (a.itemId !== b.itemId) return a.itemId - b.itemId
117
+ return a.stats < b.stats ? -1 : a.stats > b.stats ? 1 : 0
118
+ }
119
+
120
+ function toNum(v: unknown): number | undefined {
121
+ if (v === undefined || v === null) return undefined
122
+ if (typeof v === 'number') return v
123
+ if (typeof v === 'bigint') return Number(v)
124
+ if (typeof (v as {toNumber?: unknown}).toNumber === 'function') {
125
+ return (v as {toNumber(): number}).toNumber()
126
+ }
127
+ return Number(v as number)
128
+ }
129
+
130
+ function fmt(v: unknown): string {
131
+ if (v === undefined) return 'undefined'
132
+ if (v === null) return 'null'
133
+ return String(v)
134
+ }
135
+
136
+ function normaliseStatBlock(block: unknown): Record<string, number> {
137
+ const out: Record<string, number> = {}
138
+ const obj = block as Record<string, unknown>
139
+ for (const k of Object.keys(obj).sort()) {
140
+ out[k] = toNum(obj[k]) ?? 0
141
+ }
142
+ return out
143
+ }
@@ -28,6 +28,7 @@ import {
28
28
  type Distance,
29
29
  MAX_ORBITAL_ALTITUDE,
30
30
  MIN_ORBITAL_ALTITUDE,
31
+ MIN_TRANSFER_DISTANCE,
31
32
  PRECISION,
32
33
  type ShipLike,
33
34
  TaskType,
@@ -77,6 +78,59 @@ export function lerp(
77
78
  }
78
79
  }
79
80
 
81
+ export interface FloatPosition {
82
+ x: number
83
+ y: number
84
+ }
85
+
86
+ export function easeFlightProgress(t: number): number {
87
+ if (t <= 0) return 0
88
+ if (t >= 1) return 1
89
+ return t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) * (1 - t)
90
+ }
91
+
92
+ export function flightSpeedFactor(t: number): number {
93
+ if (t <= 0 || t >= 1) return 0
94
+ return t < 0.5 ? 4 * t : 4 * (1 - t)
95
+ }
96
+
97
+ export function interpolateFlightPosition(
98
+ origin: {x: Int64Type | number; y: Int64Type | number},
99
+ destination: {x: Int64Type | number; y: Int64Type | number},
100
+ taskProgress: number,
101
+ options?: {easing?: 'physics' | 'linear'}
102
+ ): FloatPosition {
103
+ const t = options?.easing === 'linear' ? taskProgress : easeFlightProgress(taskProgress)
104
+ return {
105
+ x: (1 - t) * Number(origin.x) + t * Number(destination.x),
106
+ y: (1 - t) * Number(origin.y) + t * Number(destination.y),
107
+ }
108
+ }
109
+
110
+ export function getInterpolatedPosition(
111
+ entity: HasScheduleAndLocation,
112
+ taskIndex: number,
113
+ taskProgress: number
114
+ ): FloatPosition {
115
+ if (!entity.schedule || entity.schedule.tasks.length === 0) {
116
+ return {x: Number(entity.coordinates.x), y: Number(entity.coordinates.y)}
117
+ }
118
+ if (taskIndex < 0) {
119
+ const settled = getFlightOrigin(entity, entity.schedule.tasks.length)
120
+ return {x: Number(settled.x), y: Number(settled.y)}
121
+ }
122
+ const task = entity.schedule.tasks[taskIndex]
123
+ if (!task.type.equals(TaskType.TRAVEL) || !task.coordinates) {
124
+ const origin = getFlightOrigin(entity, taskIndex)
125
+ return {x: Number(origin.x), y: Number(origin.y)}
126
+ }
127
+ return interpolateFlightPosition(
128
+ getFlightOrigin(entity, taskIndex),
129
+ task.coordinates,
130
+ taskProgress
131
+ )
132
+ }
133
+
80
134
  export function rotation(
81
135
  origin: ServerContract.ActionParams.Type.coordinates,
82
136
  destination: ServerContract.ActionParams.Type.coordinates
@@ -408,14 +462,18 @@ export function getDestinationLocation(
408
462
  return undefined
409
463
  }
410
464
 
465
+ /** Returns chain-tile coordinates (rounded). For visual position use getInterpolatedPosition. */
411
466
  export function getPositionAt(
412
467
  entity: HasScheduleAndLocation,
413
468
  taskIndex: number,
414
469
  taskProgress: number
415
470
  ): ServerContract.ActionParams.Type.coordinates {
416
- if (!entity.schedule || entity.schedule.tasks.length === 0 || taskIndex < 0) {
471
+ if (!entity.schedule || entity.schedule.tasks.length === 0) {
417
472
  return entity.coordinates
418
473
  }
474
+ if (taskIndex < 0) {
475
+ return getFlightOrigin(entity, entity.schedule.tasks.length)
476
+ }
419
477
 
420
478
  const task = entity.schedule.tasks[taskIndex]
421
479
 
@@ -490,7 +548,8 @@ export function calc_transfer_duration(
490
548
  : (source.location.z?.toNumber() ?? 0)
491
549
  const destZ =
492
550
  typeof dest.location.z === 'number' ? dest.location.z : (dest.location.z?.toNumber() ?? 0)
493
- const distance = Math.abs(sourceZ - destZ)
551
+ const rawDistance = Math.abs(sourceZ - destZ)
552
+ const distance = rawDistance < MIN_TRANSFER_DISTANCE ? MIN_TRANSFER_DISTANCE : rawDistance
494
553
 
495
554
  const totalMass = cargoMass + totalLoaderMass
496
555
  const acceleration = calc_acceleration(totalThrust, totalMass)
@@ -1,3 +1,2 @@
1
1
  export * from './capabilities'
2
2
  export * from './entity'
3
- export * from './entity-traits'