@shipload/sdk 2.0.0-rc2 → 2.0.0-rc21
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.
- package/README.md +1 -349
- package/lib/shipload.d.ts +1729 -1127
- package/lib/shipload.js +7944 -3165
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +7487 -2840
- package/lib/shipload.m.js.map +1 -1
- package/package.json +6 -4
- package/src/capabilities/crafting.ts +22 -0
- package/src/capabilities/gathering.ts +36 -0
- package/src/capabilities/guards.ts +3 -8
- package/src/capabilities/hauling.ts +22 -0
- package/src/capabilities/index.ts +4 -1
- package/src/capabilities/modules.ts +86 -0
- package/src/capabilities/storage.ts +101 -9
- package/src/contracts/server.ts +785 -293
- package/src/data/capabilities.ts +408 -0
- package/src/data/categories.ts +55 -0
- package/src/data/colors.ts +71 -0
- package/src/data/entities.json +50 -0
- package/src/data/item-ids.ts +75 -0
- package/src/data/items.json +252 -0
- package/src/data/locations.ts +53 -0
- package/src/data/metadata.ts +208 -0
- package/src/data/nebula-adjectives.json +211 -0
- package/src/data/nebula-nouns.json +151 -0
- package/src/data/recipes-runtime.ts +65 -0
- package/src/data/recipes.json +878 -0
- package/src/data/syllables.json +1386 -780
- package/src/data/tiers.ts +45 -0
- package/src/derivation/crafting.ts +348 -0
- package/src/derivation/index.ts +30 -0
- package/src/derivation/location-size.ts +15 -0
- package/src/derivation/resources.ts +112 -0
- package/src/derivation/stats.ts +146 -0
- package/src/derivation/stratum.ts +134 -0
- package/src/derivation/tiers.ts +54 -0
- package/src/entities/cargo-utils.ts +10 -68
- package/src/entities/container.ts +37 -0
- package/src/entities/entity-inventory.ts +13 -13
- package/src/entities/inventory-accessor.ts +2 -6
- package/src/entities/location.ts +5 -200
- package/src/entities/makers.ts +144 -17
- package/src/entities/player.ts +1 -274
- package/src/entities/ship-deploy.ts +258 -0
- package/src/entities/ship.ts +28 -34
- package/src/entities/warehouse.ts +35 -7
- package/src/errors.ts +59 -5
- package/src/format.ts +12 -0
- package/src/index-module.ts +188 -50
- package/src/managers/actions.ts +138 -88
- package/src/managers/context.ts +19 -9
- package/src/managers/index.ts +0 -1
- package/src/managers/locations.ts +2 -85
- package/src/market/items.ts +41 -0
- package/src/nft/description.ts +176 -0
- package/src/nft/deserializers.ts +83 -0
- package/src/nft/index.ts +2 -0
- package/src/resolution/describe-module.ts +165 -0
- package/src/resolution/display-name.ts +43 -0
- package/src/resolution/resolve-item.ts +358 -0
- package/src/scheduling/projection.ts +200 -67
- package/src/scheduling/schedule.ts +2 -2
- package/src/shipload.ts +10 -5
- package/src/subscriptions/connection.ts +154 -0
- package/src/subscriptions/debug.ts +17 -0
- package/src/subscriptions/index.ts +5 -0
- package/src/subscriptions/manager.ts +240 -0
- package/src/subscriptions/mappers.ts +28 -0
- package/src/subscriptions/types.ts +143 -0
- package/src/travel/travel.ts +37 -23
- package/src/types/capabilities.ts +11 -14
- package/src/types/entity-traits.ts +3 -4
- package/src/types/entity.ts +9 -6
- package/src/types.ts +72 -72
- package/src/utils/system.ts +66 -53
- package/src/capabilities/extraction.ts +0 -37
- package/src/data/goods.json +0 -23
- package/src/managers/trades.ts +0 -119
- package/src/market/goods.ts +0 -31
- package/src/market/market.ts +0 -208
- package/src/market/rolls.ts +0 -8
- package/src/trading/collect.ts +0 -938
- package/src/trading/deal.ts +0 -207
- package/src/trading/trade.ts +0 -203
|
@@ -8,28 +8,44 @@ import {
|
|
|
8
8
|
EntityCapabilities,
|
|
9
9
|
EntityState,
|
|
10
10
|
} from '../types/capabilities'
|
|
11
|
+
import {
|
|
12
|
+
ENTITY_CAPACITY_EXCEEDED,
|
|
13
|
+
RECIPE_INPUTS_EXCESS,
|
|
14
|
+
RECIPE_INPUTS_INSUFFICIENT,
|
|
15
|
+
RECIPE_INPUTS_INVALID,
|
|
16
|
+
RECIPE_NOT_FOUND,
|
|
17
|
+
SHIP_CARGO_NOT_LOADED,
|
|
18
|
+
} from '../errors'
|
|
19
|
+
import {getRecipe, RecipeInput} from '../data/recipes-runtime'
|
|
20
|
+
import {getItem} from '../market/items'
|
|
11
21
|
import {distanceBetweenCoordinates, lerp} from '../travel/travel'
|
|
12
|
-
import {
|
|
13
|
-
|
|
22
|
+
import {
|
|
23
|
+
calcStacksMass,
|
|
24
|
+
cargoItemToStack,
|
|
25
|
+
CargoStack,
|
|
26
|
+
mergeStacks,
|
|
27
|
+
removeFromStacks,
|
|
28
|
+
stackToCargoItem,
|
|
29
|
+
} from '../capabilities/storage'
|
|
14
30
|
import * as schedule from './schedule'
|
|
15
31
|
import {ScheduleData} from './schedule'
|
|
16
32
|
|
|
17
33
|
export interface ProjectedEntity {
|
|
18
34
|
location: Coordinates
|
|
19
35
|
energy: UInt16
|
|
20
|
-
|
|
36
|
+
cargo: CargoStack[]
|
|
21
37
|
shipMass: UInt32
|
|
22
38
|
capacity?: UInt64
|
|
23
39
|
engines?: ServerContract.Types.movement_stats
|
|
24
40
|
loaders?: ServerContract.Types.loader_stats
|
|
25
41
|
generator?: ServerContract.Types.energy_stats
|
|
26
|
-
|
|
42
|
+
hauler?: ServerContract.Types.hauler_stats
|
|
43
|
+
readonly cargoMass: UInt64
|
|
27
44
|
readonly totalMass: UInt64
|
|
28
45
|
|
|
29
46
|
hasMovement(): boolean
|
|
30
47
|
hasStorage(): boolean
|
|
31
48
|
hasLoaders(): boolean
|
|
32
|
-
hasTrade(): boolean
|
|
33
49
|
|
|
34
50
|
capabilities(): EntityCapabilities
|
|
35
51
|
state(): EntityState
|
|
@@ -42,7 +58,7 @@ export interface Projectable extends ScheduleData {
|
|
|
42
58
|
generator?: ServerContract.Types.energy_stats
|
|
43
59
|
engines?: ServerContract.Types.movement_stats
|
|
44
60
|
loaders?: ServerContract.Types.loader_stats
|
|
45
|
-
|
|
61
|
+
hauler?: ServerContract.Types.hauler_stats
|
|
46
62
|
capacity?: UInt32
|
|
47
63
|
cargo: ServerContract.Types.cargo_item[]
|
|
48
64
|
cargomass: UInt32
|
|
@@ -54,24 +70,29 @@ function getHullMass(entity: Projectable): UInt32 {
|
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
57
|
-
const cargoMass = calcCargoMass(entity)
|
|
58
73
|
const shipMass = getHullMass(entity)
|
|
59
74
|
const loaders = entity.loaders
|
|
60
75
|
const engines = entity.engines
|
|
61
76
|
const generator = entity.generator
|
|
62
|
-
const
|
|
77
|
+
const hauler = entity.hauler
|
|
63
78
|
const capacity = entity.capacity
|
|
64
79
|
|
|
80
|
+
const cargo: CargoStack[] = entity.cargo.map(cargoItemToStack)
|
|
81
|
+
|
|
65
82
|
const projected: ProjectedEntity = {
|
|
66
83
|
location: Coordinates.from(entity.coordinates),
|
|
67
84
|
energy: UInt16.from(entity.energy ?? 0),
|
|
68
|
-
|
|
85
|
+
cargo,
|
|
69
86
|
shipMass,
|
|
70
87
|
capacity: capacity ? UInt64.from(capacity) : undefined,
|
|
71
88
|
engines,
|
|
72
89
|
generator,
|
|
90
|
+
hauler,
|
|
73
91
|
loaders,
|
|
74
|
-
|
|
92
|
+
|
|
93
|
+
get cargoMass() {
|
|
94
|
+
return calcStacksMass(this.cargo)
|
|
95
|
+
},
|
|
75
96
|
|
|
76
97
|
get totalMass() {
|
|
77
98
|
let mass = UInt64.from(this.shipMass).adding(this.cargoMass)
|
|
@@ -93,10 +114,6 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
|
93
114
|
return capsHasLoaders(this.capabilities())
|
|
94
115
|
},
|
|
95
116
|
|
|
96
|
-
hasTrade() {
|
|
97
|
-
return this.trade !== undefined
|
|
98
|
-
},
|
|
99
|
-
|
|
100
117
|
capabilities(): EntityCapabilities {
|
|
101
118
|
return {
|
|
102
119
|
hullmass: this.shipMass,
|
|
@@ -104,7 +121,6 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
|
104
121
|
engines: this.engines,
|
|
105
122
|
generator: this.generator,
|
|
106
123
|
loaders: this.loaders,
|
|
107
|
-
trade: this.trade,
|
|
108
124
|
}
|
|
109
125
|
},
|
|
110
126
|
|
|
@@ -114,7 +130,7 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
|
114
130
|
location: ServerContract.Types.coordinates.from(this.location),
|
|
115
131
|
energy: this.energy,
|
|
116
132
|
cargomass: UInt32.from(this.cargoMass),
|
|
117
|
-
cargo:
|
|
133
|
+
cargo: this.cargo.map(stackToCargoItem),
|
|
118
134
|
}
|
|
119
135
|
},
|
|
120
136
|
}
|
|
@@ -169,76 +185,191 @@ function applyFlightTask(
|
|
|
169
185
|
}
|
|
170
186
|
}
|
|
171
187
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
function addCargoItem(projected: ProjectedEntity, item: ServerContract.Types.cargo_item): void {
|
|
189
|
+
projected.cargo = mergeStacks(projected.cargo, cargoItemToStack(item))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function removeCargoItem(projected: ProjectedEntity, item: ServerContract.Types.cargo_item): void {
|
|
193
|
+
projected.cargo = removeFromStacks(projected.cargo, cargoItemToStack(item))
|
|
175
194
|
}
|
|
176
195
|
|
|
177
|
-
function
|
|
196
|
+
function applyAddCargoTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
178
197
|
for (const item of task.cargo) {
|
|
179
|
-
|
|
180
|
-
projected.cargoMass = projected.cargoMass.adding(good_mass.multiplying(item.quantity))
|
|
198
|
+
addCargoItem(projected, item)
|
|
181
199
|
}
|
|
182
200
|
}
|
|
183
201
|
|
|
184
|
-
function
|
|
202
|
+
function applyRemoveCargoTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
185
203
|
for (const item of task.cargo) {
|
|
186
|
-
|
|
187
|
-
const cargoMass = good_mass.multiplying(item.quantity)
|
|
188
|
-
projected.cargoMass = projected.cargoMass.gt(cargoMass)
|
|
189
|
-
? projected.cargoMass.subtracting(cargoMass)
|
|
190
|
-
: UInt64.from(0)
|
|
204
|
+
removeCargoItem(projected, item)
|
|
191
205
|
}
|
|
192
206
|
}
|
|
193
207
|
|
|
194
|
-
function
|
|
208
|
+
function applyEnergyCost(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
209
|
+
if (!task.energy_cost) return
|
|
210
|
+
const energyCost = UInt16.from(task.energy_cost)
|
|
211
|
+
projected.energy = projected.energy.gt(energyCost)
|
|
212
|
+
? UInt16.from(projected.energy.subtracting(energyCost))
|
|
213
|
+
: UInt16.from(0)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function applyGatherTask(
|
|
195
217
|
projected: ProjectedEntity,
|
|
196
218
|
task: ServerContract.Types.task,
|
|
197
219
|
options: {complete: boolean}
|
|
198
220
|
): void {
|
|
199
221
|
if (!options.complete) return
|
|
222
|
+
applyEnergyCost(projected, task)
|
|
223
|
+
if (!task.entitytarget) {
|
|
224
|
+
applyAddCargoTask(projected, task)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
200
227
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
228
|
+
function applyCraftTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
229
|
+
applyEnergyCost(projected, task)
|
|
230
|
+
if (task.cargo.length === 0) return
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < task.cargo.length - 1; i++) {
|
|
233
|
+
removeCargoItem(projected, task.cargo[i])
|
|
206
234
|
}
|
|
235
|
+
addCargoItem(projected, task.cargo[task.cargo.length - 1])
|
|
236
|
+
}
|
|
207
237
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
238
|
+
function applyDeployTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
239
|
+
applyEnergyCost(projected, task)
|
|
240
|
+
if (task.cargo.length > 0) {
|
|
241
|
+
removeCargoItem(projected, task.cargo[0])
|
|
211
242
|
}
|
|
212
243
|
}
|
|
213
244
|
|
|
214
|
-
|
|
245
|
+
function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
246
|
+
switch (task.type.toNumber()) {
|
|
247
|
+
case TaskType.RECHARGE:
|
|
248
|
+
applyRechargeTask(projected, task, {complete: true})
|
|
249
|
+
break
|
|
250
|
+
case TaskType.TRAVEL:
|
|
251
|
+
applyFlightTask(projected, task, {complete: true})
|
|
252
|
+
break
|
|
253
|
+
case TaskType.LOAD:
|
|
254
|
+
case TaskType.UNWRAP:
|
|
255
|
+
applyAddCargoTask(projected, task)
|
|
256
|
+
break
|
|
257
|
+
case TaskType.UNLOAD:
|
|
258
|
+
case TaskType.WRAP:
|
|
259
|
+
applyRemoveCargoTask(projected, task)
|
|
260
|
+
break
|
|
261
|
+
case TaskType.GATHER:
|
|
262
|
+
applyGatherTask(projected, task, {complete: true})
|
|
263
|
+
break
|
|
264
|
+
case TaskType.CRAFT:
|
|
265
|
+
applyCraftTask(projected, task)
|
|
266
|
+
break
|
|
267
|
+
case TaskType.DEPLOY:
|
|
268
|
+
applyDeployTask(projected, task)
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface ProjectionOptions {
|
|
274
|
+
upToTaskIndex?: number
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function projectEntity(entity: Projectable, options?: ProjectionOptions): ProjectedEntity {
|
|
215
278
|
const projected = createProjectedEntity(entity)
|
|
279
|
+
if (!entity.schedule || entity.schedule.tasks.length === 0) return projected
|
|
216
280
|
|
|
217
|
-
|
|
218
|
-
|
|
281
|
+
const tasks = entity.schedule.tasks
|
|
282
|
+
const taskCount =
|
|
283
|
+
options?.upToTaskIndex !== undefined
|
|
284
|
+
? Math.max(0, Math.min(options.upToTaskIndex, tasks.length))
|
|
285
|
+
: tasks.length
|
|
286
|
+
|
|
287
|
+
for (let i = 0; i < taskCount; i++) {
|
|
288
|
+
applyTask(projected, tasks[i])
|
|
219
289
|
}
|
|
290
|
+
return projected
|
|
291
|
+
}
|
|
220
292
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
293
|
+
function getRecipeInputsForOutput(outputItemId: number): RecipeInput[] | undefined {
|
|
294
|
+
const recipe = getRecipe(outputItemId)
|
|
295
|
+
return recipe?.inputs
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function validateCraftTask(task: ServerContract.Types.task, projected: ProjectedEntity): void {
|
|
299
|
+
if (task.cargo.length === 0) return
|
|
300
|
+
|
|
301
|
+
const output = task.cargo[task.cargo.length - 1]
|
|
302
|
+
const inputs = task.cargo.slice(0, -1)
|
|
303
|
+
const craftQuantity = output.quantity.toNumber()
|
|
304
|
+
|
|
305
|
+
const recipe = getRecipeInputsForOutput(output.item_id.toNumber())
|
|
306
|
+
if (!recipe) throw new Error(RECIPE_NOT_FOUND)
|
|
307
|
+
|
|
308
|
+
const groupedInputs: ServerContract.Types.cargo_item[][] = recipe.map(() => [])
|
|
309
|
+
for (const input of inputs) {
|
|
310
|
+
let matched = false
|
|
311
|
+
for (let ri = 0; ri < recipe.length; ri++) {
|
|
312
|
+
const req = recipe[ri]
|
|
313
|
+
if ('itemId' in req) {
|
|
314
|
+
if (input.item_id.toNumber() === req.itemId) {
|
|
315
|
+
groupedInputs[ri].push(input)
|
|
316
|
+
matched = true
|
|
317
|
+
break
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
const item = getItem(input.item_id)
|
|
321
|
+
if (item.category === req.category && item.tier === req.tier) {
|
|
322
|
+
groupedInputs[ri].push(input)
|
|
323
|
+
matched = true
|
|
324
|
+
break
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!matched) throw new Error(RECIPE_INPUTS_INVALID)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (let ri = 0; ri < recipe.length; ri++) {
|
|
332
|
+
const stacks = groupedInputs[ri]
|
|
333
|
+
let provided = 0
|
|
334
|
+
for (const stack of stacks) {
|
|
335
|
+
provided += stack.quantity.toNumber()
|
|
336
|
+
}
|
|
337
|
+
const required = recipe[ri].quantity * craftQuantity
|
|
338
|
+
if (provided < required) throw new Error(RECIPE_INPUTS_INSUFFICIENT)
|
|
339
|
+
if (provided !== required) throw new Error(RECIPE_INPUTS_EXCESS)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const input of inputs) {
|
|
343
|
+
let found = false
|
|
344
|
+
for (const pc of projected.cargo) {
|
|
345
|
+
if (
|
|
346
|
+
pc.item_id.toNumber() === input.item_id.toNumber() &&
|
|
347
|
+
pc.stats.toString() === input.stats.toString()
|
|
348
|
+
) {
|
|
349
|
+
if (pc.quantity.toNumber() < input.quantity.toNumber()) {
|
|
350
|
+
throw new Error(RECIPE_INPUTS_INSUFFICIENT)
|
|
351
|
+
}
|
|
352
|
+
found = true
|
|
237
353
|
break
|
|
354
|
+
}
|
|
238
355
|
}
|
|
356
|
+
if (!found) throw new Error(SHIP_CARGO_NOT_LOADED)
|
|
239
357
|
}
|
|
358
|
+
}
|
|
240
359
|
|
|
241
|
-
|
|
360
|
+
export function validateSchedule(entity: Projectable): void {
|
|
361
|
+
if (!entity.schedule || entity.schedule.tasks.length === 0) return
|
|
362
|
+
|
|
363
|
+
const projected = createProjectedEntity(entity)
|
|
364
|
+
for (const task of entity.schedule.tasks) {
|
|
365
|
+
if (task.type.toNumber() === TaskType.CRAFT) {
|
|
366
|
+
validateCraftTask(task, projected)
|
|
367
|
+
}
|
|
368
|
+
applyTask(projected, task)
|
|
369
|
+
if (projected.capacity && projected.cargoMass.gt(projected.capacity)) {
|
|
370
|
+
throw new Error(ENTITY_CAPACITY_EXCEEDED)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
242
373
|
}
|
|
243
374
|
|
|
244
375
|
export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity {
|
|
@@ -269,19 +400,21 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
|
|
|
269
400
|
applyFlightTask(projected, task, {complete: taskComplete, progress})
|
|
270
401
|
break
|
|
271
402
|
case TaskType.LOAD:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
403
|
+
case TaskType.UNWRAP:
|
|
404
|
+
if (taskComplete) applyAddCargoTask(projected, task)
|
|
275
405
|
break
|
|
276
406
|
case TaskType.UNLOAD:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
407
|
+
case TaskType.WRAP:
|
|
408
|
+
if (taskComplete) applyRemoveCargoTask(projected, task)
|
|
280
409
|
break
|
|
281
|
-
case TaskType.
|
|
282
|
-
if (taskComplete) {
|
|
283
|
-
|
|
284
|
-
|
|
410
|
+
case TaskType.GATHER:
|
|
411
|
+
if (taskComplete) applyGatherTask(projected, task, {complete: true})
|
|
412
|
+
break
|
|
413
|
+
case TaskType.CRAFT:
|
|
414
|
+
if (taskComplete) applyCraftTask(projected, task)
|
|
415
|
+
break
|
|
416
|
+
case TaskType.DEPLOY:
|
|
417
|
+
if (taskComplete) applyDeployTask(projected, task)
|
|
285
418
|
break
|
|
286
419
|
}
|
|
287
420
|
}
|
|
@@ -174,6 +174,6 @@ export function isUnloading(entity: ScheduleData, now: Date): boolean {
|
|
|
174
174
|
return isTaskType(entity, TaskType.UNLOAD, now)
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
export function
|
|
178
|
-
return isTaskType(entity, TaskType.
|
|
177
|
+
export function isGathering(entity: ScheduleData, now: Date): boolean {
|
|
178
|
+
return isTaskType(entity, TaskType.GATHER, now)
|
|
179
179
|
}
|
package/src/shipload.ts
CHANGED
|
@@ -7,15 +7,16 @@ import {GameContext} from './managers/context'
|
|
|
7
7
|
import {EntitiesManager} from './managers/entities'
|
|
8
8
|
import {PlayersManager} from './managers/players'
|
|
9
9
|
import {LocationsManager} from './managers/locations'
|
|
10
|
-
import {TradesManager} from './managers/trades'
|
|
11
10
|
import {EpochsManager} from './managers/epochs'
|
|
12
11
|
import {ActionsManager} from './managers/actions'
|
|
12
|
+
import {SubscriptionsManager} from './subscriptions/manager'
|
|
13
13
|
import {GameState} from './entities/gamestate'
|
|
14
14
|
|
|
15
15
|
interface ShiploadOptions {
|
|
16
16
|
platformContractName?: string
|
|
17
17
|
serverContractName?: string
|
|
18
18
|
client?: APIClient
|
|
19
|
+
subscriptionsUrl?: string
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
interface ShiploadConstructorOptions extends ShiploadOptions {
|
|
@@ -39,6 +40,10 @@ export class Shipload {
|
|
|
39
40
|
: new ServerContract.Contract({client: apiClient})
|
|
40
41
|
|
|
41
42
|
this._context = new GameContext(apiClient, server, platform)
|
|
43
|
+
|
|
44
|
+
if (constructorOptions?.subscriptionsUrl) {
|
|
45
|
+
this._context.setSubscriptionsUrl(constructorOptions.subscriptionsUrl)
|
|
46
|
+
}
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
static async load(
|
|
@@ -94,10 +99,6 @@ export class Shipload {
|
|
|
94
99
|
return this._context.locations
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
get trades(): TradesManager {
|
|
98
|
-
return this._context.trades
|
|
99
|
-
}
|
|
100
|
-
|
|
101
102
|
get epochs(): EpochsManager {
|
|
102
103
|
return this._context.epochs
|
|
103
104
|
}
|
|
@@ -106,6 +107,10 @@ export class Shipload {
|
|
|
106
107
|
return this._context.actions
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
get subscriptions(): SubscriptionsManager {
|
|
111
|
+
return this._context.subscriptions
|
|
112
|
+
}
|
|
113
|
+
|
|
109
114
|
async getGame(reload = false): Promise<PlatformContract.Types.game_row> {
|
|
110
115
|
return this._context.getGame(reload)
|
|
111
116
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type {ClientMessage, ServerMessage} from './types'
|
|
2
|
+
import {debug} from './debug'
|
|
3
|
+
|
|
4
|
+
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting'
|
|
5
|
+
|
|
6
|
+
export interface WebSocketConnectionOptions {
|
|
7
|
+
url: string
|
|
8
|
+
onMessage: (message: ServerMessage) => void
|
|
9
|
+
onStateChange?: (state: ConnectionState) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class WebSocketConnection {
|
|
13
|
+
private ws: WebSocket | null = null
|
|
14
|
+
private url: string
|
|
15
|
+
private onMessage: (message: ServerMessage) => void
|
|
16
|
+
private onStateChange?: (state: ConnectionState) => void
|
|
17
|
+
private reconnectAttempts = 0
|
|
18
|
+
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null
|
|
19
|
+
private _state: ConnectionState = 'disconnected'
|
|
20
|
+
private shouldReconnect = true
|
|
21
|
+
private sendQueue: string[] = []
|
|
22
|
+
|
|
23
|
+
private static readonly MIN_RECONNECT_DELAY = 1000
|
|
24
|
+
private static readonly MAX_RECONNECT_DELAY = 30000
|
|
25
|
+
private static readonly RECONNECT_MULTIPLIER = 2
|
|
26
|
+
|
|
27
|
+
constructor(options: WebSocketConnectionOptions) {
|
|
28
|
+
this.url = options.url
|
|
29
|
+
this.onMessage = options.onMessage
|
|
30
|
+
this.onStateChange = options.onStateChange
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get state(): ConnectionState {
|
|
34
|
+
return this._state
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private setState(state: ConnectionState) {
|
|
38
|
+
if (this._state !== state) {
|
|
39
|
+
this._state = state
|
|
40
|
+
this.onStateChange?.(state)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
connect() {
|
|
45
|
+
if (this.ws) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.shouldReconnect = true
|
|
50
|
+
this.setState('connecting')
|
|
51
|
+
debug('Connecting to', this.url)
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
this.ws = new WebSocket(this.url)
|
|
55
|
+
|
|
56
|
+
this.ws.onopen = () => {
|
|
57
|
+
debug('Connected')
|
|
58
|
+
this.reconnectAttempts = 0
|
|
59
|
+
this.setState('connected')
|
|
60
|
+
while (
|
|
61
|
+
this.sendQueue.length > 0 &&
|
|
62
|
+
this.ws &&
|
|
63
|
+
this.ws.readyState === WebSocket.OPEN
|
|
64
|
+
) {
|
|
65
|
+
this.ws.send(this.sendQueue.shift()!)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.ws.onmessage = (event) => {
|
|
70
|
+
try {
|
|
71
|
+
const message = JSON.parse(event.data) as ServerMessage
|
|
72
|
+
this.onMessage(message)
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.error('[WS] Failed to parse message:', e)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.ws.onclose = () => {
|
|
80
|
+
this.ws = null
|
|
81
|
+
this.sendQueue.length = 0
|
|
82
|
+
|
|
83
|
+
if (this.shouldReconnect) {
|
|
84
|
+
this.setState('reconnecting')
|
|
85
|
+
this.scheduleReconnect()
|
|
86
|
+
} else {
|
|
87
|
+
this.setState('disconnected')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.error('[WS] Failed to create connection:', e)
|
|
93
|
+
this.ws = null
|
|
94
|
+
if (this.shouldReconnect) {
|
|
95
|
+
this.setState('reconnecting')
|
|
96
|
+
this.scheduleReconnect()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private scheduleReconnect() {
|
|
102
|
+
if (this.reconnectTimeout) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const delay = Math.min(
|
|
107
|
+
WebSocketConnection.MIN_RECONNECT_DELAY *
|
|
108
|
+
Math.pow(WebSocketConnection.RECONNECT_MULTIPLIER, this.reconnectAttempts),
|
|
109
|
+
WebSocketConnection.MAX_RECONNECT_DELAY
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
debug(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`)
|
|
113
|
+
|
|
114
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
115
|
+
this.reconnectTimeout = null
|
|
116
|
+
this.reconnectAttempts++
|
|
117
|
+
this.connect()
|
|
118
|
+
}, delay)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
disconnect() {
|
|
122
|
+
this.shouldReconnect = false
|
|
123
|
+
|
|
124
|
+
if (this.reconnectTimeout) {
|
|
125
|
+
clearTimeout(this.reconnectTimeout)
|
|
126
|
+
this.reconnectTimeout = null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.ws) {
|
|
130
|
+
this.ws.close()
|
|
131
|
+
this.ws = null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.sendQueue.length = 0
|
|
135
|
+
this.setState('disconnected')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
close() {
|
|
139
|
+
this.disconnect()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
send(message: ClientMessage) {
|
|
143
|
+
const data = JSON.stringify(message)
|
|
144
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
145
|
+
this.ws.send(data)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
this.sendQueue.push(data)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get isConnected(): boolean {
|
|
152
|
+
return this._state === 'connected'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
let enabled = false
|
|
4
|
+
|
|
5
|
+
export function setSubscriptionsDebug(on: boolean): void {
|
|
6
|
+
enabled = on
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isSubscriptionsDebugEnabled(): boolean {
|
|
10
|
+
return enabled
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function debug(...args: unknown[]): void {
|
|
14
|
+
if (enabled) {
|
|
15
|
+
console.log('[WS]', ...args)
|
|
16
|
+
}
|
|
17
|
+
}
|