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

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 (156) hide show
  1. package/lib/scan.d.ts +52 -0
  2. package/lib/scan.js +162 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +152 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +3601 -1364
  7. package/lib/shipload.js +15671 -5261
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +15392 -5209
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1173 -0
  12. package/lib/testing.js +4971 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +4965 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +35 -0
  20. package/src/capabilities/crafting.ts +34 -7
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +37 -19
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +27 -30
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.ts +16 -1
  28. package/src/contracts/platform.ts +231 -3
  29. package/src/contracts/server.ts +1518 -501
  30. package/src/coordinates/address.ts +88 -0
  31. package/src/coordinates/constants.test.ts +15 -0
  32. package/src/coordinates/constants.ts +23 -0
  33. package/src/coordinates/index.ts +15 -0
  34. package/src/coordinates/memo.test.ts +47 -0
  35. package/src/coordinates/memo.ts +20 -0
  36. package/src/coordinates/permutation.ts +77 -0
  37. package/src/coordinates/regions.ts +48 -0
  38. package/src/coordinates/sectors.ts +115 -0
  39. package/src/data/capabilities.ts +49 -18
  40. package/src/data/capability-formulas.ts +26 -14
  41. package/src/data/catalog.ts +7 -8
  42. package/src/data/colors.ts +14 -48
  43. package/src/data/entities.json +380 -15
  44. package/src/data/item-ids.ts +51 -13
  45. package/src/data/items.json +318 -77
  46. package/src/data/kind-registry.json +195 -0
  47. package/src/data/kind-registry.ts +182 -0
  48. package/src/data/metadata.ts +291 -51
  49. package/src/data/recipes-runtime.ts +6 -23
  50. package/src/data/recipes.json +2260 -173
  51. package/src/data/tiers.ts +9 -6
  52. package/src/derivation/build-methods.test.ts +13 -0
  53. package/src/derivation/build-methods.ts +48 -0
  54. package/src/derivation/capabilities.test.ts +348 -0
  55. package/src/derivation/capabilities.ts +693 -0
  56. package/src/derivation/capability-mappings.ts +60 -16
  57. package/src/derivation/crafting.test.ts +17 -0
  58. package/src/derivation/crafting.ts +57 -34
  59. package/src/derivation/index.ts +27 -2
  60. package/src/derivation/recipe-usage.test.ts +90 -0
  61. package/src/derivation/recipe-usage.ts +141 -0
  62. package/src/derivation/reserve-regen.ts +34 -0
  63. package/src/derivation/resources.ts +9 -1
  64. package/src/derivation/rollups.test.ts +55 -0
  65. package/src/derivation/rollups.ts +72 -0
  66. package/src/derivation/stars.test.ts +67 -0
  67. package/src/derivation/stars.ts +25 -0
  68. package/src/derivation/stat-scaling.ts +12 -0
  69. package/src/derivation/stats.ts +6 -6
  70. package/src/derivation/stratum.ts +26 -22
  71. package/src/derivation/tiers.ts +40 -7
  72. package/src/derivation/upgrades.ts +14 -0
  73. package/src/derivation/wormhole.ts +154 -0
  74. package/src/entities/entity.ts +102 -0
  75. package/src/entities/gamestate.ts +3 -28
  76. package/src/entities/makers.ts +141 -135
  77. package/src/entities/slot-multiplier.ts +43 -0
  78. package/src/errors.ts +12 -16
  79. package/src/format.ts +26 -4
  80. package/src/index-module.ts +333 -35
  81. package/src/managers/actions.ts +668 -103
  82. package/src/managers/base.ts +6 -2
  83. package/src/managers/cluster.test.ts +39 -0
  84. package/src/managers/cluster.ts +53 -0
  85. package/src/managers/construction-types.ts +80 -0
  86. package/src/managers/construction.ts +443 -0
  87. package/src/managers/context.ts +29 -1
  88. package/src/managers/coordinates.ts +14 -0
  89. package/src/managers/entities.ts +27 -66
  90. package/src/managers/epochs.ts +40 -0
  91. package/src/managers/index.ts +20 -1
  92. package/src/managers/locations.ts +25 -29
  93. package/src/managers/nft.test.ts +14 -0
  94. package/src/managers/nft.ts +70 -0
  95. package/src/managers/players.ts +25 -0
  96. package/src/managers/plot.ts +122 -0
  97. package/src/nft/atomicassets.abi.json +1342 -0
  98. package/src/nft/atomicassets.ts +237 -0
  99. package/src/nft/atomicdata.ts +130 -0
  100. package/src/nft/buildImmutableData.ts +388 -0
  101. package/src/nft/description.ts +215 -99
  102. package/src/nft/index.ts +3 -0
  103. package/src/planner/index.ts +242 -0
  104. package/src/planner/planner.test.ts +334 -0
  105. package/src/resolution/describe-module.ts +43 -25
  106. package/src/resolution/display-name.ts +38 -10
  107. package/src/resolution/resolve-item.test.ts +48 -0
  108. package/src/resolution/resolve-item.ts +112 -58
  109. package/src/scan/index.ts +244 -0
  110. package/src/scan/scan-wasm.base64.ts +2 -0
  111. package/src/scheduling/accessor.ts +65 -23
  112. package/src/scheduling/availability.test.ts +204 -0
  113. package/src/scheduling/availability.ts +211 -0
  114. package/src/scheduling/cancel.test.ts +542 -0
  115. package/src/scheduling/cancel.ts +254 -0
  116. package/src/scheduling/cluster-stock.test.ts +31 -0
  117. package/src/scheduling/cluster-stock.ts +15 -0
  118. package/src/scheduling/energy.ts +47 -0
  119. package/src/scheduling/idle-resolve.ts +45 -0
  120. package/src/scheduling/jobs.ts +71 -0
  121. package/src/scheduling/lane-core.ts +128 -0
  122. package/src/scheduling/lanes.test.ts +250 -0
  123. package/src/scheduling/lanes.ts +227 -0
  124. package/src/scheduling/projection.ts +257 -133
  125. package/src/scheduling/schedule.test.ts +119 -0
  126. package/src/scheduling/schedule.ts +257 -117
  127. package/src/scheduling/task-cargo.test.ts +44 -0
  128. package/src/scheduling/task-cargo.ts +46 -0
  129. package/src/scheduling/unwrap.test.ts +86 -0
  130. package/src/scheduling/unwrap.ts +190 -0
  131. package/src/shipload.ts +26 -1
  132. package/src/subscriptions/manager.cluster.test.ts +51 -0
  133. package/src/subscriptions/manager.ts +245 -172
  134. package/src/subscriptions/mappers.ts +5 -8
  135. package/src/subscriptions/types.ts +28 -3
  136. package/src/testing/catalog-hash.ts +19 -0
  137. package/src/testing/index.ts +2 -0
  138. package/src/testing/projection-parity.ts +167 -0
  139. package/src/travel/reach.ts +21 -0
  140. package/src/travel/route-planner.ts +288 -0
  141. package/src/travel/route-simulator.ts +170 -0
  142. package/src/travel/travel.ts +188 -122
  143. package/src/types/capabilities.ts +37 -8
  144. package/src/types/entity.ts +3 -3
  145. package/src/types/index.ts +0 -1
  146. package/src/types.ts +51 -16
  147. package/src/utils/cargo.test.ts +14 -0
  148. package/src/utils/cargo.ts +29 -0
  149. package/src/utils/display-name.ts +70 -0
  150. package/src/utils/system.ts +15 -16
  151. package/src/capabilities/loading.ts +0 -8
  152. package/src/entities/container.ts +0 -123
  153. package/src/entities/ship-deploy.ts +0 -295
  154. package/src/entities/ship.ts +0 -221
  155. package/src/entities/warehouse.ts +0 -127
  156. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,250 @@
1
+ import {UInt8, UInt16, UInt64} from '@wharfkit/antelope'
2
+ import {expect, test} from 'bun:test'
3
+ import {encodeStats} from '../derivation/crafting'
4
+ import {computeGathererDepth, computeGathererDrain} from '../nft/description'
5
+ import {computeGathererYield} from '../derivation/capabilities'
6
+ import {computeLoaderThrust, computeLoaderMass} from '../nft/description'
7
+ import {computeCrafterSpeed, computeCrafterDrain} from '../nft/description'
8
+ import {applySlotMultiplier} from '../entities/slot-multiplier'
9
+ import {getSlotAmp} from '../entities/slot-multiplier'
10
+ import {getEntityLayout} from '../data/recipes-runtime'
11
+ import {ITEM_GATHERER_T1, ITEM_LOADER_T1, ITEM_CRAFTER_T1} from '../data/item-ids'
12
+ import {
13
+ workerLaneKey,
14
+ resolveLaneGatherer,
15
+ resolveLaneCrafter,
16
+ resolveLaneLoader,
17
+ selectGatherLane,
18
+ } from './lanes'
19
+ import {LANE_MOBILITY} from './schedule'
20
+ import type {ServerContract} from '../contracts'
21
+
22
+ type ModuleEntry = ServerContract.Types.module_entry
23
+ type Lane = ServerContract.Types.lane
24
+
25
+ // Real gatherer-bearing entity: extractor 10203 has [generator@0, gatherer@1].
26
+ const EXTRACTOR_GATHERER = 10203
27
+ const GATHERER_SLOT_IDX = 1
28
+ const GATHERER_LANE_KEY = GATHERER_SLOT_IDX + 1 // = 2
29
+
30
+ function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
31
+ return {
32
+ type: UInt8.from(0),
33
+ installed: {
34
+ item_id: UInt16.from(itemId),
35
+ stats: UInt64.from(stats),
36
+ },
37
+ } as unknown as ModuleEntry
38
+ }
39
+
40
+ function makeBusyLane(laneKey: number): Lane {
41
+ return {
42
+ lane_key: UInt8.from(laneKey),
43
+ schedule: {
44
+ started: {toDate: () => new Date()} as any,
45
+ tasks: [{duration: UInt64.from(100)} as any],
46
+ },
47
+ } as unknown as Lane
48
+ }
49
+
50
+ // gatherer: stats = [str=300, tol=200, con=400], tier=1
51
+ const GATH_STR = 300
52
+ const GATH_TOL = 200
53
+ const GATH_CON = 400
54
+ const gathererStats1 = encodeStats([GATH_STR, GATH_TOL, GATH_CON])
55
+
56
+ // loader: stats = [ins=300, pla=500]
57
+ const LOADER_INS = 300
58
+ const LOADER_PLA = 500
59
+ const loaderStats = encodeStats([LOADER_INS, LOADER_PLA])
60
+
61
+ // crafter: stats = [rea=400, fin=300]
62
+ const CRAFTER_REA = 400
63
+ const CRAFTER_FIN = 300
64
+ const crafterStats = encodeStats([CRAFTER_REA, CRAFTER_FIN])
65
+
66
+ // --- resolveLaneGatherer ---
67
+
68
+ test('resolveLaneGatherer reads the layout slot amp and applies it to yield (parity formula)', () => {
69
+ // Gatherer at the entity's real gatherer slot (index 1 => laneKey 2); amp comes from getEntityLayout(10203).
70
+ const modules: ModuleEntry[] = [
71
+ makeModuleEntry(ITEM_GATHERER_T1, gathererStats1), // slot 0 (generator slot, ignored)
72
+ makeModuleEntry(ITEM_GATHERER_T1, gathererStats1), // slot 1 (gatherer slot)
73
+ ]
74
+ const result = resolveLaneGatherer(modules, EXTRACTOR_GATHERER, GATHERER_LANE_KEY)
75
+
76
+ const layout = getEntityLayout(EXTRACTOR_GATHERER)?.slots ?? []
77
+ const ampFromLayout = getSlotAmp(layout, GATHERER_SLOT_IDX)
78
+
79
+ // The resolver routes through the real layout's amp, not a hardcoded 100.
80
+ expect(result.outputPct).toBe(ampFromLayout)
81
+ // Yield equals the contract formula clamp_to_uint16(compute_gatherer_yield(str, tier) * amp / 100).
82
+ expect(result.yield).toBe(applySlotMultiplier(computeGathererYield(GATH_STR, 1), ampFromLayout))
83
+ expect(result.drain).toBe(computeGathererDrain(GATH_CON))
84
+ expect(result.depth).toBe(computeGathererDepth(GATH_TOL, 1))
85
+ expect(result.slotIndex).toBe(GATHERER_SLOT_IDX)
86
+ })
87
+
88
+ test('resolveLaneGatherer amp-scaling parity holds for a non-100 amp', () => {
89
+ // Parity for a non-100 amp: clamp_to_uint16(value * amp / 100).
90
+ expect(applySlotMultiplier(computeGathererYield(GATH_STR, 1), 80)).toBe(
91
+ Math.min(Math.floor((computeGathererYield(GATH_STR, 1) * 80) / 100), 65535)
92
+ )
93
+ expect(applySlotMultiplier(computeGathererYield(GATH_STR, 1), 120)).toBe(
94
+ Math.floor((computeGathererYield(GATH_STR, 1) * 120) / 100)
95
+ )
96
+ })
97
+
98
+ test('resolveLaneGatherer throws on out-of-range laneKey', () => {
99
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_GATHERER_T1, gathererStats1)]
100
+ expect(() => resolveLaneGatherer(modules, ITEM_GATHERER_T1, 5)).toThrow(
101
+ 'gatherer lane has no module'
102
+ )
103
+ })
104
+
105
+ test('resolveLaneGatherer throws on laneKey=0 (slot=255 off-by-one boundary)', () => {
106
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_GATHERER_T1, gathererStats1)]
107
+ expect(() => resolveLaneGatherer(modules, ITEM_GATHERER_T1, 0)).toThrow(
108
+ 'gatherer lane has no module'
109
+ )
110
+ })
111
+
112
+ // --- resolveLaneCrafter ---
113
+
114
+ test('resolveLaneCrafter returns correct stats for slot 0 (laneKey=1)', () => {
115
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_CRAFTER_T1, crafterStats)]
116
+ const result = resolveLaneCrafter(modules, ITEM_CRAFTER_T1, 1)
117
+ const layout = getEntityLayout(ITEM_CRAFTER_T1)?.slots ?? []
118
+ const amp = getSlotAmp(layout, 0)
119
+ expect(result.slotIndex).toBe(0)
120
+ expect(result.speed).toBe(applySlotMultiplier(computeCrafterSpeed(CRAFTER_REA, 1), amp))
121
+ expect(result.drain).toBe(computeCrafterDrain(CRAFTER_FIN))
122
+ expect(result.outputPct).toBe(amp)
123
+ })
124
+
125
+ test('resolveLaneCrafter throws on out-of-range laneKey', () => {
126
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_CRAFTER_T1, crafterStats)]
127
+ expect(() => resolveLaneCrafter(modules, ITEM_CRAFTER_T1, 5)).toThrow(
128
+ 'crafter lane has no module'
129
+ )
130
+ })
131
+
132
+ test('resolveLaneCrafter throws on laneKey=0 (slot=255 off-by-one boundary)', () => {
133
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_CRAFTER_T1, crafterStats)]
134
+ expect(() => resolveLaneCrafter(modules, ITEM_CRAFTER_T1, 0)).toThrow(
135
+ 'crafter lane has no module'
136
+ )
137
+ })
138
+
139
+ // --- resolveLaneLoader ---
140
+
141
+ test('resolveLaneLoader returns correct stats for slot 0 (laneKey=1)', () => {
142
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_LOADER_T1, loaderStats)]
143
+ const result = resolveLaneLoader(modules, ITEM_LOADER_T1, 1)
144
+ const layout = getEntityLayout(ITEM_LOADER_T1)?.slots ?? []
145
+ const amp = getSlotAmp(layout, 0)
146
+ expect(result.valid).toBe(true)
147
+ expect(result.slotIndex).toBe(0)
148
+ expect(result.thrust).toBe(applySlotMultiplier(computeLoaderThrust(LOADER_PLA, 1), amp))
149
+ expect(result.mass).toBe(computeLoaderMass(LOADER_INS))
150
+ expect(result.outputPct).toBe(amp)
151
+ })
152
+
153
+ test('resolveLaneLoader soft-returns invalid for LANE_MOBILITY (key 0), never throws', () => {
154
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_LOADER_T1, loaderStats)]
155
+ const result = resolveLaneLoader(modules, ITEM_LOADER_T1, LANE_MOBILITY)
156
+ expect(result.valid).toBe(false)
157
+ expect(result.thrust).toBe(0)
158
+ expect(result.mass).toBe(0)
159
+ })
160
+
161
+ test('resolveLaneLoader soft-returns invalid for a missing/out-of-range module, never throws', () => {
162
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_LOADER_T1, loaderStats)]
163
+ const result = resolveLaneLoader(modules, ITEM_LOADER_T1, 5)
164
+ expect(result.valid).toBe(false)
165
+ expect(result.thrust).toBe(0)
166
+ expect(result.mass).toBe(0)
167
+ })
168
+
169
+ // --- depth-aware workerLaneKey ---
170
+
171
+ test('workerLaneKey with stratum picks first-free reaching gatherer', () => {
172
+ const shallowStats = encodeStats([100, 200, 100]) // tol=200 => depth=1500 for tier1
173
+ const deepStats = encodeStats([100, 900, 100]) // tol=900 => depth=5000 for tier1
174
+ const modules: ModuleEntry[] = [
175
+ makeModuleEntry(ITEM_GATHERER_T1, shallowStats),
176
+ makeModuleEntry(ITEM_GATHERER_T1, deepStats),
177
+ ]
178
+ const laneKey = workerLaneKey(modules, 'gatherer', [], 2000)
179
+ expect(laneKey).toBe(2) // slot 1 => laneKey 2
180
+ })
181
+
182
+ test('workerLaneKey with stratum: first free reaching is preferred over busy reaching', () => {
183
+ const deepStats = encodeStats([100, 900, 100]) // tol=900 => depth=5000 for tier1
184
+ const modules: ModuleEntry[] = [
185
+ makeModuleEntry(ITEM_GATHERER_T1, deepStats),
186
+ makeModuleEntry(ITEM_GATHERER_T1, deepStats),
187
+ ]
188
+ const lanes: Lane[] = [makeBusyLane(1)] // slot 0 is busy
189
+ const laneKey = workerLaneKey(modules, 'gatherer', lanes, 2000)
190
+ expect(laneKey).toBe(2) // slot 1 (laneKey=2) is free and reaching
191
+ })
192
+
193
+ test('workerLaneKey with stratum: returns lowest reaching (busy) when all reaching are busy', () => {
194
+ const deepStats = encodeStats([100, 900, 100]) // tol=900 => depth=5000 for tier1
195
+ const modules: ModuleEntry[] = [
196
+ makeModuleEntry(ITEM_GATHERER_T1, deepStats),
197
+ makeModuleEntry(ITEM_GATHERER_T1, deepStats),
198
+ ]
199
+ const lanes: Lane[] = [makeBusyLane(1), makeBusyLane(2)]
200
+ const laneKey = workerLaneKey(modules, 'gatherer', lanes, 2000)
201
+ expect(laneKey).toBe(1) // lowest reaching is slot 0 => laneKey 1
202
+ })
203
+
204
+ test('workerLaneKey with stratum throws "no gatherer reaches this stratum" when none reach', () => {
205
+ const shallowStats = encodeStats([100, 10, 100]) // tol=10 => depth=550 for tier1
206
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_GATHERER_T1, shallowStats)]
207
+ expect(() => workerLaneKey(modules, 'gatherer', [], 5000)).toThrow(
208
+ 'no gatherer reaches this stratum'
209
+ )
210
+ })
211
+
212
+ test('workerLaneKey without stratum still works (non-gatherer path)', () => {
213
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_CRAFTER_T1, crafterStats)]
214
+ const laneKey = workerLaneKey(modules, 'crafter', [])
215
+ expect(laneKey).toBe(1)
216
+ })
217
+
218
+ // --- selectGatherLane: both real error paths ---
219
+
220
+ test('selectGatherLane auto-pick returns the depth-aware lane', () => {
221
+ const shallowStats = encodeStats([100, 200, 100]) // depth=1500 for tier1
222
+ const deepStats = encodeStats([100, 900, 100]) // depth=5000 for tier1
223
+ const modules: ModuleEntry[] = [
224
+ makeModuleEntry(ITEM_GATHERER_T1, shallowStats),
225
+ makeModuleEntry(ITEM_GATHERER_T1, deepStats),
226
+ ]
227
+ expect(selectGatherLane(modules, ITEM_GATHERER_T1, [], 2000)).toBe(2)
228
+ })
229
+
230
+ test('selectGatherLane auto-pick throws "no gatherer reaches this stratum" when none reach', () => {
231
+ const shallowStats = encodeStats([100, 10, 100]) // depth=550 for tier1
232
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_GATHERER_T1, shallowStats)]
233
+ expect(() => selectGatherLane(modules, ITEM_GATHERER_T1, [], 5000)).toThrow(
234
+ 'no gatherer reaches this stratum'
235
+ )
236
+ })
237
+
238
+ test('selectGatherLane explicit slot returns its laneKey when stratum is within depth', () => {
239
+ const deepStats = encodeStats([100, 900, 100]) // depth=5000 for tier1
240
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_GATHERER_T1, deepStats)]
241
+ expect(selectGatherLane(modules, ITEM_GATHERER_T1, [], 2000, 0)).toBe(1) // slot 0 => laneKey 1
242
+ })
243
+
244
+ test('selectGatherLane explicit slot too shallow throws "stratum exceeds gatherer depth"', () => {
245
+ const shallowStats = encodeStats([100, 10, 100]) // depth=550 for tier1
246
+ const modules: ModuleEntry[] = [makeModuleEntry(ITEM_GATHERER_T1, shallowStats)]
247
+ expect(() => selectGatherLane(modules, ITEM_GATHERER_T1, [], 5000, 0)).toThrow(
248
+ 'stratum exceeds gatherer depth'
249
+ )
250
+ })
@@ -0,0 +1,227 @@
1
+ import type {ServerContract} from '../contracts'
2
+ import {getItem} from '../data/catalog'
3
+ import {getEntityLayout} from '../data/recipes-runtime'
4
+ import {decodeStat} from '../derivation/crafting'
5
+ import {gathererDepthForTier, computeGathererYield} from '../derivation/capabilities'
6
+ import {applySlotMultiplier, getSlotAmp} from '../entities/slot-multiplier'
7
+ import {
8
+ computeGathererDrain,
9
+ computeLoaderThrust,
10
+ computeLoaderMass,
11
+ computeCrafterSpeed,
12
+ computeCrafterDrain,
13
+ computeBuilderSpeed,
14
+ computeBuilderDrain,
15
+ } from '../nft/description'
16
+ import type {ModuleType} from '../types'
17
+ import {getLane, LANE_MOBILITY, type ScheduleData} from './schedule'
18
+
19
+ type ModuleEntry = ServerContract.Types.module_entry
20
+ type Lane = ServerContract.Types.lane
21
+ type Schedule = ServerContract.Types.schedule
22
+
23
+ export interface ResolvedGathererLane {
24
+ slotIndex: number
25
+ yield: number
26
+ drain: number
27
+ depth: number
28
+ outputPct: number
29
+ }
30
+
31
+ export interface ResolvedBuilderLane {
32
+ slotIndex: number
33
+ speed: number
34
+ drain: number
35
+ outputPct: number
36
+ }
37
+
38
+ export interface ResolvedCrafterLane {
39
+ slotIndex: number
40
+ speed: number
41
+ drain: number
42
+ outputPct: number
43
+ }
44
+
45
+ export interface ResolvedLoaderLane {
46
+ slotIndex: number
47
+ thrust: number
48
+ mass: number
49
+ outputPct: number
50
+ valid: boolean
51
+ }
52
+
53
+ export function laneKeyForModule(slotIndex: number): number {
54
+ return slotIndex + 1
55
+ }
56
+
57
+ function laneIsFree(lanes: Lane[], laneKey: number): boolean {
58
+ const lane = lanes.find((entry) => entry.lane_key.toNumber() === laneKey)
59
+ return lane ? lane.schedule.tasks.length === 0 : true
60
+ }
61
+
62
+ export function resolveLaneGatherer(
63
+ modules: ModuleEntry[],
64
+ entityItemId: number,
65
+ laneKey: number
66
+ ): ResolvedGathererLane {
67
+ const idx = laneKey - 1
68
+ const installed = idx >= 0 && idx < modules.length ? modules[idx].installed : undefined
69
+ if (!installed) throw new Error('gatherer lane has no module')
70
+ const item = getItem(Number(installed.item_id.value ?? installed.item_id))
71
+ if (item.moduleType !== 'gatherer') throw new Error('lane module is not a gatherer')
72
+ const stats = BigInt(installed.stats.toString())
73
+ const str = decodeStat(stats, 0)
74
+ const tol = decodeStat(stats, 1)
75
+ const con = decodeStat(stats, 2)
76
+ const layout = getEntityLayout(entityItemId)?.slots ?? []
77
+ const amp = getSlotAmp(layout, idx)
78
+ const yieldVal = applySlotMultiplier(computeGathererYield(str, item.tier ?? 1), amp)
79
+ const drain = computeGathererDrain(con)
80
+ const depth = gathererDepthForTier(tol, item.tier ?? 1)
81
+ return {slotIndex: idx, yield: yieldVal, drain, depth, outputPct: amp}
82
+ }
83
+
84
+ // Encapsulates the gather handler's lane selection (gathering.cpp:108-112): both error paths.
85
+ export function selectGatherLane(
86
+ modules: ModuleEntry[],
87
+ entityItemId: number,
88
+ lanes: Lane[],
89
+ stratum: number,
90
+ explicitSlot?: number
91
+ ): number {
92
+ if (explicitSlot !== undefined) {
93
+ const laneKey = laneKeyForModule(explicitSlot)
94
+ const lane = resolveLaneGatherer(modules, entityItemId, laneKey)
95
+ if (stratum > lane.depth) throw new Error('stratum exceeds gatherer depth')
96
+ return laneKey
97
+ }
98
+ return workerLaneKey(modules, 'gatherer', lanes, stratum)
99
+ }
100
+
101
+ export function resolveLaneCrafter(
102
+ modules: ModuleEntry[],
103
+ entityItemId: number,
104
+ laneKey: number
105
+ ): ResolvedCrafterLane {
106
+ const idx = laneKey - 1
107
+ const installed = idx >= 0 && idx < modules.length ? modules[idx].installed : undefined
108
+ if (!installed) throw new Error('crafter lane has no module')
109
+ const item = getItem(Number(installed.item_id.value ?? installed.item_id))
110
+ if (item.moduleType !== 'crafter') throw new Error('lane module is not a crafter')
111
+ const stats = BigInt(installed.stats.toString())
112
+ const rea = decodeStat(stats, 0)
113
+ const fin = decodeStat(stats, 1)
114
+ const layout = getEntityLayout(entityItemId)?.slots ?? []
115
+ const amp = getSlotAmp(layout, idx)
116
+ const speed = applySlotMultiplier(computeCrafterSpeed(rea, item.tier ?? 1), amp)
117
+ const drain = computeCrafterDrain(fin)
118
+ return {slotIndex: idx, speed, drain, outputPct: amp}
119
+ }
120
+
121
+ export function resolveLaneBuilder(
122
+ modules: ModuleEntry[],
123
+ entityItemId: number,
124
+ laneKey: number
125
+ ): ResolvedBuilderLane {
126
+ const idx = laneKey - 1
127
+ const installed = idx >= 0 && idx < modules.length ? modules[idx].installed : undefined
128
+ if (!installed) throw new Error('builder lane has no module')
129
+ const item = getItem(Number(installed.item_id.value ?? installed.item_id))
130
+ if (item.moduleType !== 'builder') throw new Error('lane module is not a builder')
131
+ const stats = BigInt(installed.stats.toString())
132
+ const res = decodeStat(stats, 0)
133
+ const fin = decodeStat(stats, 1)
134
+ const layout = getEntityLayout(entityItemId)?.slots ?? []
135
+ const amp = getSlotAmp(layout, idx)
136
+ const speed = applySlotMultiplier(computeBuilderSpeed(res, item.tier ?? 1), amp)
137
+ const drain = computeBuilderDrain(fin)
138
+ return {slotIndex: idx, speed, drain, outputPct: amp}
139
+ }
140
+
141
+ // LANE_MOBILITY or a missing module soft-returns valid=false (never throws); callers check `valid`.
142
+ export function resolveLaneLoader(
143
+ modules: ModuleEntry[],
144
+ entityItemId: number,
145
+ laneKey: number
146
+ ): ResolvedLoaderLane {
147
+ if (laneKey === LANE_MOBILITY) {
148
+ return {slotIndex: -1, thrust: 0, mass: 0, outputPct: 0, valid: false}
149
+ }
150
+ const idx = laneKey - 1
151
+ const installed = idx >= 0 && idx < modules.length ? modules[idx].installed : undefined
152
+ if (!installed) {
153
+ return {slotIndex: idx, thrust: 0, mass: 0, outputPct: 0, valid: false}
154
+ }
155
+ const item = getItem(Number(installed.item_id.value ?? installed.item_id))
156
+ const stats = BigInt(installed.stats.toString())
157
+ const ins = decodeStat(stats, 0)
158
+ const pla = decodeStat(stats, 1)
159
+ const layout = getEntityLayout(entityItemId)?.slots ?? []
160
+ const amp = getSlotAmp(layout, idx)
161
+ const thrust = applySlotMultiplier(computeLoaderThrust(pla, item.tier ?? 1), amp)
162
+ const mass = computeLoaderMass(ins)
163
+ return {slotIndex: idx, thrust, mass, outputPct: amp, valid: true}
164
+ }
165
+
166
+ export function workerLaneKey(
167
+ modules: ModuleEntry[],
168
+ moduleSubtype: ModuleType,
169
+ lanes: Lane[],
170
+ stratum?: number
171
+ ): number {
172
+ if (moduleSubtype === 'gatherer' && stratum !== undefined) {
173
+ let lowestReaching: number | undefined
174
+ for (let i = 0; i < modules.length; i++) {
175
+ const installed = modules[i].installed
176
+ if (!installed) continue
177
+ const item = getItem(Number(installed.item_id.value ?? installed.item_id))
178
+ if (item.moduleType !== 'gatherer') continue
179
+ const stats = BigInt(installed.stats.toString())
180
+ const tol = decodeStat(stats, 1)
181
+ const depth = gathererDepthForTier(tol, item.tier ?? 1)
182
+ if (depth < stratum) continue
183
+ const laneKey = laneKeyForModule(i)
184
+ if (lowestReaching === undefined) lowestReaching = laneKey
185
+ if (laneIsFree(lanes, laneKey)) return laneKey
186
+ }
187
+ if (lowestReaching === undefined) throw new Error('no gatherer reaches this stratum')
188
+ return lowestReaching
189
+ }
190
+
191
+ const occupiedMatchingLaneKeys: number[] = []
192
+
193
+ for (let slotIndex = 0; slotIndex < modules.length; slotIndex++) {
194
+ const installed = modules[slotIndex].installed
195
+ if (!installed) continue
196
+ if (getItem(installed.item_id).moduleType !== moduleSubtype) continue
197
+
198
+ const laneKey = laneKeyForModule(slotIndex)
199
+ if (laneIsFree(lanes, laneKey)) return laneKey
200
+ occupiedMatchingLaneKeys.push(laneKey)
201
+ }
202
+
203
+ if (occupiedMatchingLaneKeys.length > 0) {
204
+ return Math.min(...occupiedMatchingLaneKeys)
205
+ }
206
+
207
+ throw new Error(`No installed ${moduleSubtype} worker module`)
208
+ }
209
+
210
+ export function rawScheduleEnd(schedule: Schedule): Date {
211
+ const durationSec = schedule.tasks.reduce((sum, task) => sum + task.duration.toNumber(), 0)
212
+ return new Date(schedule.started.toDate().getTime() + durationSec * 1000)
213
+ }
214
+
215
+ export function candidateLaneCompletesAt(
216
+ entity: ScheduleData,
217
+ laneKey: number,
218
+ durationSec: number,
219
+ now: Date
220
+ ): Date {
221
+ const lane = getLane(entity, laneKey)
222
+ const startMs = lane
223
+ ? Math.max(rawScheduleEnd(lane.schedule).getTime(), now.getTime())
224
+ : now.getTime()
225
+
226
+ return new Date(startMs + durationSec * 1000)
227
+ }