dredless 0.2.0 → 0.2.1

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 (77) hide show
  1. package/README.md +48 -48
  2. package/docs/API_REFERENCE.md +2481 -2481
  3. package/docs/BROWSER_API.md +98 -98
  4. package/docs/NODE_API.md +359 -359
  5. package/package.json +34 -34
  6. package/src/browser.d.ts +212 -212
  7. package/src/browser.js +133 -133
  8. package/src/client/core.js +1202 -1202
  9. package/src/client/domains/index.js +1818 -1818
  10. package/src/client/index.d.ts +2 -2
  11. package/src/client/index.js +1 -1
  12. package/src/compression/lz4.d.ts +1 -1
  13. package/src/compression/lz4.js +112 -112
  14. package/src/constants.js +74 -74
  15. package/src/crypto/chacha-native.d.ts +9 -9
  16. package/src/crypto/chacha-native.js +48 -48
  17. package/src/crypto/chacha.d.ts +25 -25
  18. package/src/crypto/chacha.js +274 -274
  19. package/src/events.js +39 -39
  20. package/src/index.d.ts +2277 -2277
  21. package/src/index.js +125 -125
  22. package/src/network/browser-session.js +291 -291
  23. package/src/network/connection.d.ts +1 -1
  24. package/src/network/connection.js +31 -31
  25. package/src/network/cookies.js +85 -85
  26. package/src/network/http.js +34 -34
  27. package/src/network/index.d.ts +4 -4
  28. package/src/network/index.js +4 -4
  29. package/src/network/servers.d.ts +7 -7
  30. package/src/network/servers.js +133 -133
  31. package/src/network/session.d.ts +7 -7
  32. package/src/network/session.js +320 -320
  33. package/src/network/ships.d.ts +12 -12
  34. package/src/network/ships.js +73 -73
  35. package/src/node.d.ts +2 -2
  36. package/src/node.js +8 -8
  37. package/src/protocol/codec/binary.d.ts +3 -3
  38. package/src/protocol/codec/binary.js +52 -52
  39. package/src/protocol/codec/msgpack.d.ts +3 -3
  40. package/src/protocol/codec/msgpack.js +555 -555
  41. package/src/protocol/inbound/frame.d.ts +1 -1
  42. package/src/protocol/inbound/frame.js +8 -8
  43. package/src/protocol/inbound/world-payload.d.ts +3 -3
  44. package/src/protocol/inbound/world-payload.js +21 -21
  45. package/src/protocol/index.d.ts +11 -11
  46. package/src/protocol/index.js +19 -19
  47. package/src/protocol/outbound/blueprint.d.ts +22 -22
  48. package/src/protocol/outbound/blueprint.js +36 -36
  49. package/src/protocol/outbound/commands.d.ts +3 -3
  50. package/src/protocol/outbound/commands.js +149 -149
  51. package/src/protocol/outbound/comms.d.ts +28 -28
  52. package/src/protocol/outbound/comms.js +40 -40
  53. package/src/protocol/outbound/inventory.d.ts +64 -64
  54. package/src/protocol/outbound/inventory.js +147 -147
  55. package/src/protocol/outbound/ship-management.d.ts +84 -84
  56. package/src/protocol/outbound/ship-management.js +221 -221
  57. package/src/protocol/outbound/sign.d.ts +18 -18
  58. package/src/protocol/outbound/sign.js +45 -45
  59. package/src/protocol/outbound/ui-config.d.ts +157 -157
  60. package/src/protocol/outbound/ui-config.js +578 -578
  61. package/src/runtime.js +36 -36
  62. package/src/state/generator-maze.d.ts +32 -32
  63. package/src/state/generator-maze.js +370 -370
  64. package/src/state/index.d.ts +3 -3
  65. package/src/state/index.js +3 -3
  66. package/src/state/items.js +20 -20
  67. package/src/state/model/decoder.js +1554 -1554
  68. package/src/state/model/index.d.ts +33 -33
  69. package/src/state/model/index.js +2 -2
  70. package/src/state/model/loader-config.js +1020 -1020
  71. package/src/state/model/reader.js +69 -69
  72. package/src/state/model/state.js +1257 -1257
  73. package/src/state/overworld.js +72 -72
  74. package/src/state/tilesets.js +53 -53
  75. package/src/state/world/index.d.ts +29 -29
  76. package/src/state/world/index.js +180 -180
  77. package/src/state/world/state.js +297 -297
@@ -1,1202 +1,1202 @@
1
- import {INITIAL_OUTFIT_MESSAGE, JOIN_USER_AGENT, KEEPALIVE_INTERVAL_MS, KEEPALIVE_MESSAGE} from "../constants.js";
2
- import {getWebSocket, isNode} from "../runtime.js";
3
- import {EventBus} from "../events.js";
4
- import {cookieHeader} from "../network/cookies.js";
5
- import {fetchServers} from "../network/servers.js";
6
- import {Connection} from "../network/connection.js";
7
- import {itemEquipmentSlotFromId} from "../state/items.js";
8
- import {WorldStore} from "../state/world/index.js";
9
- import {buildBlueprintPlacementMessage} from "../protocol/outbound/blueprint.js";
10
- import {buildCommsMessage, normalizeCommsEvent} from "../protocol/outbound/comms.js";
11
- import {buildSignedCommandPacket} from "../protocol/outbound/commands.js";
12
- import {buildInventoryDragCommand, normalizeEquipmentSlot, normalizeInventoryEvent} from "../protocol/outbound/inventory.js";
13
- import {
14
- buildBanPlayerMessage,
15
- buildDemoteSelfMessage,
16
- buildInviteResetMessage,
17
- buildKickPlayerMessage,
18
- buildPlayerListMessage,
19
- buildSetPlayerRankMessage,
20
- buildShipManagementMessage,
21
- buildShipPrivacyMessage,
22
- buildStarterRecoveryMessage,
23
- normalizeCaptainSubrank,
24
- normalizeShipConfig,
25
- normalizeShipPlayerList
26
- } from "../protocol/outbound/ship-management.js";
27
- import {buildSignTextMessage} from "../protocol/outbound/sign.js";
28
- import {
29
- buildCargoEjectorClipboardDirectionData,
30
- buildCargoEjectorCopyConfigData,
31
- buildCargoEjectorDirectionData,
32
- buildCargoEjectorPasteConfigData,
33
- buildCargoHatchCopyConfigData,
34
- buildCargoHatchFilterConfigData,
35
- buildCargoHatchFilterItemsData,
36
- buildCargoHatchFullConfigData,
37
- buildClipboardConfigData,
38
- buildClipboardFixedAngleData,
39
- buildExpandoClipboardAngleData,
40
- buildGeneratorClipboardDirectionData,
41
- buildGeneratorMazePuzzleData,
42
- buildLoaderClipboardConfigData,
43
- buildLoaderConfigData,
44
- buildLoaderCopyConfigData,
45
- buildLoaderFilterConfigData,
46
- buildLoaderFilterItemsData,
47
- buildLoaderFullConfigData,
48
- buildNavigationUnitClipboardConfigData,
49
- buildNavigationUnitConfigData,
50
- buildNavigationUnitPasteConfigData,
51
- buildPusherConfigData,
52
- buildPusherFilterItemsData
53
- } from "../protocol/outbound/ui-config.js";
54
- import {cloneCommand, encodeMsgpack} from "../protocol/codec/msgpack.js";
55
- import {decodeIncomingFrame} from "../protocol/inbound/frame.js";
56
- import {
57
- ClientNetDomain,
58
- ClientDebugDomain,
59
- PlayerDomain,
60
- ShipManagementDomain,
61
- InventoryDomain,
62
- WorldDomain,
63
- ShipDomain,
64
- OverworldDomain,
65
- EntityHandle,
66
- WrenchMode,
67
- TurretMode,
68
- normalizeAttachMode,
69
- normalizeScannerResultPacket,
70
- normalizeShipReadOptions,
71
- shipReadSummary,
72
- currentPlayerSummary,
73
- summariesFor,
74
- emptyMachineSummary,
75
- normalizeDegrees,
76
- normalizeLauncherPower,
77
- normalizeWrenchMode,
78
- normalizeTurretMode,
79
- normalizePositiveFinite,
80
- normalizePositiveInteger
81
- } from "./domains/index.js";
82
-
83
- export {WrenchMode, TurretMode} from "./domains/index.js";
84
-
85
- // Defaults for retained history. Every one of these buffers was previously
86
- // unbounded; the surrounding buffers (chat, warnings, scanner results) already
87
- // capped at 200, so these match that convention.
88
- const DEFAULT_PACKET_HISTORY = 200;
89
- const DEFAULT_EVENT_HISTORY = 200;
90
- const DEFAULT_MODEL_PACKET_HISTORY = 100;
91
- const DEFAULT_CHUNK_HISTORY = 64;
92
-
93
- function normalizeHistoryLimit(value, fallback) {
94
- if (value === Infinity) return Infinity;
95
- const limit = Number(value);
96
- if (!Number.isFinite(limit) || limit < 0) return fallback;
97
- return Math.floor(limit);
98
- }
99
-
100
- export class DredlessClient extends EventBus {
101
- constructor(connection, {
102
- connect = true,
103
- attach = false,
104
- mode = null,
105
- baseUrl = null,
106
- session = null,
107
- serverId = null,
108
- server = null,
109
- netPort = null,
110
- gameToken = "",
111
- packetHistory = DEFAULT_PACKET_HISTORY,
112
- eventHistory = DEFAULT_EVENT_HISTORY,
113
- modelPacketHistory = DEFAULT_MODEL_PACKET_HISTORY,
114
- chunkHistory = DEFAULT_CHUNK_HISTORY
115
- } = {}) {
116
- super();
117
- const attachMode = attach ? normalizeAttachMode(mode) : null;
118
- if (!(connection instanceof Connection) && !attachMode) throw new Error("DredlessClient requires a Connection");
119
-
120
- this.connection = connection || null;
121
- this.session = connection?.session ?? session ?? null;
122
- this.baseUrl = connection?.baseUrl ?? baseUrl ?? this.session?.baseUrl ?? null;
123
- this.serverId = connection?.serverId ?? serverId ?? server?.index ?? null;
124
- this.server = connection?.server ?? server ?? null;
125
- this.netPort = connection?.netPort ?? netPort ?? null;
126
- this.gameToken = connection?.gameToken ?? gameToken ?? "";
127
- this.ws = null;
128
- this.sid = null;
129
- this.connected = false;
130
- this.ready = false;
131
- this.packetCount = 0;
132
- this.lastPacket = null;
133
- this.packets = [];
134
- // Retained history is bounded by default: these arrays previously grew for
135
- // the lifetime of the client, holding every decoded packet and its binary
136
- // payloads. Pass 0 to disable a buffer or Infinity to restore unbounded
137
- // capture behaviour.
138
- this.packetHistory = normalizeHistoryLimit(packetHistory, DEFAULT_PACKET_HISTORY);
139
- this.worlds = new WorldStore({eventHistory, modelPacketHistory, chunkHistory});
140
- this.net = new ClientNetDomain(this);
141
- this.debug = new ClientDebugDomain(this);
142
- this.player = new PlayerDomain(this);
143
- this.inventory = new InventoryDomain(this);
144
- this.management = new ShipManagementDomain(this);
145
- this.cpuLoad = null;
146
- this.inventoryState = null;
147
- this.puiPanels = new Map();
148
- this.commsPanels = new Map();
149
- this.currentCommsPanel = null;
150
- this.warnings = [];
151
- this.effects = [];
152
- this.chat = [];
153
- this.motd = [];
154
- this.sessionMessages = [];
155
- this.scannerResults = [];
156
- this.lastScannerResult = null;
157
- this.shipConfig = null;
158
- this.captainSubrank = null;
159
- this.playerList = null;
160
- this.outfits = new Map();
161
- this.commandAcks = new Map();
162
- this.lastCommandAck = null;
163
- this.decodeErrors = [];
164
- this.readyPromise = new Promise((resolve, reject) => {
165
- this.#resolveReady = resolve;
166
- this.#rejectReady = reject;
167
- });
168
-
169
- this.attachMode = attachMode;
170
- this.attached = Boolean(attachMode);
171
- if (connect) this.#connect().catch((error) => this.#fail(error));
172
- }
173
-
174
- static attachWebSocket(websocket, options = {}) {
175
- if (!websocket) throw new Error("DredlessClient.attachWebSocket requires a websocket");
176
- const mode = normalizeAttachMode(options.mode);
177
- const client = new DredlessClient(null, {...options, attach: true, connect: false, mode});
178
- client.#attachSocket(websocket, {...options, mode});
179
- return client;
180
- }
181
-
182
- #commandNumber = 1;
183
- #queuedCommands = [];
184
- #queuedMessages = [];
185
- #keepalive = null;
186
- #bootstrapped = false;
187
- #ownsSocket = true;
188
- #allowWrites = true;
189
- #attachedOpened = false;
190
- #resolveReady = null;
191
- #rejectReady = null;
192
- #inputSettings = {
193
- wrench_mode: 1,
194
- turret_mode: 0
195
- };
196
-
197
- whenReady() {
198
- return this.readyPromise;
199
- }
200
-
201
- send(command = {}) {
202
- this.#assertWritable();
203
- const normalized = cloneCommand({...this.#inputSettings, ...command});
204
- if (normalized.n == null) normalized.n = this.#commandNumber++;
205
- if (!this.sid) {
206
- this.#queuedCommands.push(normalized);
207
- return this;
208
- }
209
- this.ws.send(buildSignedCommandPacket(normalized, this.sid));
210
- this.emit("command", normalized);
211
- return this;
212
- }
213
-
214
- sendMessage(message, {afterReady = true} = {}) {
215
- this.#assertWritable();
216
- if (!this.connected || (afterReady && !this.ready)) {
217
- this.#queuedMessages.push({message, afterReady});
218
- return this;
219
- }
220
- this.ws.send(encodeMsgpack(message));
221
- this.emit("message", message);
222
- return this;
223
- }
224
-
225
- sendRaw(message, options = {}) {
226
- return this.sendMessage(message, options);
227
- }
228
-
229
- sendBlueprintPlacement(placement) {
230
- return this.sendMessage(buildBlueprintPlacementMessage(placement));
231
- }
232
-
233
- setOutfit(outfit) {
234
- return this.sendMessage({type: 7, outfit});
235
- }
236
-
237
- sendShipManagement(act, arg = null) {
238
- return this.sendMessage(buildShipManagementMessage(act, arg));
239
- }
240
-
241
- requestPlayerList() {
242
- return this.sendMessage(buildPlayerListMessage());
243
- }
244
-
245
- resetInvite() {
246
- return this.sendMessage(buildInviteResetMessage());
247
- }
248
-
249
- setShipPrivacy(privacy) {
250
- return this.sendMessage(buildShipPrivacyMessage(privacy));
251
- }
252
-
253
- recoverStarterItem(itemId) {
254
- return this.sendMessage(buildStarterRecoveryMessage(itemId));
255
- }
256
-
257
- setPlayerRank(refId, rank) {
258
- return this.sendMessage(buildSetPlayerRankMessage(refId, rank));
259
- }
260
-
261
- promotePlayerToCaptain(refId) {
262
- return this.setPlayerRank(refId, "captain");
263
- }
264
-
265
- demotePlayerToCrew(refId) {
266
- return this.setPlayerRank(refId, "crew");
267
- }
268
-
269
- demotePlayerToGuest(refId) {
270
- return this.setPlayerRank(refId, "guest");
271
- }
272
-
273
- kickPlayer(refId) {
274
- return this.sendMessage(buildKickPlayerMessage(refId));
275
- }
276
-
277
- banPlayer(refId) {
278
- return this.sendMessage(buildBanPlayerMessage(refId));
279
- }
280
-
281
- demoteSelf() {
282
- return this.sendMessage(buildDemoteSelfMessage());
283
- }
284
-
285
- sendEntityCommand(cmd, args = [-1, -1, -1]) {
286
- return this.sendMessage({type: 5, cmd, args});
287
- }
288
-
289
- sendCommsMessage(message = "") {
290
- return this.sendMessage(buildCommsMessage(message));
291
- }
292
-
293
- sendFabricatorMessage(cmd, args = [-1, -1, -1]) {
294
- return this.sendEntityCommand(cmd, args);
295
- }
296
-
297
- sendFabricatorCommand(itemId, count = 1, index = -1) {
298
- return this.craftAdd(itemId, count, index);
299
- }
300
-
301
- craftAdd(itemId, count = 1, index = -1) {
302
- return this.sendFabricatorMessage("craft_add", [itemId, count, index]);
303
- }
304
-
305
- craftSub(itemId, count = 1, index = 0) {
306
- return this.sendFabricatorMessage("craft_sub", [itemId, count, index]);
307
- }
308
-
309
- craftClearQueue() {
310
- return this.sendFabricatorMessage("clear_queue");
311
- }
312
-
313
- craftToggleRepeat() {
314
- return this.sendFabricatorMessage("toggle_repeat");
315
- }
316
-
317
- fabricatorLockResource(row) {
318
- return this.sendFabricatorMessage("lock", [row, -1, -1]);
319
- }
320
-
321
- fabricatorUnlockResource(row) {
322
- return this.sendFabricatorMessage("unlock", [row, -1, -1]);
323
- }
324
-
325
- fabricatorEject(row) {
326
- return this.sendFabricatorMessage("eject", [row, -1, -1]);
327
- }
328
-
329
- setLauncherAngle(angle) {
330
- return this.sendEntityCommand("angle", [normalizeDegrees(angle)]);
331
- }
332
-
333
- setLauncherPower(power) {
334
- return this.sendEntityCommand("power", [normalizeLauncherPower(power)]);
335
- }
336
-
337
- setSignText(text = "", mode = 0) {
338
- return this.sendMessage(buildSignTextMessage(text, mode));
339
- }
340
-
341
- sendUiConfig(data) {
342
- return this.sendMessage({type: 8, data});
343
- }
344
-
345
- solveGeneratorPuzzle(entity, solution) {
346
- return this.sendUiConfig(buildGeneratorMazePuzzleData(entity, solution));
347
- }
348
-
349
- sendPusherConfig(entity, config = {}) {
350
- return this.sendUiConfig(buildPusherConfigData(entity, this.#pusherConfigDefaults(entity, config)));
351
- }
352
-
353
- setPusherAngle(entity, angle, config = {}) {
354
- return this.sendPusherConfig(entity, {...config, angle});
355
- }
356
-
357
- setPusherSpeed(entity, speed, config = {}) {
358
- return this.sendPusherConfig(entity, {...config, speed});
359
- }
360
-
361
- setPusherLength(entity, length, config = {}) {
362
- return this.sendPusherConfig(entity, {...config, length});
363
- }
364
-
365
- setPusherMode(entity, mode, config = {}) {
366
- return this.sendPusherConfig(entity, {...config, mode});
367
- }
368
-
369
- setPusherFilteredMode(entity, filteredMode, config = {}) {
370
- return this.sendPusherConfig(entity, {...config, filteredMode});
371
- }
372
-
373
- setPusherFilterInventory(entity, filterInventory, config = {}) {
374
- return this.sendPusherConfig(entity, {...config, filterInventory});
375
- }
376
-
377
- setPusherFilterItems(entity, filterSlots = []) {
378
- return this.sendUiConfig(buildPusherFilterItemsData(entity, filterSlots));
379
- }
380
-
381
- sendLoaderConfig(entity, config = {}) {
382
- return this.sendUiConfig(buildLoaderConfigData(entity, this.#loaderConfigDefaults(entity, config)));
383
- }
384
-
385
- sendLoaderFullConfig(entity, config = {}) {
386
- return this.sendUiConfig(buildLoaderFullConfigData(entity, this.#loaderFullConfigDefaults(entity, config)));
387
- }
388
-
389
- copyLoaderConfig(entity, config = {}) {
390
- return this.sendUiConfig(buildLoaderCopyConfigData(this.#loaderFullConfigDefaults(entity, config)));
391
- }
392
-
393
- sendLoaderClipboardConfig(config = {}) {
394
- return this.sendUiConfig(buildLoaderClipboardConfigData(config));
395
- }
396
-
397
- sendClipboardConfig(target, commandName, values = []) {
398
- return this.sendUiConfig(buildClipboardConfigData(target, commandName, values));
399
- }
400
-
401
- setClipboardFixedAngle(target, direction) {
402
- return this.sendUiConfig(buildClipboardFixedAngleData(target, direction));
403
- }
404
-
405
- setGeneratorClipboardDirection(direction) {
406
- return this.sendUiConfig(buildGeneratorClipboardDirectionData(direction));
407
- }
408
-
409
- setExpandoClipboardAngle(angle) {
410
- return this.sendUiConfig(buildExpandoClipboardAngleData(angle));
411
- }
412
-
413
- setCargoEjectorDirection(entity, direction) {
414
- return this.sendUiConfig(buildCargoEjectorDirectionData(entity, direction));
415
- }
416
-
417
- pasteCargoEjectorConfig(entity, direction = "right") {
418
- return this.sendUiConfig(buildCargoEjectorPasteConfigData(entity, direction));
419
- }
420
-
421
- copyCargoEjectorConfig(entity, direction = "right") {
422
- return this.sendUiConfig(buildCargoEjectorCopyConfigData(direction));
423
- }
424
-
425
- setCargoEjectorClipboardDirection(direction) {
426
- return this.sendUiConfig(buildCargoEjectorClipboardDirectionData(direction));
427
- }
428
-
429
- setLoaderPickPlace(entity, pick, place, config = {}) {
430
- return this.sendLoaderConfig(entity, {...config, pick, place});
431
- }
432
-
433
- setLoaderPriority(entity, priority, config = {}) {
434
- return this.sendLoaderConfig(entity, {...config, priority});
435
- }
436
-
437
- setLoaderStack(entity, stack, config = {}) {
438
- return this.sendLoaderConfig(entity, {...config, stack});
439
- }
440
-
441
- setLoaderCycle(entity, cycle, config = {}) {
442
- return this.sendLoaderConfig(entity, {...config, cycle});
443
- }
444
-
445
- setLoaderRequireOutput(entity, requireOutput, config = {}) {
446
- return this.sendLoaderConfig(entity, {...config, requireOutput});
447
- }
448
-
449
- setLoaderWaitForStack(entity, waitForStack, config = {}) {
450
- return this.sendLoaderConfig(entity, {...config, waitForStack});
451
- }
452
-
453
- setLoaderFilterMode(entity, filterMode) {
454
- return this.sendUiConfig(buildLoaderFilterConfigData(entity, filterMode));
455
- }
456
-
457
- setLoaderFilterItems(entity, filterSlots = []) {
458
- return this.sendUiConfig(buildLoaderFilterItemsData(entity, filterSlots));
459
- }
460
-
461
- setCargoHatchFilterMode(entity, filterMode) {
462
- return this.sendUiConfig(buildCargoHatchFilterConfigData(entity, filterMode));
463
- }
464
-
465
- setCargoHatchFilterItems(entity, filterSlots = []) {
466
- return this.sendUiConfig(buildCargoHatchFilterItemsData(entity, filterSlots));
467
- }
468
-
469
- sendCargoHatchFullConfig(entity, config = {}) {
470
- return this.sendUiConfig(buildCargoHatchFullConfigData(entity, this.#cargoHatchConfigDefaults(entity, config)));
471
- }
472
-
473
- pasteCargoHatchConfig(entity, config = {}) {
474
- return this.sendCargoHatchFullConfig(entity, config);
475
- }
476
-
477
- copyCargoHatchConfig(entity, config = {}) {
478
- return this.sendUiConfig(buildCargoHatchCopyConfigData(this.#cargoHatchConfigDefaults(entity, config)));
479
- }
480
-
481
- inputSettings() {
482
- return {
483
- wrenchMode: wrenchModeName(this.#inputSettings.wrench_mode),
484
- turretMode: turretModeName(this.#inputSettings.turret_mode),
485
- viewWidth: this.#inputSettings.vx ?? null,
486
- viewHeight: this.#inputSettings.vy ?? null,
487
- screenWidth: this.#inputSettings.scr_w ?? null,
488
- screenHeight: this.#inputSettings.scr_h ?? null
489
- };
490
- }
491
-
492
- setInputSettings(settings = {}, {send = true} = {}) {
493
- const wrench = settings.wrenchMode;
494
- const turret = settings.turretMode;
495
- const viewWidth = settings.viewWidth;
496
- const viewHeight = settings.viewHeight;
497
- const screenWidth = settings.screenWidth;
498
- const screenHeight = settings.screenHeight;
499
- if (wrench != null) this.#inputSettings.wrench_mode = normalizeWrenchMode(wrench);
500
- if (turret != null) this.#inputSettings.turret_mode = normalizeTurretMode(turret);
501
- if (viewWidth != null) this.#inputSettings.vx = normalizePositiveFinite(viewWidth, "viewWidth");
502
- if (viewHeight != null) this.#inputSettings.vy = normalizePositiveFinite(viewHeight, "viewHeight");
503
- if (screenWidth != null) this.#inputSettings.scr_w = normalizePositiveInteger(screenWidth, "screenWidth");
504
- if (screenHeight != null) this.#inputSettings.scr_h = normalizePositiveInteger(screenHeight, "screenHeight");
505
- return send ? this.send({}) : this;
506
- }
507
-
508
- setView(width, height, options = {}) {
509
- return this.setInputSettings({viewWidth: width, viewHeight: height}, options);
510
- }
511
-
512
- setScreenSize(width, height, options = {}) {
513
- return this.setInputSettings({screenWidth: width, screenHeight: height}, options);
514
- }
515
-
516
- setWrenchMode(mode, options = {}) {
517
- return this.setInputSettings({wrenchMode: mode}, options);
518
- }
519
-
520
- setWrenchAction(mode, options = {}) {
521
- return this.setWrenchMode(mode, options);
522
- }
523
-
524
- setTurretMode(mode, options = {}) {
525
- return this.setInputSettings({turretMode: mode}, options);
526
- }
527
-
528
- sendNavigationUnitConfig(entity, config = {}) {
529
- return this.sendUiConfig(buildNavigationUnitConfigData(entity, this.#navigationUnitConfigDefaults(entity, config)));
530
- }
531
-
532
- copyNavigationUnitConfig(entity, config = {}) {
533
- return this.sendUiConfig(buildNavigationUnitClipboardConfigData(this.#navigationUnitConfigDefaults(entity, config)));
534
- }
535
-
536
- pasteNavigationUnitConfig(entity, config = {}) {
537
- return this.sendUiConfig(buildNavigationUnitPasteConfigData(entity, this.#navigationUnitConfigDefaults(entity, config)));
538
- }
539
-
540
- setNavigationDestination(entity, destination, config = {}) {
541
- return this.sendNavigationUnitConfig(entity, {...config, destination});
542
- }
543
-
544
- setNavigationAutoWarp(entity, config = {}) {
545
- return this.sendNavigationUnitConfig(entity, {page: 1, ...config});
546
- }
547
-
548
- startWarp(entity, config = {}) {
549
- return this.sendNavigationUnitConfig(entity, {page: 1, ...config, warp: "start"});
550
- }
551
-
552
- cancelWarp(entity, config = {}) {
553
- return this.sendNavigationUnitConfig(entity, {page: 1, ...config, warp: "cancel"});
554
- }
555
-
556
- move(x = 0, y = 0, command = {}) {
557
- return this.send({...command, x, y});
558
- }
559
-
560
- aim(mx = 0, my = 0, command = {}) {
561
- return this.send({...command, mx, my});
562
- }
563
-
564
- action(flags = {}, command = {}) {
565
- return this.send({...command, ...flags});
566
- }
567
-
568
- useEntity(entity, {invSlot = 0, hold = true} = {}, command = {}) {
569
- return this.send({
570
- ...command,
571
- focus_ent: entity,
572
- inv_slot: invSlot,
573
- act1: true,
574
- act1_held: Boolean(hold)
575
- });
576
- }
577
-
578
- useHeldItem({invSlot = 0, hold = true} = {}, command = {}) {
579
- return this.send({
580
- ...command,
581
- focus_ent: null,
582
- inv_slot: invSlot,
583
- act1: true,
584
- act1_held: Boolean(hold)
585
- });
586
- }
587
-
588
- placeHeldItem(options = {}, command = {}) {
589
- return this.useHeldItem(options, command);
590
- }
591
-
592
- placeBlueprint(placement, {invSlot = 0, hold = true, mx = null, my = null} = {}, command = {}) {
593
- const message = buildBlueprintPlacementMessage(placement);
594
- this.sendMessage(message);
595
- return this.placeHeldItem(
596
- {invSlot, hold},
597
- {
598
- ...command,
599
- mx: mx ?? message.x,
600
- my: my ?? message.y
601
- }
602
- );
603
- }
604
-
605
- rotateHeldItem({invSlot = 0, hold = true} = {}, command = {}) {
606
- return this.send({
607
- ...command,
608
- focus_ent: null,
609
- inv_slot: invSlot,
610
- act_alt: true,
611
- act_alt_held: Boolean(hold)
612
- });
613
- }
614
-
615
-
616
- close(code = 1000, reason = "client") {
617
- if (!this.#allowWrites) return this;
618
- try {
619
- this.ws?.close(code, reason);
620
- } catch (_) {
621
- }
622
- return this;
623
- }
624
-
625
- disconnect(code = 1000, reason = "client") {
626
- return this.close(code, reason);
627
- }
628
-
629
- snapshot({includeTiles = false, includeModel = false} = {}) {
630
- return {
631
- baseUrl: this.baseUrl,
632
- session: this.session?.toJSON?.() || this.session,
633
- connection: this.connection?.toJSON?.() || null,
634
- attachMode: this.attachMode,
635
- serverId: this.serverId,
636
- server: this.server,
637
- netPort: this.netPort,
638
- sid: this.sid,
639
- ready: this.ready,
640
- connected: this.connected,
641
- currentWorldId: this.worlds.currentWorldId,
642
- worlds: this.worlds.snapshot({includeTiles, includeModel}),
643
- cpuLoad: this.cpuLoad,
644
- inventory: this.inventoryState,
645
- puiPanels: [...this.puiPanels.values()],
646
- commsPanels: [...this.commsPanels.values()],
647
- currentCommsPanel: this.currentCommsPanel,
648
- warnings: this.warnings.slice(-50),
649
- effects: this.effects.slice(-50),
650
- chat: this.chat.slice(-50),
651
- motd: this.motd.slice(-20),
652
- sessionMessages: this.sessionMessages.slice(-50),
653
- scannerResults: this.scannerResults.slice(-20),
654
- lastScannerResult: this.lastScannerResult,
655
- shipConfig: this.shipConfig,
656
- captainSubrank: this.captainSubrank,
657
- playerList: this.playerList,
658
- outfits: [...this.outfits.entries()].map(([sid, outfit]) => ({sid, outfit})),
659
- commandAcks: [...this.commandAcks.entries()].map(([world, commandNumber]) => ({world, commandNumber})),
660
- lastCommandAck: this.lastCommandAck,
661
- decodeErrors: this.decodeErrors.slice(-50),
662
- packetCount: this.packetCount,
663
- lastPacket: this.lastPacket
664
- };
665
- }
666
-
667
- state(options = {}) {
668
- return this.snapshot(options);
669
- }
670
-
671
- world(id) {
672
- const world = this.worlds.worlds.get(Number(id)) || null;
673
- return world ? new WorldDomain(this, world.id) : null;
674
- }
675
-
676
- overworld() {
677
- const world = this.worlds.overworld();
678
- return world ? new OverworldDomain(this, world.id) : null;
679
- }
680
-
681
- currentShip() {
682
- const world = this.worlds.shipWorld();
683
- return world ? new ShipDomain(this, world.id) : null;
684
- }
685
-
686
- shipWorld(options = {}) {
687
- return this.worlds.shipWorld()?.snapshot(options) || null;
688
- }
689
-
690
- ship() {
691
- return this.currentShip();
692
- }
693
-
694
- shipEntity() {
695
- return this.worlds.currentShipEntity();
696
- }
697
-
698
- currentPlayerEntity() {
699
- const player = currentPlayerSummary(this);
700
- return player ? new EntityHandle(this, player.entity, "current") : null;
701
- }
702
-
703
- entities(scope = "ship") {
704
- return summariesFor(this, scope).map(entitySnapshotFor);
705
- }
706
-
707
- entity(entityId, scope = "ship") {
708
- const summary = this.#readWorld(scope)?.entity(entityId) || null;
709
- return entitySnapshotFor(summary);
710
- }
711
-
712
- blocks(scope = "ship") {
713
- return this.#readWorld(scope)?.blocks() || [];
714
- }
715
-
716
- materials(scope = "ship") {
717
- return this.#readWorld(scope)?.materials() || [];
718
- }
719
-
720
- machines(scope = "ship") {
721
- return this.#readWorld(scope)?.model.machines() || emptyMachineSummary();
722
- }
723
-
724
- players(scope = "ship") {
725
- return this.#readWorld(scope)?.model.players() || [];
726
- }
727
-
728
- shipControls(scope = "overworld") {
729
- return this.#readWorld(scope)?.model.shipControls() || [];
730
- }
731
-
732
- ships(options = {}) {
733
- const normalized = normalizeShipReadOptions(options);
734
- const overworld = this.worlds.overworld();
735
- if (!overworld) return [];
736
- // Resolve the reference entity once rather than per ship.
737
- const current = this.worlds.currentShipEntity();
738
- const ships = overworld.entities()
739
- .filter((entity) => entity?.contents?.shipControl)
740
- .map((entity) => shipReadSummary(entity, normalized, this.worlds, current));
741
- if (normalized.sort === "distance") {
742
- ships.sort((a, b) => (a.distance ?? Number.POSITIVE_INFINITY) - (b.distance ?? Number.POSITIVE_INFINITY));
743
- }
744
- return ships;
745
- }
746
-
747
- shipByHex(hexCode, options = {}) {
748
- const normalized = String(hexCode ?? "").toUpperCase();
749
- return this.#findShip(options, (control) => String(control.hexCode ?? "").toUpperCase() === normalized);
750
- }
751
-
752
- shipByEntity(entityId, options = {}) {
753
- const id = Number(entityId);
754
- return this.#findShip(options, (control, entity) => entity.entity === id);
755
- }
756
-
757
- // Match against the ship control record and summarise only the hit. These
758
- // lookups used to build a full summary -- including a world snapshot -- for
759
- // every ship in the overworld before discarding all but one.
760
- #findShip(options, predicate) {
761
- const overworld = this.worlds.overworld();
762
- if (!overworld) return null;
763
- for (const entity of overworld.entities()) {
764
- const control = entity?.contents?.shipControl;
765
- if (!control || !predicate(control, entity)) continue;
766
- return shipReadSummary(entity, normalizeShipReadOptions(options), this.worlds);
767
- }
768
- return null;
769
- }
770
-
771
- get packetsRaw() {
772
- return this.packets.slice();
773
- }
774
-
775
- async #connect() {
776
- if (!this.server) {
777
- const servers = await fetchServers(this.baseUrl);
778
- this.server = servers.find((server) => server.index === this.serverId) || null;
779
- }
780
- if (!this.server?.domain) throw new Error(`Unable to resolve game server ${this.serverId}`);
781
-
782
- const WebSocket = await getWebSocket();
783
- const wsUrl = `wss://${this.server.domain}:${this.netPort}`;
784
- this.ws = this.#openSocket(WebSocket, wsUrl, this.#wsHeaders());
785
- this.ws.binaryType = "arraybuffer";
786
- this.#ownsSocket = true;
787
- this.#allowWrites = true;
788
- this.#bindSocket();
789
- }
790
-
791
- #attachSocket(websocket, {connected = null, ready = false, sid = null, mode = "observe"} = {}) {
792
- this.ws = websocket;
793
- this.attachMode = normalizeAttachMode(mode);
794
- this.#ownsSocket = this.attachMode === "bootstrap";
795
- this.#allowWrites = this.attachMode !== "readonly";
796
- try {
797
- this.ws.binaryType = "arraybuffer";
798
- } catch (_) {
799
- }
800
- if (sid != null) this.sid = sid >>> 0;
801
- this.connected = connected ?? this.#socketLooksOpen(websocket);
802
- if (ready) {
803
- this.ready = true;
804
- if (this.#ownsSocket) {
805
- this.#sendBootstrap();
806
- this.#startKeepalive();
807
- }
808
- if (this.#allowWrites) {
809
- this.#flushMessages();
810
- this.#flushCommands();
811
- }
812
- this.#resolveReady?.(this);
813
- this.#resolveReady = null;
814
- }
815
- this.#bindAttachedSocket();
816
- if (this.connected && this.#ownsSocket) this.#attachedOpen();
817
- this.emit("attach", this);
818
- }
819
-
820
- #bindSocket() {
821
- this.ws.onopen = () => {
822
- this.connected = true;
823
- this.ws.send(encodeMsgpack({type: 1}));
824
- this.emit("open", this);
825
- this.#flushMessages();
826
- };
827
-
828
- this.ws.onmessage = (event) => this.#handleSocketMessage(event.data);
829
- this.ws.onerror = (event) => this.#fail(event?.error || new Error("WebSocket error"));
830
- this.ws.onclose = (event) => this.#handleSocketClose(event);
831
- }
832
-
833
- #bindAttachedSocket() {
834
- const open = () => this.#attachedOpen();
835
- const message = (event) => this.#handleSocketMessage(event?.data ?? event);
836
- const error = (event) => this.#fail(event?.error || new Error("WebSocket error"));
837
- const close = (event) => this.#handleSocketClose(event);
838
-
839
- if (typeof this.ws.addEventListener === "function") {
840
- this.ws.addEventListener("open", open);
841
- this.ws.addEventListener("message", message);
842
- this.ws.addEventListener("error", error);
843
- this.ws.addEventListener("close", close);
844
- return;
845
- }
846
- if (typeof this.ws.on === "function") {
847
- this.ws.on("open", open);
848
- this.ws.on("message", (data) => this.#handleMessage(data));
849
- this.ws.on("error", error);
850
- this.ws.on("close", close);
851
- return;
852
- }
853
- const previousMessage = this.ws.onmessage;
854
- this.ws.onmessage = (event) => {
855
- if (typeof previousMessage === "function") previousMessage.call(this.ws, event);
856
- message(event);
857
- };
858
- }
859
-
860
- #attachedOpen() {
861
- this.connected = true;
862
- if (this.#attachedOpened) return;
863
- this.#attachedOpened = true;
864
- if (this.#ownsSocket) this.ws.send(encodeMsgpack({type: 1}));
865
- this.emit("open", this);
866
- if (this.#allowWrites) this.#flushMessages();
867
- }
868
-
869
- #handleSocketClose(event) {
870
- this.connected = false;
871
- clearInterval(this.#keepalive);
872
- this.#keepalive = null;
873
- this.emit("close", event);
874
- }
875
-
876
- #socketLooksOpen(websocket) {
877
- return websocket?.readyState == null || websocket.readyState === 1 || websocket.readyState === websocket.OPEN;
878
- }
879
-
880
- #assertWritable() {
881
- if (!this.#allowWrites) throw new Error("DredlessClient is attached in readonly mode");
882
- }
883
-
884
- #handleSocketMessage(data) {
885
- if (data && typeof data.arrayBuffer === "function") {
886
- data.arrayBuffer()
887
- .then((buffer) => this.#handleMessage(buffer))
888
- .catch((error) => this.#fail(error));
889
- return;
890
- }
891
- this.#handleMessage(data);
892
- }
893
-
894
- #handleMessage(data) {
895
- let packet;
896
- try {
897
- packet = decodeIncomingFrame(data);
898
- } catch (error) {
899
- this.#fail(error);
900
- return;
901
- }
902
- this.packetCount += 1;
903
- this.lastPacket = packet;
904
- this.#pushLimited(this.packets, packet, this.packetHistory);
905
- this.emit("packet", packet);
906
-
907
- if (!packet || typeof packet !== "object") return;
908
- try {
909
- if (packet.type === 21) return this.#markReady(packet);
910
- const worldUpdate = this.worlds.apply(packet);
911
- if (worldUpdate) {
912
- this.#handleWorldUpdate(worldUpdate);
913
- } else {
914
- this.#handlePacketEvent(packet);
915
- }
916
- } catch (error) {
917
- this.#recordDecodeError(error, packet);
918
- }
919
- }
920
-
921
- #markReady(packet) {
922
- this.sid = packet.sid >>> 0;
923
- this.worlds.currentWorldId = packet.world ?? this.worlds.currentWorldId;
924
- this.ready = true;
925
- if (this.#ownsSocket) {
926
- this.#sendBootstrap();
927
- this.#startKeepalive();
928
- }
929
- if (this.#allowWrites) {
930
- this.#flushMessages();
931
- this.#flushCommands();
932
- }
933
- this.#resolveReady?.(this);
934
- this.#resolveReady = null;
935
- this.emit("ready", this);
936
- }
937
-
938
- #sendBootstrap() {
939
- if (this.#bootstrapped || !this.connected || !this.sid) return;
940
- this.#bootstrapped = true;
941
- this.ws.send(encodeMsgpack(INITIAL_OUTFIT_MESSAGE));
942
- this.emit("bootstrap", INITIAL_OUTFIT_MESSAGE);
943
- }
944
-
945
- #startKeepalive() {
946
- if (this.#keepalive) return;
947
- const payload = encodeMsgpack(KEEPALIVE_MESSAGE);
948
- this.#keepalive = setInterval(() => {
949
- if (this.connected) this.ws.send(payload);
950
- }, KEEPALIVE_INTERVAL_MS);
951
- }
952
-
953
- #flushCommands() {
954
- while (this.#queuedCommands.length) this.send(this.#queuedCommands.shift());
955
- }
956
-
957
- #flushMessages() {
958
- if (!this.connected) return;
959
- const pending = [];
960
- for (const item of this.#queuedMessages) {
961
- if (item.afterReady && !this.ready) pending.push(item);
962
- else this.sendMessage(item.message, {afterReady: item.afterReady});
963
- }
964
- this.#queuedMessages = pending;
965
- }
966
-
967
- #handleWorldUpdate(worldUpdate) {
968
- this.emit(worldUpdate.type, worldUpdate);
969
- if (worldUpdate.type === "world-removed") {
970
- this.emit("world-removed", worldUpdate.packet);
971
- return;
972
- }
973
- for (const error of worldUpdate.errors || []) this.#recordDecodeError(error, worldUpdate.packet, worldUpdate.world);
974
-
975
- const result = worldUpdate.result;
976
- if (result?.error) this.#recordDecodeError(result.error, worldUpdate.packet, worldUpdate.world);
977
- if (result?.model?.error) this.#recordDecodeError(result.model.error, worldUpdate.packet, worldUpdate.world);
978
- if (result?.timing?.cpuLoad != null) this.cpuLoad = result.timing.cpuLoad;
979
- if (result?.commandNumber != null) this.#ackCommand(result.worldId, result.commandNumber);
980
- for (const event of result?.events || []) this.#handleSideEvent(event, worldUpdate.world);
981
- }
982
-
983
- #handlePacketEvent(packet) {
984
- switch (packet.type) {
985
- case 14:
986
- if (packet.sid != null) this.outfits.set(packet.sid, packet.outfit);
987
- this.emit("outfit", packet);
988
- break;
989
- case 15:
990
- this.lastScannerResult = normalizeScannerResultPacket(packet);
991
- this.#pushLimited(this.scannerResults, this.lastScannerResult);
992
- this.emit("scanner-result", this.lastScannerResult, packet);
993
- break;
994
- case 18:
995
- this.cpuLoad = packet.cpu_load ?? null;
996
- this.emit("cpu", packet);
997
- break;
998
- case 24:
999
- this.#pushLimited(this.chat, packet);
1000
- this.emit("chat", packet);
1001
- break;
1002
- case 25:
1003
- this.#pushLimited(this.sessionMessages, packet);
1004
- this.#handleSessionSubmessage(packet);
1005
- this.emit("session", packet);
1006
- break;
1007
- case 26:
1008
- this.#pushLimited(this.motd, packet);
1009
- this.emit("motd", packet);
1010
- break;
1011
- default:
1012
- this.emit("event", packet);
1013
- break;
1014
- }
1015
- }
1016
-
1017
- #handleSessionSubmessage(packet) {
1018
- const submessage = packet?.submessage;
1019
- if (!submessage || typeof submessage !== "object") return;
1020
- if (submessage.type === "config") {
1021
- this.shipConfig = normalizeShipConfig(submessage);
1022
- this.emit("ship-config", this.shipConfig, packet);
1023
- return;
1024
- }
1025
- if (submessage.type === "captain_subrank") {
1026
- this.captainSubrank = normalizeCaptainSubrank(submessage);
1027
- if (this.playerList) this.playerList = normalizeShipPlayerList({}, this.playerList, this.captainSubrank);
1028
- this.emit("captain-subrank", this.captainSubrank, packet);
1029
- return;
1030
- }
1031
- if (submessage.type === "player_list") {
1032
- this.playerList = normalizeShipPlayerList(submessage, this.playerList, this.captainSubrank);
1033
- this.emit("player-list", this.playerList, packet);
1034
- }
1035
- }
1036
-
1037
- #handleSideEvent(event, world) {
1038
- if (!event || typeof event !== "object") return;
1039
- if (event.type === "inventory") {
1040
- this.inventoryState = normalizeInventoryEvent(event);
1041
- this.emit("inventory", this.inventoryState, world);
1042
- return;
1043
- }
1044
- if (event.type === "pui") {
1045
- const panel = {...event, world: world?.id ?? null};
1046
- if (event.ent_id != null) this.puiPanels.set(event.ent_id, panel);
1047
- this.emit("pui", panel, world);
1048
- return;
1049
- }
1050
- if (event.type === "comms") {
1051
- const panel = {...normalizeCommsEvent(event), world: world?.id ?? null};
1052
- if (panel.entity == null) {
1053
- this.currentCommsPanel = null;
1054
- this.commsPanels.clear();
1055
- } else {
1056
- this.currentCommsPanel = panel;
1057
- this.commsPanels.set(panel.entity, panel);
1058
- }
1059
- this.emit("comms", panel, world);
1060
- return;
1061
- }
1062
- if (event.type === "tip_warn") {
1063
- this.#pushLimited(this.warnings, event);
1064
- this.emit("tip_warn", event, world);
1065
- return;
1066
- }
1067
- if (event.type === "sfx") {
1068
- this.#pushLimited(this.effects, event);
1069
- this.emit("sfx", event, world);
1070
- return;
1071
- }
1072
- this.emit("side-event", event, world);
1073
- }
1074
-
1075
- #ackCommand(worldId, commandNumber) {
1076
- this.commandAcks.set(Number(worldId), commandNumber);
1077
- if (commandNumber >= 0 && (this.lastCommandAck == null || commandNumber > this.lastCommandAck.commandNumber)) {
1078
- this.lastCommandAck = {world: Number(worldId), commandNumber};
1079
- this.emit("ack", this.lastCommandAck);
1080
- }
1081
- }
1082
-
1083
- #pushLimited(target, value, limit = 200) {
1084
- if (limit === 0) return;
1085
- target.push(value);
1086
- if (target.length > limit) target.splice(0, target.length - limit);
1087
- }
1088
-
1089
- #recordDecodeError(error, packet = null, world = null) {
1090
- const entry = {
1091
- time: Date.now(),
1092
- message: error?.message || String(error),
1093
- packetType: packet && typeof packet === "object" ? packet.type ?? null : null,
1094
- world: world?.id ?? (packet && typeof packet === "object" ? packet.world ?? null : null)
1095
- };
1096
- this.#pushLimited(this.decodeErrors, entry);
1097
- try {
1098
- this.emit("decode-error", entry, error, packet, world);
1099
- } catch (_) {
1100
- }
1101
- }
1102
-
1103
- #wsHeaders() {
1104
- const headers = {
1105
- origin: this.baseUrl,
1106
- cookie: this.session ? cookieHeader(this.baseUrl, this.session) : ""
1107
- };
1108
- if (isNode()) {
1109
- Object.assign(headers, {
1110
- "user-agent": JOIN_USER_AGENT,
1111
- accept: "*/*",
1112
- "accept-language": "en-US,en;q=0.9",
1113
- dnt: "1",
1114
- "sec-gpc": "1",
1115
- "sec-fetch-dest": "empty",
1116
- "sec-fetch-mode": "websocket",
1117
- "sec-fetch-site": "same-site",
1118
- pragma: "no-cache",
1119
- "cache-control": "no-cache"
1120
- });
1121
- }
1122
- return headers;
1123
- }
1124
-
1125
- #navigationUnitConfigDefaults(entity, config = {}) {
1126
- const summary = this.entity(entity)?.contents?.navigationUnit || null;
1127
- return {
1128
- destination: config.destination ?? summary?.destination,
1129
- page: config.page ?? 0,
1130
- warp: config.warp ?? "idle",
1131
- autoWarpOnShieldFailure: config.autoWarpOnShieldFailure ?? summary?.autoWarpOnShieldFailure ?? false,
1132
- autoWarpOnNoCaptains: config.autoWarpOnNoCaptains ?? summary?.autoWarpOnNoCaptains ?? false
1133
- };
1134
- }
1135
-
1136
- #pusherConfigDefaults(entity, config = {}) {
1137
- const summary = this.entity(entity)?.contents?.pusher || null;
1138
- return {
1139
- mode: config.mode ?? summary?.mode ?? 2,
1140
- filteredMode: config.filteredMode ?? summary?.filteredMode ?? 0,
1141
- angle: config.angle ?? summary?.angle ?? 0,
1142
- speed: config.speed ?? summary?.speed ?? 20,
1143
- filterInventory: config.filterInventory ?? summary?.filterInventory ?? false,
1144
- length: config.length ?? summary?.length ?? 1000
1145
- };
1146
- }
1147
-
1148
- #loaderConfigDefaults(entity, config = {}) {
1149
- const summary = this.entity(entity)?.contents?.loader || null;
1150
- return {
1151
- pick: config.pick ?? summary?.pick ?? 0,
1152
- place: config.place ?? summary?.place ?? 2,
1153
- priority: config.priority ?? summary?.priority ?? 0,
1154
- stack: config.stack ?? summary?.stack ?? 16,
1155
- cycle: config.cycle ?? summary?.cycle ?? 1,
1156
- requireOutput: config.requireOutput ?? summary?.requireOutput ?? false,
1157
- waitForStack: config.waitForStack ?? summary?.waitForStack ?? false
1158
- };
1159
- }
1160
-
1161
- #loaderFullConfigDefaults(entity, config = {}) {
1162
- const summary = this.entity(entity)?.contents?.loader || null;
1163
- return {
1164
- ...this.#loaderConfigDefaults(entity, config),
1165
- filterMode: config.filterMode ?? summary?.filterMode ?? 0,
1166
- filterSlots: config.filterSlots ?? summary?.filterSlots ?? []
1167
- };
1168
- }
1169
-
1170
- #cargoHatchConfigDefaults(entity, config = {}) {
1171
- const summary = this.entity(entity)?.contents?.cargoHatch || null;
1172
- return {
1173
- filterMode: config.filterMode ?? summary?.filterMode ?? 0,
1174
- filterSlots: config.filterSlots ?? summary?.filterSlots ?? []
1175
- };
1176
- }
1177
-
1178
- #openSocket(WebSocket, url, headers) {
1179
- if (!isNode()) return new WebSocket(url);
1180
- try {
1181
- return new WebSocket(url, {headers});
1182
- } catch (_) {
1183
- return new WebSocket(url);
1184
- }
1185
- }
1186
-
1187
- #readWorld(scope) {
1188
- if (scope == null || scope === "ship" || scope === "current") return this.worlds.shipWorld();
1189
- if (scope === "overworld") return this.worlds.overworld();
1190
- if (typeof scope === "number" || typeof scope === "string") return this.worlds.worlds.get(Number(scope)) || null;
1191
- return null;
1192
- }
1193
-
1194
- #fail(error) {
1195
- this.#rejectReady?.(error);
1196
- this.#rejectReady = null;
1197
- try {
1198
- this.emit("error", error);
1199
- } catch (_) {
1200
- }
1201
- }
1202
- }
1
+ import {INITIAL_OUTFIT_MESSAGE, JOIN_USER_AGENT, KEEPALIVE_INTERVAL_MS, KEEPALIVE_MESSAGE} from "../constants.js";
2
+ import {getWebSocket, isNode} from "../runtime.js";
3
+ import {EventBus} from "../events.js";
4
+ import {cookieHeader} from "../network/cookies.js";
5
+ import {fetchServers} from "../network/servers.js";
6
+ import {Connection} from "../network/connection.js";
7
+ import {itemEquipmentSlotFromId} from "../state/items.js";
8
+ import {WorldStore} from "../state/world/index.js";
9
+ import {buildBlueprintPlacementMessage} from "../protocol/outbound/blueprint.js";
10
+ import {buildCommsMessage, normalizeCommsEvent} from "../protocol/outbound/comms.js";
11
+ import {buildSignedCommandPacket} from "../protocol/outbound/commands.js";
12
+ import {buildInventoryDragCommand, normalizeEquipmentSlot, normalizeInventoryEvent} from "../protocol/outbound/inventory.js";
13
+ import {
14
+ buildBanPlayerMessage,
15
+ buildDemoteSelfMessage,
16
+ buildInviteResetMessage,
17
+ buildKickPlayerMessage,
18
+ buildPlayerListMessage,
19
+ buildSetPlayerRankMessage,
20
+ buildShipManagementMessage,
21
+ buildShipPrivacyMessage,
22
+ buildStarterRecoveryMessage,
23
+ normalizeCaptainSubrank,
24
+ normalizeShipConfig,
25
+ normalizeShipPlayerList
26
+ } from "../protocol/outbound/ship-management.js";
27
+ import {buildSignTextMessage} from "../protocol/outbound/sign.js";
28
+ import {
29
+ buildCargoEjectorClipboardDirectionData,
30
+ buildCargoEjectorCopyConfigData,
31
+ buildCargoEjectorDirectionData,
32
+ buildCargoEjectorPasteConfigData,
33
+ buildCargoHatchCopyConfigData,
34
+ buildCargoHatchFilterConfigData,
35
+ buildCargoHatchFilterItemsData,
36
+ buildCargoHatchFullConfigData,
37
+ buildClipboardConfigData,
38
+ buildClipboardFixedAngleData,
39
+ buildExpandoClipboardAngleData,
40
+ buildGeneratorClipboardDirectionData,
41
+ buildGeneratorMazePuzzleData,
42
+ buildLoaderClipboardConfigData,
43
+ buildLoaderConfigData,
44
+ buildLoaderCopyConfigData,
45
+ buildLoaderFilterConfigData,
46
+ buildLoaderFilterItemsData,
47
+ buildLoaderFullConfigData,
48
+ buildNavigationUnitClipboardConfigData,
49
+ buildNavigationUnitConfigData,
50
+ buildNavigationUnitPasteConfigData,
51
+ buildPusherConfigData,
52
+ buildPusherFilterItemsData
53
+ } from "../protocol/outbound/ui-config.js";
54
+ import {cloneCommand, encodeMsgpack} from "../protocol/codec/msgpack.js";
55
+ import {decodeIncomingFrame} from "../protocol/inbound/frame.js";
56
+ import {
57
+ ClientNetDomain,
58
+ ClientDebugDomain,
59
+ PlayerDomain,
60
+ ShipManagementDomain,
61
+ InventoryDomain,
62
+ WorldDomain,
63
+ ShipDomain,
64
+ OverworldDomain,
65
+ EntityHandle,
66
+ WrenchMode,
67
+ TurretMode,
68
+ normalizeAttachMode,
69
+ normalizeScannerResultPacket,
70
+ normalizeShipReadOptions,
71
+ shipReadSummary,
72
+ currentPlayerSummary,
73
+ summariesFor,
74
+ emptyMachineSummary,
75
+ normalizeDegrees,
76
+ normalizeLauncherPower,
77
+ normalizeWrenchMode,
78
+ normalizeTurretMode,
79
+ normalizePositiveFinite,
80
+ normalizePositiveInteger
81
+ } from "./domains/index.js";
82
+
83
+ export {WrenchMode, TurretMode} from "./domains/index.js";
84
+
85
+ // Defaults for retained history. Every one of these buffers was previously
86
+ // unbounded; the surrounding buffers (chat, warnings, scanner results) already
87
+ // capped at 200, so these match that convention.
88
+ const DEFAULT_PACKET_HISTORY = 200;
89
+ const DEFAULT_EVENT_HISTORY = 200;
90
+ const DEFAULT_MODEL_PACKET_HISTORY = 100;
91
+ const DEFAULT_CHUNK_HISTORY = 64;
92
+
93
+ function normalizeHistoryLimit(value, fallback) {
94
+ if (value === Infinity) return Infinity;
95
+ const limit = Number(value);
96
+ if (!Number.isFinite(limit) || limit < 0) return fallback;
97
+ return Math.floor(limit);
98
+ }
99
+
100
+ export class DredlessClient extends EventBus {
101
+ constructor(connection, {
102
+ connect = true,
103
+ attach = false,
104
+ mode = null,
105
+ baseUrl = null,
106
+ session = null,
107
+ serverId = null,
108
+ server = null,
109
+ netPort = null,
110
+ gameToken = "",
111
+ packetHistory = DEFAULT_PACKET_HISTORY,
112
+ eventHistory = DEFAULT_EVENT_HISTORY,
113
+ modelPacketHistory = DEFAULT_MODEL_PACKET_HISTORY,
114
+ chunkHistory = DEFAULT_CHUNK_HISTORY
115
+ } = {}) {
116
+ super();
117
+ const attachMode = attach ? normalizeAttachMode(mode) : null;
118
+ if (!(connection instanceof Connection) && !attachMode) throw new Error("DredlessClient requires a Connection");
119
+
120
+ this.connection = connection || null;
121
+ this.session = connection?.session ?? session ?? null;
122
+ this.baseUrl = connection?.baseUrl ?? baseUrl ?? this.session?.baseUrl ?? null;
123
+ this.serverId = connection?.serverId ?? serverId ?? server?.index ?? null;
124
+ this.server = connection?.server ?? server ?? null;
125
+ this.netPort = connection?.netPort ?? netPort ?? null;
126
+ this.gameToken = connection?.gameToken ?? gameToken ?? "";
127
+ this.ws = null;
128
+ this.sid = null;
129
+ this.connected = false;
130
+ this.ready = false;
131
+ this.packetCount = 0;
132
+ this.lastPacket = null;
133
+ this.packets = [];
134
+ // Retained history is bounded by default: these arrays previously grew for
135
+ // the lifetime of the client, holding every decoded packet and its binary
136
+ // payloads. Pass 0 to disable a buffer or Infinity to restore unbounded
137
+ // capture behaviour.
138
+ this.packetHistory = normalizeHistoryLimit(packetHistory, DEFAULT_PACKET_HISTORY);
139
+ this.worlds = new WorldStore({eventHistory, modelPacketHistory, chunkHistory});
140
+ this.net = new ClientNetDomain(this);
141
+ this.debug = new ClientDebugDomain(this);
142
+ this.player = new PlayerDomain(this);
143
+ this.inventory = new InventoryDomain(this);
144
+ this.management = new ShipManagementDomain(this);
145
+ this.cpuLoad = null;
146
+ this.inventoryState = null;
147
+ this.puiPanels = new Map();
148
+ this.commsPanels = new Map();
149
+ this.currentCommsPanel = null;
150
+ this.warnings = [];
151
+ this.effects = [];
152
+ this.chat = [];
153
+ this.motd = [];
154
+ this.sessionMessages = [];
155
+ this.scannerResults = [];
156
+ this.lastScannerResult = null;
157
+ this.shipConfig = null;
158
+ this.captainSubrank = null;
159
+ this.playerList = null;
160
+ this.outfits = new Map();
161
+ this.commandAcks = new Map();
162
+ this.lastCommandAck = null;
163
+ this.decodeErrors = [];
164
+ this.readyPromise = new Promise((resolve, reject) => {
165
+ this.#resolveReady = resolve;
166
+ this.#rejectReady = reject;
167
+ });
168
+
169
+ this.attachMode = attachMode;
170
+ this.attached = Boolean(attachMode);
171
+ if (connect) this.#connect().catch((error) => this.#fail(error));
172
+ }
173
+
174
+ static attachWebSocket(websocket, options = {}) {
175
+ if (!websocket) throw new Error("DredlessClient.attachWebSocket requires a websocket");
176
+ const mode = normalizeAttachMode(options.mode);
177
+ const client = new DredlessClient(null, {...options, attach: true, connect: false, mode});
178
+ client.#attachSocket(websocket, {...options, mode});
179
+ return client;
180
+ }
181
+
182
+ #commandNumber = 1;
183
+ #queuedCommands = [];
184
+ #queuedMessages = [];
185
+ #keepalive = null;
186
+ #bootstrapped = false;
187
+ #ownsSocket = true;
188
+ #allowWrites = true;
189
+ #attachedOpened = false;
190
+ #resolveReady = null;
191
+ #rejectReady = null;
192
+ #inputSettings = {
193
+ wrench_mode: 1,
194
+ turret_mode: 0
195
+ };
196
+
197
+ whenReady() {
198
+ return this.readyPromise;
199
+ }
200
+
201
+ send(command = {}) {
202
+ this.#assertWritable();
203
+ const normalized = cloneCommand({...this.#inputSettings, ...command});
204
+ if (normalized.n == null) normalized.n = this.#commandNumber++;
205
+ if (!this.sid) {
206
+ this.#queuedCommands.push(normalized);
207
+ return this;
208
+ }
209
+ this.ws.send(buildSignedCommandPacket(normalized, this.sid));
210
+ this.emit("command", normalized);
211
+ return this;
212
+ }
213
+
214
+ sendMessage(message, {afterReady = true} = {}) {
215
+ this.#assertWritable();
216
+ if (!this.connected || (afterReady && !this.ready)) {
217
+ this.#queuedMessages.push({message, afterReady});
218
+ return this;
219
+ }
220
+ this.ws.send(encodeMsgpack(message));
221
+ this.emit("message", message);
222
+ return this;
223
+ }
224
+
225
+ sendRaw(message, options = {}) {
226
+ return this.sendMessage(message, options);
227
+ }
228
+
229
+ sendBlueprintPlacement(placement) {
230
+ return this.sendMessage(buildBlueprintPlacementMessage(placement));
231
+ }
232
+
233
+ setOutfit(outfit) {
234
+ return this.sendMessage({type: 7, outfit});
235
+ }
236
+
237
+ sendShipManagement(act, arg = null) {
238
+ return this.sendMessage(buildShipManagementMessage(act, arg));
239
+ }
240
+
241
+ requestPlayerList() {
242
+ return this.sendMessage(buildPlayerListMessage());
243
+ }
244
+
245
+ resetInvite() {
246
+ return this.sendMessage(buildInviteResetMessage());
247
+ }
248
+
249
+ setShipPrivacy(privacy) {
250
+ return this.sendMessage(buildShipPrivacyMessage(privacy));
251
+ }
252
+
253
+ recoverStarterItem(itemId) {
254
+ return this.sendMessage(buildStarterRecoveryMessage(itemId));
255
+ }
256
+
257
+ setPlayerRank(refId, rank) {
258
+ return this.sendMessage(buildSetPlayerRankMessage(refId, rank));
259
+ }
260
+
261
+ promotePlayerToCaptain(refId) {
262
+ return this.setPlayerRank(refId, "captain");
263
+ }
264
+
265
+ demotePlayerToCrew(refId) {
266
+ return this.setPlayerRank(refId, "crew");
267
+ }
268
+
269
+ demotePlayerToGuest(refId) {
270
+ return this.setPlayerRank(refId, "guest");
271
+ }
272
+
273
+ kickPlayer(refId) {
274
+ return this.sendMessage(buildKickPlayerMessage(refId));
275
+ }
276
+
277
+ banPlayer(refId) {
278
+ return this.sendMessage(buildBanPlayerMessage(refId));
279
+ }
280
+
281
+ demoteSelf() {
282
+ return this.sendMessage(buildDemoteSelfMessage());
283
+ }
284
+
285
+ sendEntityCommand(cmd, args = [-1, -1, -1]) {
286
+ return this.sendMessage({type: 5, cmd, args});
287
+ }
288
+
289
+ sendCommsMessage(message = "") {
290
+ return this.sendMessage(buildCommsMessage(message));
291
+ }
292
+
293
+ sendFabricatorMessage(cmd, args = [-1, -1, -1]) {
294
+ return this.sendEntityCommand(cmd, args);
295
+ }
296
+
297
+ sendFabricatorCommand(itemId, count = 1, index = -1) {
298
+ return this.craftAdd(itemId, count, index);
299
+ }
300
+
301
+ craftAdd(itemId, count = 1, index = -1) {
302
+ return this.sendFabricatorMessage("craft_add", [itemId, count, index]);
303
+ }
304
+
305
+ craftSub(itemId, count = 1, index = 0) {
306
+ return this.sendFabricatorMessage("craft_sub", [itemId, count, index]);
307
+ }
308
+
309
+ craftClearQueue() {
310
+ return this.sendFabricatorMessage("clear_queue");
311
+ }
312
+
313
+ craftToggleRepeat() {
314
+ return this.sendFabricatorMessage("toggle_repeat");
315
+ }
316
+
317
+ fabricatorLockResource(row) {
318
+ return this.sendFabricatorMessage("lock", [row, -1, -1]);
319
+ }
320
+
321
+ fabricatorUnlockResource(row) {
322
+ return this.sendFabricatorMessage("unlock", [row, -1, -1]);
323
+ }
324
+
325
+ fabricatorEject(row) {
326
+ return this.sendFabricatorMessage("eject", [row, -1, -1]);
327
+ }
328
+
329
+ setLauncherAngle(angle) {
330
+ return this.sendEntityCommand("angle", [normalizeDegrees(angle)]);
331
+ }
332
+
333
+ setLauncherPower(power) {
334
+ return this.sendEntityCommand("power", [normalizeLauncherPower(power)]);
335
+ }
336
+
337
+ setSignText(text = "", mode = 0) {
338
+ return this.sendMessage(buildSignTextMessage(text, mode));
339
+ }
340
+
341
+ sendUiConfig(data) {
342
+ return this.sendMessage({type: 8, data});
343
+ }
344
+
345
+ solveGeneratorPuzzle(entity, solution) {
346
+ return this.sendUiConfig(buildGeneratorMazePuzzleData(entity, solution));
347
+ }
348
+
349
+ sendPusherConfig(entity, config = {}) {
350
+ return this.sendUiConfig(buildPusherConfigData(entity, this.#pusherConfigDefaults(entity, config)));
351
+ }
352
+
353
+ setPusherAngle(entity, angle, config = {}) {
354
+ return this.sendPusherConfig(entity, {...config, angle});
355
+ }
356
+
357
+ setPusherSpeed(entity, speed, config = {}) {
358
+ return this.sendPusherConfig(entity, {...config, speed});
359
+ }
360
+
361
+ setPusherLength(entity, length, config = {}) {
362
+ return this.sendPusherConfig(entity, {...config, length});
363
+ }
364
+
365
+ setPusherMode(entity, mode, config = {}) {
366
+ return this.sendPusherConfig(entity, {...config, mode});
367
+ }
368
+
369
+ setPusherFilteredMode(entity, filteredMode, config = {}) {
370
+ return this.sendPusherConfig(entity, {...config, filteredMode});
371
+ }
372
+
373
+ setPusherFilterInventory(entity, filterInventory, config = {}) {
374
+ return this.sendPusherConfig(entity, {...config, filterInventory});
375
+ }
376
+
377
+ setPusherFilterItems(entity, filterSlots = []) {
378
+ return this.sendUiConfig(buildPusherFilterItemsData(entity, filterSlots));
379
+ }
380
+
381
+ sendLoaderConfig(entity, config = {}) {
382
+ return this.sendUiConfig(buildLoaderConfigData(entity, this.#loaderConfigDefaults(entity, config)));
383
+ }
384
+
385
+ sendLoaderFullConfig(entity, config = {}) {
386
+ return this.sendUiConfig(buildLoaderFullConfigData(entity, this.#loaderFullConfigDefaults(entity, config)));
387
+ }
388
+
389
+ copyLoaderConfig(entity, config = {}) {
390
+ return this.sendUiConfig(buildLoaderCopyConfigData(this.#loaderFullConfigDefaults(entity, config)));
391
+ }
392
+
393
+ sendLoaderClipboardConfig(config = {}) {
394
+ return this.sendUiConfig(buildLoaderClipboardConfigData(config));
395
+ }
396
+
397
+ sendClipboardConfig(target, commandName, values = []) {
398
+ return this.sendUiConfig(buildClipboardConfigData(target, commandName, values));
399
+ }
400
+
401
+ setClipboardFixedAngle(target, direction) {
402
+ return this.sendUiConfig(buildClipboardFixedAngleData(target, direction));
403
+ }
404
+
405
+ setGeneratorClipboardDirection(direction) {
406
+ return this.sendUiConfig(buildGeneratorClipboardDirectionData(direction));
407
+ }
408
+
409
+ setExpandoClipboardAngle(angle) {
410
+ return this.sendUiConfig(buildExpandoClipboardAngleData(angle));
411
+ }
412
+
413
+ setCargoEjectorDirection(entity, direction) {
414
+ return this.sendUiConfig(buildCargoEjectorDirectionData(entity, direction));
415
+ }
416
+
417
+ pasteCargoEjectorConfig(entity, direction = "right") {
418
+ return this.sendUiConfig(buildCargoEjectorPasteConfigData(entity, direction));
419
+ }
420
+
421
+ copyCargoEjectorConfig(entity, direction = "right") {
422
+ return this.sendUiConfig(buildCargoEjectorCopyConfigData(direction));
423
+ }
424
+
425
+ setCargoEjectorClipboardDirection(direction) {
426
+ return this.sendUiConfig(buildCargoEjectorClipboardDirectionData(direction));
427
+ }
428
+
429
+ setLoaderPickPlace(entity, pick, place, config = {}) {
430
+ return this.sendLoaderConfig(entity, {...config, pick, place});
431
+ }
432
+
433
+ setLoaderPriority(entity, priority, config = {}) {
434
+ return this.sendLoaderConfig(entity, {...config, priority});
435
+ }
436
+
437
+ setLoaderStack(entity, stack, config = {}) {
438
+ return this.sendLoaderConfig(entity, {...config, stack});
439
+ }
440
+
441
+ setLoaderCycle(entity, cycle, config = {}) {
442
+ return this.sendLoaderConfig(entity, {...config, cycle});
443
+ }
444
+
445
+ setLoaderRequireOutput(entity, requireOutput, config = {}) {
446
+ return this.sendLoaderConfig(entity, {...config, requireOutput});
447
+ }
448
+
449
+ setLoaderWaitForStack(entity, waitForStack, config = {}) {
450
+ return this.sendLoaderConfig(entity, {...config, waitForStack});
451
+ }
452
+
453
+ setLoaderFilterMode(entity, filterMode) {
454
+ return this.sendUiConfig(buildLoaderFilterConfigData(entity, filterMode));
455
+ }
456
+
457
+ setLoaderFilterItems(entity, filterSlots = []) {
458
+ return this.sendUiConfig(buildLoaderFilterItemsData(entity, filterSlots));
459
+ }
460
+
461
+ setCargoHatchFilterMode(entity, filterMode) {
462
+ return this.sendUiConfig(buildCargoHatchFilterConfigData(entity, filterMode));
463
+ }
464
+
465
+ setCargoHatchFilterItems(entity, filterSlots = []) {
466
+ return this.sendUiConfig(buildCargoHatchFilterItemsData(entity, filterSlots));
467
+ }
468
+
469
+ sendCargoHatchFullConfig(entity, config = {}) {
470
+ return this.sendUiConfig(buildCargoHatchFullConfigData(entity, this.#cargoHatchConfigDefaults(entity, config)));
471
+ }
472
+
473
+ pasteCargoHatchConfig(entity, config = {}) {
474
+ return this.sendCargoHatchFullConfig(entity, config);
475
+ }
476
+
477
+ copyCargoHatchConfig(entity, config = {}) {
478
+ return this.sendUiConfig(buildCargoHatchCopyConfigData(this.#cargoHatchConfigDefaults(entity, config)));
479
+ }
480
+
481
+ inputSettings() {
482
+ return {
483
+ wrenchMode: wrenchModeName(this.#inputSettings.wrench_mode),
484
+ turretMode: turretModeName(this.#inputSettings.turret_mode),
485
+ viewWidth: this.#inputSettings.vx ?? null,
486
+ viewHeight: this.#inputSettings.vy ?? null,
487
+ screenWidth: this.#inputSettings.scr_w ?? null,
488
+ screenHeight: this.#inputSettings.scr_h ?? null
489
+ };
490
+ }
491
+
492
+ setInputSettings(settings = {}, {send = true} = {}) {
493
+ const wrench = settings.wrenchMode;
494
+ const turret = settings.turretMode;
495
+ const viewWidth = settings.viewWidth;
496
+ const viewHeight = settings.viewHeight;
497
+ const screenWidth = settings.screenWidth;
498
+ const screenHeight = settings.screenHeight;
499
+ if (wrench != null) this.#inputSettings.wrench_mode = normalizeWrenchMode(wrench);
500
+ if (turret != null) this.#inputSettings.turret_mode = normalizeTurretMode(turret);
501
+ if (viewWidth != null) this.#inputSettings.vx = normalizePositiveFinite(viewWidth, "viewWidth");
502
+ if (viewHeight != null) this.#inputSettings.vy = normalizePositiveFinite(viewHeight, "viewHeight");
503
+ if (screenWidth != null) this.#inputSettings.scr_w = normalizePositiveInteger(screenWidth, "screenWidth");
504
+ if (screenHeight != null) this.#inputSettings.scr_h = normalizePositiveInteger(screenHeight, "screenHeight");
505
+ return send ? this.send({}) : this;
506
+ }
507
+
508
+ setView(width, height, options = {}) {
509
+ return this.setInputSettings({viewWidth: width, viewHeight: height}, options);
510
+ }
511
+
512
+ setScreenSize(width, height, options = {}) {
513
+ return this.setInputSettings({screenWidth: width, screenHeight: height}, options);
514
+ }
515
+
516
+ setWrenchMode(mode, options = {}) {
517
+ return this.setInputSettings({wrenchMode: mode}, options);
518
+ }
519
+
520
+ setWrenchAction(mode, options = {}) {
521
+ return this.setWrenchMode(mode, options);
522
+ }
523
+
524
+ setTurretMode(mode, options = {}) {
525
+ return this.setInputSettings({turretMode: mode}, options);
526
+ }
527
+
528
+ sendNavigationUnitConfig(entity, config = {}) {
529
+ return this.sendUiConfig(buildNavigationUnitConfigData(entity, this.#navigationUnitConfigDefaults(entity, config)));
530
+ }
531
+
532
+ copyNavigationUnitConfig(entity, config = {}) {
533
+ return this.sendUiConfig(buildNavigationUnitClipboardConfigData(this.#navigationUnitConfigDefaults(entity, config)));
534
+ }
535
+
536
+ pasteNavigationUnitConfig(entity, config = {}) {
537
+ return this.sendUiConfig(buildNavigationUnitPasteConfigData(entity, this.#navigationUnitConfigDefaults(entity, config)));
538
+ }
539
+
540
+ setNavigationDestination(entity, destination, config = {}) {
541
+ return this.sendNavigationUnitConfig(entity, {...config, destination});
542
+ }
543
+
544
+ setNavigationAutoWarp(entity, config = {}) {
545
+ return this.sendNavigationUnitConfig(entity, {page: 1, ...config});
546
+ }
547
+
548
+ startWarp(entity, config = {}) {
549
+ return this.sendNavigationUnitConfig(entity, {page: 1, ...config, warp: "start"});
550
+ }
551
+
552
+ cancelWarp(entity, config = {}) {
553
+ return this.sendNavigationUnitConfig(entity, {page: 1, ...config, warp: "cancel"});
554
+ }
555
+
556
+ move(x = 0, y = 0, command = {}) {
557
+ return this.send({...command, x, y});
558
+ }
559
+
560
+ aim(mx = 0, my = 0, command = {}) {
561
+ return this.send({...command, mx, my});
562
+ }
563
+
564
+ action(flags = {}, command = {}) {
565
+ return this.send({...command, ...flags});
566
+ }
567
+
568
+ useEntity(entity, {invSlot = 0, hold = true} = {}, command = {}) {
569
+ return this.send({
570
+ ...command,
571
+ focus_ent: entity,
572
+ inv_slot: invSlot,
573
+ act1: true,
574
+ act1_held: Boolean(hold)
575
+ });
576
+ }
577
+
578
+ useHeldItem({invSlot = 0, hold = true} = {}, command = {}) {
579
+ return this.send({
580
+ ...command,
581
+ focus_ent: null,
582
+ inv_slot: invSlot,
583
+ act1: true,
584
+ act1_held: Boolean(hold)
585
+ });
586
+ }
587
+
588
+ placeHeldItem(options = {}, command = {}) {
589
+ return this.useHeldItem(options, command);
590
+ }
591
+
592
+ placeBlueprint(placement, {invSlot = 0, hold = true, mx = null, my = null} = {}, command = {}) {
593
+ const message = buildBlueprintPlacementMessage(placement);
594
+ this.sendMessage(message);
595
+ return this.placeHeldItem(
596
+ {invSlot, hold},
597
+ {
598
+ ...command,
599
+ mx: mx ?? message.x,
600
+ my: my ?? message.y
601
+ }
602
+ );
603
+ }
604
+
605
+ rotateHeldItem({invSlot = 0, hold = true} = {}, command = {}) {
606
+ return this.send({
607
+ ...command,
608
+ focus_ent: null,
609
+ inv_slot: invSlot,
610
+ act_alt: true,
611
+ act_alt_held: Boolean(hold)
612
+ });
613
+ }
614
+
615
+
616
+ close(code = 1000, reason = "client") {
617
+ if (!this.#allowWrites) return this;
618
+ try {
619
+ this.ws?.close(code, reason);
620
+ } catch (_) {
621
+ }
622
+ return this;
623
+ }
624
+
625
+ disconnect(code = 1000, reason = "client") {
626
+ return this.close(code, reason);
627
+ }
628
+
629
+ snapshot({includeTiles = false, includeModel = false} = {}) {
630
+ return {
631
+ baseUrl: this.baseUrl,
632
+ session: this.session?.toJSON?.() || this.session,
633
+ connection: this.connection?.toJSON?.() || null,
634
+ attachMode: this.attachMode,
635
+ serverId: this.serverId,
636
+ server: this.server,
637
+ netPort: this.netPort,
638
+ sid: this.sid,
639
+ ready: this.ready,
640
+ connected: this.connected,
641
+ currentWorldId: this.worlds.currentWorldId,
642
+ worlds: this.worlds.snapshot({includeTiles, includeModel}),
643
+ cpuLoad: this.cpuLoad,
644
+ inventory: this.inventoryState,
645
+ puiPanels: [...this.puiPanels.values()],
646
+ commsPanels: [...this.commsPanels.values()],
647
+ currentCommsPanel: this.currentCommsPanel,
648
+ warnings: this.warnings.slice(-50),
649
+ effects: this.effects.slice(-50),
650
+ chat: this.chat.slice(-50),
651
+ motd: this.motd.slice(-20),
652
+ sessionMessages: this.sessionMessages.slice(-50),
653
+ scannerResults: this.scannerResults.slice(-20),
654
+ lastScannerResult: this.lastScannerResult,
655
+ shipConfig: this.shipConfig,
656
+ captainSubrank: this.captainSubrank,
657
+ playerList: this.playerList,
658
+ outfits: [...this.outfits.entries()].map(([sid, outfit]) => ({sid, outfit})),
659
+ commandAcks: [...this.commandAcks.entries()].map(([world, commandNumber]) => ({world, commandNumber})),
660
+ lastCommandAck: this.lastCommandAck,
661
+ decodeErrors: this.decodeErrors.slice(-50),
662
+ packetCount: this.packetCount,
663
+ lastPacket: this.lastPacket
664
+ };
665
+ }
666
+
667
+ state(options = {}) {
668
+ return this.snapshot(options);
669
+ }
670
+
671
+ world(id) {
672
+ const world = this.worlds.worlds.get(Number(id)) || null;
673
+ return world ? new WorldDomain(this, world.id) : null;
674
+ }
675
+
676
+ overworld() {
677
+ const world = this.worlds.overworld();
678
+ return world ? new OverworldDomain(this, world.id) : null;
679
+ }
680
+
681
+ currentShip() {
682
+ const world = this.worlds.shipWorld();
683
+ return world ? new ShipDomain(this, world.id) : null;
684
+ }
685
+
686
+ shipWorld(options = {}) {
687
+ return this.worlds.shipWorld()?.snapshot(options) || null;
688
+ }
689
+
690
+ ship() {
691
+ return this.currentShip();
692
+ }
693
+
694
+ shipEntity() {
695
+ return this.worlds.currentShipEntity();
696
+ }
697
+
698
+ currentPlayerEntity() {
699
+ const player = currentPlayerSummary(this);
700
+ return player ? new EntityHandle(this, player.entity, "current") : null;
701
+ }
702
+
703
+ entities(scope = "ship") {
704
+ return summariesFor(this, scope).map(entitySnapshotFor);
705
+ }
706
+
707
+ entity(entityId, scope = "ship") {
708
+ const summary = this.#readWorld(scope)?.entity(entityId) || null;
709
+ return entitySnapshotFor(summary);
710
+ }
711
+
712
+ blocks(scope = "ship") {
713
+ return this.#readWorld(scope)?.blocks() || [];
714
+ }
715
+
716
+ materials(scope = "ship") {
717
+ return this.#readWorld(scope)?.materials() || [];
718
+ }
719
+
720
+ machines(scope = "ship") {
721
+ return this.#readWorld(scope)?.model.machines() || emptyMachineSummary();
722
+ }
723
+
724
+ players(scope = "ship") {
725
+ return this.#readWorld(scope)?.model.players() || [];
726
+ }
727
+
728
+ shipControls(scope = "overworld") {
729
+ return this.#readWorld(scope)?.model.shipControls() || [];
730
+ }
731
+
732
+ ships(options = {}) {
733
+ const normalized = normalizeShipReadOptions(options);
734
+ const overworld = this.worlds.overworld();
735
+ if (!overworld) return [];
736
+ // Resolve the reference entity once rather than per ship.
737
+ const current = this.worlds.currentShipEntity();
738
+ const ships = overworld.entities()
739
+ .filter((entity) => entity?.contents?.shipControl)
740
+ .map((entity) => shipReadSummary(entity, normalized, this.worlds, current));
741
+ if (normalized.sort === "distance") {
742
+ ships.sort((a, b) => (a.distance ?? Number.POSITIVE_INFINITY) - (b.distance ?? Number.POSITIVE_INFINITY));
743
+ }
744
+ return ships;
745
+ }
746
+
747
+ shipByHex(hexCode, options = {}) {
748
+ const normalized = String(hexCode ?? "").toUpperCase();
749
+ return this.#findShip(options, (control) => String(control.hexCode ?? "").toUpperCase() === normalized);
750
+ }
751
+
752
+ shipByEntity(entityId, options = {}) {
753
+ const id = Number(entityId);
754
+ return this.#findShip(options, (control, entity) => entity.entity === id);
755
+ }
756
+
757
+ // Match against the ship control record and summarise only the hit. These
758
+ // lookups used to build a full summary -- including a world snapshot -- for
759
+ // every ship in the overworld before discarding all but one.
760
+ #findShip(options, predicate) {
761
+ const overworld = this.worlds.overworld();
762
+ if (!overworld) return null;
763
+ for (const entity of overworld.entities()) {
764
+ const control = entity?.contents?.shipControl;
765
+ if (!control || !predicate(control, entity)) continue;
766
+ return shipReadSummary(entity, normalizeShipReadOptions(options), this.worlds);
767
+ }
768
+ return null;
769
+ }
770
+
771
+ get packetsRaw() {
772
+ return this.packets.slice();
773
+ }
774
+
775
+ async #connect() {
776
+ if (!this.server) {
777
+ const servers = await fetchServers(this.baseUrl);
778
+ this.server = servers.find((server) => server.index === this.serverId) || null;
779
+ }
780
+ if (!this.server?.domain) throw new Error(`Unable to resolve game server ${this.serverId}`);
781
+
782
+ const WebSocket = await getWebSocket();
783
+ const wsUrl = `wss://${this.server.domain}:${this.netPort}`;
784
+ this.ws = this.#openSocket(WebSocket, wsUrl, this.#wsHeaders());
785
+ this.ws.binaryType = "arraybuffer";
786
+ this.#ownsSocket = true;
787
+ this.#allowWrites = true;
788
+ this.#bindSocket();
789
+ }
790
+
791
+ #attachSocket(websocket, {connected = null, ready = false, sid = null, mode = "observe"} = {}) {
792
+ this.ws = websocket;
793
+ this.attachMode = normalizeAttachMode(mode);
794
+ this.#ownsSocket = this.attachMode === "bootstrap";
795
+ this.#allowWrites = this.attachMode !== "readonly";
796
+ try {
797
+ this.ws.binaryType = "arraybuffer";
798
+ } catch (_) {
799
+ }
800
+ if (sid != null) this.sid = sid >>> 0;
801
+ this.connected = connected ?? this.#socketLooksOpen(websocket);
802
+ if (ready) {
803
+ this.ready = true;
804
+ if (this.#ownsSocket) {
805
+ this.#sendBootstrap();
806
+ this.#startKeepalive();
807
+ }
808
+ if (this.#allowWrites) {
809
+ this.#flushMessages();
810
+ this.#flushCommands();
811
+ }
812
+ this.#resolveReady?.(this);
813
+ this.#resolveReady = null;
814
+ }
815
+ this.#bindAttachedSocket();
816
+ if (this.connected && this.#ownsSocket) this.#attachedOpen();
817
+ this.emit("attach", this);
818
+ }
819
+
820
+ #bindSocket() {
821
+ this.ws.onopen = () => {
822
+ this.connected = true;
823
+ this.ws.send(encodeMsgpack({type: 1}));
824
+ this.emit("open", this);
825
+ this.#flushMessages();
826
+ };
827
+
828
+ this.ws.onmessage = (event) => this.#handleSocketMessage(event.data);
829
+ this.ws.onerror = (event) => this.#fail(event?.error || new Error("WebSocket error"));
830
+ this.ws.onclose = (event) => this.#handleSocketClose(event);
831
+ }
832
+
833
+ #bindAttachedSocket() {
834
+ const open = () => this.#attachedOpen();
835
+ const message = (event) => this.#handleSocketMessage(event?.data ?? event);
836
+ const error = (event) => this.#fail(event?.error || new Error("WebSocket error"));
837
+ const close = (event) => this.#handleSocketClose(event);
838
+
839
+ if (typeof this.ws.addEventListener === "function") {
840
+ this.ws.addEventListener("open", open);
841
+ this.ws.addEventListener("message", message);
842
+ this.ws.addEventListener("error", error);
843
+ this.ws.addEventListener("close", close);
844
+ return;
845
+ }
846
+ if (typeof this.ws.on === "function") {
847
+ this.ws.on("open", open);
848
+ this.ws.on("message", (data) => this.#handleMessage(data));
849
+ this.ws.on("error", error);
850
+ this.ws.on("close", close);
851
+ return;
852
+ }
853
+ const previousMessage = this.ws.onmessage;
854
+ this.ws.onmessage = (event) => {
855
+ if (typeof previousMessage === "function") previousMessage.call(this.ws, event);
856
+ message(event);
857
+ };
858
+ }
859
+
860
+ #attachedOpen() {
861
+ this.connected = true;
862
+ if (this.#attachedOpened) return;
863
+ this.#attachedOpened = true;
864
+ if (this.#ownsSocket) this.ws.send(encodeMsgpack({type: 1}));
865
+ this.emit("open", this);
866
+ if (this.#allowWrites) this.#flushMessages();
867
+ }
868
+
869
+ #handleSocketClose(event) {
870
+ this.connected = false;
871
+ clearInterval(this.#keepalive);
872
+ this.#keepalive = null;
873
+ this.emit("close", event);
874
+ }
875
+
876
+ #socketLooksOpen(websocket) {
877
+ return websocket?.readyState == null || websocket.readyState === 1 || websocket.readyState === websocket.OPEN;
878
+ }
879
+
880
+ #assertWritable() {
881
+ if (!this.#allowWrites) throw new Error("DredlessClient is attached in readonly mode");
882
+ }
883
+
884
+ #handleSocketMessage(data) {
885
+ if (data && typeof data.arrayBuffer === "function") {
886
+ data.arrayBuffer()
887
+ .then((buffer) => this.#handleMessage(buffer))
888
+ .catch((error) => this.#fail(error));
889
+ return;
890
+ }
891
+ this.#handleMessage(data);
892
+ }
893
+
894
+ #handleMessage(data) {
895
+ let packet;
896
+ try {
897
+ packet = decodeIncomingFrame(data);
898
+ } catch (error) {
899
+ this.#fail(error);
900
+ return;
901
+ }
902
+ this.packetCount += 1;
903
+ this.lastPacket = packet;
904
+ this.#pushLimited(this.packets, packet, this.packetHistory);
905
+ this.emit("packet", packet);
906
+
907
+ if (!packet || typeof packet !== "object") return;
908
+ try {
909
+ if (packet.type === 21) return this.#markReady(packet);
910
+ const worldUpdate = this.worlds.apply(packet);
911
+ if (worldUpdate) {
912
+ this.#handleWorldUpdate(worldUpdate);
913
+ } else {
914
+ this.#handlePacketEvent(packet);
915
+ }
916
+ } catch (error) {
917
+ this.#recordDecodeError(error, packet);
918
+ }
919
+ }
920
+
921
+ #markReady(packet) {
922
+ this.sid = packet.sid >>> 0;
923
+ this.worlds.currentWorldId = packet.world ?? this.worlds.currentWorldId;
924
+ this.ready = true;
925
+ if (this.#ownsSocket) {
926
+ this.#sendBootstrap();
927
+ this.#startKeepalive();
928
+ }
929
+ if (this.#allowWrites) {
930
+ this.#flushMessages();
931
+ this.#flushCommands();
932
+ }
933
+ this.#resolveReady?.(this);
934
+ this.#resolveReady = null;
935
+ this.emit("ready", this);
936
+ }
937
+
938
+ #sendBootstrap() {
939
+ if (this.#bootstrapped || !this.connected || !this.sid) return;
940
+ this.#bootstrapped = true;
941
+ this.ws.send(encodeMsgpack(INITIAL_OUTFIT_MESSAGE));
942
+ this.emit("bootstrap", INITIAL_OUTFIT_MESSAGE);
943
+ }
944
+
945
+ #startKeepalive() {
946
+ if (this.#keepalive) return;
947
+ const payload = encodeMsgpack(KEEPALIVE_MESSAGE);
948
+ this.#keepalive = setInterval(() => {
949
+ if (this.connected) this.ws.send(payload);
950
+ }, KEEPALIVE_INTERVAL_MS);
951
+ }
952
+
953
+ #flushCommands() {
954
+ while (this.#queuedCommands.length) this.send(this.#queuedCommands.shift());
955
+ }
956
+
957
+ #flushMessages() {
958
+ if (!this.connected) return;
959
+ const pending = [];
960
+ for (const item of this.#queuedMessages) {
961
+ if (item.afterReady && !this.ready) pending.push(item);
962
+ else this.sendMessage(item.message, {afterReady: item.afterReady});
963
+ }
964
+ this.#queuedMessages = pending;
965
+ }
966
+
967
+ #handleWorldUpdate(worldUpdate) {
968
+ this.emit(worldUpdate.type, worldUpdate);
969
+ if (worldUpdate.type === "world-removed") {
970
+ this.emit("world-removed", worldUpdate.packet);
971
+ return;
972
+ }
973
+ for (const error of worldUpdate.errors || []) this.#recordDecodeError(error, worldUpdate.packet, worldUpdate.world);
974
+
975
+ const result = worldUpdate.result;
976
+ if (result?.error) this.#recordDecodeError(result.error, worldUpdate.packet, worldUpdate.world);
977
+ if (result?.model?.error) this.#recordDecodeError(result.model.error, worldUpdate.packet, worldUpdate.world);
978
+ if (result?.timing?.cpuLoad != null) this.cpuLoad = result.timing.cpuLoad;
979
+ if (result?.commandNumber != null) this.#ackCommand(result.worldId, result.commandNumber);
980
+ for (const event of result?.events || []) this.#handleSideEvent(event, worldUpdate.world);
981
+ }
982
+
983
+ #handlePacketEvent(packet) {
984
+ switch (packet.type) {
985
+ case 14:
986
+ if (packet.sid != null) this.outfits.set(packet.sid, packet.outfit);
987
+ this.emit("outfit", packet);
988
+ break;
989
+ case 15:
990
+ this.lastScannerResult = normalizeScannerResultPacket(packet);
991
+ this.#pushLimited(this.scannerResults, this.lastScannerResult);
992
+ this.emit("scanner-result", this.lastScannerResult, packet);
993
+ break;
994
+ case 18:
995
+ this.cpuLoad = packet.cpu_load ?? null;
996
+ this.emit("cpu", packet);
997
+ break;
998
+ case 24:
999
+ this.#pushLimited(this.chat, packet);
1000
+ this.emit("chat", packet);
1001
+ break;
1002
+ case 25:
1003
+ this.#pushLimited(this.sessionMessages, packet);
1004
+ this.#handleSessionSubmessage(packet);
1005
+ this.emit("session", packet);
1006
+ break;
1007
+ case 26:
1008
+ this.#pushLimited(this.motd, packet);
1009
+ this.emit("motd", packet);
1010
+ break;
1011
+ default:
1012
+ this.emit("event", packet);
1013
+ break;
1014
+ }
1015
+ }
1016
+
1017
+ #handleSessionSubmessage(packet) {
1018
+ const submessage = packet?.submessage;
1019
+ if (!submessage || typeof submessage !== "object") return;
1020
+ if (submessage.type === "config") {
1021
+ this.shipConfig = normalizeShipConfig(submessage);
1022
+ this.emit("ship-config", this.shipConfig, packet);
1023
+ return;
1024
+ }
1025
+ if (submessage.type === "captain_subrank") {
1026
+ this.captainSubrank = normalizeCaptainSubrank(submessage);
1027
+ if (this.playerList) this.playerList = normalizeShipPlayerList({}, this.playerList, this.captainSubrank);
1028
+ this.emit("captain-subrank", this.captainSubrank, packet);
1029
+ return;
1030
+ }
1031
+ if (submessage.type === "player_list") {
1032
+ this.playerList = normalizeShipPlayerList(submessage, this.playerList, this.captainSubrank);
1033
+ this.emit("player-list", this.playerList, packet);
1034
+ }
1035
+ }
1036
+
1037
+ #handleSideEvent(event, world) {
1038
+ if (!event || typeof event !== "object") return;
1039
+ if (event.type === "inventory") {
1040
+ this.inventoryState = normalizeInventoryEvent(event);
1041
+ this.emit("inventory", this.inventoryState, world);
1042
+ return;
1043
+ }
1044
+ if (event.type === "pui") {
1045
+ const panel = {...event, world: world?.id ?? null};
1046
+ if (event.ent_id != null) this.puiPanels.set(event.ent_id, panel);
1047
+ this.emit("pui", panel, world);
1048
+ return;
1049
+ }
1050
+ if (event.type === "comms") {
1051
+ const panel = {...normalizeCommsEvent(event), world: world?.id ?? null};
1052
+ if (panel.entity == null) {
1053
+ this.currentCommsPanel = null;
1054
+ this.commsPanels.clear();
1055
+ } else {
1056
+ this.currentCommsPanel = panel;
1057
+ this.commsPanels.set(panel.entity, panel);
1058
+ }
1059
+ this.emit("comms", panel, world);
1060
+ return;
1061
+ }
1062
+ if (event.type === "tip_warn") {
1063
+ this.#pushLimited(this.warnings, event);
1064
+ this.emit("tip_warn", event, world);
1065
+ return;
1066
+ }
1067
+ if (event.type === "sfx") {
1068
+ this.#pushLimited(this.effects, event);
1069
+ this.emit("sfx", event, world);
1070
+ return;
1071
+ }
1072
+ this.emit("side-event", event, world);
1073
+ }
1074
+
1075
+ #ackCommand(worldId, commandNumber) {
1076
+ this.commandAcks.set(Number(worldId), commandNumber);
1077
+ if (commandNumber >= 0 && (this.lastCommandAck == null || commandNumber > this.lastCommandAck.commandNumber)) {
1078
+ this.lastCommandAck = {world: Number(worldId), commandNumber};
1079
+ this.emit("ack", this.lastCommandAck);
1080
+ }
1081
+ }
1082
+
1083
+ #pushLimited(target, value, limit = 200) {
1084
+ if (limit === 0) return;
1085
+ target.push(value);
1086
+ if (target.length > limit) target.splice(0, target.length - limit);
1087
+ }
1088
+
1089
+ #recordDecodeError(error, packet = null, world = null) {
1090
+ const entry = {
1091
+ time: Date.now(),
1092
+ message: error?.message || String(error),
1093
+ packetType: packet && typeof packet === "object" ? packet.type ?? null : null,
1094
+ world: world?.id ?? (packet && typeof packet === "object" ? packet.world ?? null : null)
1095
+ };
1096
+ this.#pushLimited(this.decodeErrors, entry);
1097
+ try {
1098
+ this.emit("decode-error", entry, error, packet, world);
1099
+ } catch (_) {
1100
+ }
1101
+ }
1102
+
1103
+ #wsHeaders() {
1104
+ const headers = {
1105
+ origin: this.baseUrl,
1106
+ cookie: this.session ? cookieHeader(this.baseUrl, this.session) : ""
1107
+ };
1108
+ if (isNode()) {
1109
+ Object.assign(headers, {
1110
+ "user-agent": JOIN_USER_AGENT,
1111
+ accept: "*/*",
1112
+ "accept-language": "en-US,en;q=0.9",
1113
+ dnt: "1",
1114
+ "sec-gpc": "1",
1115
+ "sec-fetch-dest": "empty",
1116
+ "sec-fetch-mode": "websocket",
1117
+ "sec-fetch-site": "same-site",
1118
+ pragma: "no-cache",
1119
+ "cache-control": "no-cache"
1120
+ });
1121
+ }
1122
+ return headers;
1123
+ }
1124
+
1125
+ #navigationUnitConfigDefaults(entity, config = {}) {
1126
+ const summary = this.entity(entity)?.contents?.navigationUnit || null;
1127
+ return {
1128
+ destination: config.destination ?? summary?.destination,
1129
+ page: config.page ?? 0,
1130
+ warp: config.warp ?? "idle",
1131
+ autoWarpOnShieldFailure: config.autoWarpOnShieldFailure ?? summary?.autoWarpOnShieldFailure ?? false,
1132
+ autoWarpOnNoCaptains: config.autoWarpOnNoCaptains ?? summary?.autoWarpOnNoCaptains ?? false
1133
+ };
1134
+ }
1135
+
1136
+ #pusherConfigDefaults(entity, config = {}) {
1137
+ const summary = this.entity(entity)?.contents?.pusher || null;
1138
+ return {
1139
+ mode: config.mode ?? summary?.mode ?? 2,
1140
+ filteredMode: config.filteredMode ?? summary?.filteredMode ?? 0,
1141
+ angle: config.angle ?? summary?.angle ?? 0,
1142
+ speed: config.speed ?? summary?.speed ?? 20,
1143
+ filterInventory: config.filterInventory ?? summary?.filterInventory ?? false,
1144
+ length: config.length ?? summary?.length ?? 1000
1145
+ };
1146
+ }
1147
+
1148
+ #loaderConfigDefaults(entity, config = {}) {
1149
+ const summary = this.entity(entity)?.contents?.loader || null;
1150
+ return {
1151
+ pick: config.pick ?? summary?.pick ?? 0,
1152
+ place: config.place ?? summary?.place ?? 2,
1153
+ priority: config.priority ?? summary?.priority ?? 0,
1154
+ stack: config.stack ?? summary?.stack ?? 16,
1155
+ cycle: config.cycle ?? summary?.cycle ?? 1,
1156
+ requireOutput: config.requireOutput ?? summary?.requireOutput ?? false,
1157
+ waitForStack: config.waitForStack ?? summary?.waitForStack ?? false
1158
+ };
1159
+ }
1160
+
1161
+ #loaderFullConfigDefaults(entity, config = {}) {
1162
+ const summary = this.entity(entity)?.contents?.loader || null;
1163
+ return {
1164
+ ...this.#loaderConfigDefaults(entity, config),
1165
+ filterMode: config.filterMode ?? summary?.filterMode ?? 0,
1166
+ filterSlots: config.filterSlots ?? summary?.filterSlots ?? []
1167
+ };
1168
+ }
1169
+
1170
+ #cargoHatchConfigDefaults(entity, config = {}) {
1171
+ const summary = this.entity(entity)?.contents?.cargoHatch || null;
1172
+ return {
1173
+ filterMode: config.filterMode ?? summary?.filterMode ?? 0,
1174
+ filterSlots: config.filterSlots ?? summary?.filterSlots ?? []
1175
+ };
1176
+ }
1177
+
1178
+ #openSocket(WebSocket, url, headers) {
1179
+ if (!isNode()) return new WebSocket(url);
1180
+ try {
1181
+ return new WebSocket(url, {headers});
1182
+ } catch (_) {
1183
+ return new WebSocket(url);
1184
+ }
1185
+ }
1186
+
1187
+ #readWorld(scope) {
1188
+ if (scope == null || scope === "ship" || scope === "current") return this.worlds.shipWorld();
1189
+ if (scope === "overworld") return this.worlds.overworld();
1190
+ if (typeof scope === "number" || typeof scope === "string") return this.worlds.worlds.get(Number(scope)) || null;
1191
+ return null;
1192
+ }
1193
+
1194
+ #fail(error) {
1195
+ this.#rejectReady?.(error);
1196
+ this.#rejectReady = null;
1197
+ try {
1198
+ this.emit("error", error);
1199
+ } catch (_) {
1200
+ }
1201
+ }
1202
+ }