@shipload/sdk 2.0.0-rc2 → 2.0.0-rc20
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 +1658 -1126
- package/lib/shipload.js +6847 -3082
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +6468 -2793
- 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 +57 -0
- package/src/capabilities/storage.ts +101 -9
- package/src/contracts/server.ts +717 -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/items.json +17 -0
- package/src/data/locations.ts +53 -0
- package/src/data/nebula-adjectives.json +211 -0
- package/src/data/nebula-nouns.json +151 -0
- package/src/data/recipes.ts +587 -0
- package/src/data/syllables.json +1386 -780
- package/src/data/tiers.ts +45 -0
- package/src/derivation/crafting.ts +287 -0
- package/src/derivation/index.ts +30 -0
- package/src/derivation/location-size.ts +15 -0
- package/src/derivation/resources.ts +136 -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 +136 -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 +233 -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 +93 -0
- package/src/nft/description.ts +176 -0
- package/src/nft/deserializers.ts +81 -0
- package/src/nft/index.ts +2 -0
- package/src/resolution/describe-module.ts +165 -0
- package/src/resolution/display-name.ts +39 -0
- package/src/resolution/resolve-item.ts +343 -0
- package/src/scheduling/projection.ts +220 -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 +30 -17
- 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 +61 -55
- 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,50 @@ 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_INPUTS_MIXED,
|
|
17
|
+
RECIPE_NOT_FOUND,
|
|
18
|
+
SHIP_CARGO_NOT_LOADED,
|
|
19
|
+
} from '../errors'
|
|
20
|
+
import {
|
|
21
|
+
getComponentById,
|
|
22
|
+
getEntityRecipeByItemId,
|
|
23
|
+
getModuleRecipeByItemId,
|
|
24
|
+
RecipeInput,
|
|
25
|
+
} from '../data/recipes'
|
|
26
|
+
import {getItem} from '../market/items'
|
|
11
27
|
import {distanceBetweenCoordinates, lerp} from '../travel/travel'
|
|
12
|
-
import {
|
|
13
|
-
|
|
28
|
+
import {
|
|
29
|
+
calcStacksMass,
|
|
30
|
+
cargoItemToStack,
|
|
31
|
+
CargoStack,
|
|
32
|
+
mergeStacks,
|
|
33
|
+
removeFromStacks,
|
|
34
|
+
stackToCargoItem,
|
|
35
|
+
} from '../capabilities/storage'
|
|
14
36
|
import * as schedule from './schedule'
|
|
15
37
|
import {ScheduleData} from './schedule'
|
|
16
38
|
|
|
17
39
|
export interface ProjectedEntity {
|
|
18
40
|
location: Coordinates
|
|
19
41
|
energy: UInt16
|
|
20
|
-
|
|
42
|
+
cargo: CargoStack[]
|
|
21
43
|
shipMass: UInt32
|
|
22
44
|
capacity?: UInt64
|
|
23
45
|
engines?: ServerContract.Types.movement_stats
|
|
24
46
|
loaders?: ServerContract.Types.loader_stats
|
|
25
47
|
generator?: ServerContract.Types.energy_stats
|
|
26
|
-
|
|
48
|
+
hauler?: ServerContract.Types.hauler_stats
|
|
49
|
+
readonly cargoMass: UInt64
|
|
27
50
|
readonly totalMass: UInt64
|
|
28
51
|
|
|
29
52
|
hasMovement(): boolean
|
|
30
53
|
hasStorage(): boolean
|
|
31
54
|
hasLoaders(): boolean
|
|
32
|
-
hasTrade(): boolean
|
|
33
55
|
|
|
34
56
|
capabilities(): EntityCapabilities
|
|
35
57
|
state(): EntityState
|
|
@@ -42,7 +64,7 @@ export interface Projectable extends ScheduleData {
|
|
|
42
64
|
generator?: ServerContract.Types.energy_stats
|
|
43
65
|
engines?: ServerContract.Types.movement_stats
|
|
44
66
|
loaders?: ServerContract.Types.loader_stats
|
|
45
|
-
|
|
67
|
+
hauler?: ServerContract.Types.hauler_stats
|
|
46
68
|
capacity?: UInt32
|
|
47
69
|
cargo: ServerContract.Types.cargo_item[]
|
|
48
70
|
cargomass: UInt32
|
|
@@ -54,24 +76,29 @@ function getHullMass(entity: Projectable): UInt32 {
|
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
57
|
-
const cargoMass = calcCargoMass(entity)
|
|
58
79
|
const shipMass = getHullMass(entity)
|
|
59
80
|
const loaders = entity.loaders
|
|
60
81
|
const engines = entity.engines
|
|
61
82
|
const generator = entity.generator
|
|
62
|
-
const
|
|
83
|
+
const hauler = entity.hauler
|
|
63
84
|
const capacity = entity.capacity
|
|
64
85
|
|
|
86
|
+
const cargo: CargoStack[] = entity.cargo.map(cargoItemToStack)
|
|
87
|
+
|
|
65
88
|
const projected: ProjectedEntity = {
|
|
66
89
|
location: Coordinates.from(entity.coordinates),
|
|
67
90
|
energy: UInt16.from(entity.energy ?? 0),
|
|
68
|
-
|
|
91
|
+
cargo,
|
|
69
92
|
shipMass,
|
|
70
93
|
capacity: capacity ? UInt64.from(capacity) : undefined,
|
|
71
94
|
engines,
|
|
72
95
|
generator,
|
|
96
|
+
hauler,
|
|
73
97
|
loaders,
|
|
74
|
-
|
|
98
|
+
|
|
99
|
+
get cargoMass() {
|
|
100
|
+
return calcStacksMass(this.cargo)
|
|
101
|
+
},
|
|
75
102
|
|
|
76
103
|
get totalMass() {
|
|
77
104
|
let mass = UInt64.from(this.shipMass).adding(this.cargoMass)
|
|
@@ -93,10 +120,6 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
|
93
120
|
return capsHasLoaders(this.capabilities())
|
|
94
121
|
},
|
|
95
122
|
|
|
96
|
-
hasTrade() {
|
|
97
|
-
return this.trade !== undefined
|
|
98
|
-
},
|
|
99
|
-
|
|
100
123
|
capabilities(): EntityCapabilities {
|
|
101
124
|
return {
|
|
102
125
|
hullmass: this.shipMass,
|
|
@@ -104,7 +127,6 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
|
104
127
|
engines: this.engines,
|
|
105
128
|
generator: this.generator,
|
|
106
129
|
loaders: this.loaders,
|
|
107
|
-
trade: this.trade,
|
|
108
130
|
}
|
|
109
131
|
},
|
|
110
132
|
|
|
@@ -114,7 +136,7 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
|
|
|
114
136
|
location: ServerContract.Types.coordinates.from(this.location),
|
|
115
137
|
energy: this.energy,
|
|
116
138
|
cargomass: UInt32.from(this.cargoMass),
|
|
117
|
-
cargo:
|
|
139
|
+
cargo: this.cargo.map(stackToCargoItem),
|
|
118
140
|
}
|
|
119
141
|
},
|
|
120
142
|
}
|
|
@@ -169,76 +191,205 @@ function applyFlightTask(
|
|
|
169
191
|
}
|
|
170
192
|
}
|
|
171
193
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
function addCargoItem(projected: ProjectedEntity, item: ServerContract.Types.cargo_item): void {
|
|
195
|
+
projected.cargo = mergeStacks(projected.cargo, cargoItemToStack(item))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function removeCargoItem(projected: ProjectedEntity, item: ServerContract.Types.cargo_item): void {
|
|
199
|
+
projected.cargo = removeFromStacks(projected.cargo, cargoItemToStack(item))
|
|
175
200
|
}
|
|
176
201
|
|
|
177
|
-
function
|
|
202
|
+
function applyAddCargoTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
178
203
|
for (const item of task.cargo) {
|
|
179
|
-
|
|
180
|
-
projected.cargoMass = projected.cargoMass.adding(good_mass.multiplying(item.quantity))
|
|
204
|
+
addCargoItem(projected, item)
|
|
181
205
|
}
|
|
182
206
|
}
|
|
183
207
|
|
|
184
|
-
function
|
|
208
|
+
function applyRemoveCargoTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
185
209
|
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)
|
|
210
|
+
removeCargoItem(projected, item)
|
|
191
211
|
}
|
|
192
212
|
}
|
|
193
213
|
|
|
194
|
-
function
|
|
214
|
+
function applyEnergyCost(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
215
|
+
if (!task.energy_cost) return
|
|
216
|
+
const energyCost = UInt16.from(task.energy_cost)
|
|
217
|
+
projected.energy = projected.energy.gt(energyCost)
|
|
218
|
+
? UInt16.from(projected.energy.subtracting(energyCost))
|
|
219
|
+
: UInt16.from(0)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function applyGatherTask(
|
|
195
223
|
projected: ProjectedEntity,
|
|
196
224
|
task: ServerContract.Types.task,
|
|
197
225
|
options: {complete: boolean}
|
|
198
226
|
): void {
|
|
199
227
|
if (!options.complete) return
|
|
228
|
+
applyEnergyCost(projected, task)
|
|
229
|
+
if (!task.entitytarget) {
|
|
230
|
+
applyAddCargoTask(projected, task)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
200
233
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
234
|
+
function applyCraftTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
235
|
+
applyEnergyCost(projected, task)
|
|
236
|
+
if (task.cargo.length === 0) return
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < task.cargo.length - 1; i++) {
|
|
239
|
+
removeCargoItem(projected, task.cargo[i])
|
|
206
240
|
}
|
|
241
|
+
addCargoItem(projected, task.cargo[task.cargo.length - 1])
|
|
242
|
+
}
|
|
207
243
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
244
|
+
function applyDeployTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
245
|
+
applyEnergyCost(projected, task)
|
|
246
|
+
if (task.cargo.length > 0) {
|
|
247
|
+
removeCargoItem(projected, task.cargo[0])
|
|
211
248
|
}
|
|
212
249
|
}
|
|
213
250
|
|
|
214
|
-
|
|
251
|
+
function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
|
|
252
|
+
switch (task.type.toNumber()) {
|
|
253
|
+
case TaskType.RECHARGE:
|
|
254
|
+
applyRechargeTask(projected, task, {complete: true})
|
|
255
|
+
break
|
|
256
|
+
case TaskType.TRAVEL:
|
|
257
|
+
applyFlightTask(projected, task, {complete: true})
|
|
258
|
+
break
|
|
259
|
+
case TaskType.LOAD:
|
|
260
|
+
case TaskType.UNWRAP:
|
|
261
|
+
applyAddCargoTask(projected, task)
|
|
262
|
+
break
|
|
263
|
+
case TaskType.UNLOAD:
|
|
264
|
+
case TaskType.WRAP:
|
|
265
|
+
applyRemoveCargoTask(projected, task)
|
|
266
|
+
break
|
|
267
|
+
case TaskType.GATHER:
|
|
268
|
+
applyGatherTask(projected, task, {complete: true})
|
|
269
|
+
break
|
|
270
|
+
case TaskType.CRAFT:
|
|
271
|
+
applyCraftTask(projected, task)
|
|
272
|
+
break
|
|
273
|
+
case TaskType.DEPLOY:
|
|
274
|
+
applyDeployTask(projected, task)
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface ProjectionOptions {
|
|
280
|
+
upToTaskIndex?: number
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function projectEntity(entity: Projectable, options?: ProjectionOptions): ProjectedEntity {
|
|
215
284
|
const projected = createProjectedEntity(entity)
|
|
285
|
+
if (!entity.schedule || entity.schedule.tasks.length === 0) return projected
|
|
216
286
|
|
|
217
|
-
|
|
218
|
-
|
|
287
|
+
const tasks = entity.schedule.tasks
|
|
288
|
+
const taskCount =
|
|
289
|
+
options?.upToTaskIndex !== undefined
|
|
290
|
+
? Math.max(0, Math.min(options.upToTaskIndex, tasks.length))
|
|
291
|
+
: tasks.length
|
|
292
|
+
|
|
293
|
+
for (let i = 0; i < taskCount; i++) {
|
|
294
|
+
applyTask(projected, tasks[i])
|
|
219
295
|
}
|
|
296
|
+
return projected
|
|
297
|
+
}
|
|
220
298
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
299
|
+
function getRecipeForOutput(outputItemId: number): RecipeInput[] | undefined {
|
|
300
|
+
const component = getComponentById(outputItemId)
|
|
301
|
+
if (component) return component.recipe
|
|
302
|
+
const moduleRecipe = getModuleRecipeByItemId(outputItemId)
|
|
303
|
+
if (moduleRecipe) return moduleRecipe.recipe
|
|
304
|
+
const entityRecipe = getEntityRecipeByItemId(outputItemId)
|
|
305
|
+
if (entityRecipe) return entityRecipe.recipe
|
|
306
|
+
return undefined
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function validateCraftTask(task: ServerContract.Types.task, projected: ProjectedEntity): void {
|
|
310
|
+
if (task.cargo.length === 0) return
|
|
311
|
+
|
|
312
|
+
const output = task.cargo[task.cargo.length - 1]
|
|
313
|
+
const inputs = task.cargo.slice(0, -1)
|
|
314
|
+
const craftQuantity = output.quantity.toNumber()
|
|
315
|
+
|
|
316
|
+
const recipe = getRecipeForOutput(output.item_id.toNumber())
|
|
317
|
+
if (!recipe) throw new Error(RECIPE_NOT_FOUND)
|
|
318
|
+
|
|
319
|
+
const groupedInputs: ServerContract.Types.cargo_item[][] = recipe.map(() => [])
|
|
320
|
+
for (const input of inputs) {
|
|
321
|
+
let matched = false
|
|
322
|
+
for (let ri = 0; ri < recipe.length; ri++) {
|
|
323
|
+
const req = recipe[ri]
|
|
324
|
+
if (req.itemId && req.itemId > 0) {
|
|
325
|
+
if (input.item_id.toNumber() === req.itemId) {
|
|
326
|
+
groupedInputs[ri].push(input)
|
|
327
|
+
matched = true
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
} else if (req.category) {
|
|
331
|
+
const item = getItem(input.item_id)
|
|
332
|
+
if (item.category === req.category) {
|
|
333
|
+
groupedInputs[ri].push(input)
|
|
334
|
+
matched = true
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (!matched) throw new Error(RECIPE_INPUTS_INVALID)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (let ri = 0; ri < recipe.length; ri++) {
|
|
343
|
+
const stacks = groupedInputs[ri]
|
|
344
|
+
let provided = 0
|
|
345
|
+
for (const stack of stacks) {
|
|
346
|
+
provided += stack.quantity.toNumber()
|
|
347
|
+
}
|
|
348
|
+
const required = recipe[ri].quantity * craftQuantity
|
|
349
|
+
if (provided < required) throw new Error(RECIPE_INPUTS_INSUFFICIENT)
|
|
350
|
+
if (provided !== required) throw new Error(RECIPE_INPUTS_EXCESS)
|
|
351
|
+
|
|
352
|
+
if (!recipe[ri].itemId && stacks.length > 1) {
|
|
353
|
+
const firstItemId = stacks[0].item_id.toNumber()
|
|
354
|
+
for (let si = 1; si < stacks.length; si++) {
|
|
355
|
+
if (stacks[si].item_id.toNumber() !== firstItemId) {
|
|
356
|
+
throw new Error(RECIPE_INPUTS_MIXED)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const input of inputs) {
|
|
363
|
+
let found = false
|
|
364
|
+
for (const pc of projected.cargo) {
|
|
365
|
+
if (
|
|
366
|
+
pc.item_id.toNumber() === input.item_id.toNumber() &&
|
|
367
|
+
pc.stats.toString() === input.stats.toString()
|
|
368
|
+
) {
|
|
369
|
+
if (pc.quantity.toNumber() < input.quantity.toNumber()) {
|
|
370
|
+
throw new Error(RECIPE_INPUTS_INSUFFICIENT)
|
|
371
|
+
}
|
|
372
|
+
found = true
|
|
237
373
|
break
|
|
374
|
+
}
|
|
238
375
|
}
|
|
376
|
+
if (!found) throw new Error(SHIP_CARGO_NOT_LOADED)
|
|
239
377
|
}
|
|
378
|
+
}
|
|
240
379
|
|
|
241
|
-
|
|
380
|
+
export function validateSchedule(entity: Projectable): void {
|
|
381
|
+
if (!entity.schedule || entity.schedule.tasks.length === 0) return
|
|
382
|
+
|
|
383
|
+
const projected = createProjectedEntity(entity)
|
|
384
|
+
for (const task of entity.schedule.tasks) {
|
|
385
|
+
if (task.type.toNumber() === TaskType.CRAFT) {
|
|
386
|
+
validateCraftTask(task, projected)
|
|
387
|
+
}
|
|
388
|
+
applyTask(projected, task)
|
|
389
|
+
if (projected.capacity && projected.cargoMass.gt(projected.capacity)) {
|
|
390
|
+
throw new Error(ENTITY_CAPACITY_EXCEEDED)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
242
393
|
}
|
|
243
394
|
|
|
244
395
|
export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity {
|
|
@@ -269,19 +420,21 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
|
|
|
269
420
|
applyFlightTask(projected, task, {complete: taskComplete, progress})
|
|
270
421
|
break
|
|
271
422
|
case TaskType.LOAD:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
423
|
+
case TaskType.UNWRAP:
|
|
424
|
+
if (taskComplete) applyAddCargoTask(projected, task)
|
|
275
425
|
break
|
|
276
426
|
case TaskType.UNLOAD:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
427
|
+
case TaskType.WRAP:
|
|
428
|
+
if (taskComplete) applyRemoveCargoTask(projected, task)
|
|
280
429
|
break
|
|
281
|
-
case TaskType.
|
|
282
|
-
if (taskComplete) {
|
|
283
|
-
|
|
284
|
-
|
|
430
|
+
case TaskType.GATHER:
|
|
431
|
+
if (taskComplete) applyGatherTask(projected, task, {complete: true})
|
|
432
|
+
break
|
|
433
|
+
case TaskType.CRAFT:
|
|
434
|
+
if (taskComplete) applyCraftTask(projected, task)
|
|
435
|
+
break
|
|
436
|
+
case TaskType.DEPLOY:
|
|
437
|
+
if (taskComplete) applyDeployTask(projected, task)
|
|
285
438
|
break
|
|
286
439
|
}
|
|
287
440
|
}
|
|
@@ -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
|
+
}
|