dredless 0.7.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +22 -58
  2. package/dist/browser/dredless.global.js +794 -98
  3. package/dist/browser/dredless.global.js.map +4 -4
  4. package/docs/ACCOUNT.md +146 -0
  5. package/docs/API_REFERENCE.md +63 -2563
  6. package/docs/BROWSER_API.md +151 -70
  7. package/docs/CLIENT.md +183 -0
  8. package/docs/EVENTS.md +136 -0
  9. package/docs/GETTING_STARTED.md +135 -0
  10. package/docs/MACHINES.md +351 -0
  11. package/docs/MANAGEMENT.md +285 -0
  12. package/docs/NODE_API.md +120 -318
  13. package/docs/PLAYER_INVENTORY.md +235 -0
  14. package/docs/PROTOCOL.md +227 -0
  15. package/docs/README.md +44 -0
  16. package/docs/TROUBLESHOOTING.md +91 -0
  17. package/docs/WORLD_STATE.md +183 -0
  18. package/package.json +1 -2
  19. package/src/browser.d.ts +31 -1
  20. package/src/browser.js +10 -0
  21. package/src/client/core.js +302 -15
  22. package/src/client/domains/index.js +57 -7
  23. package/src/client/index.d.ts +1 -1
  24. package/src/index.d.ts +299 -13
  25. package/src/index.js +10 -0
  26. package/src/network/account.js +182 -0
  27. package/src/network/disconnect.d.ts +2 -0
  28. package/src/network/disconnect.js +53 -0
  29. package/src/network/index.d.ts +1 -0
  30. package/src/network/index.js +1 -0
  31. package/src/network/invites.d.ts +0 -3
  32. package/src/network/invites.js +3 -40
  33. package/src/network/session.d.ts +2 -0
  34. package/src/network/session.js +21 -0
  35. package/src/protocol/outbound/ship-management.d.ts +66 -1
  36. package/src/protocol/outbound/ship-management.js +83 -0
  37. package/src/state/model/decoder.js +70 -10
  38. package/src/state/model/index.d.ts +4 -0
  39. package/src/state/model/state.js +22 -7
  40. package/src/state/world/index.js +27 -1
@@ -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
- return this.sendMessage(buildInviteResetMessage());
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
- recoverStarterItem(itemId) {
258
- return this.sendMessage(buildStarterRecoveryMessage(itemId));
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
- demoteSelf() {
286
- return this.sendMessage(buildDemoteSelfMessage());
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 previousMessage = this.ws.onmessage;
862
- this.ws.onmessage = (event) => {
863
- if (typeof previousMessage === "function") previousMessage.call(this.ws, event);
864
- message(event);
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.emit("close", event);
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
- recoverStarterItem(itemId) {
207
- return this.client.recoverStarterItem(itemId);
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
- demoteSelf() {
223
- return this.client.demoteSelf();
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.state?.q109) || null;
1655
+ return players.find((player) => player.isLocal) || null;
1606
1656
  }
1607
1657
 
1608
1658
  function currentPlayerRank(client) {
@@ -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";