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

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 +1709 -1044
  2. package/lib/shipload.js +6762 -4658
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +6653 -4614
  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 +149 -40
  46. package/src/managers/actions.ts +184 -82
  47. package/src/managers/base.ts +2 -2
  48. package/src/managers/construction-types.ts +47 -0
  49. package/src/managers/construction.ts +147 -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 +14 -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
@@ -26,8 +26,9 @@ import {
26
26
  computeLoaderCapabilities,
27
27
  computeShipHullCapabilities,
28
28
  computeWarehouseHullCapabilities,
29
- } from '../entities/ship-deploy'
30
- import {computeContainerCapabilities, computeContainerT2Capabilities} from '../entities/container'
29
+ computeContainerCapabilities,
30
+ computeContainerT2Capabilities,
31
+ } from '../derivation/capabilities'
31
32
  import {
32
33
  categoryColors,
33
34
  categoryIcons,
@@ -39,6 +40,7 @@ import type {ServerContract} from '../contracts'
39
40
  import {
40
41
  ITEM_CONTAINER_T1_PACKED,
41
42
  ITEM_CONTAINER_T2_PACKED,
43
+ ITEM_EXTRACTOR_T1_PACKED,
42
44
  ITEM_SHIP_T1_PACKED,
43
45
  ITEM_WAREHOUSE_T1_PACKED,
44
46
  } from '../data/item-ids'
@@ -155,7 +157,8 @@ function resolveComponent(id: number, stats?: UInt64Type): ResolvedItem {
155
157
 
156
158
  function computeCapabilityGroup(
157
159
  moduleType: number,
158
- stats: Record<string, number>
160
+ stats: Record<string, number>,
161
+ tier: number
159
162
  ): ResolvedAttributeGroup | undefined {
160
163
  switch (moduleType) {
161
164
  case MODULE_ENGINE: {
@@ -179,14 +182,13 @@ function computeCapabilityGroup(
179
182
  }
180
183
  }
181
184
  case MODULE_GATHERER: {
182
- const caps = computeGathererCapabilities(stats)
185
+ const caps = computeGathererCapabilities(stats, tier)
183
186
  return {
184
187
  capability: 'Gatherer',
185
188
  attributes: [
186
189
  {label: 'Yield', value: caps.yield},
187
190
  {label: 'Drain', value: caps.drain},
188
191
  {label: 'Depth', value: caps.depth},
189
- {label: 'Speed', value: caps.speed},
190
192
  ],
191
193
  }
192
194
  }
@@ -223,10 +225,11 @@ function computeCapabilityGroup(
223
225
  }
224
226
  }
225
227
  case MODULE_STORAGE: {
226
- const str = stats.strength ?? 500
227
- const hrd = stats.hardness ?? 500
228
- const sat = stats.saturation ?? 500
229
- const statSum = str + hrd + sat
228
+ const str = stats.strength
229
+ const den = stats.density
230
+ const hrd = stats.hardness
231
+ const sat = stats.saturation
232
+ const statSum = str + den + hrd + sat
230
233
  const pct = 10 + Math.floor((statSum * 10) / 2997)
231
234
  return {capability: 'Storage', attributes: [{label: 'Capacity Bonus', value: pct}]}
232
235
  }
@@ -241,7 +244,7 @@ function resolveModule(id: number, stats?: UInt64Type): ResolvedItem {
241
244
  if (stats !== undefined) {
242
245
  const decoded = decodeCraftedItemStats(id, toBigStats(stats))
243
246
  const modType = getModuleCapabilityType(id)
244
- const group = computeCapabilityGroup(modType, decoded)
247
+ const group = computeCapabilityGroup(modType, decoded, item.tier)
245
248
  if (group) attributes = [group]
246
249
  }
247
250
  return {
@@ -268,6 +271,8 @@ function hullCapsForEntity(
268
271
  return computeShipHullCapabilities(decoded)
269
272
  case ITEM_WAREHOUSE_T1_PACKED:
270
273
  return computeWarehouseHullCapabilities(decoded)
274
+ case ITEM_EXTRACTOR_T1_PACKED:
275
+ return computeShipHullCapabilities(decoded)
271
276
  case ITEM_CONTAINER_T1_PACKED:
272
277
  return computeContainerCapabilities(decoded)
273
278
  case ITEM_CONTAINER_T2_PACKED:
@@ -310,13 +315,16 @@ function resolveEntity(
310
315
  const modStats = BigInt(mod.installed.stats.toString())
311
316
  const decodedStats = decodeCraftedItemStats(modItemId, modStats)
312
317
  const modType = getModuleCapabilityType(modItemId)
313
- const group = computeCapabilityGroup(modType, decodedStats)
314
318
  let modName = 'Module'
319
+ let modTier = 1
315
320
  try {
316
- modName = getItem(modItemId).name
321
+ const modItem = getItem(modItemId)
322
+ modName = modItem.name
323
+ modTier = modItem.tier
317
324
  } catch {
318
325
  modName = itemMetadata[modItemId]?.name ?? 'Module'
319
326
  }
327
+ const group = computeCapabilityGroup(modType, decodedStats, modTier)
320
328
  return {
321
329
  name: modName,
322
330
  installed: true,
@@ -72,6 +72,10 @@ export class ScheduleAccessor {
72
72
  return schedule.currentTaskProgress(this.entity, now)
73
73
  }
74
74
 
75
+ currentTaskProgressFloat(now: Date): number {
76
+ return schedule.currentTaskProgressFloat(this.entity, now)
77
+ }
78
+
75
79
  progress(now: Date): number {
76
80
  return schedule.scheduleProgress(this.entity, now)
77
81
  }
@@ -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
+ }