dredless 0.2.0 → 0.3.0

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 (80) hide show
  1. package/README.md +65 -48
  2. package/dist/browser/dredless.global.js +12047 -0
  3. package/dist/browser/dredless.global.js.map +7 -0
  4. package/docs/API_REFERENCE.md +2481 -2481
  5. package/docs/BROWSER_API.md +98 -98
  6. package/docs/NODE_API.md +359 -359
  7. package/package.json +40 -35
  8. package/src/browser-global.js +3 -0
  9. package/src/browser.d.ts +212 -212
  10. package/src/browser.js +133 -133
  11. package/src/client/core.js +1202 -1202
  12. package/src/client/domains/index.js +1818 -1818
  13. package/src/client/index.d.ts +2 -2
  14. package/src/client/index.js +1 -1
  15. package/src/compression/lz4.d.ts +1 -1
  16. package/src/compression/lz4.js +112 -112
  17. package/src/constants.js +74 -74
  18. package/src/crypto/chacha-native.d.ts +9 -9
  19. package/src/crypto/chacha-native.js +48 -48
  20. package/src/crypto/chacha.d.ts +25 -25
  21. package/src/crypto/chacha.js +274 -274
  22. package/src/events.js +39 -39
  23. package/src/index.d.ts +2277 -2277
  24. package/src/index.js +125 -125
  25. package/src/network/browser-session.js +291 -291
  26. package/src/network/connection.d.ts +1 -1
  27. package/src/network/connection.js +31 -31
  28. package/src/network/cookies.js +85 -85
  29. package/src/network/http.js +34 -34
  30. package/src/network/index.d.ts +4 -4
  31. package/src/network/index.js +4 -4
  32. package/src/network/servers.d.ts +7 -7
  33. package/src/network/servers.js +133 -133
  34. package/src/network/session.d.ts +7 -7
  35. package/src/network/session.js +320 -320
  36. package/src/network/ships.d.ts +12 -12
  37. package/src/network/ships.js +73 -73
  38. package/src/node.d.ts +2 -2
  39. package/src/node.js +8 -8
  40. package/src/protocol/codec/binary.d.ts +3 -3
  41. package/src/protocol/codec/binary.js +52 -52
  42. package/src/protocol/codec/msgpack.d.ts +3 -3
  43. package/src/protocol/codec/msgpack.js +555 -555
  44. package/src/protocol/inbound/frame.d.ts +1 -1
  45. package/src/protocol/inbound/frame.js +8 -8
  46. package/src/protocol/inbound/world-payload.d.ts +3 -3
  47. package/src/protocol/inbound/world-payload.js +21 -21
  48. package/src/protocol/index.d.ts +11 -11
  49. package/src/protocol/index.js +19 -19
  50. package/src/protocol/outbound/blueprint.d.ts +22 -22
  51. package/src/protocol/outbound/blueprint.js +36 -36
  52. package/src/protocol/outbound/commands.d.ts +3 -3
  53. package/src/protocol/outbound/commands.js +149 -149
  54. package/src/protocol/outbound/comms.d.ts +28 -28
  55. package/src/protocol/outbound/comms.js +40 -40
  56. package/src/protocol/outbound/inventory.d.ts +64 -64
  57. package/src/protocol/outbound/inventory.js +147 -147
  58. package/src/protocol/outbound/ship-management.d.ts +84 -84
  59. package/src/protocol/outbound/ship-management.js +221 -221
  60. package/src/protocol/outbound/sign.d.ts +18 -18
  61. package/src/protocol/outbound/sign.js +45 -45
  62. package/src/protocol/outbound/ui-config.d.ts +157 -157
  63. package/src/protocol/outbound/ui-config.js +578 -578
  64. package/src/runtime.js +36 -36
  65. package/src/state/generator-maze.d.ts +32 -32
  66. package/src/state/generator-maze.js +370 -370
  67. package/src/state/index.d.ts +3 -3
  68. package/src/state/index.js +3 -3
  69. package/src/state/items.js +20 -20
  70. package/src/state/model/decoder.js +1554 -1554
  71. package/src/state/model/index.d.ts +33 -33
  72. package/src/state/model/index.js +2 -2
  73. package/src/state/model/loader-config.js +1020 -1020
  74. package/src/state/model/reader.js +69 -69
  75. package/src/state/model/state.js +1257 -1257
  76. package/src/state/overworld.js +72 -72
  77. package/src/state/tilesets.js +53 -53
  78. package/src/state/world/index.d.ts +29 -29
  79. package/src/state/world/index.js +180 -180
  80. 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
+ }