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.
Files changed (40) hide show
  1. package/README.md +22 -57
  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
@@ -232,9 +232,9 @@
232
232
  if (host === "local.drednot.io") return "local_";
233
233
  return "";
234
234
  }
235
- function parseCookies(text = "") {
235
+ function parseCookies(text2 = "") {
236
236
  const cookies = /* @__PURE__ */ new Map();
237
- for (const part of String(text).split(/;\s*/)) {
237
+ for (const part of String(text2).split(/;\s*/)) {
238
238
  const index = part.indexOf("=");
239
239
  if (index <= 0) continue;
240
240
  cookies.set(part.slice(0, index).trim(), decodeURIComponent(part.slice(index + 1).trim()));
@@ -396,8 +396,8 @@
396
396
  async function fetchNoticeVersion(baseUrl = DEFAULT_BASE_URL) {
397
397
  const response = await new HttpClient({ baseUrl }).request("index.html");
398
398
  if (!response.ok) throw new Error(`Failed to fetch notice version: ${response.status} ${response.statusText}`);
399
- const text = await response.text();
400
- const version = parseNoticeVersion(text);
399
+ const text2 = await response.text();
400
+ const version = parseNoticeVersion(text2);
401
401
  if (version == null) throw new Error("Unable to locate notice version in drednot page");
402
402
  return version;
403
403
  }
@@ -411,14 +411,14 @@
411
411
  if (server2 && typeof server2 === "object") return Number(server2.index ?? server2.id);
412
412
  return Number(server2);
413
413
  }
414
- function parseGameVersion(text) {
415
- return text.match(/Object\.defineProperty\(exports,"GAME_VERSION",\{enumerable:true,get:\(\)=>e\}\);const e="([^"]+)"/)?.[1] || null;
414
+ function parseGameVersion(text2) {
415
+ return text2.match(/Object\.defineProperty\(exports,"GAME_VERSION",\{enumerable:true,get:\(\)=>e\}\);const e="([^"]+)"/)?.[1] || null;
416
416
  }
417
- function parseNoticeVersion(text) {
418
- const moduleMatch = text.match(/Object\.defineProperty\(exports,"checkNotice"[\s\S]{0,8000}?setCookie\)\("notice_version",([A-Za-z_$][\w$]*)\)/);
417
+ function parseNoticeVersion(text2) {
418
+ const moduleMatch = text2.match(/Object\.defineProperty\(exports,"checkNotice"[\s\S]{0,8000}?setCookie\)\("notice_version",([A-Za-z_$][\w$]*)\)/);
419
419
  if (moduleMatch) {
420
420
  const constant = moduleMatch[1];
421
- const prefix = text.slice(Math.max(0, moduleMatch.index - 1e3), moduleMatch.index + moduleMatch[0].length);
421
+ const prefix = text2.slice(Math.max(0, moduleMatch.index - 1e3), moduleMatch.index + moduleMatch[0].length);
422
422
  const constantMatch = prefix.match(new RegExp(`const\\s+${constant}\\s*=\\s*(\\d+)\\s*;`));
423
423
  if (constantMatch) return Number(constantMatch[1]);
424
424
  }
@@ -429,14 +429,14 @@
429
429
  /notice[^0-9]{0,40}version[^0-9]{0,20}(\d+)/i
430
430
  ];
431
431
  for (const pattern of patterns) {
432
- const match = text.match(pattern);
432
+ const match = text2.match(pattern);
433
433
  if (match) return Number(match[1]);
434
434
  }
435
435
  return null;
436
436
  }
437
- function parseServerInfo(text, baseUrl) {
437
+ function parseServerInfo(text2, baseUrl) {
438
438
  const host = hostName(baseUrl);
439
- const match = text.match(/const n=(true|false);const \$="([^"]+)";const o="([^"]+)";const T="([^"]+)";const t="([^"]+)";const S=\[([^\]]+)\];const c=\[([^\]]+)\];const A="([^"]+)";const I="([^"]+)"/s);
439
+ const match = text2.match(/const n=(true|false);const \$="([^"]+)";const o="([^"]+)";const T="([^"]+)";const t="([^"]+)";const S=\[([^\]]+)\];const c=\[([^\]]+)\];const A="([^"]+)";const I="([^"]+)"/s);
440
440
  if (!match) return null;
441
441
  const [localDomain, baseDomain, testDomain, testGameDomain] = match.slice(2, 6);
442
442
  const domains = [...match[6].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1]);
@@ -512,8 +512,8 @@
512
512
  async #fetchMeta() {
513
513
  const response = await new HttpClient({ baseUrl: this.baseUrl }).request("index.html");
514
514
  if (!response.ok) throw new Error(`Failed to fetch bundle: ${response.status} ${response.statusText}`);
515
- const text = await response.text();
516
- return { text, version: parseGameVersion(text), serverInfo: parseServerInfo(text, this.baseUrl) };
515
+ const text2 = await response.text();
516
+ return { text: text2, version: parseGameVersion(text2), serverInfo: parseServerInfo(text2, this.baseUrl) };
517
517
  }
518
518
  };
519
519
  fetchGameVersion = (baseUrl = DEFAULT_BASE_URL) => new ServerDirectory(baseUrl).version();
@@ -550,6 +550,57 @@
550
550
  }
551
551
  };
552
552
 
553
+ // src/network/disconnect.js
554
+ var DropReason = Object.freeze({
555
+ ProtocolError: 4e3,
556
+ BadVersion: 4001,
557
+ ConnectLimit: 4002,
558
+ ConnectRate: 4003,
559
+ TeamLimit: 4004,
560
+ FullPlayers: 4005,
561
+ FullTeams: 4006,
562
+ TeamNotAvailable: 4007,
563
+ Kicked: 4008,
564
+ Banned: 4009,
565
+ Idle: 4010,
566
+ ShipDestruct: 4011,
567
+ JoinBlocked: 4012,
568
+ HardBanned: 4013,
569
+ NoLoad: 4014,
570
+ Saved: 4015,
571
+ InvalidInvite: 4016,
572
+ Shutdown: 4017,
573
+ ServerError: 4018,
574
+ GTFO: 4019
575
+ });
576
+ var reasonNames = new Map(Object.entries(DropReason).map(([name, code]) => [code, name]));
577
+ var textDecoder = new TextDecoder();
578
+ function normalizeDisconnect(eventOrCode, reason = "") {
579
+ const event = eventOrCode != null && typeof eventOrCode === "object" ? eventOrCode : null;
580
+ const code = event?.code ?? eventOrCode;
581
+ const rawReason = event?.reason ?? reason;
582
+ let text2 = "";
583
+ if (typeof rawReason === "string") text2 = rawReason;
584
+ else if (ArrayBuffer.isView(rawReason)) {
585
+ text2 = textDecoder.decode(new Uint8Array(rawReason.buffer, rawReason.byteOffset, rawReason.byteLength));
586
+ } else if (rawReason instanceof ArrayBuffer) text2 = textDecoder.decode(rawReason);
587
+ const normalizedCode = Number.isInteger(code) ? code : 1006;
588
+ return Object.freeze({
589
+ code: normalizedCode,
590
+ name: reasonNames.get(normalizedCode) ?? null,
591
+ reason: text2,
592
+ // EventEmitter close callbacks don't report whether the handshake was clean.
593
+ wasClean: typeof event?.wasClean === "boolean" ? event.wasClean : null
594
+ });
595
+ }
596
+ var DisconnectError = class extends Error {
597
+ constructor(disconnect, message = "Disconnected") {
598
+ super(`${message}: ${disconnect.name ?? "Unknown"} (${disconnect.code})${disconnect.reason ? ` \u2014 ${disconnect.reason}` : ""}`);
599
+ this.name = "DisconnectError";
600
+ this.disconnect = disconnect;
601
+ }
602
+ };
603
+
553
604
  // src/client/core.js
554
605
  init_constants();
555
606
  init_runtime();
@@ -588,16 +639,68 @@
588
639
  }
589
640
  };
590
641
 
642
+ // src/protocol/outbound/comms.js
643
+ var COMMS_MESSAGE_TYPE = 3;
644
+ function buildCommsMessage(message = "") {
645
+ return {
646
+ type: COMMS_MESSAGE_TYPE,
647
+ msg: String(message)
648
+ };
649
+ }
650
+ function normalizeCommsEvent(event) {
651
+ const rawMessages = Array.isArray(event?.msgs_text) ? event.msgs_text : [];
652
+ return {
653
+ ...event,
654
+ entity: event?.ent_id != null && Number.isFinite(Number(event.ent_id)) ? Number(event.ent_id) : null,
655
+ rawMessages,
656
+ messages: rawMessages.map((message) => ({
657
+ raw: message,
658
+ text: flattenRichText(message)
659
+ }))
660
+ };
661
+ }
662
+ function flattenRichText(value) {
663
+ if (value == null) return "";
664
+ if (typeof value === "string") return value;
665
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
666
+ if (Array.isArray(value)) return value.map(flattenRichText).join("");
667
+ if (typeof value === "object") {
668
+ if (Object.hasOwn(value, "c")) return flattenRichText(value.c);
669
+ if (Object.hasOwn(value, "text")) return flattenRichText(value.text);
670
+ }
671
+ return "";
672
+ }
673
+
591
674
  // src/protocol/outbound/ship-management.js
592
675
  var SHIP_MANAGEMENT_TYPE = 4;
676
+ var CREW_LOG_LENGTH = 500;
593
677
  var SET_PRIVACY_ACTION = "set_privacy";
678
+ var GRAVITY_ACTION = "gravity";
679
+ var SET_PATRON_PERKS_ACTION = "set_patron_perks";
594
680
  var STARTER_RECOVERY_ACTION = "starter_recovery";
595
681
  var PLAYER_LIST_ACTION = "player_list";
596
682
  var INVITE_RESET_ACTION = "invite_reset";
597
683
  var SET_RANK_ACTION = "set_rank";
598
684
  var KICK_ACTION = "kick";
599
685
  var BAN_ACTION = "ban";
686
+ var UNBAN_ACTION = "unban";
600
687
  var DEMOTE_SELF_ACTION = "demote_self";
688
+ var SAVE_SHIP_ACTION = "save_team";
689
+ var RENAME_REQUEST_ACTION = "rename_request";
690
+ var RENAME_COMMIT_ACTION = "rename_commit";
691
+ var GravityDirection = Object.freeze({
692
+ Down: "down",
693
+ Up: "up",
694
+ Left: "left",
695
+ Right: "right"
696
+ });
697
+ function normalizeGravityDirection(value) {
698
+ if (value === GravityDirection.Down || value === 0) return 0;
699
+ if (value === GravityDirection.Up || value === 1) return 1;
700
+ if (value === GravityDirection.Left || value === 2) return 2;
701
+ if (value === GravityDirection.Right || value === 3) return 3;
702
+ throw new RangeError('direction must be "down", "up", "left", "right", or an integer from 0 to 3');
703
+ }
601
704
  function requireNonNegativeInteger(value, name) {
602
705
  const number = Number(value);
603
706
  if (!Number.isInteger(number) || number < 0) throw new RangeError(`${name} must be a non-negative integer`);
@@ -626,6 +729,13 @@
626
729
  function buildShipPrivacyMessage(privacy) {
627
730
  return buildShipManagementMessage(SET_PRIVACY_ACTION, normalizePrivacy(privacy));
628
731
  }
732
+ function buildShipGravityMessage(direction) {
733
+ return buildShipManagementMessage(GRAVITY_ACTION, normalizeGravityDirection(direction));
734
+ }
735
+ function buildSetPatronPerksMessage(enabled) {
736
+ if (typeof enabled !== "boolean") throw new TypeError("enabled must be a boolean");
737
+ return buildShipManagementMessage(SET_PATRON_PERKS_ACTION, enabled);
738
+ }
629
739
  function buildStarterRecoveryMessage(itemId) {
630
740
  return buildShipManagementMessage(STARTER_RECOVERY_ACTION, requireNonNegativeInteger(itemId, "itemId"));
631
741
  }
@@ -644,9 +754,26 @@
644
754
  function buildBanPlayerMessage(refId) {
645
755
  return buildShipManagementMessage(BAN_ACTION, normalizePlayerRef(refId));
646
756
  }
757
+ function buildUnbanPlayerMessage(refId) {
758
+ return buildShipManagementMessage(UNBAN_ACTION, normalizePlayerRef(refId));
759
+ }
647
760
  function buildDemoteSelfMessage() {
648
761
  return buildShipManagementMessage(DEMOTE_SELF_ACTION);
649
762
  }
763
+ function buildSaveShipMessage() {
764
+ return buildShipManagementMessage(SAVE_SHIP_ACTION);
765
+ }
766
+ function buildShipRenameRequestMessage() {
767
+ return buildShipManagementMessage(RENAME_REQUEST_ACTION);
768
+ }
769
+ function buildShipRenameCommitMessage(name, color) {
770
+ if (typeof name !== "string") throw new TypeError("name must be a string");
771
+ if (name.length > 32) throw new RangeError("name must be at most 32 UTF-16 code units");
772
+ if (typeof color !== "string" || !/^#[0-9a-f]{6}$/i.test(color)) {
773
+ throw new TypeError("color must be a #RRGGBB hex string");
774
+ }
775
+ return buildShipManagementMessage(RENAME_COMMIT_ACTION, name, { arg2: color });
776
+ }
650
777
  function normalizeShipConfig(event = {}) {
651
778
  const source = event && typeof event === "object" ? event : {};
652
779
  const config = source.config && typeof source.config === "object" ? source.config : {};
@@ -665,6 +792,27 @@
665
792
  enableCheats: Boolean(source.enable_cheats)
666
793
  };
667
794
  }
795
+ function normalizeCrewLog(event = {}, previous = null) {
796
+ const source = event && typeof event === "object" ? event : {};
797
+ const reset = Boolean(source.reset);
798
+ const amend = Boolean(source.amend);
799
+ const entries = reset ? [] : (previous?.entries || []).slice();
800
+ if (amend) entries.pop();
801
+ const changes = Array.isArray(source.entries) ? source.entries.map((entry) => {
802
+ const value = entry && typeof entry === "object" ? entry : {};
803
+ return {
804
+ icon: value.icon ?? null,
805
+ text: flattenRichText(value.text),
806
+ rawText: value.text ?? null,
807
+ count: value.count ?? 1,
808
+ refId: value.ref ?? null,
809
+ secondaryRefId: value.ref2 ?? null
810
+ };
811
+ }) : [];
812
+ for (const entry of changes) entries.push(entry);
813
+ if (entries.length > CREW_LOG_LENGTH) entries.splice(0, entries.length - CREW_LOG_LENGTH);
814
+ return { entries, changes, reset, amend };
815
+ }
668
816
  function normalizeShipPlayerList(event = {}, previous = null, currentCaptainSubrank = null) {
669
817
  const source = event && typeof event === "object" ? event : {};
670
818
  const changes = Array.isArray(source.player_list) ? source.player_list.map(normalizePlayerListChange) : [];
@@ -934,7 +1082,7 @@
934
1082
  const timeout = timeoutValue(options);
935
1083
  if (!handle.exists()) throw new Error("Rank target no longer exists");
936
1084
  if (handle.rank === value) return this.client;
937
- const text = { 0: "Restrict to Guests", 1: "Restrict to Crew", 3: "Restrict to Captains" }[value];
1085
+ const text2 = { 0: "Restrict to Guests", 1: "Restrict to Crew", 3: "Restrict to Captains" }[value];
938
1086
  await this.#open(handle, options);
939
1087
  await waitFor(
940
1088
  this.client,
@@ -942,7 +1090,7 @@
942
1090
  () => handle.rank === value,
943
1091
  timeout,
944
1092
  "access rank update",
945
- () => this.#select(text)
1093
+ () => this.#select(text2)
946
1094
  );
947
1095
  return this.client;
948
1096
  });
@@ -3550,8 +3698,8 @@
3550
3698
  }
3551
3699
  return out;
3552
3700
  }
3553
- function encodeStringBytes(text) {
3554
- const bytes = encoder.encode(String(text));
3701
+ function encodeStringBytes(text2) {
3702
+ const bytes = encoder.encode(String(text2));
3555
3703
  const length = bytes.length;
3556
3704
  if (length <= 31) {
3557
3705
  const out2 = new Uint8Array(1 + length);
@@ -4453,8 +4601,8 @@
4453
4601
  if (end > reader.bytes.length) return false;
4454
4602
  const blob = reader.bytes.slice(peek.offset, end);
4455
4603
  try {
4456
- const text = decoder.decode(blob);
4457
- return text.length > 0 && !/[\u0000-\u0008\u000e-\u001f]/.test(text);
4604
+ const text2 = decoder.decode(blob);
4605
+ return text2.length > 0 && !/[\u0000-\u0008\u000e-\u001f]/.test(text2);
4458
4606
  } catch (_) {
4459
4607
  return false;
4460
4608
  }
@@ -4646,6 +4794,7 @@
4646
4794
  }],
4647
4795
  [55, {
4648
4796
  name: "player",
4797
+ defaults: { q20: 1, q60: 10, q64: 10, q68: 10 },
4649
4798
  packedBeforeValues: true,
4650
4799
  fields: numericFields({
4651
4800
  256: 20,
@@ -4680,8 +4829,10 @@
4680
4829
  ]
4681
4830
  }],
4682
4831
  [56, {
4683
- name: "numeric_snapshot",
4684
- fields: numericFields({ 1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 64: 40, 256: 44, 1024: 48 })
4832
+ name: "player_appearance",
4833
+ // Official StateOutfit: four body colors, then four color/style pairs.
4834
+ defaults: { q20: 0, q24: 0, q28: 0, q32: 0, q36: 0, q40: 0, q44: 0, q48: 0, q52: 0, q56: 0, q60: 0, q64: 0 },
4835
+ fields: numericFields({ 1: 20, 2: 24, 4: 28, 8: 32, 16: 36, 32: 40, 64: 44, 128: 48, 256: 52, 512: 56, 1024: 60, 2048: 64 })
4685
4836
  }],
4686
4837
  [59, { name: "numeric_sparse", fields: numericFields({ 1: 20, 2: 24, 4: 28, 8: 32, 16: 36 }) }],
4687
4838
  [60, { name: "fluid_tank", packedBits: [{ bit: 4, offset: 29 }], ...TWO_FIELD_SPEC }],
@@ -5063,7 +5214,9 @@
5063
5214
  const tau = 2 * Math.PI;
5064
5215
  const absoluteAngleRadians = relativeAngleRadians == null || mountingAngleRadians == null ? null : ((mountingAngleRadians + relativeAngleRadians) % tau + tau) % tau;
5065
5216
  const ammoItemId = record.q24 == null ? null : CANNON_AMMO_COLOR_ITEM_IDS.get(record.q24) ?? null;
5066
- const charge = record.q40 ?? null;
5217
+ const indicatorValue = (record.q40 ?? 0) / 50;
5218
+ const indicatorVisible = indicatorValue >= 1;
5219
+ const indicatorBlinking = indicatorValue === 4;
5067
5220
  const recoil = record.q36 ?? null;
5068
5221
  const recoil2 = record.q48 ?? null;
5069
5222
  return {
@@ -5078,13 +5231,21 @@
5078
5231
  recoil,
5079
5232
  recoil2,
5080
5233
  recoils: [recoil, recoil2],
5081
- charge,
5082
- charged: charge == null ? null : charge >= 50,
5234
+ indicatorValue,
5235
+ indicatorVisible,
5236
+ indicatorColor: !indicatorVisible ? null : indicatorBlinking ? 16711680 : cannonIndicatorColor(indicatorValue),
5237
+ indicatorBlinking,
5238
+ indicatorBlinkColor: indicatorBlinking ? 16776960 : null,
5083
5239
  spin: record.q52 ?? null,
5084
- coolingCellCount: record.q56 ?? 0,
5240
+ resourceCount: record.q56 ?? 0,
5085
5241
  state: cloneRecord(record)
5086
5242
  };
5087
5243
  }
5244
+ function cannonIndicatorColor(value) {
5245
+ const distance = Math.abs(Math.fround(2 - Math.fround(value)));
5246
+ const channel = Math.max(0, Math.min(255, Math.trunc(Math.fround(Math.fround(1 - distance) * 255))));
5247
+ return value < 2 ? channel << 16 | 65280 : 16711680 | channel << 8;
5248
+ }
5088
5249
  function summarizeThruster(entity, typeId, record) {
5089
5250
  if (!THRUSTER_TYPE_IDS.has(typeId)) return null;
5090
5251
  const facing = Number.isFinite(Number(record?.q20)) ? Number(record.q20) : 0;
@@ -5662,7 +5823,36 @@
5662
5823
  state: cloneRecord(record)
5663
5824
  };
5664
5825
  }
5665
- function summarizePlayer(entity, record, previewRecord = null, blueprintItems = []) {
5826
+ function summarizePlayerAppearance(record, player) {
5827
+ if (!record) return null;
5828
+ const style = (colorOffset, styleOffset) => ({
5829
+ color: record[qKey(colorOffset)] ?? 0,
5830
+ styleId: record[qKey(styleOffset)] ?? 0
5831
+ });
5832
+ const override = (offset) => {
5833
+ const id = player[qKey(offset)] ?? 0;
5834
+ return id === 0 ? null : { itemId: id, itemName: entityNameFromType(id) };
5835
+ };
5836
+ return {
5837
+ bodyColor: record.q20 ?? 0,
5838
+ legsColor: record.q24 ?? 0,
5839
+ skinColor: record.q28 ?? 0,
5840
+ feetColor: record.q32 ?? 0,
5841
+ hair: style(36, 40),
5842
+ hat: style(44, 48),
5843
+ face1: style(52, 56),
5844
+ face2: style(60, 64),
5845
+ itemOverrides: {
5846
+ back: override(36),
5847
+ hands: override(40),
5848
+ feet: override(44),
5849
+ head1: override(48),
5850
+ head2: override(52)
5851
+ },
5852
+ state: cloneRecord(record)
5853
+ };
5854
+ }
5855
+ function summarizePlayer(entity, record, previewRecord = null, blueprintItems = [], appearanceRecord = null, localPlayerEntity = null) {
5666
5856
  if (!record) return null;
5667
5857
  const rawHeldItemId = record.q28 == null ? null : Number(record.q28);
5668
5858
  const heldItemId = Number.isFinite(rawHeldItemId) && rawHeldItemId !== 0 ? rawHeldItemId : null;
@@ -5673,14 +5863,29 @@
5673
5863
  name: decodeText(record.blob92),
5674
5864
  heldItemId,
5675
5865
  heldItemName: entityNameFromType(heldItemId),
5676
- repairTargetDistance: record.q56 == null ? null : record.q56 / 10,
5677
- repairTargetAngle: record.q80 == null ? null : record.q80 / 100,
5866
+ itemMode: record.q32 ?? null,
5867
+ buildMultiplier: numberOrNull(record.q68, 10),
5868
+ heldItemBeamDistance: numberOrNull(record.q56, 10),
5869
+ heldItemAngle: numberOrNull(record.q80, 200),
5678
5870
  shipRank: PLAYER_SHIP_RANKS.get(teamRank) || null,
5679
5871
  patronTier: patronTierName(gameRank),
5680
5872
  isDeveloper: gameRank === 1,
5681
5873
  isPatron: gameRank >= 2 && gameRank <= 6,
5682
- piloting: Boolean(record.q107),
5874
+ isLocal: entity === localPlayerEntity,
5875
+ zoomedOut: Boolean(record.q107),
5683
5876
  muted: Boolean(record.q112),
5877
+ invisible: Boolean(record.q106),
5878
+ cheats: {
5879
+ noclip: Boolean(record.q105),
5880
+ sandboxBuild: Boolean(record.q108)
5881
+ },
5882
+ itemEffectActive: Boolean(record.q111),
5883
+ animation: {
5884
+ facing: record.q20 ?? 1,
5885
+ orientation: record.q24 ?? 0,
5886
+ walking: Boolean(record.q109)
5887
+ },
5888
+ appearance: summarizePlayerAppearance(appearanceRecord, record),
5684
5889
  actionPreview: summarizePlayerPreview(entity, previewRecord, blueprintItems),
5685
5890
  state: cloneRecord(record)
5686
5891
  };
@@ -5938,6 +6143,7 @@
5938
6143
  defineLazyProperty: defineLazyProperty2
5939
6144
  } = decoder_exports;
5940
6145
  var ModelState = class {
6146
+ #localPlayerEntity = null;
5941
6147
  #blueprintItems = null;
5942
6148
  #helmOccupied = /* @__PURE__ */ new Map();
5943
6149
  #commsStationOccupied = /* @__PURE__ */ new Map();
@@ -5959,6 +6165,16 @@
5959
6165
  this.isOverworld = next;
5960
6166
  this.#invalidateDerived();
5961
6167
  }
6168
+ get localPlayerEntity() {
6169
+ return this.#localPlayerEntity;
6170
+ }
6171
+ setLocalPlayerEntity(entity) {
6172
+ const next = entity == null ? null : Number(entity);
6173
+ if (next !== null && (!Number.isInteger(next) || next < 0)) throw new RangeError("local player entity must be a non-negative integer or null");
6174
+ if (this.#localPlayerEntity === next) return;
6175
+ this.#localPlayerEntity = next;
6176
+ this.#invalidateDerived();
6177
+ }
5962
6178
  table(id) {
5963
6179
  return this.tables.get(Number(id)) || EMPTY_TABLE2;
5964
6180
  }
@@ -6197,25 +6413,25 @@
6197
6413
  #updateHelmOccupancy(update) {
6198
6414
  const table7 = update.sections.find((section) => section.table === 7);
6199
6415
  if (!table7) return;
6200
- const changedPilotState = /* @__PURE__ */ new Map();
6416
+ const changedZoomState = /* @__PURE__ */ new Map();
6201
6417
  for (const section of update.sections) {
6202
6418
  if (section.table !== 55) continue;
6203
6419
  for (const changed of section.records) {
6204
6420
  if (!(changed.mask & 16)) continue;
6205
6421
  const player = this.record(55, changed.entity);
6206
- changedPilotState.set(changed.entity, Boolean(player?.q107));
6422
+ changedZoomState.set(changed.entity, Boolean(player?.q107));
6207
6423
  }
6208
6424
  }
6209
- const activePilotCount = [...this.table(55).values()].filter((record) => record?.q107).length;
6425
+ const zoomedOutPlayerCount = [...this.table(55).values()].filter((record) => record?.q107).length;
6210
6426
  for (const changed of table7.records) {
6211
6427
  const record = this.record(7, changed.entity);
6212
6428
  const typeId = entityTypeIdFromRecord2(record);
6213
6429
  if (!HELM_TYPE_IDS2.has(typeId)) continue;
6214
- if (activePilotCount > 0 && record?.q20 === 0 && HELM_TYPE_IDS2.has(Number(record?.q32))) {
6430
+ if (zoomedOutPlayerCount > 0 && record?.q20 === 0 && HELM_TYPE_IDS2.has(Number(record?.q32))) {
6215
6431
  this.#helmOccupied.set(changed.entity, true);
6216
6432
  }
6217
- if (!(changed.mask & 8) || changedPilotState.size === 0) continue;
6218
- this.#helmOccupied.set(changed.entity, [...changedPilotState.values()].some(Boolean));
6433
+ if (!(changed.mask & 8) || changedZoomState.size === 0) continue;
6434
+ this.#helmOccupied.set(changed.entity, [...changedZoomState.values()].some(Boolean));
6219
6435
  }
6220
6436
  }
6221
6437
  #updateCommsStationOccupancy(update) {
@@ -6545,7 +6761,7 @@
6545
6761
  const shieldGenerator = typeId === 256 ? summarizeShieldGenerator2(entityId, shieldRecord, itemHolderRecord, shieldGeneratorBoostRecord) : null;
6546
6762
  const shieldProjector = typeId === 257 ? summarizeShieldProjector2(entityId, shieldProjectorRecord) : null;
6547
6763
  const blueprintPreview = summarizeBlueprintPreview2(entityId, blueprintPreviewRecord, transformRecord);
6548
- const player = summarizePlayer2(entityId, playerRecord, rec(14), this.#blueprintPreviewItems());
6764
+ const player = summarizePlayer2(entityId, playerRecord, rec(14), this.#blueprintPreviewItems(), rec(56), this.#localPlayerEntity);
6549
6765
  const shipControl = summarizeShipControl2(entityId, shipControlRecord);
6550
6766
  const helm = summarizeHelm2(entityId, typeId, this.#helmOccupied.get(entityId));
6551
6767
  const commsStation = summarizeCommsStation2(entityId, typeId, commsStationRecord, this.#commsStationOccupied.get(entityId));
@@ -7362,6 +7578,7 @@
7362
7578
  var WorldStore = class {
7363
7579
  #overworldId = null;
7364
7580
  #shipWorldId = null;
7581
+ #cameraFocus = null;
7365
7582
  constructor(limits = {}) {
7366
7583
  this.currentWorldId = null;
7367
7584
  this.worlds = /* @__PURE__ */ new Map();
@@ -7378,6 +7595,17 @@
7378
7595
  }
7379
7596
  apply(packet) {
7380
7597
  if (!packet || packet.world == null) return null;
7598
+ if (packet.type === 21) {
7599
+ if (this.#cameraFocus && this.#cameraFocus.world !== packet.world) {
7600
+ this.worlds.get(Number(this.#cameraFocus.world))?.model.setLocalPlayerEntity(null);
7601
+ }
7602
+ this.#cameraFocus = packet;
7603
+ this.currentWorldId = Number(packet.world);
7604
+ const world = this.get(packet.world);
7605
+ world.model.setLocalPlayerEntity(null);
7606
+ this.#resolveLocalPlayer(world);
7607
+ return { type: "camera-focus", world, packet };
7608
+ }
7381
7609
  if (packet.type === 22) return this.#applyMeta(packet);
7382
7610
  if (packet.type === 23) return this.#applyTiles(packet);
7383
7611
  if (packet.type === 20) return this.#applyModel(packet);
@@ -7423,16 +7651,25 @@
7423
7651
  if (!shipWorld || shipWorld.parentWorld == null || shipWorld.parentEntity == null) return null;
7424
7652
  return this.worlds.get(Number(shipWorld.parentWorld))?.entity(shipWorld.parentEntity) || null;
7425
7653
  }
7654
+ #resolveLocalPlayer(world) {
7655
+ if (!this.#cameraFocus || Number(this.#cameraFocus.world) !== world.id || world.seed == null) return;
7656
+ const focus = world.decodeEncrypted(this.#cameraFocus.data);
7657
+ if (Array.isArray(focus) && focus.length === 2 && Number.isInteger(focus[0]) && focus[0] >= 0 && focus[1] === 0) {
7658
+ world.model.setLocalPlayerEntity(focus[0]);
7659
+ }
7660
+ }
7426
7661
  #applyMeta(packet) {
7427
7662
  if (packet.removed) {
7428
7663
  const world2 = this.worlds.get(Number(packet.world));
7429
7664
  if (world2) world2.readMeta(packet);
7430
7665
  this.worlds.delete(Number(packet.world));
7666
+ if (Number(this.#cameraFocus?.world) === Number(packet.world)) this.#cameraFocus = null;
7431
7667
  if (this.currentWorldId === Number(packet.world)) this.currentWorldId = null;
7432
7668
  return { type: "world-removed", world: world2 || null, packet };
7433
7669
  }
7434
7670
  const world = this.get(packet.world);
7435
7671
  world.readMeta(packet);
7672
+ this.#resolveLocalPlayer(world);
7436
7673
  if (this.currentWorldId == null && !world.isOverworld) this.currentWorldId = world.id;
7437
7674
  return { type: "world", world };
7438
7675
  }
@@ -7480,7 +7717,10 @@
7480
7717
  if (packet.model_data && world.seed != null) {
7481
7718
  const previousModel = world.model;
7482
7719
  try {
7483
- if (packet.full) world.model = new ModelState({ isOverworld: world.isOverworld });
7720
+ if (packet.full) {
7721
+ world.model = new ModelState({ isOverworld: world.isOverworld });
7722
+ world.model.setLocalPlayerEntity(previousModel.localPlayerEntity);
7723
+ }
7484
7724
  result.decoded = decodeModelPayload(packet.model_data, packet.world, world.seed);
7485
7725
  result.model = world.model.apply(result.decoded, { full: Boolean(packet.full) });
7486
7726
  if (result.model?.error && packet.full && previousModel) world.model = previousModel;
@@ -7539,38 +7779,6 @@
7539
7779
  };
7540
7780
  }
7541
7781
 
7542
- // src/protocol/outbound/comms.js
7543
- var COMMS_MESSAGE_TYPE = 3;
7544
- function buildCommsMessage(message = "") {
7545
- return {
7546
- type: COMMS_MESSAGE_TYPE,
7547
- msg: String(message)
7548
- };
7549
- }
7550
- function normalizeCommsEvent(event) {
7551
- const rawMessages = Array.isArray(event?.msgs_text) ? event.msgs_text : [];
7552
- return {
7553
- ...event,
7554
- entity: event?.ent_id != null && Number.isFinite(Number(event.ent_id)) ? Number(event.ent_id) : null,
7555
- rawMessages,
7556
- messages: rawMessages.map((message) => ({
7557
- raw: message,
7558
- text: flattenRichText(message)
7559
- }))
7560
- };
7561
- }
7562
- function flattenRichText(value) {
7563
- if (value == null) return "";
7564
- if (typeof value === "string") return value;
7565
- if (typeof value === "number" || typeof value === "boolean") return String(value);
7566
- if (Array.isArray(value)) return value.map(flattenRichText).join("");
7567
- if (typeof value === "object") {
7568
- if (Object.hasOwn(value, "c")) return flattenRichText(value.c);
7569
- if (Object.hasOwn(value, "text")) return flattenRichText(value.text);
7570
- }
7571
- return "";
7572
- }
7573
-
7574
7782
  // src/protocol/outbound/commands.js
7575
7783
  init_constants();
7576
7784
  var SHA256_K = new Uint32Array([
@@ -7891,11 +8099,11 @@
7891
8099
  }
7892
8100
  return normalized;
7893
8101
  }
7894
- function buildSignTextMessage(text = "", mode = SignDisplayMode.Always) {
8102
+ function buildSignTextMessage(text2 = "", mode = SignDisplayMode.Always) {
7895
8103
  return {
7896
8104
  type: 5,
7897
8105
  cmd: SIGN_TEXT_COMMAND,
7898
- args: [String(text), normalizeSignDisplayMode(mode)]
8106
+ args: [String(text2), normalizeSignDisplayMode(mode)]
7899
8107
  };
7900
8108
  }
7901
8109
 
@@ -8427,6 +8635,9 @@
8427
8635
  get lastPacket() {
8428
8636
  return this.client.lastPacket;
8429
8637
  }
8638
+ get lastDisconnect() {
8639
+ return this.client.lastDisconnect;
8640
+ }
8430
8641
  send(command = {}) {
8431
8642
  return this.client.send(command);
8432
8643
  }
@@ -8548,17 +8759,46 @@
8548
8759
  constructor(client) {
8549
8760
  this.client = client;
8550
8761
  }
8762
+ canSave() {
8763
+ return this.client.canSaveShip();
8764
+ }
8765
+ save(options = {}) {
8766
+ return this.client.saveShip(options);
8767
+ }
8768
+ rename(name, options = {}) {
8769
+ return this.client.renameShip(name, options);
8770
+ }
8551
8771
  requestPlayerList() {
8552
8772
  return this.client.requestPlayerList();
8553
8773
  }
8554
- resetInvite() {
8555
- return this.client.resetInvite();
8774
+ resetInvite(options = {}) {
8775
+ return this.client.resetInvite(options);
8556
8776
  }
8557
8777
  setPrivacy(privacy) {
8558
8778
  return this.client.setShipPrivacy(privacy);
8559
8779
  }
8560
- recoverStarterItem(itemId) {
8561
- return this.client.recoverStarterItem(itemId);
8780
+ setGravity(direction) {
8781
+ return this.client.setShipGravity(direction);
8782
+ }
8783
+ setPatronPerks(enabled) {
8784
+ return this.client.setShipPatronPerks(enabled);
8785
+ }
8786
+ enablePatronPerks() {
8787
+ return this.setPatronPerks(true);
8788
+ }
8789
+ disablePatronPerks() {
8790
+ return this.setPatronPerks(false);
8791
+ }
8792
+ patronPerks() {
8793
+ return this.client.shipConfig?.patronPerks ?? null;
8794
+ }
8795
+ hasPatronPerks(name) {
8796
+ if (name !== void 0 && typeof name !== "string") throw new TypeError("name must be a string");
8797
+ const perks = this.patronPerks();
8798
+ return name === void 0 ? Boolean(perks?.length) : Boolean(perks?.some((perk) => perk.name === name));
8799
+ }
8800
+ recoverStarterItem(itemId, options = {}) {
8801
+ return this.client.recoverStarterItem(itemId, options);
8562
8802
  }
8563
8803
  setPlayerRank(refId, rank) {
8564
8804
  return this.client.setPlayerRank(refId, rank);
@@ -8569,8 +8809,11 @@
8569
8809
  banPlayer(refId) {
8570
8810
  return this.client.banPlayer(refId);
8571
8811
  }
8572
- demoteSelf() {
8573
- return this.client.demoteSelf();
8812
+ unbanPlayer(refId) {
8813
+ return this.client.unbanPlayer(refId);
8814
+ }
8815
+ demoteSelf(options = {}) {
8816
+ return this.client.demoteSelf(options);
8574
8817
  }
8575
8818
  config() {
8576
8819
  return this.client.shipConfig;
@@ -8581,6 +8824,9 @@
8581
8824
  playerList() {
8582
8825
  return this.client.playerList;
8583
8826
  }
8827
+ crewLog() {
8828
+ return this.client.crewLog;
8829
+ }
8584
8830
  };
8585
8831
  var InventoryDomain = class {
8586
8832
  constructor(client) {
@@ -9301,8 +9547,8 @@
9301
9547
  get state() {
9302
9548
  return summaryForEntity(this.client, this.scope, this.id)?.contents?.sign || null;
9303
9549
  }
9304
- setText(text = "", mode = 0) {
9305
- return this.client.setSignText(text, mode);
9550
+ setText(text2 = "", mode = 0) {
9551
+ return this.client.setSignText(text2, mode);
9306
9552
  }
9307
9553
  get text() {
9308
9554
  return this.state?.text;
@@ -9680,7 +9926,7 @@
9680
9926
  const sid = Number(client?.sid);
9681
9927
  if (!Number.isFinite(sid)) return null;
9682
9928
  const players = worldStateFor(client, scope)?.model.players() || [];
9683
- return players.find((player) => player.state?.q109) || null;
9929
+ return players.find((player) => player.isLocal) || null;
9684
9930
  }
9685
9931
  function currentPlayerRank(client) {
9686
9932
  const player = currentPlayerSummary(client);
@@ -9915,6 +10161,7 @@
9915
10161
  this.ready = false;
9916
10162
  this.packetCount = 0;
9917
10163
  this.lastPacket = null;
10164
+ this.lastDisconnect = null;
9918
10165
  this.packets = [];
9919
10166
  this.packetHistory = normalizeHistoryLimit(packetHistory, DEFAULT_PACKET_HISTORY);
9920
10167
  this.worlds = new WorldStore({ eventHistory, modelPacketHistory, chunkHistory });
@@ -9939,6 +10186,7 @@
9939
10186
  this.shipConfig = null;
9940
10187
  this.captainSubrank = null;
9941
10188
  this.playerList = null;
10189
+ this.crewLog = null;
9942
10190
  this.outfits = /* @__PURE__ */ new Map();
9943
10191
  this.commandAcks = /* @__PURE__ */ new Map();
9944
10192
  this.lastCommandAck = null;
@@ -9947,6 +10195,8 @@
9947
10195
  this.#resolveReady = resolve;
9948
10196
  this.#rejectReady = reject;
9949
10197
  });
10198
+ this.readyPromise.catch(() => {
10199
+ });
9950
10200
  this.attachMode = attachMode;
9951
10201
  this.attached = Boolean(attachMode);
9952
10202
  if (connect) this.#connect().catch((error) => this.#fail(error));
@@ -9968,6 +10218,10 @@
9968
10218
  #attachedOpened = false;
9969
10219
  #resolveReady = null;
9970
10220
  #rejectReady = null;
10221
+ #savePending = false;
10222
+ #renamePending = false;
10223
+ #starterRecoveryPending = false;
10224
+ #managementPending = /* @__PURE__ */ new Set();
9971
10225
  #inputSettings = {
9972
10226
  wrench_mode: 1,
9973
10227
  turret_mode: 0
@@ -10009,17 +10263,170 @@
10009
10263
  sendShipManagement(act, arg = null) {
10010
10264
  return this.sendMessage(buildShipManagementMessage(act, arg));
10011
10265
  }
10266
+ canSaveShip() {
10267
+ if (!this.connected || !this.ready) return false;
10268
+ const ship = this.worlds.shipWorld();
10269
+ if (!ship || ship.isOverworld !== false || ship.parentWorld == null) return false;
10270
+ const parent = this.worlds.worlds.get(Number(ship.parentWorld));
10271
+ return parent?.meta?.danger_level === 0;
10272
+ }
10273
+ async saveShip({ timeout = 1e4 } = {}) {
10274
+ this.#assertWritable();
10275
+ if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
10276
+ throw new RangeError("timeout must be positive milliseconds no greater than 2147483647");
10277
+ }
10278
+ if (this.#savePending) throw new Error("A ship save is already in progress");
10279
+ if (!this.canSaveShip()) throw new Error("Saving requires a connected, ready client with its ship in a known Safe Zone");
10280
+ this.#savePending = true;
10281
+ try {
10282
+ return await new Promise((resolve, reject) => {
10283
+ const cleanup = () => {
10284
+ clearTimeout(timer);
10285
+ this.off("disconnect", disconnected);
10286
+ };
10287
+ const disconnected = (info) => {
10288
+ cleanup();
10289
+ if (info.code === DropReason.Saved) resolve(info);
10290
+ else reject(new DisconnectError(info, "Ship save was not confirmed"));
10291
+ };
10292
+ const timer = setTimeout(() => {
10293
+ cleanup();
10294
+ reject(new Error("Timed out waiting for ship save confirmation (Saved close code)"));
10295
+ }, timeout);
10296
+ this.on("disconnect", disconnected);
10297
+ try {
10298
+ this.sendMessage(buildSaveShipMessage());
10299
+ } catch (error) {
10300
+ cleanup();
10301
+ reject(error);
10302
+ }
10303
+ });
10304
+ } finally {
10305
+ this.#savePending = false;
10306
+ }
10307
+ }
10308
+ async renameShip(name, { color, timeout = 1e4 } = {}) {
10309
+ this.#assertWritable();
10310
+ buildShipRenameCommitMessage(name, color === void 0 ? "#000000" : color);
10311
+ if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
10312
+ throw new RangeError("timeout must be positive milliseconds no greater than 2147483647");
10313
+ }
10314
+ if (!this.connected || !this.ready) throw new Error("Renaming requires a connected, ready client");
10315
+ if (this.#renamePending) throw new Error("A ship rename is already in progress");
10316
+ const worldId = this.worlds.currentWorldId;
10317
+ this.#renamePending = true;
10318
+ try {
10319
+ await new Promise((resolve, reject) => {
10320
+ const cleanup = () => {
10321
+ clearTimeout(timer);
10322
+ this.off("session", responded);
10323
+ this.off("disconnect", disconnected);
10324
+ this.off("demote", demoted);
10325
+ };
10326
+ const demoted = () => {
10327
+ cleanup();
10328
+ reject(new Error("Ship rename interrupted by demotion"));
10329
+ };
10330
+ const disconnected = (info) => {
10331
+ cleanup();
10332
+ reject(new DisconnectError(info, "Ship rename interrupted"));
10333
+ };
10334
+ const responded = (packet) => {
10335
+ const response = packet.submessage;
10336
+ if (response?.type !== "rename_response") return;
10337
+ cleanup();
10338
+ try {
10339
+ if (!response.allow) throw new Error(response.message || "Ship rename unavailable");
10340
+ if (this.worlds.currentWorldId !== worldId) throw new Error("World changed while requesting ship rename");
10341
+ this.sendMessage(buildShipRenameCommitMessage(name, color === void 0 ? response.initial_color : color));
10342
+ resolve();
10343
+ } catch (error) {
10344
+ reject(error);
10345
+ }
10346
+ };
10347
+ const timer = setTimeout(() => {
10348
+ cleanup();
10349
+ reject(new Error("Timed out waiting for ship rename permission"));
10350
+ }, timeout);
10351
+ this.on("session", responded);
10352
+ this.on("disconnect", disconnected);
10353
+ this.on("demote", demoted);
10354
+ try {
10355
+ this.sendMessage(buildShipRenameRequestMessage());
10356
+ } catch (error) {
10357
+ cleanup();
10358
+ reject(error);
10359
+ }
10360
+ });
10361
+ } finally {
10362
+ this.#renamePending = false;
10363
+ }
10364
+ }
10012
10365
  requestPlayerList() {
10013
10366
  return this.sendMessage(buildPlayerListMessage());
10014
10367
  }
10015
- resetInvite() {
10016
- return this.sendMessage(buildInviteResetMessage());
10368
+ resetInvite(options = {}) {
10369
+ const previousKey = this.shipConfig?.inviteKey;
10370
+ return this.#awaitManagementConfirmation("Invite reset", buildInviteResetMessage(), "ship-config", (config) => typeof config?.inviteKey === "string" && config.inviteKey.length > 0 && config.inviteKey !== previousKey, options);
10017
10371
  }
10018
10372
  setShipPrivacy(privacy) {
10019
10373
  return this.sendMessage(buildShipPrivacyMessage(privacy));
10020
10374
  }
10021
- recoverStarterItem(itemId) {
10022
- return this.sendMessage(buildStarterRecoveryMessage(itemId));
10375
+ setShipGravity(direction) {
10376
+ return this.sendMessage(buildShipGravityMessage(direction));
10377
+ }
10378
+ setShipPatronPerks(enabled) {
10379
+ return this.sendMessage(buildSetPatronPerksMessage(enabled));
10380
+ }
10381
+ async recoverStarterItem(itemId, { timeout = 1e4 } = {}) {
10382
+ this.#assertWritable();
10383
+ const message = buildStarterRecoveryMessage(itemId);
10384
+ if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
10385
+ throw new RangeError("timeout must be positive milliseconds no greater than 2147483647");
10386
+ }
10387
+ if (!this.connected || !this.ready) throw new Error("Starter recovery requires a connected, ready client");
10388
+ if (this.#starterRecoveryPending) throw new Error("A starter recovery is already in progress");
10389
+ const worldId = this.worlds.currentWorldId;
10390
+ this.#starterRecoveryPending = true;
10391
+ try {
10392
+ await new Promise((resolve, reject) => {
10393
+ const cleanup = () => {
10394
+ clearTimeout(timer);
10395
+ this.off("starter-recovery", responded);
10396
+ this.off("disconnect", disconnected);
10397
+ this.off("demote", demoted);
10398
+ };
10399
+ const disconnected = (info) => {
10400
+ cleanup();
10401
+ reject(new DisconnectError(info, "Starter recovery interrupted"));
10402
+ };
10403
+ const demoted = () => {
10404
+ cleanup();
10405
+ reject(new Error("Starter recovery interrupted by demotion"));
10406
+ };
10407
+ const responded = (result) => {
10408
+ cleanup();
10409
+ if (this.worlds.currentWorldId !== worldId) reject(new Error("World changed during starter recovery"));
10410
+ else if (!result.success) reject(new Error(result.failReason));
10411
+ else resolve();
10412
+ };
10413
+ const timer = setTimeout(() => {
10414
+ cleanup();
10415
+ reject(new Error("Timed out waiting for starter recovery response"));
10416
+ }, timeout);
10417
+ this.on("starter-recovery", responded);
10418
+ this.on("disconnect", disconnected);
10419
+ this.on("demote", demoted);
10420
+ try {
10421
+ this.sendMessage(message);
10422
+ } catch (error) {
10423
+ cleanup();
10424
+ reject(error);
10425
+ }
10426
+ });
10427
+ } finally {
10428
+ this.#starterRecoveryPending = false;
10429
+ }
10023
10430
  }
10024
10431
  setPlayerRank(refId, rank) {
10025
10432
  return this.sendMessage(buildSetPlayerRankMessage(refId, rank));
@@ -10039,8 +10446,56 @@
10039
10446
  banPlayer(refId) {
10040
10447
  return this.sendMessage(buildBanPlayerMessage(refId));
10041
10448
  }
10042
- demoteSelf() {
10043
- return this.sendMessage(buildDemoteSelfMessage());
10449
+ unbanPlayer(refId) {
10450
+ return this.sendMessage(buildUnbanPlayerMessage(refId));
10451
+ }
10452
+ async demoteSelf(options = {}) {
10453
+ await this.#awaitManagementConfirmation("Self-demotion", buildDemoteSelfMessage(), "demote", () => true, options);
10454
+ }
10455
+ async #awaitManagementConfirmation(operation, message, event, accept, { timeout = 1e4 } = {}) {
10456
+ this.#assertWritable();
10457
+ if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 2147483647) {
10458
+ throw new RangeError("timeout must be positive milliseconds no greater than 2147483647");
10459
+ }
10460
+ if (!this.connected || !this.ready) throw new Error(`${operation} requires a connected, ready client`);
10461
+ if (this.#managementPending.has(operation)) throw new Error(`${operation} is already in progress`);
10462
+ const worldId = this.worlds.currentWorldId;
10463
+ this.#managementPending.add(operation);
10464
+ try {
10465
+ return await new Promise((resolve, reject) => {
10466
+ const cleanup = () => {
10467
+ clearTimeout(timer);
10468
+ this.off(event, responded);
10469
+ this.off("disconnect", disconnected);
10470
+ if (event !== "demote") this.off("demote", demoted);
10471
+ };
10472
+ const fail = (error) => {
10473
+ cleanup();
10474
+ reject(error);
10475
+ };
10476
+ const disconnected = (info) => fail(new DisconnectError(info, `${operation} interrupted`));
10477
+ const demoted = () => fail(new Error(`${operation} interrupted by demotion`));
10478
+ const responded = (result) => {
10479
+ if (this.worlds.currentWorldId !== worldId) {
10480
+ fail(new Error(`World changed during ${operation.toLowerCase()}`));
10481
+ } else if (accept(result)) {
10482
+ cleanup();
10483
+ resolve(result);
10484
+ }
10485
+ };
10486
+ const timer = setTimeout(() => fail(new Error(`Timed out waiting for ${operation.toLowerCase()} confirmation`)), timeout);
10487
+ this.on(event, responded);
10488
+ this.on("disconnect", disconnected);
10489
+ if (event !== "demote") this.on("demote", demoted);
10490
+ try {
10491
+ this.sendMessage(message);
10492
+ } catch (error) {
10493
+ fail(error);
10494
+ }
10495
+ });
10496
+ } finally {
10497
+ this.#managementPending.delete(operation);
10498
+ }
10044
10499
  }
10045
10500
  sendEntityCommand(cmd, args = [-1, -1, -1]) {
10046
10501
  return this.sendMessage({ type: 5, cmd, args });
@@ -10084,8 +10539,8 @@
10084
10539
  setLauncherPower(power) {
10085
10540
  return this.sendEntityCommand("power", [normalizeLauncherPower(power)]);
10086
10541
  }
10087
- setSignText(text = "", mode = 0) {
10088
- return this.sendMessage(buildSignTextMessage(text, mode));
10542
+ setSignText(text2 = "", mode = 0) {
10543
+ return this.sendMessage(buildSignTextMessage(text2, mode));
10089
10544
  }
10090
10545
  sendUiConfig(data) {
10091
10546
  return this.sendMessage({ type: 8, data });
@@ -10344,12 +10799,14 @@
10344
10799
  shipConfig: this.shipConfig,
10345
10800
  captainSubrank: this.captainSubrank,
10346
10801
  playerList: this.playerList,
10802
+ crewLog: this.crewLog,
10347
10803
  outfits: [...this.outfits.entries()].map(([sid, outfit]) => ({ sid, outfit })),
10348
10804
  commandAcks: [...this.commandAcks.entries()].map(([world, commandNumber]) => ({ world, commandNumber })),
10349
10805
  lastCommandAck: this.lastCommandAck,
10350
10806
  decodeErrors: this.decodeErrors.slice(-50),
10351
10807
  packetCount: this.packetCount,
10352
- lastPacket: this.lastPacket
10808
+ lastPacket: this.lastPacket,
10809
+ lastDisconnect: this.lastDisconnect
10353
10810
  };
10354
10811
  }
10355
10812
  state(options = {}) {
@@ -10474,6 +10931,7 @@
10474
10931
  }
10475
10932
  this.#resolveReady?.(this);
10476
10933
  this.#resolveReady = null;
10934
+ this.#rejectReady = null;
10477
10935
  }
10478
10936
  this.#bindAttachedSocket();
10479
10937
  if (this.connected && this.#ownsSocket) this.#attachedOpen();
@@ -10481,6 +10939,7 @@
10481
10939
  }
10482
10940
  #bindSocket() {
10483
10941
  this.ws.onopen = () => {
10942
+ this.lastDisconnect = null;
10484
10943
  this.connected = true;
10485
10944
  this.ws.send(encodeMsgpack({ type: 1 }));
10486
10945
  this.emit("open", this);
@@ -10494,7 +10953,7 @@
10494
10953
  const open = () => this.#attachedOpen();
10495
10954
  const message = (event) => this.#handleSocketMessage(event?.data ?? event);
10496
10955
  const error = (event) => this.#fail(event?.error || new Error("WebSocket error"));
10497
- const close = (event) => this.#handleSocketClose(event);
10956
+ const close = (event, reason) => this.#handleSocketClose(event, reason);
10498
10957
  if (typeof this.ws.addEventListener === "function") {
10499
10958
  this.ws.addEventListener("open", open);
10500
10959
  this.ws.addEventListener("message", message);
@@ -10509,13 +10968,16 @@
10509
10968
  this.ws.on("close", close);
10510
10969
  return;
10511
10970
  }
10512
- const previousMessage = this.ws.onmessage;
10513
- this.ws.onmessage = (event) => {
10514
- if (typeof previousMessage === "function") previousMessage.call(this.ws, event);
10515
- message(event);
10516
- };
10971
+ for (const [type, handler] of Object.entries({ open, message, error, close })) {
10972
+ const previous = this.ws[`on${type}`];
10973
+ this.ws[`on${type}`] = (...args) => {
10974
+ if (typeof previous === "function") previous.apply(this.ws, args);
10975
+ handler(...args);
10976
+ };
10977
+ }
10517
10978
  }
10518
10979
  #attachedOpen() {
10980
+ this.lastDisconnect = null;
10519
10981
  this.connected = true;
10520
10982
  if (this.#attachedOpened) return;
10521
10983
  this.#attachedOpened = true;
@@ -10523,17 +10985,39 @@
10523
10985
  this.emit("open", this);
10524
10986
  if (this.#allowWrites) this.#flushMessages();
10525
10987
  }
10526
- #handleSocketClose(event) {
10988
+ #handleSocketClose(event, reason) {
10989
+ const info = normalizeDisconnect(event, reason);
10527
10990
  this.connected = false;
10991
+ this.ready = false;
10992
+ this.sid = null;
10993
+ this.lastDisconnect = info;
10528
10994
  clearInterval(this.#keepalive);
10529
10995
  this.#keepalive = null;
10530
- this.emit("close", event);
10996
+ this.#bootstrapped = false;
10997
+ this.#attachedOpened = false;
10998
+ this.#queuedCommands.length = 0;
10999
+ this.#queuedMessages.length = 0;
11000
+ this.shipConfig = null;
11001
+ this.captainSubrank = null;
11002
+ this.playerList = null;
11003
+ this.crewLog = null;
11004
+ this.puiPanels.clear();
11005
+ this.commsPanels.clear();
11006
+ this.currentCommsPanel = null;
11007
+ this.commandAcks.clear();
11008
+ this.lastCommandAck = null;
11009
+ this.#rejectReady?.(new DisconnectError(info, "Disconnected before the client was ready"));
11010
+ this.#resolveReady = null;
11011
+ this.#rejectReady = null;
11012
+ this.emit("disconnect", info);
11013
+ this.emit("close", event, reason);
10531
11014
  }
10532
11015
  #socketLooksOpen(websocket) {
10533
11016
  return websocket?.readyState == null || websocket.readyState === 1 || websocket.readyState === websocket.OPEN;
10534
11017
  }
10535
11018
  #assertWritable() {
10536
11019
  if (!this.#allowWrites) throw new Error("DredlessClient is attached in readonly mode");
11020
+ if (this.lastDisconnect) throw new DisconnectError(this.lastDisconnect);
10537
11021
  }
10538
11022
  #handleSocketMessage(data) {
10539
11023
  if (data && typeof data.arrayBuffer === "function") {
@@ -10543,6 +11027,7 @@
10543
11027
  this.#handleMessage(data);
10544
11028
  }
10545
11029
  #handleMessage(data) {
11030
+ if (this.lastDisconnect) return;
10546
11031
  let packet;
10547
11032
  try {
10548
11033
  packet = decodeIncomingFrame(data);
@@ -10569,6 +11054,7 @@
10569
11054
  }
10570
11055
  #markReady(packet) {
10571
11056
  this.sid = packet.sid >>> 0;
11057
+ this.worlds.apply(packet);
10572
11058
  this.worlds.currentWorldId = packet.world ?? this.worlds.currentWorldId;
10573
11059
  this.ready = true;
10574
11060
  if (this.#ownsSocket) {
@@ -10581,6 +11067,7 @@
10581
11067
  }
10582
11068
  this.#resolveReady?.(this);
10583
11069
  this.#resolveReady = null;
11070
+ this.#rejectReady = null;
10584
11071
  this.emit("ready", this);
10585
11072
  }
10586
11073
  #sendBootstrap() {
@@ -10658,6 +11145,32 @@
10658
11145
  #handleSessionSubmessage(packet) {
10659
11146
  const submessage = packet?.submessage;
10660
11147
  if (!submessage || typeof submessage !== "object") return;
11148
+ if (submessage.type === "demote") {
11149
+ this.shipConfig = null;
11150
+ this.captainSubrank = null;
11151
+ this.playerList = null;
11152
+ this.crewLog = null;
11153
+ this.emit("demote", packet);
11154
+ this.emit("ship-config", null, packet);
11155
+ this.emit("captain-subrank", null, packet);
11156
+ this.emit("player-list", null, packet);
11157
+ this.emit("crew-log", null, packet);
11158
+ return;
11159
+ }
11160
+ if (submessage.type === "log") {
11161
+ this.crewLog = normalizeCrewLog(submessage, this.crewLog);
11162
+ this.emit("crew-log", this.crewLog, packet);
11163
+ return;
11164
+ }
11165
+ if (submessage.type === "starter_recovery_response") {
11166
+ const failReason = submessage.fail_reason == null ? null : String(submessage.fail_reason);
11167
+ this.emit("starter-recovery", Object.freeze({ success: failReason === null, failReason }), packet);
11168
+ return;
11169
+ }
11170
+ if (submessage.type === "perk_response") {
11171
+ this.emit("patron-perks-response", Object.freeze({ failure: Boolean(submessage.failure) }), packet);
11172
+ return;
11173
+ }
10661
11174
  if (submessage.type === "config") {
10662
11175
  this.shipConfig = normalizeShipConfig(submessage);
10663
11176
  this.emit("ship-config", this.shipConfig, packet);
@@ -10910,6 +11423,174 @@
10910
11423
  init_cookies();
10911
11424
  init_http();
10912
11425
  init_servers();
11426
+
11427
+ // src/network/account.js
11428
+ init_runtime();
11429
+ async function fetchAccountDetails(session) {
11430
+ const response = await session.request("account/", {
11431
+ method: "GET",
11432
+ headers: { accept: "text/html" },
11433
+ credentials: "include",
11434
+ cache: "no-store"
11435
+ });
11436
+ session.mergeSetCookies(response);
11437
+ if (!response.ok) throw new Error(`Account page request failed: ${response.status} ${response.statusText}`);
11438
+ return parseAccountDetails(await response.text());
11439
+ }
11440
+ async function fetchClanTag(session) {
11441
+ return (await fetchAccountDetails(session)).clan;
11442
+ }
11443
+ async function setClanTag(session, tag) {
11444
+ if (typeof tag !== "string") throw new TypeError("tag must be a string");
11445
+ return updateClanTag(session, tag);
11446
+ }
11447
+ async function clearClanTag(session) {
11448
+ return updateClanTag(session, "");
11449
+ }
11450
+ async function updateClanTag(session, tag) {
11451
+ const body = tag === "" ? "" : new URLSearchParams({ tag }).toString();
11452
+ await postAccountForm(session, "account/clan", body, "Clan tag update");
11453
+ const clan = await fetchClanTag(session);
11454
+ if ((clan.tag ?? "") !== tag) throw new Error("Clan tag update was not confirmed by the account page");
11455
+ return clan;
11456
+ }
11457
+ async function removeShipPerks(session, shipId) {
11458
+ const id = requirePerkShipId(shipId);
11459
+ await postAccountForm(session, "account/remove_perks", new URLSearchParams({ ship: id }).toString(), "Ship perk removal");
11460
+ const { shipsWithPerks } = await fetchAccountDetails(session);
11461
+ if (shipsWithPerks === null || shipsWithPerks.some((ship) => ship.shipId === id)) {
11462
+ throw new Error("Ship perk removal was not confirmed by the account page");
11463
+ }
11464
+ return shipsWithPerks;
11465
+ }
11466
+ async function postAccountForm(session, path, body, operation) {
11467
+ const response = await session.request(path, {
11468
+ method: "POST",
11469
+ headers: {
11470
+ accept: "text/html",
11471
+ "content-type": "application/x-www-form-urlencoded",
11472
+ ...!isBrowser() ? { origin: new URL(session.baseUrl).origin } : {}
11473
+ },
11474
+ body,
11475
+ credentials: "include",
11476
+ referrer: `${session.baseUrl}/account/`,
11477
+ // Node must merge cookies before following the form's redirect.
11478
+ redirect: isBrowser() ? "follow" : "manual"
11479
+ });
11480
+ session.mergeSetCookies(response);
11481
+ if (!response.ok && response.status !== 302 && response.status !== 303) {
11482
+ throw new Error(`${operation} failed: ${response.status} ${response.statusText}`);
11483
+ }
11484
+ await response.text();
11485
+ }
11486
+ function parseAccountDetails(html) {
11487
+ const source = String(html).replace(/<!--[\s\S]*?-->|<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, "");
11488
+ let tag = null;
11489
+ let found = false;
11490
+ for (const form of source.matchAll(/<form\b((?:"[^"]*"|'[^']*'|[^'">])*)>([\s\S]*?)<\/form\s*>/gi)) {
11491
+ const action = attributes(form[1]).get("action");
11492
+ if (!action || new URL(action, "https://drednot.io/").pathname !== "/account/clan") continue;
11493
+ for (const input of form[2].matchAll(/<input\b((?:"[^"]*"|'[^']*'|[^'">])*)>/gi)) {
11494
+ const attrs = attributes(input[1]);
11495
+ if (attrs.get("name") !== "tag") continue;
11496
+ tag = attrs.get("value") || null;
11497
+ found = true;
11498
+ break;
11499
+ }
11500
+ if (found) break;
11501
+ }
11502
+ if (!found) throw new Error("Account page has no clan form; authentication may be required or the page format has changed");
11503
+ const fields = /* @__PURE__ */ new Map();
11504
+ let name = null;
11505
+ let color = null;
11506
+ for (const paragraph of source.matchAll(/<p\b[^>]*>([\s\S]*?)<\/p\s*>/gi)) {
11507
+ const label = paragraph[1].match(/<b\b[^>]*>([\s\S]*?)<\/b\s*>/i);
11508
+ const code = paragraph[1].match(/<code\b((?:"[^"]*"|'[^']*'|[^'">])*)>([\s\S]*?)<\/code\s*>/i);
11509
+ if (!code) continue;
11510
+ const attrs = attributes(code[1]);
11511
+ if (label) fields.set(text(label[1]).replace(/:\s*$/, "").toLowerCase(), attrs.get("data-info") ?? null);
11512
+ if ((attrs.get("class") || "").split(/\s+/).includes("user") && /^You are:$/i.test(text(paragraph[1].slice(0, code.index)))) {
11513
+ name = decodeHtml(code[2].replace(/<[^>]*>/g, ""));
11514
+ color = parseAccountColor(attrs.get("style") || "");
11515
+ }
11516
+ }
11517
+ const rawIdentity = fields.get("identity (service)") || null;
11518
+ const identityParts = rawIdentity?.match(/^(.*?)\s*\(([^()]*)\)\s*$/);
11519
+ const count = text(source).match(/Your current clan tag has\s+([\d,]+)\s+other users?\b/i);
11520
+ return {
11521
+ name,
11522
+ color,
11523
+ email: fields.get("email") || null,
11524
+ identity: identityParts ? identityParts[1].trim() : rawIdentity,
11525
+ identityService: identityParts ? identityParts[2].trim() : null,
11526
+ clan: { tag, otherUsers: tag !== null && count ? Number(count[1].replace(/,/g, "")) : null },
11527
+ shipsWithPerks: parseShipsWithPerks(source)
11528
+ };
11529
+ }
11530
+ function parseAccountColor(style) {
11531
+ let color = null;
11532
+ for (const declaration of style.split(";")) {
11533
+ const match = declaration.match(/^\s*background(?:-color)?\s*:\s*#([0-9a-f]{6}|[0-9a-f]{3})\s*(?:!important\s*)?$/i);
11534
+ if (!match) continue;
11535
+ const hex = match[1].length === 3 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
11536
+ color = parseInt(hex, 16);
11537
+ }
11538
+ return color;
11539
+ }
11540
+ function parseShipsWithPerks(source) {
11541
+ const heading = source.match(/<(b|strong)\b[^>]*>\s*Ships\s+with\s+Perks\s+Applied\s*:\s*<\/\1\s*>/i);
11542
+ if (!heading) return null;
11543
+ const list = source.slice(heading.index + heading[0].length).match(/^\s*(?:<\/[^>]+>\s*)*<ul\b[^>]*>([\s\S]*?)<\/ul\s*>/i);
11544
+ if (!list) throw new Error("Account page's applied-perks list could not be parsed");
11545
+ const ships = [];
11546
+ for (const item of list[1].matchAll(/<li\b[^>]*>([\s\S]*?)<\/li\s*>/gi)) {
11547
+ const form = item[1].match(/<form\b((?:"[^"]*"|'[^']*'|[^'">])*)>([\s\S]*?)<\/form\s*>/i);
11548
+ const action = form && attributes(form[1]).get("action");
11549
+ if (!action || new URL(action, "https://drednot.io/").pathname !== "/account/remove_perks") {
11550
+ throw new Error("Account page's applied-perks entry has no removal form");
11551
+ }
11552
+ let shipId = null;
11553
+ for (const input of form[2].matchAll(/<input\b((?:"[^"]*"|'[^']*'|[^'">])*)>/gi)) {
11554
+ const attrs = attributes(input[1]);
11555
+ if (attrs.get("name") === "ship") shipId = requirePerkShipId(attrs.get("value"));
11556
+ }
11557
+ if (shipId === null) throw new Error("Account page's applied-perks entry has no ship identifier");
11558
+ const label = decodeHtml(item[1].slice(0, form.index).replace(/<[^>]*>/g, ""));
11559
+ const parts = label.match(/^([\s\S]*?) \{([^{}]+)\}\s*$/);
11560
+ ships.push({ shipId, name: parts ? parts[1] : label.trim(), hexCode: parts ? parts[2] : null });
11561
+ }
11562
+ return ships;
11563
+ }
11564
+ function requirePerkShipId(value) {
11565
+ if (typeof value === "number" && (!Number.isSafeInteger(value) || value <= 0)) {
11566
+ throw new RangeError("shipId must be a positive safe integer or a decimal ID string from shipsWithPerks");
11567
+ }
11568
+ if (typeof value !== "string" && typeof value !== "number" || !/^[1-9]\d*$/.test(String(value))) {
11569
+ throw new TypeError("shipId must be a positive integer or a decimal ID string from shipsWithPerks");
11570
+ }
11571
+ return String(value);
11572
+ }
11573
+ function attributes(source) {
11574
+ const result = /* @__PURE__ */ new Map();
11575
+ for (const match of source.matchAll(/([^\s=<>"'`/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g)) {
11576
+ result.set(match[1].toLowerCase(), decodeHtml(match[2] ?? match[3] ?? match[4] ?? ""));
11577
+ }
11578
+ return result;
11579
+ }
11580
+ function text(source) {
11581
+ return decodeHtml(source.replace(/<[^>]*>/g, " ")).replace(/\s+/g, " ").trim();
11582
+ }
11583
+ function decodeHtml(value) {
11584
+ const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
11585
+ return value.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi, (raw, entity) => {
11586
+ const name = entity.toLowerCase();
11587
+ if (Object.hasOwn(named, name)) return named[name];
11588
+ const code = name.startsWith("#x") ? parseInt(name.slice(2), 16) : Number(name.slice(1));
11589
+ return code > 0 && code <= 1114111 && !(code >= 55296 && code <= 57343) ? String.fromCodePoint(code) : "\uFFFD";
11590
+ });
11591
+ }
11592
+
11593
+ // src/network/session.js
10913
11594
  var Session = class {
10914
11595
  constructor(gameSession = null, gameVersion = null, baseUrl = DEFAULT_BASE_URL) {
10915
11596
  this.baseUrl = baseUrl;
@@ -10957,6 +11638,21 @@
10957
11638
  this.#readStatus(raw);
10958
11639
  return raw;
10959
11640
  }
11641
+ fetchAccountDetails() {
11642
+ return fetchAccountDetails(this);
11643
+ }
11644
+ fetchClanTag() {
11645
+ return fetchClanTag(this);
11646
+ }
11647
+ setClanTag(tag) {
11648
+ return setClanTag(this, tag);
11649
+ }
11650
+ clearClanTag() {
11651
+ return clearClanTag(this);
11652
+ }
11653
+ removeShipPerks(shipId) {
11654
+ return removeShipPerks(this, shipId);
11655
+ }
10960
11656
  async fetchShips(server2) {
10961
11657
  return fetchShips(this, server2);
10962
11658
  }