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