@shipload/sdk 1.0.0-next.0 → 1.0.0-next.10

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 (51) hide show
  1. package/lib/shipload.d.ts +512 -320
  2. package/lib/shipload.js +1960 -1060
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +1920 -1056
  5. package/lib/shipload.m.js.map +1 -1
  6. package/package.json +8 -3
  7. package/src/capabilities/modules.ts +3 -0
  8. package/src/capabilities/storage.ts +1 -1
  9. package/src/contracts/platform.ts +13 -1
  10. package/src/contracts/server.ts +227 -282
  11. package/src/data/capabilities.ts +5 -330
  12. package/src/data/capability-formulas.ts +70 -0
  13. package/src/data/catalog.ts +0 -5
  14. package/src/data/colors.ts +2 -16
  15. package/src/data/entities.json +33 -10
  16. package/src/data/item-ids.ts +3 -1
  17. package/src/data/items.json +258 -0
  18. package/src/data/metadata.ts +57 -1
  19. package/src/data/recipes-runtime.ts +1 -0
  20. package/src/data/recipes.json +82 -11
  21. package/src/derivation/capability-mappings.ts +122 -0
  22. package/src/derivation/index.ts +1 -0
  23. package/src/derivation/resources.ts +116 -37
  24. package/src/derivation/stats.ts +1 -2
  25. package/src/entities/container.ts +25 -10
  26. package/src/entities/extractor.ts +144 -0
  27. package/src/entities/gamestate.ts +0 -9
  28. package/src/entities/makers.ts +71 -20
  29. package/src/entities/ship-deploy.ts +114 -56
  30. package/src/entities/ship.ts +17 -0
  31. package/src/entities/slot-multiplier.ts +21 -0
  32. package/src/entities/warehouse.ts +20 -3
  33. package/src/index-module.ts +67 -26
  34. package/src/managers/actions.ts +53 -80
  35. package/src/managers/entities.ts +31 -17
  36. package/src/managers/locations.ts +2 -20
  37. package/src/nft/atomicdata.ts +125 -0
  38. package/src/nft/description.ts +41 -7
  39. package/src/nft/index.ts +1 -0
  40. package/src/resolution/resolve-item.ts +17 -9
  41. package/src/scheduling/accessor.ts +4 -0
  42. package/src/scheduling/projection.ts +8 -0
  43. package/src/scheduling/schedule.ts +15 -1
  44. package/src/scheduling/task-cargo.ts +47 -0
  45. package/src/subscriptions/connection.ts +50 -2
  46. package/src/subscriptions/manager.ts +81 -2
  47. package/src/travel/travel.ts +61 -2
  48. package/src/types/entity-traits.ts +64 -1
  49. package/src/types.ts +11 -1
  50. package/src/utils/cargo.ts +27 -0
  51. package/src/utils/system.ts +25 -24
@@ -1,10 +1,16 @@
1
1
  import {getItem} from '../data/catalog'
2
+ import {LocationType} from '../types'
2
3
 
3
4
  export const DEPTH_THRESHOLD_T1 = 0
4
- export const DEPTH_THRESHOLD_T2 = 2000
5
- export const DEPTH_THRESHOLD_T3 = 10000
6
- export const DEPTH_THRESHOLD_T4 = 30000
7
- export const DEPTH_THRESHOLD_T5 = 55000
5
+ export const DEPTH_THRESHOLD_T2 = 1500
6
+ export const DEPTH_THRESHOLD_T3 = 5000
7
+ export const DEPTH_THRESHOLD_T4 = 12000
8
+ export const DEPTH_THRESHOLD_T5 = 22000
9
+ export const DEPTH_THRESHOLD_T6 = 32000
10
+ export const DEPTH_THRESHOLD_T7 = 42000
11
+ export const DEPTH_THRESHOLD_T8 = 50000
12
+ export const DEPTH_THRESHOLD_T9 = 57000
13
+ export const DEPTH_THRESHOLD_T10 = 63000
8
14
 
9
15
  export const LOCATION_MIN_DEPTH = 500
10
16
  export const LOCATION_MAX_DEPTH = 65535
@@ -18,19 +24,22 @@ export const PLANET_SUBTYPE_ICY = 3
18
24
  export const PLANET_SUBTYPE_OCEAN = 4
19
25
  export const PLANET_SUBTYPE_INDUSTRIAL = 5
20
26
 
27
+ const DEPTH_THRESHOLD_TABLE = [
28
+ DEPTH_THRESHOLD_T1,
29
+ DEPTH_THRESHOLD_T2,
30
+ DEPTH_THRESHOLD_T3,
31
+ DEPTH_THRESHOLD_T4,
32
+ DEPTH_THRESHOLD_T5,
33
+ DEPTH_THRESHOLD_T6,
34
+ DEPTH_THRESHOLD_T7,
35
+ DEPTH_THRESHOLD_T8,
36
+ DEPTH_THRESHOLD_T9,
37
+ DEPTH_THRESHOLD_T10,
38
+ ]
39
+
21
40
  export function getDepthThreshold(tier: number): number {
22
- switch (tier) {
23
- case 1:
24
- return DEPTH_THRESHOLD_T1
25
- case 2:
26
- return DEPTH_THRESHOLD_T2
27
- case 3:
28
- return DEPTH_THRESHOLD_T3
29
- case 4:
30
- return DEPTH_THRESHOLD_T4
31
- default:
32
- return DEPTH_THRESHOLD_T5
33
- }
41
+ if (tier < 1 || tier > 10) return 65535
42
+ return DEPTH_THRESHOLD_TABLE[tier - 1]
34
43
  }
35
44
 
36
45
  export function getResourceTier(itemId: number): number {
@@ -46,9 +55,9 @@ export function getResourceWeight(itemId: number, stratum: number): number {
46
55
 
47
56
  switch (tier) {
48
57
  case 1:
49
- if (stratum < 2000) return 100
50
- if (stratum < 10000) return 80
51
- if (stratum < 30000) return 50
58
+ if (stratum < DEPTH_THRESHOLD_T2) return 100
59
+ if (stratum < DEPTH_THRESHOLD_T3) return 80
60
+ if (stratum < DEPTH_THRESHOLD_T4) return 50
52
61
  return 30
53
62
  case 2:
54
63
  if (depthAbove < 3000) return 40
@@ -67,37 +76,107 @@ export function getResourceWeight(itemId: number, stratum: number): number {
67
76
  }
68
77
  }
69
78
 
70
- const ASTEROID_RESOURCES = [101, 102, 103, 201, 202]
71
- const NEBULA_RESOURCES = [202, 203, 301, 302, 303]
72
- const GAS_GIANT_RESOURCES = [301, 302, 303, 401, 501]
73
- const ROCKY_RESOURCES = [101, 102, 103, 401, 402, 403, 503]
74
- const TERRESTRIAL_RESOURCES = [201, 202, 401, 402, 501, 502, 503]
75
- const ICY_RESOURCES = [101, 301, 302, 401, 403, 501, 502]
76
- const OCEAN_RESOURCES = [201, 203, 301, 303, 501, 502, 503]
77
- const INDUSTRIAL_RESOURCES = [101, 102, 103, 201, 203, 402, 403]
79
+ const RESOURCE_ORE = 0
80
+ const RESOURCE_GAS = 1
81
+ const RESOURCE_REGOLITH = 2
82
+ const RESOURCE_BIOMASS = 3
83
+ const RESOURCE_CRYSTAL = 4
78
84
 
79
- export function getLocationCandidates(locationType: number, subtype: number): number[] {
80
- if (locationType === 2) return ASTEROID_RESOURCES
81
- if (locationType === 3) return NEBULA_RESOURCES
82
- if (locationType === 1) {
85
+ interface LocationProfileEntry {
86
+ category: number
87
+ maxTier: number
88
+ }
89
+
90
+ function categoryBaseId(category: number): number {
91
+ switch (category) {
92
+ case RESOURCE_ORE:
93
+ return 100
94
+ case RESOURCE_CRYSTAL:
95
+ return 200
96
+ case RESOURCE_GAS:
97
+ return 300
98
+ case RESOURCE_REGOLITH:
99
+ return 400
100
+ case RESOURCE_BIOMASS:
101
+ return 500
102
+ default:
103
+ return 0
104
+ }
105
+ }
106
+
107
+ function resourceId(category: number, tier: number): number {
108
+ return categoryBaseId(category) + tier
109
+ }
110
+
111
+ export function getLocationProfile(locationType: number, subtype: number): LocationProfileEntry[] {
112
+ if (locationType === LocationType.ASTEROID) {
113
+ return [
114
+ {category: RESOURCE_ORE, maxTier: 5},
115
+ {category: RESOURCE_CRYSTAL, maxTier: 5},
116
+ ]
117
+ }
118
+ if (locationType === LocationType.NEBULA) {
119
+ return [
120
+ {category: RESOURCE_GAS, maxTier: 5},
121
+ {category: RESOURCE_REGOLITH, maxTier: 5},
122
+ ]
123
+ }
124
+ if (locationType === LocationType.ICE_FIELD) {
125
+ return [
126
+ {category: RESOURCE_GAS, maxTier: 5},
127
+ {category: RESOURCE_BIOMASS, maxTier: 5},
128
+ ]
129
+ }
130
+ if (locationType === LocationType.PLANET) {
83
131
  switch (subtype) {
84
132
  case PLANET_SUBTYPE_GAS_GIANT:
85
- return GAS_GIANT_RESOURCES
133
+ return [
134
+ {category: RESOURCE_GAS, maxTier: 10},
135
+ {category: RESOURCE_CRYSTAL, maxTier: 3},
136
+ ]
86
137
  case PLANET_SUBTYPE_ROCKY:
87
- return ROCKY_RESOURCES
138
+ return [
139
+ {category: RESOURCE_REGOLITH, maxTier: 10},
140
+ {category: RESOURCE_ORE, maxTier: 3},
141
+ ]
88
142
  case PLANET_SUBTYPE_TERRESTRIAL:
89
- return TERRESTRIAL_RESOURCES
143
+ return [
144
+ {category: RESOURCE_ORE, maxTier: 10},
145
+ {category: RESOURCE_BIOMASS, maxTier: 3},
146
+ ]
90
147
  case PLANET_SUBTYPE_ICY:
91
- return ICY_RESOURCES
148
+ return [
149
+ {category: RESOURCE_CRYSTAL, maxTier: 10},
150
+ {category: RESOURCE_REGOLITH, maxTier: 3},
151
+ ]
92
152
  case PLANET_SUBTYPE_OCEAN:
93
- return OCEAN_RESOURCES
153
+ return [
154
+ {category: RESOURCE_BIOMASS, maxTier: 10},
155
+ {category: RESOURCE_GAS, maxTier: 3},
156
+ ]
94
157
  case PLANET_SUBTYPE_INDUSTRIAL:
95
- return INDUSTRIAL_RESOURCES
158
+ return [
159
+ {category: RESOURCE_ORE, maxTier: 3},
160
+ {category: RESOURCE_CRYSTAL, maxTier: 3},
161
+ {category: RESOURCE_REGOLITH, maxTier: 3},
162
+ {category: RESOURCE_BIOMASS, maxTier: 3},
163
+ ]
96
164
  }
97
165
  }
98
166
  return []
99
167
  }
100
168
 
169
+ export function getLocationCandidates(locationType: number, subtype: number): number[] {
170
+ const profile = getLocationProfile(locationType, subtype)
171
+ const ids: number[] = []
172
+ for (const {category, maxTier} of profile) {
173
+ for (let tier = 1; tier <= maxTier; tier++) {
174
+ ids.push(resourceId(category, tier))
175
+ }
176
+ }
177
+ return ids
178
+ }
179
+
101
180
  export function getEligibleResources(
102
181
  locationType: number,
103
182
  subtype: number,
@@ -25,8 +25,7 @@ const ORE_STATS: StatDefinition[] = [
25
25
  key: 'density',
26
26
  label: 'Density',
27
27
  abbreviation: 'DEN',
28
- purpose: 'Mass per unit',
29
- inverted: true,
28
+ purpose: 'Structural integrity — higher rolls produce lighter hulls',
30
29
  },
31
30
  ]
32
31
 
@@ -1,6 +1,7 @@
1
1
  import {UInt64, type UInt64Type} from '@wharfkit/antelope'
2
2
  import {ServerContract} from '../contracts'
3
3
  import type {CoordinatesType} from '../types'
4
+ import {type FloatPosition, getInterpolatedPosition} from '../travel/travel'
4
5
  import {Location} from './location'
5
6
  import {ScheduleAccessor} from '../scheduling/accessor'
6
7
  import * as schedule from '../scheduling/schedule'
@@ -24,6 +25,14 @@ export class Container extends ServerContract.Types.entity_info {
24
25
  return this.entity_name
25
26
  }
26
27
 
28
+ get entityClass(): 'mobile' {
29
+ return 'mobile'
30
+ }
31
+
32
+ get canUndeploy(): boolean {
33
+ return true
34
+ }
35
+
27
36
  get sched(): ScheduleAccessor {
28
37
  this._sched ??= new ScheduleAccessor(this)
29
38
  return this._sched
@@ -33,6 +42,12 @@ export class Container extends ServerContract.Types.entity_info {
33
42
  return this.is_idle
34
43
  }
35
44
 
45
+ interpolatedPositionAt(now: Date): FloatPosition {
46
+ const taskIndex = this.sched.currentTaskIndex(now)
47
+ const progress = this.sched.currentTaskProgressFloat(now)
48
+ return getInterpolatedPosition(this, taskIndex, progress)
49
+ }
50
+
36
51
  isLoading(now: Date): boolean {
37
52
  return schedule.isLoading(this, now)
38
53
  }
@@ -75,12 +90,12 @@ export function computeContainerCapabilities(stats: Record<string, number>): {
75
90
  hullmass: number
76
91
  capacity: number
77
92
  } {
78
- const density = stats['density'] ?? 500
79
- const strength = stats['strength'] ?? 500
80
- const hardness = stats['hardness'] ?? 500
81
- const saturation = stats['saturation'] ?? 500
93
+ const density = stats.density
94
+ const strength = stats.strength
95
+ const hardness = stats.hardness
96
+ const saturation = stats.saturation
82
97
 
83
- const hullmass = 25000 + 75 * density
98
+ const hullmass = 100000 - 75 * density
84
99
 
85
100
  const statSum = strength + hardness + saturation
86
101
  const exponent = statSum / 2997
@@ -93,12 +108,12 @@ export function computeContainerT2Capabilities(stats: Record<string, number>): {
93
108
  hullmass: number
94
109
  capacity: number
95
110
  } {
96
- const strength = stats['strength'] ?? 0
97
- const density = stats['density'] ?? 0
98
- const hardness = stats['hardness'] ?? 0
99
- const saturation = stats['saturation'] ?? 0
111
+ const strength = stats.strength
112
+ const density = stats.density
113
+ const hardness = stats.hardness
114
+ const saturation = stats.saturation
100
115
 
101
- const hullmass = 20000 + 50 * density
116
+ const hullmass = 70000 - 50 * density
102
117
 
103
118
  const statSum = strength + hardness + saturation
104
119
  const exponent = statSum / 2500
@@ -0,0 +1,144 @@
1
+ import {UInt64, type UInt64Type} from '@wharfkit/antelope'
2
+ import {ServerContract} from '../contracts'
3
+ import type {CoordinatesType} from '../types'
4
+ import {Location} from './location'
5
+ import {ScheduleAccessor} from '../scheduling/accessor'
6
+ import {InventoryAccessor} from './inventory-accessor'
7
+ import type {EntityInventory} from './entity-inventory'
8
+ import type {PackedModuleInput} from './ship'
9
+ import {decodeCraftedItemStats} from '../derivation/crafting'
10
+ import {getModuleCapabilityType, MODULE_GATHERER, MODULE_GENERATOR} from '../capabilities/modules'
11
+ import {computeGathererCapabilities, computeGeneratorCapabilities} from './ship-deploy'
12
+ import {applySlotMultiplier, clampUint16, getSlotAmp, type InstalledModule} from './slot-multiplier'
13
+ import type {EntitySlot} from '../data/recipes-runtime'
14
+ import {getItem} from '../data/catalog'
15
+
16
+ export interface ExtractorStateInput {
17
+ id: UInt64Type
18
+ owner: string
19
+ name: string
20
+ coordinates: CoordinatesType | {x: number; y: number; z?: number}
21
+ hullmass?: number
22
+ capacity?: number
23
+ energy?: number
24
+ modules?: PackedModuleInput[]
25
+ schedule?: ServerContract.Types.schedule
26
+ cargo?: ServerContract.Types.cargo_item[]
27
+ }
28
+
29
+ export class Extractor extends ServerContract.Types.entity_info {
30
+ private _sched?: ScheduleAccessor
31
+ private _inv?: InventoryAccessor
32
+
33
+ get name(): string {
34
+ return this.entity_name
35
+ }
36
+
37
+ get entityClass(): 'building' {
38
+ return 'building'
39
+ }
40
+
41
+ get canDemolish(): boolean {
42
+ return true
43
+ }
44
+
45
+ get inv(): InventoryAccessor {
46
+ this._inv ??= new InventoryAccessor(this)
47
+ return this._inv
48
+ }
49
+
50
+ get inventory(): EntityInventory[] {
51
+ return this.inv.items
52
+ }
53
+
54
+ get sched(): ScheduleAccessor {
55
+ this._sched ??= new ScheduleAccessor(this)
56
+ return this._sched
57
+ }
58
+
59
+ get isIdle(): boolean {
60
+ return this.is_idle
61
+ }
62
+
63
+ get location(): Location {
64
+ return Location.from(this.coordinates)
65
+ }
66
+
67
+ get totalCargoMass(): UInt64 {
68
+ return this.inv.totalMass
69
+ }
70
+
71
+ get maxCapacity(): UInt64 {
72
+ return UInt64.from(this.capacity)
73
+ }
74
+
75
+ get availableCapacity(): UInt64 {
76
+ const cargo = this.totalCargoMass
77
+ return cargo.gte(this.maxCapacity) ? UInt64.from(0) : this.maxCapacity.subtracting(cargo)
78
+ }
79
+
80
+ get isFull(): boolean {
81
+ return this.totalCargoMass.gte(this.maxCapacity)
82
+ }
83
+
84
+ get totalMass(): UInt64 {
85
+ const hull = this.hullmass ? UInt64.from(this.hullmass) : UInt64.from(0)
86
+ return hull.adding(this.totalCargoMass)
87
+ }
88
+ }
89
+
90
+ export interface ExtractorCapabilities {
91
+ generator?: {capacity: number; recharge: number}
92
+ gatherer?: {yield: number; drain: number; depth: number; speed: number}
93
+ }
94
+
95
+ export function computeExtractorCapabilities(
96
+ modules: InstalledModule[],
97
+ layout: EntitySlot[]
98
+ ): ExtractorCapabilities {
99
+ const out: ExtractorCapabilities = {}
100
+
101
+ const genModules = modules.filter((m) => getModuleCapabilityType(m.itemId) === MODULE_GENERATOR)
102
+ if (genModules.length > 0) {
103
+ let totalCapacity = 0
104
+ let totalRecharge = 0
105
+ for (const m of genModules) {
106
+ const caps = computeGeneratorCapabilities(decodeCraftedItemStats(m.itemId, m.stats))
107
+ const amp = getSlotAmp(layout, m.slotIndex)
108
+ totalCapacity += applySlotMultiplier(caps.capacity, amp)
109
+ totalRecharge += applySlotMultiplier(caps.recharge, amp)
110
+ }
111
+ out.generator = {
112
+ capacity: clampUint16(totalCapacity),
113
+ recharge: clampUint16(totalRecharge),
114
+ }
115
+ }
116
+
117
+ const gathModules = modules.filter((m) => getModuleCapabilityType(m.itemId) === MODULE_GATHERER)
118
+ if (gathModules.length > 0) {
119
+ let totalYield = 0
120
+ let totalDrain = 0
121
+ let maxDepth = 0
122
+ let totalSpeed = 0
123
+ for (const m of gathModules) {
124
+ const tier = getItem(m.itemId).tier
125
+ const caps = computeGathererCapabilities(
126
+ decodeCraftedItemStats(m.itemId, m.stats),
127
+ tier
128
+ )
129
+ const amp = getSlotAmp(layout, m.slotIndex)
130
+ totalYield += applySlotMultiplier(caps.yield, amp)
131
+ totalDrain += caps.drain
132
+ if (caps.depth > maxDepth) maxDepth = caps.depth
133
+ totalSpeed += applySlotMultiplier(caps.speed, amp)
134
+ }
135
+ out.gatherer = {
136
+ yield: clampUint16(totalYield),
137
+ drain: totalDrain,
138
+ depth: maxDepth,
139
+ speed: clampUint16(totalSpeed),
140
+ }
141
+ }
142
+
143
+ return out
144
+ }
@@ -58,13 +58,6 @@ export class GameState extends ServerContract.Types.state_row {
58
58
  return this.enabled
59
59
  }
60
60
 
61
- /**
62
- * Get the total number of ships in the game
63
- */
64
- get shipCount(): number {
65
- return Number(this.ships)
66
- }
67
-
68
61
  /**
69
62
  * Get the current salt value (used for random number generation)
70
63
  */
@@ -137,14 +130,12 @@ export class GameState extends ServerContract.Types.state_row {
137
130
  get summary(): {
138
131
  enabled: boolean
139
132
  epoch: string
140
- ships: number
141
133
  hasSeed: boolean
142
134
  hasCommit: boolean
143
135
  } {
144
136
  return {
145
137
  enabled: this.enabled,
146
138
  epoch: this.epoch.toString(),
147
- ships: this.shipCount,
148
139
  hasSeed: !this.seed.equals(Checksum256.from('0'.repeat(64))),
149
140
  hasCommit: !this.commit.equals(Checksum256.from('0'.repeat(64))),
150
141
  }
@@ -3,8 +3,13 @@ import {ServerContract} from '../contracts'
3
3
  import {type PackedModuleInput, Ship, type ShipStateInput} from './ship'
4
4
  import {computeWarehouseCapabilities, Warehouse, type WarehouseStateInput} from './warehouse'
5
5
  import {Container, type ContainerStateInput} from './container'
6
- import {ITEM_SHIP_T1_PACKED, ITEM_WAREHOUSE_T1_PACKED} from '../data/item-ids'
7
- import {getEntityLayout} from '../data/recipes-runtime'
6
+ import {Extractor, computeExtractorCapabilities, type ExtractorStateInput} from './extractor'
7
+ import {
8
+ ITEM_EXTRACTOR_T1_PACKED,
9
+ ITEM_SHIP_T1_PACKED,
10
+ ITEM_WAREHOUSE_T1_PACKED,
11
+ } from '../data/item-ids'
12
+ import {getEntityLayout, type EntitySlot} from '../data/recipes-runtime'
8
13
  import {itemMetadata} from '../data/metadata'
9
14
  import {getItem} from '../data/catalog'
10
15
  import {
@@ -14,6 +19,7 @@ import {
14
19
  moduleSlotTypeToCode,
15
20
  } from '../capabilities/modules'
16
21
  import {computeShipCapabilities, computeStorageCapabilities} from './ship-deploy'
22
+ import type {InstalledModule} from './slot-multiplier'
17
23
  import {decodeCraftedItemStats} from '../derivation/crafting'
18
24
 
19
25
  function assignModulesToSlots(
@@ -56,19 +62,22 @@ function assignModulesToSlots(
56
62
  )
57
63
  }
58
64
 
59
- function decodePackedInput(m: PackedModuleInput): {itemId: number; stats: bigint} {
60
- return {
61
- itemId: Number(UInt16.from(m.itemId).value.toString()),
62
- stats: BigInt(UInt64.from(m.stats).toString()),
63
- }
65
+ function toInstalledModules(entries: ServerContract.Types.module_entry[]): InstalledModule[] {
66
+ const installed: InstalledModule[] = []
67
+ entries.forEach((entry, slotIndex) => {
68
+ if (!entry.installed) return
69
+ installed.push({
70
+ slotIndex,
71
+ itemId: Number(UInt16.from(entry.installed.item_id).value.toString()),
72
+ stats: BigInt(UInt64.from(entry.installed.stats).toString()),
73
+ })
74
+ })
75
+ return installed
64
76
  }
65
77
 
66
- function computeStorageBonus(
67
- decoded: {itemId: number; stats: bigint}[],
68
- baseCapacity: number
69
- ): number {
78
+ function computeStorageBonus(modules: InstalledModule[], baseCapacity: number): number {
70
79
  let totalBonus = 0
71
- for (const m of decoded) {
80
+ for (const m of modules) {
72
81
  if (getModuleCapabilityType(m.itemId) !== MODULE_STORAGE) continue
73
82
  const stats = decodeCraftedItemStats(m.itemId, m.stats)
74
83
  const {capacityBonus} = computeStorageCapabilities(stats, baseCapacity)
@@ -78,15 +87,16 @@ function computeStorageBonus(
78
87
  }
79
88
 
80
89
  function deriveShipFromModules(
81
- modules: PackedModuleInput[],
90
+ moduleEntries: ServerContract.Types.module_entry[],
91
+ layout: EntitySlot[],
82
92
  baseCapacity: number
83
93
  ): {
84
94
  capabilities: ReturnType<typeof computeShipCapabilities>
85
95
  finalCapacity: number
86
96
  } {
87
- const decoded = modules.map(decodePackedInput)
88
- const capabilities = computeShipCapabilities(decoded)
89
- const totalBonus = computeStorageBonus(decoded, baseCapacity)
97
+ const installed = toInstalledModules(moduleEntries)
98
+ const capabilities = computeShipCapabilities(installed, layout)
99
+ const totalBonus = computeStorageBonus(installed, baseCapacity)
90
100
  return {capabilities, finalCapacity: baseCapacity + totalBonus}
91
101
  }
92
102
 
@@ -109,10 +119,12 @@ export function makeShip(state: ShipStateInput): Ship {
109
119
  if (state.schedule) info.schedule = state.schedule
110
120
 
111
121
  let moduleEntries: ServerContract.Types.module_entry[] = []
122
+ const shipLayout = getEntityLayout(ITEM_SHIP_T1_PACKED)?.slots ?? []
112
123
  if (state.modules && state.modules.length > 0) {
113
124
  moduleEntries = assignModulesToSlots(ITEM_SHIP_T1_PACKED, state.modules, 'Ship T1')
114
125
  const {capabilities, finalCapacity} = deriveShipFromModules(
115
- state.modules,
126
+ moduleEntries,
127
+ shipLayout,
116
128
  state.capacity ?? 0
117
129
  )
118
130
  if (capabilities.engines) info.engines = capabilities.engines
@@ -152,17 +164,18 @@ export function makeWarehouse(state: WarehouseStateInput): Warehouse {
152
164
  if (state.schedule) info.schedule = state.schedule
153
165
 
154
166
  let moduleEntries: ServerContract.Types.module_entry[] = []
167
+ const warehouseLayout = getEntityLayout(ITEM_WAREHOUSE_T1_PACKED)?.slots ?? []
155
168
  if (state.modules && state.modules.length > 0) {
156
169
  moduleEntries = assignModulesToSlots(
157
170
  ITEM_WAREHOUSE_T1_PACKED,
158
171
  state.modules,
159
172
  'Warehouse T1'
160
173
  )
161
- const decoded = state.modules.map(decodePackedInput)
162
- const capabilities = computeWarehouseCapabilities(decoded)
174
+ const installed = toInstalledModules(moduleEntries)
175
+ const capabilities = computeWarehouseCapabilities(installed, warehouseLayout)
163
176
  if (capabilities.loaders) info.loaders = capabilities.loaders
164
177
 
165
- const totalBonus = computeStorageBonus(decoded, state.capacity)
178
+ const totalBonus = computeStorageBonus(installed, state.capacity)
166
179
  info.capacity = UInt32.from(state.capacity + totalBonus)
167
180
  } else {
168
181
  moduleEntries = assignModulesToSlots(ITEM_WAREHOUSE_T1_PACKED, [], 'Warehouse T1')
@@ -174,6 +187,44 @@ export function makeWarehouse(state: WarehouseStateInput): Warehouse {
174
187
  return new Warehouse(entityInfo)
175
188
  }
176
189
 
190
+ export function makeExtractor(state: ExtractorStateInput): Extractor {
191
+ const info: Record<string, unknown> = {
192
+ type: Name.from('extractor'),
193
+ id: UInt64.from(state.id),
194
+ owner: Name.from(state.owner),
195
+ entity_name: state.name,
196
+ coordinates: ServerContract.Types.coordinates.from(state.coordinates),
197
+ cargomass: UInt32.from(0),
198
+ cargo: state.cargo || [],
199
+ is_idle: !state.schedule,
200
+ current_task_elapsed: UInt32.from(0),
201
+ current_task_remaining: UInt32.from(0),
202
+ pending_tasks: [],
203
+ }
204
+ if (state.hullmass !== undefined) info.hullmass = UInt32.from(state.hullmass)
205
+ if (state.energy !== undefined) info.energy = UInt16.from(state.energy)
206
+ if (state.schedule) info.schedule = state.schedule
207
+ if (state.capacity !== undefined) info.capacity = UInt32.from(state.capacity)
208
+
209
+ const moduleEntries = assignModulesToSlots(
210
+ ITEM_EXTRACTOR_T1_PACKED,
211
+ state.modules ?? [],
212
+ 'Extractor T1'
213
+ )
214
+ if (state.modules && state.modules.length > 0) {
215
+ const layout = getEntityLayout(ITEM_EXTRACTOR_T1_PACKED)?.slots ?? []
216
+ const installed = toInstalledModules(moduleEntries)
217
+ const capabilities = computeExtractorCapabilities(installed, layout)
218
+ if (capabilities.generator) info.generator = capabilities.generator
219
+ if (capabilities.gatherer) info.gatherer = capabilities.gatherer
220
+ }
221
+
222
+ info.modules = moduleEntries
223
+
224
+ const entityInfo = ServerContract.Types.entity_info.from(info)
225
+ return new Extractor(entityInfo)
226
+ }
227
+
177
228
  export function makeContainer(state: ContainerStateInput): Container {
178
229
  const entityInfo = ServerContract.Types.entity_info.from({
179
230
  type: Name.from('container'),