dredless 0.7.0 → 1.0.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.
- package/README.md +22 -57
- package/dist/browser/dredless.global.js +794 -98
- package/dist/browser/dredless.global.js.map +4 -4
- package/docs/ACCOUNT.md +146 -0
- package/docs/API_REFERENCE.md +63 -2563
- package/docs/BROWSER_API.md +151 -70
- package/docs/CLIENT.md +183 -0
- package/docs/EVENTS.md +136 -0
- package/docs/GETTING_STARTED.md +135 -0
- package/docs/MACHINES.md +351 -0
- package/docs/MANAGEMENT.md +285 -0
- package/docs/NODE_API.md +120 -318
- package/docs/PLAYER_INVENTORY.md +235 -0
- package/docs/PROTOCOL.md +227 -0
- package/docs/README.md +44 -0
- package/docs/TROUBLESHOOTING.md +91 -0
- package/docs/WORLD_STATE.md +183 -0
- package/package.json +1 -2
- package/src/browser.d.ts +31 -1
- package/src/browser.js +10 -0
- package/src/client/core.js +302 -15
- package/src/client/domains/index.js +57 -7
- package/src/client/index.d.ts +1 -1
- package/src/index.d.ts +299 -13
- package/src/index.js +10 -0
- package/src/network/account.js +182 -0
- package/src/network/disconnect.d.ts +2 -0
- package/src/network/disconnect.js +53 -0
- package/src/network/index.d.ts +1 -0
- package/src/network/index.js +1 -0
- package/src/network/invites.d.ts +0 -3
- package/src/network/invites.js +3 -40
- package/src/network/session.d.ts +2 -0
- package/src/network/session.js +21 -0
- package/src/protocol/outbound/ship-management.d.ts +66 -1
- package/src/protocol/outbound/ship-management.js +83 -0
- package/src/state/model/decoder.js +70 -10
- package/src/state/model/index.d.ts +4 -0
- package/src/state/model/state.js +22 -7
- package/src/state/world/index.js +27 -1
package/src/client/core.js
CHANGED
|
@@ -5,6 +5,7 @@ import {TipDomain} from "./domains/tips.js";
|
|
|
5
5
|
import {cookieHeader} from "../network/cookies.js";
|
|
6
6
|
import {fetchServers} from "../network/servers.js";
|
|
7
7
|
import {Connection} from "../network/connection.js";
|
|
8
|
+
import {DropReason, DisconnectError, normalizeDisconnect} from "../network/disconnect.js";
|
|
8
9
|
import {itemEquipmentSlotFromId} from "../state/items.js";
|
|
9
10
|
import {WorldStore} from "../state/world/index.js";
|
|
10
11
|
import {buildBlueprintPlacementMessage} from "../protocol/outbound/blueprint.js";
|
|
@@ -14,15 +15,22 @@ import {buildSignedCommandPacket} from "../protocol/outbound/commands.js";
|
|
|
14
15
|
import {buildInventoryDragCommand, normalizeEquipmentSlot, normalizeInventoryEvent} from "../protocol/outbound/inventory.js";
|
|
15
16
|
import {
|
|
16
17
|
buildBanPlayerMessage,
|
|
18
|
+
buildUnbanPlayerMessage,
|
|
17
19
|
buildDemoteSelfMessage,
|
|
18
20
|
buildInviteResetMessage,
|
|
19
21
|
buildKickPlayerMessage,
|
|
20
22
|
buildPlayerListMessage,
|
|
23
|
+
buildSaveShipMessage,
|
|
24
|
+
buildShipRenameRequestMessage,
|
|
25
|
+
buildShipRenameCommitMessage,
|
|
21
26
|
buildSetPlayerRankMessage,
|
|
22
27
|
buildShipManagementMessage,
|
|
23
28
|
buildShipPrivacyMessage,
|
|
29
|
+
buildShipGravityMessage,
|
|
30
|
+
buildSetPatronPerksMessage,
|
|
24
31
|
buildStarterRecoveryMessage,
|
|
25
32
|
normalizeCaptainSubrank,
|
|
33
|
+
normalizeCrewLog,
|
|
26
34
|
normalizeShipConfig,
|
|
27
35
|
normalizeShipPlayerList
|
|
28
36
|
} from "../protocol/outbound/ship-management.js";
|
|
@@ -133,6 +141,7 @@ export class DredlessClient extends EventBus {
|
|
|
133
141
|
this.ready = false;
|
|
134
142
|
this.packetCount = 0;
|
|
135
143
|
this.lastPacket = null;
|
|
144
|
+
this.lastDisconnect = null;
|
|
136
145
|
this.packets = [];
|
|
137
146
|
// Retained history is bounded by default: these arrays previously grew for
|
|
138
147
|
// the lifetime of the client, holding every decoded packet and its binary
|
|
@@ -161,6 +170,7 @@ export class DredlessClient extends EventBus {
|
|
|
161
170
|
this.shipConfig = null;
|
|
162
171
|
this.captainSubrank = null;
|
|
163
172
|
this.playerList = null;
|
|
173
|
+
this.crewLog = null;
|
|
164
174
|
this.outfits = new Map();
|
|
165
175
|
this.commandAcks = new Map();
|
|
166
176
|
this.lastCommandAck = null;
|
|
@@ -169,6 +179,9 @@ export class DredlessClient extends EventBus {
|
|
|
169
179
|
this.#resolveReady = resolve;
|
|
170
180
|
this.#rejectReady = reject;
|
|
171
181
|
});
|
|
182
|
+
// Consumers may attach only close listeners. Keep the original promise
|
|
183
|
+
// rejectable through whenReady() without an unhandled rejection on close.
|
|
184
|
+
this.readyPromise.catch(() => {});
|
|
172
185
|
|
|
173
186
|
this.attachMode = attachMode;
|
|
174
187
|
this.attached = Boolean(attachMode);
|
|
@@ -193,6 +206,10 @@ export class DredlessClient extends EventBus {
|
|
|
193
206
|
#attachedOpened = false;
|
|
194
207
|
#resolveReady = null;
|
|
195
208
|
#rejectReady = null;
|
|
209
|
+
#savePending = false;
|
|
210
|
+
#renamePending = false;
|
|
211
|
+
#starterRecoveryPending = false;
|
|
212
|
+
#managementPending = new Set();
|
|
196
213
|
#inputSettings = {
|
|
197
214
|
wrench_mode: 1,
|
|
198
215
|
turret_mode: 0
|
|
@@ -242,20 +259,181 @@ export class DredlessClient extends EventBus {
|
|
|
242
259
|
return this.sendMessage(buildShipManagementMessage(act, arg));
|
|
243
260
|
}
|
|
244
261
|
|
|
262
|
+
canSaveShip() {
|
|
263
|
+
if (!this.connected || !this.ready) return false;
|
|
264
|
+
const ship = this.worlds.shipWorld();
|
|
265
|
+
if (!ship || ship.isOverworld !== false || ship.parentWorld == null) return false;
|
|
266
|
+
const parent = this.worlds.worlds.get(Number(ship.parentWorld));
|
|
267
|
+
return parent?.meta?.danger_level === 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async saveShip({timeout = 10000} = {}) {
|
|
271
|
+
this.#assertWritable();
|
|
272
|
+
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
|
|
273
|
+
throw new RangeError("timeout must be positive milliseconds no greater than 2147483647");
|
|
274
|
+
}
|
|
275
|
+
if (this.#savePending) throw new Error("A ship save is already in progress");
|
|
276
|
+
if (!this.canSaveShip()) throw new Error("Saving requires a connected, ready client with its ship in a known Safe Zone");
|
|
277
|
+
this.#savePending = true;
|
|
278
|
+
try {
|
|
279
|
+
return await new Promise((resolve, reject) => {
|
|
280
|
+
const cleanup = () => {
|
|
281
|
+
clearTimeout(timer);
|
|
282
|
+
this.off("disconnect", disconnected);
|
|
283
|
+
};
|
|
284
|
+
const disconnected = (info) => {
|
|
285
|
+
cleanup();
|
|
286
|
+
if (info.code === DropReason.Saved) resolve(info);
|
|
287
|
+
else reject(new DisconnectError(info, "Ship save was not confirmed"));
|
|
288
|
+
};
|
|
289
|
+
const timer = setTimeout(() => {
|
|
290
|
+
cleanup();
|
|
291
|
+
reject(new Error("Timed out waiting for ship save confirmation (Saved close code)"));
|
|
292
|
+
}, timeout);
|
|
293
|
+
// Subscribe before sending; attached sockets can close synchronously.
|
|
294
|
+
this.on("disconnect", disconnected);
|
|
295
|
+
try {
|
|
296
|
+
this.sendMessage(buildSaveShipMessage());
|
|
297
|
+
} catch (error) {
|
|
298
|
+
cleanup();
|
|
299
|
+
reject(error);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
} finally {
|
|
303
|
+
this.#savePending = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async renameShip(name, {color, timeout = 10000} = {}) {
|
|
308
|
+
this.#assertWritable();
|
|
309
|
+
// Validate before requesting permission. Omitted color comes from the response.
|
|
310
|
+
buildShipRenameCommitMessage(name, color === undefined ? "#000000" : color);
|
|
311
|
+
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
|
|
312
|
+
throw new RangeError("timeout must be positive milliseconds no greater than 2147483647");
|
|
313
|
+
}
|
|
314
|
+
if (!this.connected || !this.ready) throw new Error("Renaming requires a connected, ready client");
|
|
315
|
+
if (this.#renamePending) throw new Error("A ship rename is already in progress");
|
|
316
|
+
const worldId = this.worlds.currentWorldId;
|
|
317
|
+
this.#renamePending = true;
|
|
318
|
+
try {
|
|
319
|
+
await new Promise((resolve, reject) => {
|
|
320
|
+
const cleanup = () => {
|
|
321
|
+
clearTimeout(timer);
|
|
322
|
+
this.off("session", responded);
|
|
323
|
+
this.off("disconnect", disconnected);
|
|
324
|
+
this.off("demote", demoted);
|
|
325
|
+
};
|
|
326
|
+
const demoted = () => {
|
|
327
|
+
cleanup();
|
|
328
|
+
reject(new Error("Ship rename interrupted by demotion"));
|
|
329
|
+
};
|
|
330
|
+
const disconnected = (info) => {
|
|
331
|
+
cleanup();
|
|
332
|
+
reject(new DisconnectError(info, "Ship rename interrupted"));
|
|
333
|
+
};
|
|
334
|
+
const responded = (packet) => {
|
|
335
|
+
const response = packet.submessage;
|
|
336
|
+
if (response?.type !== "rename_response") return;
|
|
337
|
+
cleanup();
|
|
338
|
+
try {
|
|
339
|
+
if (!response.allow) throw new Error(response.message || "Ship rename unavailable");
|
|
340
|
+
if (this.worlds.currentWorldId !== worldId) throw new Error("World changed while requesting ship rename");
|
|
341
|
+
this.sendMessage(buildShipRenameCommitMessage(name, color === undefined ? response.initial_color : color));
|
|
342
|
+
resolve();
|
|
343
|
+
} catch (error) {
|
|
344
|
+
reject(error);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const timer = setTimeout(() => {
|
|
348
|
+
cleanup();
|
|
349
|
+
reject(new Error("Timed out waiting for ship rename permission"));
|
|
350
|
+
}, timeout);
|
|
351
|
+
this.on("session", responded);
|
|
352
|
+
this.on("disconnect", disconnected);
|
|
353
|
+
this.on("demote", demoted);
|
|
354
|
+
try {
|
|
355
|
+
this.sendMessage(buildShipRenameRequestMessage());
|
|
356
|
+
} catch (error) {
|
|
357
|
+
cleanup();
|
|
358
|
+
reject(error);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
} finally {
|
|
362
|
+
this.#renamePending = false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
245
366
|
requestPlayerList() {
|
|
246
367
|
return this.sendMessage(buildPlayerListMessage());
|
|
247
368
|
}
|
|
248
369
|
|
|
249
|
-
resetInvite() {
|
|
250
|
-
|
|
370
|
+
resetInvite(options = {}) {
|
|
371
|
+
const previousKey = this.shipConfig?.inviteKey;
|
|
372
|
+
return this.#awaitManagementConfirmation("Invite reset", buildInviteResetMessage(), "ship-config", (config) =>
|
|
373
|
+
typeof config?.inviteKey === "string" && config.inviteKey.length > 0 && config.inviteKey !== previousKey, options);
|
|
251
374
|
}
|
|
252
375
|
|
|
253
376
|
setShipPrivacy(privacy) {
|
|
254
377
|
return this.sendMessage(buildShipPrivacyMessage(privacy));
|
|
255
378
|
}
|
|
256
379
|
|
|
257
|
-
|
|
258
|
-
return this.sendMessage(
|
|
380
|
+
setShipGravity(direction) {
|
|
381
|
+
return this.sendMessage(buildShipGravityMessage(direction));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
setShipPatronPerks(enabled) {
|
|
385
|
+
return this.sendMessage(buildSetPatronPerksMessage(enabled));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async recoverStarterItem(itemId, {timeout = 10000} = {}) {
|
|
389
|
+
this.#assertWritable();
|
|
390
|
+
const message = buildStarterRecoveryMessage(itemId);
|
|
391
|
+
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
|
|
392
|
+
throw new RangeError("timeout must be positive milliseconds no greater than 2147483647");
|
|
393
|
+
}
|
|
394
|
+
if (!this.connected || !this.ready) throw new Error("Starter recovery requires a connected, ready client");
|
|
395
|
+
if (this.#starterRecoveryPending) throw new Error("A starter recovery is already in progress");
|
|
396
|
+
const worldId = this.worlds.currentWorldId;
|
|
397
|
+
this.#starterRecoveryPending = true;
|
|
398
|
+
try {
|
|
399
|
+
await new Promise((resolve, reject) => {
|
|
400
|
+
const cleanup = () => {
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
this.off("starter-recovery", responded);
|
|
403
|
+
this.off("disconnect", disconnected);
|
|
404
|
+
this.off("demote", demoted);
|
|
405
|
+
};
|
|
406
|
+
const disconnected = (info) => {
|
|
407
|
+
cleanup();
|
|
408
|
+
reject(new DisconnectError(info, "Starter recovery interrupted"));
|
|
409
|
+
};
|
|
410
|
+
const demoted = () => {
|
|
411
|
+
cleanup();
|
|
412
|
+
reject(new Error("Starter recovery interrupted by demotion"));
|
|
413
|
+
};
|
|
414
|
+
const responded = (result) => {
|
|
415
|
+
cleanup();
|
|
416
|
+
if (this.worlds.currentWorldId !== worldId) reject(new Error("World changed during starter recovery"));
|
|
417
|
+
else if (!result.success) reject(new Error(result.failReason));
|
|
418
|
+
else resolve();
|
|
419
|
+
};
|
|
420
|
+
const timer = setTimeout(() => {
|
|
421
|
+
cleanup();
|
|
422
|
+
reject(new Error("Timed out waiting for starter recovery response"));
|
|
423
|
+
}, timeout);
|
|
424
|
+
this.on("starter-recovery", responded);
|
|
425
|
+
this.on("disconnect", disconnected);
|
|
426
|
+
this.on("demote", demoted);
|
|
427
|
+
try {
|
|
428
|
+
this.sendMessage(message);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
cleanup();
|
|
431
|
+
reject(error);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
} finally {
|
|
435
|
+
this.#starterRecoveryPending = false;
|
|
436
|
+
}
|
|
259
437
|
}
|
|
260
438
|
|
|
261
439
|
setPlayerRank(refId, rank) {
|
|
@@ -282,8 +460,58 @@ export class DredlessClient extends EventBus {
|
|
|
282
460
|
return this.sendMessage(buildBanPlayerMessage(refId));
|
|
283
461
|
}
|
|
284
462
|
|
|
285
|
-
|
|
286
|
-
return this.sendMessage(
|
|
463
|
+
unbanPlayer(refId) {
|
|
464
|
+
return this.sendMessage(buildUnbanPlayerMessage(refId));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async demoteSelf(options = {}) {
|
|
468
|
+
await this.#awaitManagementConfirmation("Self-demotion", buildDemoteSelfMessage(), "demote", () => true, options);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async #awaitManagementConfirmation(operation, message, event, accept, {timeout = 10000} = {}) {
|
|
472
|
+
this.#assertWritable();
|
|
473
|
+
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
|
|
474
|
+
throw new RangeError("timeout must be positive milliseconds no greater than 2147483647");
|
|
475
|
+
}
|
|
476
|
+
if (!this.connected || !this.ready) throw new Error(`${operation} requires a connected, ready client`);
|
|
477
|
+
if (this.#managementPending.has(operation)) throw new Error(`${operation} is already in progress`);
|
|
478
|
+
const worldId = this.worlds.currentWorldId;
|
|
479
|
+
this.#managementPending.add(operation);
|
|
480
|
+
try {
|
|
481
|
+
return await new Promise((resolve, reject) => {
|
|
482
|
+
const cleanup = () => {
|
|
483
|
+
clearTimeout(timer);
|
|
484
|
+
this.off(event, responded);
|
|
485
|
+
this.off("disconnect", disconnected);
|
|
486
|
+
if (event !== "demote") this.off("demote", demoted);
|
|
487
|
+
};
|
|
488
|
+
const fail = (error) => {
|
|
489
|
+
cleanup();
|
|
490
|
+
reject(error);
|
|
491
|
+
};
|
|
492
|
+
const disconnected = (info) => fail(new DisconnectError(info, `${operation} interrupted`));
|
|
493
|
+
const demoted = () => fail(new Error(`${operation} interrupted by demotion`));
|
|
494
|
+
const responded = (result) => {
|
|
495
|
+
if (this.worlds.currentWorldId !== worldId) {
|
|
496
|
+
fail(new Error(`World changed during ${operation.toLowerCase()}`));
|
|
497
|
+
} else if (accept(result)) {
|
|
498
|
+
cleanup();
|
|
499
|
+
resolve(result);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
const timer = setTimeout(() => fail(new Error(`Timed out waiting for ${operation.toLowerCase()} confirmation`)), timeout);
|
|
503
|
+
this.on(event, responded);
|
|
504
|
+
this.on("disconnect", disconnected);
|
|
505
|
+
if (event !== "demote") this.on("demote", demoted);
|
|
506
|
+
try {
|
|
507
|
+
this.sendMessage(message);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
fail(error);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
} finally {
|
|
513
|
+
this.#managementPending.delete(operation);
|
|
514
|
+
}
|
|
287
515
|
}
|
|
288
516
|
|
|
289
517
|
sendEntityCommand(cmd, args = [-1, -1, -1]) {
|
|
@@ -663,12 +891,14 @@ export class DredlessClient extends EventBus {
|
|
|
663
891
|
shipConfig: this.shipConfig,
|
|
664
892
|
captainSubrank: this.captainSubrank,
|
|
665
893
|
playerList: this.playerList,
|
|
894
|
+
crewLog: this.crewLog,
|
|
666
895
|
outfits: [...this.outfits.entries()].map(([sid, outfit]) => ({sid, outfit})),
|
|
667
896
|
commandAcks: [...this.commandAcks.entries()].map(([world, commandNumber]) => ({world, commandNumber})),
|
|
668
897
|
lastCommandAck: this.lastCommandAck,
|
|
669
898
|
decodeErrors: this.decodeErrors.slice(-50),
|
|
670
899
|
packetCount: this.packetCount,
|
|
671
|
-
lastPacket: this.lastPacket
|
|
900
|
+
lastPacket: this.lastPacket,
|
|
901
|
+
lastDisconnect: this.lastDisconnect
|
|
672
902
|
};
|
|
673
903
|
}
|
|
674
904
|
|
|
@@ -819,6 +1049,7 @@ export class DredlessClient extends EventBus {
|
|
|
819
1049
|
}
|
|
820
1050
|
this.#resolveReady?.(this);
|
|
821
1051
|
this.#resolveReady = null;
|
|
1052
|
+
this.#rejectReady = null;
|
|
822
1053
|
}
|
|
823
1054
|
this.#bindAttachedSocket();
|
|
824
1055
|
if (this.connected && this.#ownsSocket) this.#attachedOpen();
|
|
@@ -827,6 +1058,7 @@ export class DredlessClient extends EventBus {
|
|
|
827
1058
|
|
|
828
1059
|
#bindSocket() {
|
|
829
1060
|
this.ws.onopen = () => {
|
|
1061
|
+
this.lastDisconnect = null;
|
|
830
1062
|
this.connected = true;
|
|
831
1063
|
this.ws.send(encodeMsgpack({type: 1}));
|
|
832
1064
|
this.emit("open", this);
|
|
@@ -842,7 +1074,7 @@ export class DredlessClient extends EventBus {
|
|
|
842
1074
|
const open = () => this.#attachedOpen();
|
|
843
1075
|
const message = (event) => this.#handleSocketMessage(event?.data ?? event);
|
|
844
1076
|
const error = (event) => this.#fail(event?.error || new Error("WebSocket error"));
|
|
845
|
-
const close = (event) => this.#handleSocketClose(event);
|
|
1077
|
+
const close = (event, reason) => this.#handleSocketClose(event, reason);
|
|
846
1078
|
|
|
847
1079
|
if (typeof this.ws.addEventListener === "function") {
|
|
848
1080
|
this.ws.addEventListener("open", open);
|
|
@@ -858,14 +1090,17 @@ export class DredlessClient extends EventBus {
|
|
|
858
1090
|
this.ws.on("close", close);
|
|
859
1091
|
return;
|
|
860
1092
|
}
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1093
|
+
for (const [type, handler] of Object.entries({open, message, error, close})) {
|
|
1094
|
+
const previous = this.ws[`on${type}`];
|
|
1095
|
+
this.ws[`on${type}`] = (...args) => {
|
|
1096
|
+
if (typeof previous === "function") previous.apply(this.ws, args);
|
|
1097
|
+
handler(...args);
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
866
1100
|
}
|
|
867
1101
|
|
|
868
1102
|
#attachedOpen() {
|
|
1103
|
+
this.lastDisconnect = null;
|
|
869
1104
|
this.connected = true;
|
|
870
1105
|
if (this.#attachedOpened) return;
|
|
871
1106
|
this.#attachedOpened = true;
|
|
@@ -874,11 +1109,32 @@ export class DredlessClient extends EventBus {
|
|
|
874
1109
|
if (this.#allowWrites) this.#flushMessages();
|
|
875
1110
|
}
|
|
876
1111
|
|
|
877
|
-
#handleSocketClose(event) {
|
|
1112
|
+
#handleSocketClose(event, reason) {
|
|
1113
|
+
const info = normalizeDisconnect(event, reason);
|
|
878
1114
|
this.connected = false;
|
|
1115
|
+
this.ready = false;
|
|
1116
|
+
this.sid = null;
|
|
1117
|
+
this.lastDisconnect = info;
|
|
879
1118
|
clearInterval(this.#keepalive);
|
|
880
1119
|
this.#keepalive = null;
|
|
881
|
-
this
|
|
1120
|
+
this.#bootstrapped = false;
|
|
1121
|
+
this.#attachedOpened = false;
|
|
1122
|
+
this.#queuedCommands.length = 0;
|
|
1123
|
+
this.#queuedMessages.length = 0;
|
|
1124
|
+
this.shipConfig = null;
|
|
1125
|
+
this.captainSubrank = null;
|
|
1126
|
+
this.playerList = null;
|
|
1127
|
+
this.crewLog = null;
|
|
1128
|
+
this.puiPanels.clear();
|
|
1129
|
+
this.commsPanels.clear();
|
|
1130
|
+
this.currentCommsPanel = null;
|
|
1131
|
+
this.commandAcks.clear();
|
|
1132
|
+
this.lastCommandAck = null;
|
|
1133
|
+
this.#rejectReady?.(new DisconnectError(info, "Disconnected before the client was ready"));
|
|
1134
|
+
this.#resolveReady = null;
|
|
1135
|
+
this.#rejectReady = null;
|
|
1136
|
+
this.emit("disconnect", info);
|
|
1137
|
+
this.emit("close", event, reason);
|
|
882
1138
|
}
|
|
883
1139
|
|
|
884
1140
|
#socketLooksOpen(websocket) {
|
|
@@ -887,6 +1143,7 @@ export class DredlessClient extends EventBus {
|
|
|
887
1143
|
|
|
888
1144
|
#assertWritable() {
|
|
889
1145
|
if (!this.#allowWrites) throw new Error("DredlessClient is attached in readonly mode");
|
|
1146
|
+
if (this.lastDisconnect) throw new DisconnectError(this.lastDisconnect);
|
|
890
1147
|
}
|
|
891
1148
|
|
|
892
1149
|
#handleSocketMessage(data) {
|
|
@@ -900,6 +1157,7 @@ export class DredlessClient extends EventBus {
|
|
|
900
1157
|
}
|
|
901
1158
|
|
|
902
1159
|
#handleMessage(data) {
|
|
1160
|
+
if (this.lastDisconnect) return;
|
|
903
1161
|
let packet;
|
|
904
1162
|
try {
|
|
905
1163
|
packet = decodeIncomingFrame(data);
|
|
@@ -928,6 +1186,7 @@ export class DredlessClient extends EventBus {
|
|
|
928
1186
|
|
|
929
1187
|
#markReady(packet) {
|
|
930
1188
|
this.sid = packet.sid >>> 0;
|
|
1189
|
+
this.worlds.apply(packet);
|
|
931
1190
|
this.worlds.currentWorldId = packet.world ?? this.worlds.currentWorldId;
|
|
932
1191
|
this.ready = true;
|
|
933
1192
|
if (this.#ownsSocket) {
|
|
@@ -940,6 +1199,7 @@ export class DredlessClient extends EventBus {
|
|
|
940
1199
|
}
|
|
941
1200
|
this.#resolveReady?.(this);
|
|
942
1201
|
this.#resolveReady = null;
|
|
1202
|
+
this.#rejectReady = null;
|
|
943
1203
|
this.emit("ready", this);
|
|
944
1204
|
}
|
|
945
1205
|
|
|
@@ -1025,6 +1285,33 @@ export class DredlessClient extends EventBus {
|
|
|
1025
1285
|
#handleSessionSubmessage(packet) {
|
|
1026
1286
|
const submessage = packet?.submessage;
|
|
1027
1287
|
if (!submessage || typeof submessage !== "object") return;
|
|
1288
|
+
if (submessage.type === "demote") {
|
|
1289
|
+
// Permission-scoped state must not survive losing captain access.
|
|
1290
|
+
this.shipConfig = null;
|
|
1291
|
+
this.captainSubrank = null;
|
|
1292
|
+
this.playerList = null;
|
|
1293
|
+
this.crewLog = null;
|
|
1294
|
+
this.emit("demote", packet);
|
|
1295
|
+
this.emit("ship-config", null, packet);
|
|
1296
|
+
this.emit("captain-subrank", null, packet);
|
|
1297
|
+
this.emit("player-list", null, packet);
|
|
1298
|
+
this.emit("crew-log", null, packet);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (submessage.type === "log") {
|
|
1302
|
+
this.crewLog = normalizeCrewLog(submessage, this.crewLog);
|
|
1303
|
+
this.emit("crew-log", this.crewLog, packet);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (submessage.type === "starter_recovery_response") {
|
|
1307
|
+
const failReason = submessage.fail_reason == null ? null : String(submessage.fail_reason);
|
|
1308
|
+
this.emit("starter-recovery", Object.freeze({success: failReason === null, failReason}), packet);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (submessage.type === "perk_response") {
|
|
1312
|
+
this.emit("patron-perks-response", Object.freeze({failure: Boolean(submessage.failure)}), packet);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1028
1315
|
if (submessage.type === "config") {
|
|
1029
1316
|
this.shipConfig = normalizeShipConfig(submessage);
|
|
1030
1317
|
this.emit("ship-config", this.shipConfig, packet);
|
|
@@ -32,6 +32,10 @@ export class ClientNetDomain {
|
|
|
32
32
|
return this.client.lastPacket;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
get lastDisconnect() {
|
|
36
|
+
return this.client.lastDisconnect;
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
send(command = {}) {
|
|
36
40
|
return this.client.send(command);
|
|
37
41
|
}
|
|
@@ -191,20 +195,58 @@ export class ShipManagementDomain {
|
|
|
191
195
|
this.client = client;
|
|
192
196
|
}
|
|
193
197
|
|
|
198
|
+
canSave() {
|
|
199
|
+
return this.client.canSaveShip();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
save(options = {}) {
|
|
203
|
+
return this.client.saveShip(options);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
rename(name, options = {}) {
|
|
207
|
+
return this.client.renameShip(name, options);
|
|
208
|
+
}
|
|
209
|
+
|
|
194
210
|
requestPlayerList() {
|
|
195
211
|
return this.client.requestPlayerList();
|
|
196
212
|
}
|
|
197
213
|
|
|
198
|
-
resetInvite() {
|
|
199
|
-
return this.client.resetInvite();
|
|
214
|
+
resetInvite(options = {}) {
|
|
215
|
+
return this.client.resetInvite(options);
|
|
200
216
|
}
|
|
201
217
|
|
|
202
218
|
setPrivacy(privacy) {
|
|
203
219
|
return this.client.setShipPrivacy(privacy);
|
|
204
220
|
}
|
|
205
221
|
|
|
206
|
-
|
|
207
|
-
return this.client.
|
|
222
|
+
setGravity(direction) {
|
|
223
|
+
return this.client.setShipGravity(direction);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
setPatronPerks(enabled) {
|
|
227
|
+
return this.client.setShipPatronPerks(enabled);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
enablePatronPerks() {
|
|
231
|
+
return this.setPatronPerks(true);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
disablePatronPerks() {
|
|
235
|
+
return this.setPatronPerks(false);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
patronPerks() {
|
|
239
|
+
return this.client.shipConfig?.patronPerks ?? null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
hasPatronPerks(name) {
|
|
243
|
+
if (name !== undefined && typeof name !== "string") throw new TypeError("name must be a string");
|
|
244
|
+
const perks = this.patronPerks();
|
|
245
|
+
return name === undefined ? Boolean(perks?.length) : Boolean(perks?.some((perk) => perk.name === name));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
recoverStarterItem(itemId, options = {}) {
|
|
249
|
+
return this.client.recoverStarterItem(itemId, options);
|
|
208
250
|
}
|
|
209
251
|
|
|
210
252
|
setPlayerRank(refId, rank) {
|
|
@@ -219,8 +261,12 @@ export class ShipManagementDomain {
|
|
|
219
261
|
return this.client.banPlayer(refId);
|
|
220
262
|
}
|
|
221
263
|
|
|
222
|
-
|
|
223
|
-
return this.client.
|
|
264
|
+
unbanPlayer(refId) {
|
|
265
|
+
return this.client.unbanPlayer(refId);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
demoteSelf(options = {}) {
|
|
269
|
+
return this.client.demoteSelf(options);
|
|
224
270
|
}
|
|
225
271
|
|
|
226
272
|
config() {
|
|
@@ -234,6 +280,10 @@ export class ShipManagementDomain {
|
|
|
234
280
|
playerList() {
|
|
235
281
|
return this.client.playerList;
|
|
236
282
|
}
|
|
283
|
+
|
|
284
|
+
crewLog() {
|
|
285
|
+
return this.client.crewLog;
|
|
286
|
+
}
|
|
237
287
|
}
|
|
238
288
|
|
|
239
289
|
export class InventoryDomain {
|
|
@@ -1602,7 +1652,7 @@ export function currentPlayerSummary(client, scope = "current") {
|
|
|
1602
1652
|
const players = worldStateFor(client, scope)?.model.players() || [];
|
|
1603
1653
|
// `sid` is the websocket session id, not the ECS entity id used by the
|
|
1604
1654
|
// player component. The local-player flag identifies the ECS entity.
|
|
1605
|
-
return players.find((player) => player.
|
|
1655
|
+
return players.find((player) => player.isLocal) || null;
|
|
1606
1656
|
}
|
|
1607
1657
|
|
|
1608
1658
|
function currentPlayerRank(client) {
|
package/src/client/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export {DredlessClient, WrenchMode, TurretMode} from "../index.js";
|
|
2
|
-
export type {DredlessClientOptions, ClientSnapshot, TipDomain, TipList, TipOptions} from "../index.js";
|
|
2
|
+
export type {DredlessClientOptions, ClientSnapshot, TipDomain, TipList, TipOptions, RenameShipOptions, ShipManagementOptions, StarterRecoveryOptions, StarterRecoveryResult, CrewLog, CrewLogEntry, ShipGravityDirection, ShipPatronPerk, PatronPerksResponse} from "../index.js";
|