dredless 0.6.3 → 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 (41) hide show
  1. package/README.md +22 -57
  2. package/dist/browser/dredless.global.js +827 -1058
  3. package/dist/browser/dredless.global.js.map +4 -4
  4. package/docs/ACCOUNT.md +146 -0
  5. package/docs/API_REFERENCE.md +63 -2550
  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 +303 -14
  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 +101 -56
  38. package/src/state/model/index.d.ts +4 -0
  39. package/src/state/model/loader-config.js +0 -997
  40. package/src/state/model/state.js +28 -80
  41. 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);
@@ -3895,7 +4043,6 @@
3895
4043
  SHIELD_GENERATOR_BOOST_STATE_NAMES: () => SHIELD_GENERATOR_BOOST_STATE_NAMES,
3896
4044
  SIGN_DISPLAY_MODE_NAMES: () => SIGN_DISPLAY_MODE_NAMES,
3897
4045
  SIMPLE_LABEL_STATE_SPEC: () => SIMPLE_LABEL_STATE_SPEC,
3898
- TABLE_78_FIELD_BITS: () => TABLE_78_FIELD_BITS,
3899
4046
  TABLE_78_NUMERIC_FIELDS: () => TABLE_78_NUMERIC_FIELDS,
3900
4047
  TEAM_RANK_NAMES: () => TEAM_RANK_NAMES,
3901
4048
  THRUSTER_FACING_NAMES: () => THRUSTER_FACING_NAMES,
@@ -3928,7 +4075,6 @@
3928
4075
  isOrangeFoolBotState: () => isOrangeFoolBotState,
3929
4076
  isRedSentryBotState: () => isRedSentryBotState,
3930
4077
  isRedSniperBotState: () => isRedSniperBotState,
3931
- isSemanticLoaderDelta: () => isSemanticLoaderDelta2,
3932
4078
  isShieldHelperBotState: () => isShieldHelperBotState,
3933
4079
  isShieldMasterBotState: () => isShieldMasterBotState,
3934
4080
  isYellowHunterBotState: () => isYellowHunterBotState,
@@ -3980,8 +4126,7 @@
3980
4126
  summarizeShipWorldMetadata: () => summarizeShipWorldMetadata,
3981
4127
  summarizeSign: () => summarizeSign,
3982
4128
  summarizeSpawnPoint: () => summarizeSpawnPoint,
3983
- summarizeThruster: () => summarizeThruster,
3984
- table78DeltaRecord: () => table78DeltaRecord
4129
+ summarizeThruster: () => summarizeThruster
3985
4130
  });
3986
4131
  init_constants();
3987
4132
 
@@ -4007,858 +4152,6 @@
4007
4152
  [2, "allow-filter"],
4008
4153
  [3, "block-all"]
4009
4154
  ]);
4010
- var DEFAULT_LOADER_PRIORITY = 0;
4011
- var DEFAULT_LOADER_FILTER_MODE = 0;
4012
- var DEFAULT_LOADER_STACK = 16;
4013
- var DEFAULT_LOADER_CYCLE = 1;
4014
- var DEFAULT_LOADER_PICK = 3;
4015
- var DEFAULT_LOADER_PLACE = 4;
4016
- var LOADER_CONFIG_NUMERIC_BITS = [1, 2, 4, 8, 16];
4017
- var LOADER_CONFIG_NUMERIC_FIELDS = ["q20", "q24", "q28", "q32", "q36"];
4018
- function wrapLoaderPosition(value) {
4019
- return value == null ? null : (value % 8 + 8) % 8;
4020
- }
4021
- function decodeIndexedLoaderSnapshotRecord(loader, mask = 0, cumulative = null, reference = null) {
4022
- const config = {
4023
- pick: 3,
4024
- place: 4,
4025
- priority: DEFAULT_LOADER_PRIORITY,
4026
- requireOutput: false,
4027
- waitForStack: false,
4028
- stack: DEFAULT_LOADER_STACK,
4029
- cycle: DEFAULT_LOADER_CYCLE
4030
- };
4031
- if (mask & 32) config.requireOutput = true;
4032
- if (mask & 64) config.waitForStack = true;
4033
- switch (mask) {
4034
- case 0:
4035
- break;
4036
- case 1:
4037
- config.pick = wrapLoaderPosition((loader.q20 ?? 0) + 3);
4038
- if (cumulative?.q40 != null) config.requireOutput = true;
4039
- break;
4040
- case 2:
4041
- if (reference && cumulative?.q40 != null && loader.q24 != null) {
4042
- if (loader.q24 <= -8) {
4043
- config.pick = wrapLoaderPosition((reference.pick ?? 3) + 1);
4044
- config.place = wrapLoaderPosition((reference.place ?? 4) + 3);
4045
- config.requireOutput = true;
4046
- } else {
4047
- config.pick = wrapLoaderPosition((reference.pick ?? 3) + 2);
4048
- config.place = wrapLoaderPosition((reference.place ?? 4) + 4);
4049
- }
4050
- } else {
4051
- config.place = wrapLoaderPosition((loader.q24 ?? 0) + 4);
4052
- }
4053
- break;
4054
- case 3:
4055
- config.pick = reference && loader.q24 <= -8 ? wrapLoaderPosition((reference.pick ?? 3) + (loader.q20 ?? 0) + 3) : wrapLoaderPosition((loader.q20 ?? 0) + 3);
4056
- config.place = wrapLoaderPosition((loader.q24 ?? 0) + 4);
4057
- if (cumulative?.q44 != null && cumulative?.q36 == null && cumulative?.q40 == null) {
4058
- config.requireOutput = true;
4059
- config.waitForStack = true;
4060
- }
4061
- break;
4062
- case 5:
4063
- config.pick = wrapLoaderPosition((loader.q28 ?? 0) + 1);
4064
- config.place = wrapLoaderPosition((loader.q28 ?? 0) + 3);
4065
- break;
4066
- case 6:
4067
- config.pick = wrapLoaderPosition((loader.q24 ?? 0) + 5);
4068
- config.place = wrapLoaderPosition((loader.q24 ?? 0) + 3);
4069
- config.requireOutput = true;
4070
- break;
4071
- case 7:
4072
- if (reference && (cumulative?.q40 != null || loader.q24 <= -8 || loader.q28 >= 2)) {
4073
- if (loader.q24 <= -8) {
4074
- config.pick = wrapLoaderPosition((reference.pick ?? 3) + (loader.q20 ?? 0) + 5);
4075
- config.place = wrapLoaderPosition(reference.place ?? 4);
4076
- config.requireOutput = true;
4077
- config.waitForStack = true;
4078
- if (loader.q28 != null) config.priority = loader.q28;
4079
- } else if (loader.q28 >= 2) {
4080
- config.pick = wrapLoaderPosition((reference.pick ?? 3) + loader.q28);
4081
- config.place = wrapLoaderPosition((reference.place ?? 4) + (loader.q20 ?? 0));
4082
- config.priority = loader.q28 - 4;
4083
- } else if ((loader.q20 ?? 0) >= 3) {
4084
- config.pick = wrapLoaderPosition((reference.pick ?? 3) + (loader.q20 ?? 0));
4085
- config.place = wrapLoaderPosition(reference.place ?? 4);
4086
- config.priority = (loader.q28 ?? 0) + 1;
4087
- config.requireOutput = Boolean(reference.requireOutput);
4088
- config.waitForStack = Boolean(reference.waitForStack);
4089
- } else {
4090
- config.pick = wrapLoaderPosition((loader.q20 ?? 0) + 2);
4091
- config.place = wrapLoaderPosition((reference.place ?? 4) + (loader.q24 ?? 0));
4092
- config.priority = (loader.q28 ?? 0) + 1;
4093
- }
4094
- } else {
4095
- config.pick = wrapLoaderPosition((loader.q20 ?? 0) + 3);
4096
- config.place = wrapLoaderPosition((loader.q24 ?? 0) + 4);
4097
- if (loader.q28 != null) config.priority = loader.q28;
4098
- }
4099
- break;
4100
- case 8:
4101
- config.pick = wrapLoaderPosition((cumulative?.q40 ?? 0) + 4);
4102
- config.place = wrapLoaderPosition((cumulative?.q28 ?? 0) + 2);
4103
- config.requireOutput = true;
4104
- break;
4105
- case 9:
4106
- config.pick = wrapLoaderPosition((loader.q20 ?? 0) + 3);
4107
- config.place = wrapLoaderPosition((cumulative?.q40 ?? 0) + 3);
4108
- break;
4109
- case 18:
4110
- config.pick = wrapLoaderPosition(loader.q24 ?? 0);
4111
- config.place = wrapLoaderPosition((loader.q36 ?? 0) + 2);
4112
- if (loader.q36 != null) config.cycle = loader.q36 / 20 + 1;
4113
- break;
4114
- case 32:
4115
- break;
4116
- case 65:
4117
- config.pick = wrapLoaderPosition((loader.q44 ?? 0) + 3);
4118
- break;
4119
- case 33:
4120
- config.pick = wrapLoaderPosition((loader.q40 ?? 0) + 3);
4121
- config.place = wrapLoaderPosition((loader.q20 ?? 0) + 4);
4122
- break;
4123
- case 34:
4124
- config.pick = wrapLoaderPosition((loader.q24 ?? 0) + 3);
4125
- config.place = wrapLoaderPosition((loader.q40 ?? 0) + 4);
4126
- break;
4127
- case 35:
4128
- config.pick = wrapLoaderPosition((loader.q24 ?? 0) + 3);
4129
- config.place = wrapLoaderPosition((loader.q40 ?? 0) + 4);
4130
- break;
4131
- case 67:
4132
- config.pick = wrapLoaderPosition((loader.q24 ?? 0) + 3);
4133
- config.place = wrapLoaderPosition((loader.q44 ?? 0) + 4);
4134
- break;
4135
- case 95:
4136
- config.pick = wrapLoaderPosition((loader.q24 ?? 0) + 3);
4137
- config.place = wrapLoaderPosition((loader.q28 ?? 0) + 4);
4138
- if (loader.q32 != null) config.priority = loader.q32;
4139
- if (loader.q36 != null) config.stack = DEFAULT_LOADER_STACK + loader.q36;
4140
- if (loader.q44 != null) config.cycle = loader.q44 / 20 + 1;
4141
- break;
4142
- case 98:
4143
- config.place = wrapLoaderPosition((loader.q40 ?? 0) + 4);
4144
- break;
4145
- case 99:
4146
- config.pick = wrapLoaderPosition((loader.q24 ?? 0) + 3);
4147
- config.place = wrapLoaderPosition((loader.q40 ?? 0) + 4);
4148
- break;
4149
- case 126:
4150
- config.pick = wrapLoaderPosition(loader.q28 ?? 0);
4151
- config.place = wrapLoaderPosition(loader.q24 ?? 0);
4152
- if (loader.q32 != null) config.priority = loader.q32;
4153
- if (loader.q36 != null) config.stack = DEFAULT_LOADER_STACK + loader.q36;
4154
- if (loader.q40 != null) config.cycle = loader.q40 / 20 + 1;
4155
- break;
4156
- default:
4157
- return null;
4158
- }
4159
- return config;
4160
- }
4161
- function decodeLoaderSnapshotRecord(loader, mask = loader?.lastMask ?? 0) {
4162
- if (!loader) return null;
4163
- const hasRequireOutput = Boolean(mask & 32);
4164
- const hasWaitForStack = Boolean(mask & 64);
4165
- const hasFlag = hasRequireOutput || hasWaitForStack;
4166
- const slots = [];
4167
- for (const [index, bit] of LOADER_CONFIG_NUMERIC_BITS.entries()) {
4168
- if (!(mask & bit)) continue;
4169
- const value = loader[LOADER_CONFIG_NUMERIC_FIELDS[index]];
4170
- if (value == null) return null;
4171
- slots.push(value);
4172
- }
4173
- if (hasRequireOutput) {
4174
- if (loader.q40 == null) return null;
4175
- slots.push(loader.q40);
4176
- } else if (hasWaitForStack) {
4177
- if (loader.q44 == null) return null;
4178
- slots.push(loader.q44);
4179
- }
4180
- if (hasFlag && slots.length) slots.shift();
4181
- const config = {
4182
- pick: 3,
4183
- place: 4,
4184
- priority: DEFAULT_LOADER_PRIORITY,
4185
- requireOutput: hasRequireOutput,
4186
- waitForStack: hasWaitForStack,
4187
- stack: DEFAULT_LOADER_STACK,
4188
- cycle: DEFAULT_LOADER_CYCLE
4189
- };
4190
- if (mask & 1) {
4191
- if (!slots.length) return null;
4192
- config.pick = wrapLoaderPosition(slots.shift() + 3);
4193
- }
4194
- if (mask & 2) {
4195
- if (!slots.length) return null;
4196
- config.place = wrapLoaderPosition(slots.shift() + 4);
4197
- }
4198
- if (mask & 4) {
4199
- if (!slots.length) return null;
4200
- config.priority = slots.shift();
4201
- }
4202
- if (mask & 8) {
4203
- if (!slots.length) return null;
4204
- config.stack = DEFAULT_LOADER_STACK + slots.shift();
4205
- }
4206
- if (mask & 16) {
4207
- if (!slots.length) return null;
4208
- config.cycle = slots.shift() / 20 + 1;
4209
- }
4210
- return config;
4211
- }
4212
- function decodeLoaderSemanticDeltaRecord(loader, mask = loader?.lastMask ?? 0) {
4213
- if (!loader) return null;
4214
- const hasRequireOutput = Boolean(mask & 32);
4215
- const hasWaitForStack = Boolean(mask & 64);
4216
- if (!hasRequireOutput && !hasWaitForStack) return null;
4217
- const slots = [];
4218
- for (const [index, bit] of LOADER_CONFIG_NUMERIC_BITS.entries()) {
4219
- if (!(mask & bit)) continue;
4220
- slots.push(loader[LOADER_CONFIG_NUMERIC_FIELDS[index]] ?? 0);
4221
- }
4222
- if (hasRequireOutput) slots.push(loader.q40 ?? 0);
4223
- else if (hasWaitForStack) slots.push(loader.q44 ?? 0);
4224
- if (slots.length) slots.shift();
4225
- const config = {};
4226
- if (mask & 1) config.pick = slots.shift() ?? 0;
4227
- if (mask & 2) config.place = slots.shift() ?? 0;
4228
- if (mask & 4) config.priority = slots.shift() ?? 0;
4229
- if (mask & 8) config.stack = slots.shift() ?? 0;
4230
- if (mask & 16) config.cycleTicks = slots.shift() ?? 0;
4231
- config.requireOutput = hasRequireOutput;
4232
- config.waitForStack = hasWaitForStack;
4233
- return config;
4234
- }
4235
- function loaderDeltaRecord(loader, mask, previous) {
4236
- const record = { lastMask: mask };
4237
- for (const [field, bit] of [
4238
- ["q20", 1],
4239
- ["q24", 2],
4240
- ["q28", 4],
4241
- ["q32", 8],
4242
- ["q36", 16],
4243
- ["q40", 32],
4244
- ["q44", 64]
4245
- ]) {
4246
- if (!(mask & bit)) continue;
4247
- if (field === "q44" && (mask & 32 || loader?.q44 == null)) continue;
4248
- record[field] = (loader?.[field] ?? 0) - (previous?.[field] ?? 0);
4249
- }
4250
- return record;
4251
- }
4252
- function isSemanticLoaderDelta(changed, full) {
4253
- return !full && changed.previous != null && (changed.mask & 96) === 96;
4254
- }
4255
- function filterSlotValue(value) {
4256
- return value == null || value === 0 ? null : value;
4257
- }
4258
- var LoaderConfigTracker = class {
4259
- constructor() {
4260
- this.pickBases = /* @__PURE__ */ new Map();
4261
- this.pickDeltaBases = /* @__PURE__ */ new Map();
4262
- this.placeBases = /* @__PURE__ */ new Map();
4263
- this.placeDeltaBases = /* @__PURE__ */ new Map();
4264
- this.currentPicks = /* @__PURE__ */ new Map();
4265
- this.currentPlaces = /* @__PURE__ */ new Map();
4266
- this.sparsePositionBaselines = /* @__PURE__ */ new Set();
4267
- this.priorities = /* @__PURE__ */ new Map();
4268
- this.priorityOffsets = /* @__PURE__ */ new Map();
4269
- this.filterModes = /* @__PURE__ */ new Map();
4270
- this.filterSources = /* @__PURE__ */ new Map();
4271
- this.requireOutputs = /* @__PURE__ */ new Map();
4272
- this.waitForStacks = /* @__PURE__ */ new Map();
4273
- this.stackBases = /* @__PURE__ */ new Map();
4274
- this.stackSources = /* @__PURE__ */ new Map();
4275
- this.cycles = /* @__PURE__ */ new Map();
4276
- this.cycleModes = /* @__PURE__ */ new Map();
4277
- this.cycleDeltaBases = /* @__PURE__ */ new Map();
4278
- this.directCycleValues = /* @__PURE__ */ new Map();
4279
- this.directCycleFields = /* @__PURE__ */ new Map();
4280
- }
4281
- updateFromModel(update) {
4282
- const world = update?.world;
4283
- const model = update?.result?.model;
4284
- if (!world || !model) return;
4285
- const seenEntities = /* @__PURE__ */ new Set();
4286
- for (const section of model.sections || []) {
4287
- if (section.table !== 78) continue;
4288
- for (const changed of section.records || []) {
4289
- if (changed.indexedLoaderConfig) {
4290
- this.updateIndexedSnapshotRecord(world.id, changed.entity, changed.record, changed.mask, changed.cumulativeRecord, changed.configEntity);
4291
- continue;
4292
- }
4293
- const loader = changed.record ?? world.model.record(78, changed.entity);
4294
- this.updateRecord(world.id, changed.entity, loader, changed.mask, changed.previous, {
4295
- repeatedInUpdate: seenEntities.has(changed.entity),
4296
- repeatedInSection: changed.repeatedInSection,
4297
- semanticSnapshot: Boolean(model.full),
4298
- semanticDelta: isSemanticLoaderDelta(changed, model.full),
4299
- deltaRecord: loaderDeltaRecord(loader, changed.mask, changed.previous)
4300
- });
4301
- seenEntities.add(changed.entity);
4302
- }
4303
- }
4304
- }
4305
- updateRecord(worldId, entityId, loader, mask = 0, previous = null, options = {}) {
4306
- if (!loader) return;
4307
- const key = this.#key(worldId, entityId);
4308
- const hasTrackedConfig = this.#hasTrackedConfig(key);
4309
- if (hasTrackedConfig && (options.repeatedInSection || options.repeatedInUpdate) && this.sparsePositionBaselines.has(key) && this.hasPositionConfig(worldId, entityId)) {
4310
- return;
4311
- }
4312
- if (!hasTrackedConfig && this.#isDirectPositionCycleBaseline(loader, mask)) {
4313
- this.pickBases.set(key, 3);
4314
- this.pickDeltaBases.set(key, 0);
4315
- this.placeBases.set(key, 4);
4316
- this.placeDeltaBases.set(key, 0);
4317
- this.#setCurrentPosition(key, loader);
4318
- if (mask & 4 && loader.q28 >= -1 && loader.q28 <= 1) this.priorities.set(key, loader.q28);
4319
- this.requireOutputs.set(key, Boolean(mask & 32));
4320
- this.waitForStacks.set(key, Boolean(mask & 64));
4321
- this.stackBases.set(key, 16);
4322
- this.stackSources.delete(key);
4323
- this.cycleModes.set(key, "direct");
4324
- this.cycleDeltaBases.set(key, 0);
4325
- this.directCycleValues.set(key, loader.q44 ?? loader.q36);
4326
- this.directCycleFields.set(key, loader.q44 == null ? "q36" : "q44");
4327
- this.cycles.set(key, (loader.q44 ?? loader.q36) / 20 + 1);
4328
- return;
4329
- }
4330
- if (!hasTrackedConfig && options.allowSparseBaseline && this.#isSparsePositionBaseline(loader, mask)) {
4331
- this.#applySparsePositionBaseline(key, loader, mask);
4332
- return;
4333
- }
4334
- const baseline = hasTrackedConfig ? null : this.#loaderBaseline(loader, mask, options.semanticSnapshot);
4335
- if (hasTrackedConfig && options.semanticDelta && this.#applySemanticDelta(worldId, entityId, loader, mask, previous, options.deltaRecord)) {
4336
- return;
4337
- }
4338
- const hasInitialConfigFields = baseline || mask & 31 && (mask & 32 || mask & 64 && mask & 4);
4339
- if (!hasTrackedConfig && hasInitialConfigFields && mask !== 32 && mask !== 64) {
4340
- if (baseline) this.#applyLoaderBaseline(key, loader, baseline);
4341
- else {
4342
- const usedQ28AsPositionBase = this.#initializePositionBaseline(key, loader, mask);
4343
- if (mask & 4 && loader.q28 != null) {
4344
- if (usedQ28AsPositionBase) this.#initializePriorityOffset(key, loader);
4345
- else this.priorities.set(key, loader.q28);
4346
- }
4347
- }
4348
- this.requireOutputs.set(key, Boolean(mask & 32));
4349
- this.waitForStacks.set(key, Boolean(mask & 64));
4350
- if (!this.stackBases.has(key)) this.#initializeStackBaseline(key, loader);
4351
- if (!this.cycles.has(key)) this.#initializeCycleBaseline(key, loader);
4352
- return;
4353
- }
4354
- const q44CycleChanged = mask & 64 && this.#usesQ44CycleBase(key) && loader.q44 != null && (this.#fieldChanged(previous, loader, "q44") || this.directCycleValues.has(key) && loader.q44 !== this.directCycleValues.get(key));
4355
- const q40DirectCycleChanged = mask & 32 && this.cycleModes.get(key) === "q40-direct" && loader.q40 != null && (this.#fieldChanged(previous, loader, "q40") || this.directCycleValues.has(key) && loader.q40 !== this.directCycleValues.get(key));
4356
- if (q44CycleChanged) {
4357
- this.directCycleValues.set(key, loader.q44);
4358
- this.#updateCycle(key, loader);
4359
- } else if (q40DirectCycleChanged) {
4360
- this.directCycleValues.set(key, loader.q40);
4361
- this.#updateCycle(key, loader);
4362
- } else if (mask & 32 && mask === 32) {
4363
- this.requireOutputs.set(key, !this.requireOutputs.get(key));
4364
- } else if (mask & 32 && !this.requireOutputs.has(key)) {
4365
- this.requireOutputs.set(key, true);
4366
- }
4367
- if (!q44CycleChanged) {
4368
- if (mask & 64 && mask === 64) this.waitForStacks.set(key, !this.waitForStacks.get(key));
4369
- else if (mask & 64 && !this.waitForStacks.has(key)) this.waitForStacks.set(key, true);
4370
- }
4371
- if (mask & 4 && loader.q28 != null) this.priorities.set(key, this.#priority(key, loader.q28));
4372
- if (mask & 8 && this.filterSources.get(key) === "q32" && loader.q32 != null) this.filterModes.set(key, loader.q32);
4373
- const q32FilterOnlyUpdate = this.filterSources.get(key) === "q32" && mask === 8;
4374
- if (mask & 8 && !options.semanticDelta && !q32FilterOnlyUpdate && loader.q32 != null && this.stackBases.has(key)) {
4375
- const previousConfig = this.getConfig(worldId, entityId, previous);
4376
- this.stackBases.set(key, previousConfig.stack - (previous?.q32 ?? 0));
4377
- this.stackSources.set(key, "q32");
4378
- }
4379
- if (mask & 16) this.#updateCycle(key, loader);
4380
- if (mask & 3 && this.hasPositionConfig(worldId, entityId)) this.#setCurrentPosition(key, loader);
4381
- }
4382
- updateIndexedSnapshotRecord(worldId, entityId, loader, mask = 0, cumulative = null, configEntity = null) {
4383
- if (!loader) return false;
4384
- const reference = configEntity == null || Number(configEntity) === Number(entityId) ? null : this.#trackedConfig(worldId, configEntity);
4385
- const config = decodeIndexedLoaderSnapshotRecord(loader, mask, cumulative, reference);
4386
- if (!config) return false;
4387
- const key = this.#key(worldId, entityId);
4388
- this.pickBases.set(key, config.pick);
4389
- this.pickDeltaBases.set(key, loader.q20 ?? 0);
4390
- this.placeBases.set(key, config.place);
4391
- this.placeDeltaBases.set(key, loader.q24 ?? 0);
4392
- this.currentPicks.set(key, config.pick);
4393
- this.currentPlaces.set(key, config.place);
4394
- this.sparsePositionBaselines.delete(key);
4395
- this.priorities.set(key, config.priority);
4396
- if (mask === 95 && loader.q28 != null) this.priorityOffsets.set(key, config.priority - loader.q28);
4397
- else this.priorityOffsets.delete(key);
4398
- this.filterModes.delete(key);
4399
- this.filterSources.delete(key);
4400
- this.requireOutputs.set(key, config.requireOutput);
4401
- this.waitForStacks.set(key, config.waitForStack);
4402
- this.stackBases.set(key, config.stack - (loader.q32 ?? 0));
4403
- this.stackSources.delete(key);
4404
- this.cycles.set(key, config.cycle);
4405
- if (mask === 95) {
4406
- this.cycleModes.set(key, "q44-q36-delta");
4407
- this.cycleDeltaBases.set(key, loader.q36 ?? 0);
4408
- this.directCycleValues.set(key, loader.q44);
4409
- this.directCycleFields.set(key, "q44");
4410
- } else if (mask === 126) {
4411
- this.cycleModes.set(key, "offset");
4412
- this.cycleDeltaBases.set(key, loader.q36 ?? 0);
4413
- this.directCycleValues.delete(key);
4414
- this.directCycleFields.delete(key);
4415
- } else {
4416
- this.cycleModes.set(key, "direct");
4417
- this.cycleDeltaBases.set(key, 0);
4418
- }
4419
- if (loader.q36 != null && mask === 18) {
4420
- this.directCycleValues.set(key, loader.q36);
4421
- this.directCycleFields.set(key, "q36");
4422
- } else if (mask !== 95 && mask !== 126) {
4423
- this.directCycleValues.delete(key);
4424
- this.directCycleFields.delete(key);
4425
- }
4426
- return true;
4427
- }
4428
- delete(worldId, entityId) {
4429
- const key = this.#key(worldId, entityId);
4430
- this.pickBases.delete(key);
4431
- this.pickDeltaBases.delete(key);
4432
- this.placeBases.delete(key);
4433
- this.placeDeltaBases.delete(key);
4434
- this.currentPicks.delete(key);
4435
- this.currentPlaces.delete(key);
4436
- this.sparsePositionBaselines.delete(key);
4437
- this.priorities.delete(key);
4438
- this.priorityOffsets.delete(key);
4439
- this.filterModes.delete(key);
4440
- this.filterSources.delete(key);
4441
- this.requireOutputs.delete(key);
4442
- this.waitForStacks.delete(key);
4443
- this.stackBases.delete(key);
4444
- this.stackSources.delete(key);
4445
- this.cycles.delete(key);
4446
- this.cycleModes.delete(key);
4447
- this.cycleDeltaBases.delete(key);
4448
- this.directCycleValues.delete(key);
4449
- this.directCycleFields.delete(key);
4450
- }
4451
- getConfig(worldId, entityId, loader, loaderFilter = null, filterSlots = null) {
4452
- const key = this.#key(worldId, entityId);
4453
- const pick = this.currentPicks.has(key) ? this.currentPicks.get(key) : this.#pick(key, loader);
4454
- const place = this.currentPlaces.has(key) ? this.currentPlaces.get(key) : this.#place(key, loader);
4455
- const stackBase = this.stackBases.get(key) ?? 16;
4456
- const stackSource = this.stackSources.get(key) ?? "q32";
4457
- const stackField = stackSource === "fixed" ? null : loader?.[stackSource];
4458
- const stack = stackField == null ? this.stackBases.has(key) || this.hasPositionConfig(worldId, entityId) ? stackBase : null : stackBase + stackField;
4459
- const cycle = this.cycles.has(key) ? this.cycles.get(key) : loader?.q40 == null || loader.q36 == null ? null : (loader.q40 + loader.q36) / 20 + 1;
4460
- return {
4461
- pick,
4462
- place,
4463
- priority: this.priorities.has(key) ? this.priorities.get(key) : DEFAULT_LOADER_PRIORITY,
4464
- requireOutput: this.requireOutputs.has(key) ? this.requireOutputs.get(key) : false,
4465
- waitForStack: this.waitForStacks.has(key) ? this.waitForStacks.get(key) : false,
4466
- stack: stack ?? DEFAULT_LOADER_STACK,
4467
- cycle: cycle ?? DEFAULT_LOADER_CYCLE,
4468
- filterMode: loaderFilter ? loaderFilter.q20 ?? DEFAULT_LOADER_FILTER_MODE : this.#filterMode(key, loader) ?? DEFAULT_LOADER_FILTER_MODE,
4469
- filterSlots: filterSlots ? [filterSlotValue(filterSlots.q20), filterSlotValue(filterSlots.q24), filterSlotValue(filterSlots.q28)] : null
4470
- };
4471
- }
4472
- hasPositionConfig(worldId, entityId) {
4473
- const key = this.#key(worldId, entityId);
4474
- return this.pickBases.has(key) && this.placeBases.has(key);
4475
- }
4476
- #hasTrackedConfig(key) {
4477
- return this.pickBases.has(key) || this.placeBases.has(key) || this.priorities.has(key) || this.priorityOffsets.has(key) || this.filterModes.has(key) || this.requireOutputs.has(key) || this.waitForStacks.has(key) || this.stackBases.has(key) || this.cycles.has(key);
4478
- }
4479
- #fieldChanged(previous, loader, field) {
4480
- return previous != null && loader != null && previous[field] !== loader[field];
4481
- }
4482
- #usesQ44CycleBase(key) {
4483
- const mode = this.cycleModes.get(key);
4484
- return mode === "q44-q36-delta";
4485
- }
4486
- #initializePositionBaseline(key, loader, mask) {
4487
- const hasQ28PositionBase = loader.q28 != null && mask & 4;
4488
- if (loader.q24 != null) {
4489
- this.pickBases.set(key, wrapLoaderPosition(loader.q24 + 3));
4490
- this.pickDeltaBases.set(key, loader.q20 ?? 0);
4491
- this.placeDeltaBases.set(key, loader.q24);
4492
- const initialPlace = hasQ28PositionBase ? loader.q28 + 4 : loader.q36 == null ? null : loader.q36 + 4;
4493
- if (initialPlace != null) this.placeBases.set(key, wrapLoaderPosition(initialPlace));
4494
- return hasQ28PositionBase;
4495
- }
4496
- if (hasQ28PositionBase) {
4497
- this.pickBases.set(key, wrapLoaderPosition(loader.q28 + 3));
4498
- this.pickDeltaBases.set(key, 0);
4499
- this.placeBases.set(key, 4);
4500
- this.placeDeltaBases.set(key, 0);
4501
- return true;
4502
- }
4503
- if (loader.q20 != null && loader.q36 != null) {
4504
- this.pickBases.set(key, wrapLoaderPosition(loader.q36 + 3));
4505
- this.pickDeltaBases.set(key, 0);
4506
- this.placeBases.set(key, 4);
4507
- this.placeDeltaBases.set(key, 0);
4508
- }
4509
- return false;
4510
- }
4511
- #initializeCycleBaseline(key, loader) {
4512
- if (loader?.q44 != null && (loader.q36 == null || loader.q40 == null)) {
4513
- this.cycleModes.set(key, "q44-q36-delta");
4514
- this.cycleDeltaBases.set(key, loader.q36 ?? 0);
4515
- this.directCycleValues.set(key, loader.q44);
4516
- this.directCycleFields.set(key, "q44");
4517
- this.cycles.set(key, loader.q44 / 20 + 1);
4518
- return;
4519
- }
4520
- if (loader?.q40 == null || loader.q36 == null) return;
4521
- this.cycleModes.set(key, "offset");
4522
- this.cycleDeltaBases.set(key, loader.q36 ?? 0);
4523
- this.directCycleValues.delete(key);
4524
- this.directCycleFields.delete(key);
4525
- this.cycles.set(key, loader.q40 / 20 + 1);
4526
- }
4527
- #initializeStackBaseline(key, loader) {
4528
- if (this.stackBases.has(key)) return;
4529
- if (loader?.q32 == null) return;
4530
- const baseline = this.#loaderBaseline(loader, loader?.lastMask);
4531
- if (baseline) {
4532
- this.stackBases.set(key, baseline.stack - loader.q32);
4533
- return;
4534
- }
4535
- if (loader.q20 == null && loader.q24 == null && loader.q28 == null && loader.q36 != null) {
4536
- this.stackBases.set(key, 17 + loader.q36);
4537
- return;
4538
- }
4539
- this.stackBases.set(key, 16);
4540
- }
4541
- #initializePriorityOffset(key, loader) {
4542
- if (loader?.q28 == null || loader?.q36 == null) return;
4543
- const offset = loader.q36 + 2;
4544
- const priority = loader.q28 + offset;
4545
- if (priority < -1 || priority > 1) return;
4546
- this.priorityOffsets.set(key, offset);
4547
- this.priorities.set(key, priority);
4548
- }
4549
- #priority(key, q28) {
4550
- return q28 + (this.priorityOffsets.get(key) ?? 0);
4551
- }
4552
- #updateCycle(key, loader) {
4553
- const mode = this.cycleModes.get(key);
4554
- if (mode === "direct") {
4555
- if (loader?.q36 != null) {
4556
- this.directCycleValues.set(key, loader.q36);
4557
- this.directCycleFields.set(key, "q36");
4558
- this.cycles.set(key, loader.q36 / 20 + 1);
4559
- } else if (loader?.q44 != null) {
4560
- this.directCycleValues.set(key, loader.q44);
4561
- this.directCycleFields.set(key, "q44");
4562
- this.cycles.set(key, loader.q44 / 20 + 1);
4563
- }
4564
- return;
4565
- }
4566
- if (mode === "q44-q36-delta") {
4567
- const base = loader?.q44 ?? this.directCycleValues.get(key);
4568
- if (base != null) {
4569
- this.directCycleValues.set(key, base);
4570
- this.directCycleFields.set(key, "q44");
4571
- const q36 = loader?.q36 ?? 0;
4572
- this.cycles.set(key, base < 0 && loader?.q36 != null ? q36 / 20 + 1 : (base + q36 - (this.cycleDeltaBases.get(key) ?? 0)) / 20 + 1);
4573
- }
4574
- return;
4575
- }
4576
- if (mode === "q40-direct") {
4577
- if (loader?.q40 != null) {
4578
- this.directCycleValues.set(key, loader.q40);
4579
- this.directCycleFields.set(key, "q40");
4580
- this.cycles.set(key, loader.q40 / 20 + 1);
4581
- }
4582
- return;
4583
- }
4584
- if (loader?.q40 == null) {
4585
- if (loader?.q44 != null || loader?.q36 != null) this.cycles.set(key, (loader.q44 ?? loader.q36) / 20 + 1);
4586
- return;
4587
- }
4588
- const deltaBase = this.cycleDeltaBases.get(key) ?? 0;
4589
- this.cycles.set(key, (loader.q40 + (loader.q36 ?? 0) - deltaBase) / 20 + 1);
4590
- }
4591
- #isDirectPositionCycleBaseline(loader, mask) {
4592
- return (mask & 19) === 19 && loader.q20 != null && loader.q24 != null && loader.q36 != null && loader.q40 == null && loader.q44 == null;
4593
- }
4594
- #trackedConfig(worldId, entityId) {
4595
- const key = this.#key(worldId, entityId);
4596
- if (!this.#hasTrackedConfig(key)) return null;
4597
- return {
4598
- pick: this.currentPicks.has(key) ? this.currentPicks.get(key) : this.pickBases.get(key),
4599
- place: this.currentPlaces.has(key) ? this.currentPlaces.get(key) : this.placeBases.get(key),
4600
- priority: this.priorities.get(key) ?? DEFAULT_LOADER_PRIORITY,
4601
- requireOutput: this.requireOutputs.get(key) ?? false,
4602
- waitForStack: this.waitForStacks.get(key) ?? false,
4603
- stack: this.stackBases.get(key) ?? DEFAULT_LOADER_STACK,
4604
- cycle: this.cycles.get(key) ?? DEFAULT_LOADER_CYCLE
4605
- };
4606
- }
4607
- #isSparsePositionBaseline(loader, mask) {
4608
- return (mask & 3) === 3 && loader.q20 != null && loader.q24 != null && loader.q28 == null && loader.q32 == null && loader.q36 == null && loader.q44 == null;
4609
- }
4610
- #applySparsePositionBaseline(key, loader, mask) {
4611
- const pickField = loader.q40 == null ? loader.q20 : loader.q24;
4612
- const placeField = loader.q40 == null ? loader.q24 : loader.q40;
4613
- const pick = wrapLoaderPosition(pickField + 3);
4614
- const place = wrapLoaderPosition(placeField + 4);
4615
- this.pickBases.set(key, pick);
4616
- this.pickDeltaBases.set(key, loader.q20 ?? 0);
4617
- this.placeBases.set(key, place);
4618
- this.placeDeltaBases.set(key, loader.q24 ?? 0);
4619
- this.currentPicks.set(key, pick);
4620
- this.currentPlaces.set(key, place);
4621
- this.sparsePositionBaselines.add(key);
4622
- this.priorities.set(key, DEFAULT_LOADER_PRIORITY);
4623
- this.requireOutputs.set(key, Boolean(mask & 32));
4624
- this.waitForStacks.set(key, Boolean(mask & 64));
4625
- this.stackBases.set(key, DEFAULT_LOADER_STACK);
4626
- this.cycles.set(key, DEFAULT_LOADER_CYCLE);
4627
- }
4628
- #filterMode(key, loader) {
4629
- if (this.filterSources.get(key) === "q32" && loader?.q32 != null) {
4630
- return this.#validFilterMode(loader.q32);
4631
- }
4632
- if (this.filterModes.has(key)) return this.#validFilterMode(this.filterModes.get(key));
4633
- const baseline = this.#loaderBaseline(loader, loader?.lastMask);
4634
- if (!baseline) return null;
4635
- return this.#validFilterMode(baseline.filterMode);
4636
- }
4637
- #validFilterMode(mode) {
4638
- return mode >= 0 && mode <= 3 ? mode : null;
4639
- }
4640
- #semanticFilterMode(loader, mask) {
4641
- const q28IsDirectPriority = loader.q28 != null && loader.q28 >= -1 && loader.q28 <= 1;
4642
- const q24FilterMode = loader.q24 != null && loader.q32 != null && loader.q40 != null ? loader.q24 + 2 : null;
4643
- const q28FilterMode = loader.q28 != null && loader.q32 != null ? loader.q28 + 1 : null;
4644
- const q32FilterMode = !q28IsDirectPriority && loader.q28 != null && loader.q32 != null && loader.q40 != null ? loader.q32 + 1 : null;
4645
- const q32DirectMode = loader.q28 == null && loader.q32 != null && mask & 32 ? loader.q32 : null;
4646
- if (q28IsDirectPriority && this.#validFilterMode(q24FilterMode) != null) return q24FilterMode;
4647
- if (this.#validFilterMode(q28FilterMode) != null) return q28FilterMode;
4648
- if (this.#validFilterMode(q32FilterMode) != null) return q32FilterMode;
4649
- if (this.#validFilterMode(q32DirectMode) != null) return q32DirectMode;
4650
- if (this.#validFilterMode(q24FilterMode) != null) return q24FilterMode;
4651
- return null;
4652
- }
4653
- #semanticFilterField(loader, mask) {
4654
- const q32DirectMode = loader.q28 == null && loader.q32 != null && mask & 32 ? loader.q32 : null;
4655
- return this.#validFilterMode(q32DirectMode) != null ? "q32" : null;
4656
- }
4657
- #loaderBaseline(loader, mask = loader?.lastMask, semanticSnapshot = false) {
4658
- const snapshot = semanticSnapshot ? decodeLoaderSnapshotRecord(loader, mask) : null;
4659
- if (snapshot) {
4660
- return {
4661
- pickBase: snapshot.pick,
4662
- pickDeltaBase: loader.q20 ?? 0,
4663
- placeBase: snapshot.place,
4664
- placeDeltaBase: loader.q24 ?? 0,
4665
- priority: snapshot.priority,
4666
- stack: snapshot.stack,
4667
- stackField: "fixed",
4668
- cycle: snapshot.cycle,
4669
- cycleMode: loader.q44 != null && loader.q40 == null ? "q44-q36-delta" : "offset",
4670
- filterMode: this.#semanticFilterMode(loader, mask),
4671
- filterField: this.#semanticFilterField(loader, mask),
4672
- q32: loader.q32 ?? 0
4673
- };
4674
- }
4675
- if (!loader || loader.q36 == null) return null;
4676
- if (loader.q40 == null && loader.q44 == null) {
4677
- if (loader.q20 != null || loader.q24 == null || loader.q28 == null || loader.q32 == null) return null;
4678
- return {
4679
- pickBase: wrapLoaderPosition(loader.q28 + 2),
4680
- pickDeltaBase: 0,
4681
- placeBase: wrapLoaderPosition(loader.q24 + 4),
4682
- placeDeltaBase: loader.q24,
4683
- priority: loader.q28,
4684
- stack: 16 + loader.q32,
4685
- cycle: loader.q36 / 20 + 1,
4686
- cycleMode: "direct",
4687
- filterMode: loader.q28 + 1,
4688
- q32: loader.q32
4689
- };
4690
- }
4691
- if (loader.q24 == null && loader.q28 == null && loader.q20 != null && loader.q32 != null && loader.q40 != null && loader.q44 != null) {
4692
- return {
4693
- pickBase: wrapLoaderPosition(loader.q20 + 7),
4694
- pickDeltaBase: loader.q20,
4695
- placeBase: 4,
4696
- placeDeltaBase: 0,
4697
- priority: 0,
4698
- stack: 16 + loader.q36,
4699
- stackField: "q36",
4700
- cycle: loader.q40 / 20 + 1,
4701
- cycleMode: "q40-direct",
4702
- filterMode: loader.q32,
4703
- filterField: "q32",
4704
- q32: loader.q32
4705
- };
4706
- }
4707
- const q20 = loader.q20 ?? 0;
4708
- const q32 = loader.q32 ?? 0;
4709
- let pickBase = null;
4710
- let placeBase = null;
4711
- if (loader.q44 != null && loader.q40 == null && loader.q28 == null && loader.q24 != null && loader.q32 != null) {
4712
- pickBase = wrapLoaderPosition(loader.q24 + 3);
4713
- placeBase = wrapLoaderPosition(loader.q32 + 4);
4714
- } else if (loader.q24 != null) {
4715
- pickBase = wrapLoaderPosition(loader.q24 + 3);
4716
- placeBase = loader.q28 != null ? wrapLoaderPosition(loader.q28 + 4) : wrapLoaderPosition(loader.q36 + 4);
4717
- } else if (loader.q28 != null) {
4718
- pickBase = wrapLoaderPosition(loader.q28 + 3);
4719
- placeBase = 4;
4720
- } else if (loader.q20 != null) {
4721
- pickBase = wrapLoaderPosition(loader.q36 + 3);
4722
- placeBase = 4;
4723
- }
4724
- if (pickBase == null || placeBase == null) return null;
4725
- const q28IsDirectPriority = loader.q28 != null && loader.q28 >= -1 && loader.q28 <= 1;
4726
- const priority = loader.q28 != null && loader.q32 != null && loader.q44 != null && loader.q40 != null ? loader.q32 : loader.q28 != null && loader.q32 != null && loader.q44 == null && loader.q40 == null ? loader.q28 : loader.q28 != null && loader.q32 != null && loader.q44 == null && loader.q40 != null ? loader.q32 : loader.q32 != null && loader.q44 != null && loader.q40 == null ? loader.q32 : null;
4727
- const q28FilterMode = loader.q28 != null && loader.q32 != null && loader.q44 != null ? loader.q28 + 1 : null;
4728
- const q24FilterMode = loader.q24 != null && loader.q32 != null && loader.q44 != null && loader.q40 != null ? loader.q24 + 2 : null;
4729
- const q32FilterMode = !q28IsDirectPriority && loader.q28 != null && loader.q32 != null && loader.q44 != null && loader.q40 != null ? loader.q32 + 1 : null;
4730
- const filterMode = q28IsDirectPriority && q24FilterMode != null && q24FilterMode >= 0 && q24FilterMode <= 3 ? q24FilterMode : q28FilterMode != null && q28FilterMode >= 0 && q28FilterMode <= 3 ? q28FilterMode : q32FilterMode != null && q32FilterMode >= 0 && q32FilterMode <= 3 ? q32FilterMode : q24FilterMode;
4731
- return {
4732
- pickBase,
4733
- pickDeltaBase: q20,
4734
- placeBase,
4735
- placeDeltaBase: loader.q24 ?? 0,
4736
- priority,
4737
- stack: loader.q32 == null ? null : 16 + loader.q36,
4738
- cycle: (loader.q40 ?? loader.q44) / 20 + 1,
4739
- cycleMode: loader.q44 != null && loader.q40 == null ? "q44-q36-delta" : "offset",
4740
- filterMode,
4741
- q32
4742
- };
4743
- }
4744
- #applyLoaderBaseline(key, loader, baseline) {
4745
- this.pickBases.set(key, baseline.pickBase);
4746
- this.pickDeltaBases.set(key, baseline.pickDeltaBase);
4747
- this.placeBases.set(key, baseline.placeBase);
4748
- this.placeDeltaBases.set(key, baseline.placeDeltaBase);
4749
- this.sparsePositionBaselines.delete(key);
4750
- this.#setCurrentPosition(key, loader);
4751
- if (baseline.filterMode != null) {
4752
- this.filterModes.set(key, baseline.filterMode);
4753
- if (baseline.filterField) this.filterSources.set(key, baseline.filterField);
4754
- else this.filterSources.delete(key);
4755
- }
4756
- if (baseline.stack != null) {
4757
- const stackField = baseline.stackField === "fixed" ? 0 : baseline.stackField ? loader[baseline.stackField] ?? 0 : baseline.q32;
4758
- this.stackBases.set(key, baseline.stack - stackField);
4759
- if (baseline.stackField) this.stackSources.set(key, baseline.stackField);
4760
- else this.stackSources.delete(key);
4761
- }
4762
- this.cycleModes.set(key, baseline.cycleMode);
4763
- if (baseline.cycleMode === "direct") {
4764
- this.cycleDeltaBases.set(key, 0);
4765
- this.directCycleValues.set(key, loader.q44 ?? loader.q36);
4766
- this.directCycleFields.set(key, loader.q44 == null ? "q36" : "q44");
4767
- } else if (baseline.cycleMode === "q44-q36-delta") {
4768
- this.cycleDeltaBases.set(key, loader.q36 ?? 0);
4769
- this.directCycleValues.set(key, loader.q44);
4770
- this.directCycleFields.set(key, "q44");
4771
- } else if (baseline.cycleMode === "q40-direct") {
4772
- this.cycleDeltaBases.set(key, 0);
4773
- this.directCycleValues.set(key, loader.q40);
4774
- this.directCycleFields.set(key, "q40");
4775
- } else {
4776
- this.cycleDeltaBases.set(key, loader.q36 ?? 0);
4777
- this.directCycleValues.delete(key);
4778
- this.directCycleFields.delete(key);
4779
- }
4780
- this.cycles.set(key, baseline.cycle);
4781
- if (baseline.priority != null && baseline.priority >= -1 && baseline.priority <= 1) {
4782
- if (loader.q28 != null) this.priorityOffsets.set(key, baseline.priority - loader.q28);
4783
- this.priorities.set(key, baseline.priority);
4784
- } else if (loader.q28 != null && loader.q36 != null) this.#initializePriorityOffset(key, loader);
4785
- }
4786
- #applySemanticDelta(worldId, entityId, loader, mask, previous, deltaRecord) {
4787
- const key = this.#key(worldId, entityId);
4788
- const delta = decodeLoaderSemanticDeltaRecord(deltaRecord ?? loaderDeltaRecord(loader, mask, previous), mask);
4789
- if (!delta) return false;
4790
- const previousConfig = this.getConfig(worldId, entityId, previous);
4791
- const pick = delta.pick == null ? previousConfig.pick : wrapLoaderPosition((previousConfig.pick ?? DEFAULT_LOADER_PICK) + delta.pick);
4792
- const place = delta.place == null ? previousConfig.place : wrapLoaderPosition((previousConfig.place ?? DEFAULT_LOADER_PLACE) + delta.place);
4793
- if (pick != null) {
4794
- this.pickBases.set(key, pick);
4795
- this.pickDeltaBases.set(key, loader?.q20 ?? 0);
4796
- this.currentPicks.set(key, pick);
4797
- }
4798
- if (place != null) {
4799
- this.placeBases.set(key, place);
4800
- this.placeDeltaBases.set(key, loader?.q24 ?? 0);
4801
- this.currentPlaces.set(key, place);
4802
- }
4803
- if (delta.priority != null) {
4804
- const priority = previousConfig.priority + delta.priority;
4805
- this.priorities.set(key, priority);
4806
- if (loader?.q28 != null) this.priorityOffsets.set(key, priority - loader.q28);
4807
- else this.priorityOffsets.delete(key);
4808
- }
4809
- if (delta.stack != null) {
4810
- this.stackBases.set(key, previousConfig.stack + delta.stack);
4811
- this.stackSources.set(key, "fixed");
4812
- }
4813
- if (delta.cycleTicks != null) {
4814
- this.cycles.set(key, previousConfig.cycle + delta.cycleTicks / 20);
4815
- if (loader?.q44 != null && loader.q40 == null) {
4816
- this.cycleModes.set(key, "q44-q36-delta");
4817
- this.cycleDeltaBases.set(key, loader.q36 ?? 0);
4818
- this.directCycleValues.set(key, loader.q44);
4819
- this.directCycleFields.set(key, "q44");
4820
- } else {
4821
- this.cycleModes.set(key, "offset");
4822
- this.cycleDeltaBases.set(key, loader?.q36 ?? 0);
4823
- this.directCycleValues.delete(key);
4824
- this.directCycleFields.delete(key);
4825
- }
4826
- }
4827
- if (delta.requireOutput) this.requireOutputs.set(key, !previousConfig.requireOutput);
4828
- if (delta.waitForStack) this.waitForStacks.set(key, !previousConfig.waitForStack);
4829
- this.sparsePositionBaselines.delete(key);
4830
- return true;
4831
- }
4832
- #setCurrentPosition(key, loader) {
4833
- const pick = this.#pick(key, loader);
4834
- const place = this.#place(key, loader);
4835
- if (pick != null) this.currentPicks.set(key, pick);
4836
- if (place != null) this.currentPlaces.set(key, place);
4837
- }
4838
- #pick(key, loader) {
4839
- if (!loader) return null;
4840
- if (this.pickBases.has(key)) return wrapLoaderPosition(this.pickBases.get(key) + ((loader.q20 ?? 0) - (this.pickDeltaBases.get(key) ?? 0)));
4841
- if (loader.q24 != null) return wrapLoaderPosition(loader.q24 + 3);
4842
- if (loader.q36 != null) return wrapLoaderPosition(loader.q36 + 3);
4843
- if (loader.q20 != null) return wrapLoaderPosition(loader.q20);
4844
- return null;
4845
- }
4846
- #place(key, loader) {
4847
- if (!loader) return null;
4848
- if (this.placeBases.has(key)) {
4849
- const base = this.placeBases.get(key);
4850
- if (loader.q24 != null && this.placeDeltaBases.has(key)) {
4851
- return wrapLoaderPosition(base + (loader.q24 - this.placeDeltaBases.get(key)));
4852
- }
4853
- return wrapLoaderPosition(base);
4854
- }
4855
- if (loader.q36 != null) return loader.q24 == null ? 4 : wrapLoaderPosition(loader.q36 + 4);
4856
- return null;
4857
- }
4858
- #key(worldId, entityId) {
4859
- return `${worldId ?? "-"}:${entityId}`;
4860
- }
4861
- };
4862
4155
 
4863
4156
  // src/state/overworld.js
4864
4157
  var OVERWORLD_ZONES = Object.freeze([
@@ -5308,8 +4601,8 @@
5308
4601
  if (end > reader.bytes.length) return false;
5309
4602
  const blob = reader.bytes.slice(peek.offset, end);
5310
4603
  try {
5311
- const text = decoder.decode(blob);
5312
- 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);
5313
4606
  } catch (_) {
5314
4607
  return false;
5315
4608
  }
@@ -5501,6 +4794,7 @@
5501
4794
  }],
5502
4795
  [55, {
5503
4796
  name: "player",
4797
+ defaults: { q20: 1, q60: 10, q64: 10, q68: 10 },
5504
4798
  packedBeforeValues: true,
5505
4799
  fields: numericFields({
5506
4800
  256: 20,
@@ -5535,8 +4829,10 @@
5535
4829
  ]
5536
4830
  }],
5537
4831
  [56, {
5538
- name: "numeric_snapshot",
5539
- 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 })
5540
4836
  }],
5541
4837
  [59, { name: "numeric_sparse", fields: numericFields({ 1: 20, 2: 24, 4: 28, 8: 32, 16: 36 }) }],
5542
4838
  [60, { name: "fluid_tank", packedBits: [{ bit: 4, offset: 29 }], ...TWO_FIELD_SPEC }],
@@ -5545,7 +4841,11 @@
5545
4841
  [63, { name: "numeric_pair", ...TWO_FIELD_SPEC }],
5546
4842
  [67, { name: "numeric_single", fields: numericFields({ 1: 20 }) }],
5547
4843
  [69, {
5548
- name: "rare_snapshot",
4844
+ name: "loader_config",
4845
+ // The official loader component starts with these values. Wire section
4846
+ // 162 sends a packed absolute flag word before the five numeric deltas.
4847
+ defaults: { q20: 3, q24: 4, q28: 1, q32: 16, q36: 20, q41: 0, q42: 0 },
4848
+ packedBeforeValues: true,
5549
4849
  packedBits: [{ bit: 32, offset: 41 }, { bit: 64, offset: 42 }],
5550
4850
  fields: numericFields({ 1: 20, 2: 24, 4: 28, 8: 32, 16: 36 })
5551
4851
  }],
@@ -5661,7 +4961,8 @@
5661
4961
  [158, 75],
5662
4962
  [160, 76],
5663
4963
  [161, 77],
5664
- [162, 78],
4964
+ [162, 69],
4965
+ // Loader configuration; distinct from navigation state (168).
5665
4966
  [163, 72],
5666
4967
  [164, 73],
5667
4968
  [165, 74],
@@ -5835,27 +5136,6 @@
5835
5136
  }
5836
5137
  return null;
5837
5138
  }
5838
- var TABLE_78_FIELD_BITS = [
5839
- ["q20", 1],
5840
- ["q24", 2],
5841
- ["q28", 4],
5842
- ["q32", 8],
5843
- ["q36", 16],
5844
- ["q40", 32],
5845
- ["q44", 64]
5846
- ];
5847
- function table78DeltaRecord(changed) {
5848
- const record = { lastMask: changed.mask };
5849
- for (const [field, bit] of TABLE_78_FIELD_BITS) {
5850
- if (!(changed.mask & bit)) continue;
5851
- if (field === "q44" && (changed.mask & 32 || changed.record?.q44 == null)) continue;
5852
- record[field] = (changed.record?.[field] ?? 0) - (changed.previous?.[field] ?? 0);
5853
- }
5854
- return record;
5855
- }
5856
- function isSemanticLoaderDelta2(changed, full) {
5857
- return !full && changed.previous != null && (changed.mask & 96) === 96;
5858
- }
5859
5139
  function decodeText(blob) {
5860
5140
  if (!(blob instanceof Uint8Array)) return null;
5861
5141
  try {
@@ -5927,10 +5207,16 @@
5927
5207
  active: record?.q33 == null ? null : Boolean(record.q33)
5928
5208
  };
5929
5209
  }
5930
- function summarizeCannon(entity, record, typeId = null) {
5210
+ function summarizeCannon(entity, record, typeId = null, transformRecord = null) {
5931
5211
  if (!record || !CANNON_TYPE_IDS.has(typeId)) return null;
5212
+ const relativeAngleRadians = numberOrNull(record.q32, 200);
5213
+ const mountingAngleRadians = numberOrNull(transformRecord?.q28, 127.324);
5214
+ const tau = 2 * Math.PI;
5215
+ const absoluteAngleRadians = relativeAngleRadians == null || mountingAngleRadians == null ? null : ((mountingAngleRadians + relativeAngleRadians) % tau + tau) % tau;
5932
5216
  const ammoItemId = record.q24 == null ? null : CANNON_AMMO_COLOR_ITEM_IDS.get(record.q24) ?? null;
5933
- const charge = record.q40 ?? null;
5217
+ const indicatorValue = (record.q40 ?? 0) / 50;
5218
+ const indicatorVisible = indicatorValue >= 1;
5219
+ const indicatorBlinking = indicatorValue === 4;
5934
5220
  const recoil = record.q36 ?? null;
5935
5221
  const recoil2 = record.q48 ?? null;
5936
5222
  return {
@@ -5940,17 +5226,26 @@
5940
5226
  ammoItemId,
5941
5227
  ammoName: entityNameFromType(ammoItemId),
5942
5228
  ammoCount: record.q28 ?? 0,
5943
- aim: record.q32 ?? null,
5229
+ relativeAngleRadians,
5230
+ absoluteAngleRadians,
5944
5231
  recoil,
5945
5232
  recoil2,
5946
5233
  recoils: [recoil, recoil2],
5947
- charge,
5948
- 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,
5949
5239
  spin: record.q52 ?? null,
5950
- coolingCellCount: record.q56 ?? 0,
5240
+ resourceCount: record.q56 ?? 0,
5951
5241
  state: cloneRecord(record)
5952
5242
  };
5953
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
+ }
5954
5249
  function summarizeThruster(entity, typeId, record) {
5955
5250
  if (!THRUSTER_TYPE_IDS.has(typeId)) return null;
5956
5251
  const facing = Number.isFinite(Number(record?.q20)) ? Number(record.q20) : 0;
@@ -6316,15 +5611,13 @@
6316
5611
  state: cloneRecord(markerRecord)
6317
5612
  };
6318
5613
  }
6319
- function summarizeLoader(entity, loaderRecord, loaderFilterRecord = null, filterSlotsRecord = null, tracker = null, includeDefaults = false, itemHolderRecord = null) {
5614
+ function summarizeLoader(entity, loaderRecord, loaderFilterRecord = null, filterSlotsRecord = null, includeDefaults = false, itemHolderRecord = null) {
6320
5615
  if (!loaderRecord && !loaderFilterRecord && !includeDefaults) return null;
6321
- const config = tracker?.getConfig(null, entity, loaderRecord, loaderFilterRecord, filterSlotsRecord) ?? {};
6322
- const hasLoaderState = Boolean(loaderRecord);
6323
- const hasPositionConfig = Boolean(tracker?.hasPositionConfig(null, entity));
6324
- const pick = hasLoaderState && hasPositionConfig ? config.pick ?? null : null;
6325
- const place = hasLoaderState && hasPositionConfig ? config.place ?? null : null;
6326
- const priority = config.priority ?? 0;
6327
- const filterMode = config.filterMode ?? null;
5616
+ const loader = { ...MODEL_TABLE_SPECS.get(69).defaults, ...loaderRecord };
5617
+ const pick = loader.q20;
5618
+ const place = loader.q24;
5619
+ const priority = loader.q28 - 1;
5620
+ const filterMode = loaderFilterRecord?.q20 ?? 0;
6328
5621
  const heldItem = itemSummary(itemHolderRecord?.q20, itemHolderRecord?.q24 ?? null);
6329
5622
  return {
6330
5623
  entity,
@@ -6334,13 +5627,13 @@
6334
5627
  placeName: enumValueName(LOADER_POSITION_NAMES, place),
6335
5628
  priority,
6336
5629
  priorityName: enumValueName(LOADER_PRIORITY_NAMES, priority),
6337
- requireOutput: config.requireOutput ?? false,
6338
- waitForStack: config.waitForStack ?? false,
6339
- stack: config.stack ?? 16,
6340
- cycle: Math.max(1, config.cycle ?? 1),
5630
+ requireOutput: Boolean(loader.q41),
5631
+ waitForStack: Boolean(loader.q42),
5632
+ stack: loader.q32,
5633
+ cycle: loader.q36 / 20,
6341
5634
  filterMode,
6342
5635
  filterModeName: enumValueName(LOADER_FILTER_MODE_NAMES, filterMode),
6343
- filterSlots: config.filterSlots ?? null,
5636
+ filterSlots: filterSlotsRecord ? [filterSlotsRecord.q20, filterSlotsRecord.q24, filterSlotsRecord.q28].map((value) => value || null) : null,
6344
5637
  heldItemId: heldItem.itemId,
6345
5638
  heldItemName: heldItem.itemName,
6346
5639
  heldCount: heldItem.count,
@@ -6378,7 +5671,7 @@
6378
5671
  filteredModeName: enumValueName(PUSHER_MODE_NAMES, filteredMode),
6379
5672
  angle: numberOrNull(pusherRecord.q28, 10),
6380
5673
  speed: pusherRecord.q32 == null ? 20 : pusherRecord.q32 / 100 + 20,
6381
- length: pusherRecord.q40 == null ? 1e3 : (pusherRecord.q40 + 50) / 10 + 995,
5674
+ length: 1e3 + (pusherRecord.q40 ?? 0) / 10,
6382
5675
  filterInventory: pusherRecord.q36 === 1,
6383
5676
  filterSlots: filterSlotsRecord ? [filterSlotsRecord.q20 ?? null, filterSlotsRecord.q24 ?? null, filterSlotsRecord.q28 ?? null] : null,
6384
5677
  state: cloneRecord(pusherRecord),
@@ -6530,7 +5823,36 @@
6530
5823
  state: cloneRecord(record)
6531
5824
  };
6532
5825
  }
6533
- 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) {
6534
5856
  if (!record) return null;
6535
5857
  const rawHeldItemId = record.q28 == null ? null : Number(record.q28);
6536
5858
  const heldItemId = Number.isFinite(rawHeldItemId) && rawHeldItemId !== 0 ? rawHeldItemId : null;
@@ -6541,14 +5863,29 @@
6541
5863
  name: decodeText(record.blob92),
6542
5864
  heldItemId,
6543
5865
  heldItemName: entityNameFromType(heldItemId),
6544
- repairTargetDistance: record.q56 == null ? null : record.q56 / 10,
6545
- 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),
6546
5870
  shipRank: PLAYER_SHIP_RANKS.get(teamRank) || null,
6547
5871
  patronTier: patronTierName(gameRank),
6548
5872
  isDeveloper: gameRank === 1,
6549
5873
  isPatron: gameRank >= 2 && gameRank <= 6,
6550
- piloting: Boolean(record.q107),
5874
+ isLocal: entity === localPlayerEntity,
5875
+ zoomedOut: Boolean(record.q107),
6551
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),
6552
5889
  actionPreview: summarizePlayerPreview(entity, previewRecord, blueprintItems),
6553
5890
  state: cloneRecord(record)
6554
5891
  };
@@ -6731,9 +6068,6 @@
6731
6068
  blobKey: blobKey2,
6732
6069
  cloneRecord: cloneRecord2,
6733
6070
  firstRecord: firstRecord2,
6734
- TABLE_78_FIELD_BITS: TABLE_78_FIELD_BITS2,
6735
- table78DeltaRecord: table78DeltaRecord2,
6736
- isSemanticLoaderDelta: isSemanticLoaderDelta3,
6737
6071
  decodeText: decodeText2,
6738
6072
  entityNameFromType: entityNameFromType2,
6739
6073
  markerTypeIdForTables: markerTypeIdForTables2,
@@ -6809,7 +6143,7 @@
6809
6143
  defineLazyProperty: defineLazyProperty2
6810
6144
  } = decoder_exports;
6811
6145
  var ModelState = class {
6812
- #loaderConfig = new LoaderConfigTracker();
6146
+ #localPlayerEntity = null;
6813
6147
  #blueprintItems = null;
6814
6148
  #helmOccupied = /* @__PURE__ */ new Map();
6815
6149
  #commsStationOccupied = /* @__PURE__ */ new Map();
@@ -6831,6 +6165,16 @@
6831
6165
  this.isOverworld = next;
6832
6166
  this.#invalidateDerived();
6833
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
+ }
6834
6178
  table(id) {
6835
6179
  return this.tables.get(Number(id)) || EMPTY_TABLE2;
6836
6180
  }
@@ -6898,8 +6242,6 @@
6898
6242
  this.totalErrorCount += 1;
6899
6243
  pushCapped2(this.errors, { message: error.message, generation: update.generation }, MAX_RETAINED_ERRORS2);
6900
6244
  }
6901
- this.#remapIndexedLoaderConfig(update);
6902
- this.#updateLoaderConfig(update);
6903
6245
  this.#updateNavigationUnitAutoWarp(update);
6904
6246
  this.#updateHelmOccupancy(update);
6905
6247
  this.#updateCommsStationOccupancy(update);
@@ -7001,53 +6343,6 @@
7001
6343
  this._derived.summariesDirty = true;
7002
6344
  this._derived.tableSummariesDirty = true;
7003
6345
  }
7004
- #updateLoaderConfig(update) {
7005
- const seenEntities = /* @__PURE__ */ new Set();
7006
- for (const section of update.sections || []) {
7007
- if (section.table !== 78) continue;
7008
- for (const changed of section.records || []) {
7009
- if (changed.indexedLoaderConfig) {
7010
- this.#loaderConfig.updateIndexedSnapshotRecord(null, changed.entity, changed.record, changed.mask, changed.cumulativeRecord, changed.configEntity);
7011
- continue;
7012
- }
7013
- const typeId = entityTypeIdFromRecord2(this.record(7, changed.entity));
7014
- this.#loaderConfig.updateRecord(null, changed.entity, changed.record ?? this.record(78, changed.entity), changed.mask, changed.previous, {
7015
- repeatedInUpdate: seenEntities.has(changed.entity),
7016
- allowSparseBaseline: typeId === LOADER_TYPE_ID2,
7017
- repeatedInSection: changed.repeatedInSection,
7018
- semanticSnapshot: Boolean(update.full),
7019
- semanticDelta: isSemanticLoaderDelta3(changed, update.full),
7020
- deltaRecord: table78DeltaRecord2(changed)
7021
- });
7022
- seenEntities.add(changed.entity);
7023
- }
7024
- }
7025
- }
7026
- #remapIndexedLoaderConfig(update) {
7027
- if (!update.full || update.error) return;
7028
- const rows = [];
7029
- for (const section of update.sections || []) {
7030
- if (section.table !== 78) continue;
7031
- for (const changed of section.records || []) rows.push(changed);
7032
- }
7033
- if (!rows.length) return;
7034
- const loaderIds = [...this.table(7).entries()].filter(([, record]) => entityTypeIdFromRecord2(record) === LOADER_TYPE_ID2).map(([entity]) => entity).sort((a, b) => a - b);
7035
- if (loaderIds.length !== rows.length) return;
7036
- if (rows.every((row, index) => row.entity === loaderIds[index])) return;
7037
- const remappedTable = /* @__PURE__ */ new Map();
7038
- for (const [index, changed] of rows.entries()) {
7039
- const rawRecord = table78DeltaRecord2(changed);
7040
- changed.configEntity = changed.entity;
7041
- changed.entity = loaderIds[index];
7042
- changed.cumulativeRecord = cloneRecord2(changed.record || {});
7043
- changed.record = cloneRecord2(rawRecord);
7044
- delete changed.previous;
7045
- changed.indexedLoaderConfig = true;
7046
- remappedTable.set(changed.entity, cloneRecord2(rawRecord));
7047
- update.changedEntities.add(changed.entity);
7048
- }
7049
- this.tables.set(78, remappedTable);
7050
- }
7051
6346
  #updateNavigationUnitAutoWarp(update) {
7052
6347
  const trackedBeforeUpdate = new Set(this.#navigationUnitAutoWarp.keys());
7053
6348
  const initializedThisUpdate = /* @__PURE__ */ new Set();
@@ -7118,25 +6413,25 @@
7118
6413
  #updateHelmOccupancy(update) {
7119
6414
  const table7 = update.sections.find((section) => section.table === 7);
7120
6415
  if (!table7) return;
7121
- const changedPilotState = /* @__PURE__ */ new Map();
6416
+ const changedZoomState = /* @__PURE__ */ new Map();
7122
6417
  for (const section of update.sections) {
7123
6418
  if (section.table !== 55) continue;
7124
6419
  for (const changed of section.records) {
7125
6420
  if (!(changed.mask & 16)) continue;
7126
6421
  const player = this.record(55, changed.entity);
7127
- changedPilotState.set(changed.entity, Boolean(player?.q107));
6422
+ changedZoomState.set(changed.entity, Boolean(player?.q107));
7128
6423
  }
7129
6424
  }
7130
- const activePilotCount = [...this.table(55).values()].filter((record) => record?.q107).length;
6425
+ const zoomedOutPlayerCount = [...this.table(55).values()].filter((record) => record?.q107).length;
7131
6426
  for (const changed of table7.records) {
7132
6427
  const record = this.record(7, changed.entity);
7133
6428
  const typeId = entityTypeIdFromRecord2(record);
7134
6429
  if (!HELM_TYPE_IDS2.has(typeId)) continue;
7135
- 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))) {
7136
6431
  this.#helmOccupied.set(changed.entity, true);
7137
6432
  }
7138
- if (!(changed.mask & 8) || changedPilotState.size === 0) continue;
7139
- 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));
7140
6435
  }
7141
6436
  }
7142
6437
  #updateCommsStationOccupancy(update) {
@@ -7414,7 +6709,8 @@
7414
6709
  const pusherRecord = rec(72);
7415
6710
  const pusherBeamRecord = rec(42);
7416
6711
  const launcherRecord = rec(44);
7417
- const loaderRecord = rec(78);
6712
+ const loaderRecord = rec(69);
6713
+ const navigationRecord = rec(78);
7418
6714
  const loaderFilterRecord = rec(76);
7419
6715
  const loaderFilterSlotsRecord = rec(77);
7420
6716
  const commsStationRecord = rec(39);
@@ -7449,14 +6745,14 @@
7449
6745
  const health = summarizeHealth2(entityId, healthRecord);
7450
6746
  const fabricator = summarizeFabricator2(entityId, fabricatorRecord, itemHolderRecord, typeRecord);
7451
6747
  const cargoEjector = summarizeCargoEjector2(entityId, typeId, cargoEjectorRecord);
7452
- const cannon = summarizeCannon2(entityId, cannonRecord, typeId);
6748
+ const cannon = summarizeCannon2(entityId, cannonRecord, typeId, transformRecord);
7453
6749
  const thruster = summarizeThruster2(entityId, typeId, thrusterRecord);
7454
6750
  const pusher = summarizePusher2(entityId, pusherRecord, loaderFilterSlotsRecord);
7455
6751
  const pusherBeam = summarizePusherBeam2(entityId, pusherBeamRecord);
7456
6752
  const launcher = summarizeItemLauncher2(entityId, typeId, launcherRecord);
7457
- const loader = typeId === LOADER_TYPE_ID2 || typeId == null && (loaderRecord || loaderFilterRecord || loaderFilterSlotsRecord) ? summarizeLoader2(entityId, loaderRecord, loaderFilterRecord, loaderFilterSlotsRecord, this.#loaderConfig, typeId === LOADER_TYPE_ID2, itemHolderRecord) : null;
6753
+ const loader = typeId === LOADER_TYPE_ID2 || typeId == null && (loaderRecord || loaderFilterRecord || loaderFilterSlotsRecord) ? summarizeLoader2(entityId, loaderRecord, loaderFilterRecord, loaderFilterSlotsRecord, typeId === LOADER_TYPE_ID2, itemHolderRecord) : null;
7458
6754
  const cargoHatch = summarizeCargoHatch2(entityId, typeId, loaderFilterRecord, loaderFilterSlotsRecord, rec(45));
7459
- const navigationUnit = summarizeNavigationUnit2(entityId, typeId, loaderRecord, this.#navigationUnitAutoWarp.get(entityId));
6755
+ const navigationUnit = summarizeNavigationUnit2(entityId, typeId, navigationRecord, this.#navigationUnitAutoWarp.get(entityId));
7460
6756
  const fluidTank = fluidTankRecord ? {
7461
6757
  entity: entityId,
7462
6758
  amount: fluidTankRecord.q24 ?? null,
@@ -7465,7 +6761,7 @@
7465
6761
  const shieldGenerator = typeId === 256 ? summarizeShieldGenerator2(entityId, shieldRecord, itemHolderRecord, shieldGeneratorBoostRecord) : null;
7466
6762
  const shieldProjector = typeId === 257 ? summarizeShieldProjector2(entityId, shieldProjectorRecord) : null;
7467
6763
  const blueprintPreview = summarizeBlueprintPreview2(entityId, blueprintPreviewRecord, transformRecord);
7468
- const player = summarizePlayer2(entityId, playerRecord, rec(14), this.#blueprintPreviewItems());
6764
+ const player = summarizePlayer2(entityId, playerRecord, rec(14), this.#blueprintPreviewItems(), rec(56), this.#localPlayerEntity);
7469
6765
  const shipControl = summarizeShipControl2(entityId, shipControlRecord);
7470
6766
  const helm = summarizeHelm2(entityId, typeId, this.#helmOccupied.get(entityId));
7471
6767
  const commsStation = summarizeCommsStation2(entityId, typeId, commsStationRecord, this.#commsStationOccupied.get(entityId));
@@ -7703,7 +6999,6 @@
7703
6999
  } else {
7704
7000
  for (const records of this.tables.values()) records.delete(entity);
7705
7001
  }
7706
- this.#loaderConfig.delete(null, entity);
7707
7002
  this.#helmOccupied.delete(entity);
7708
7003
  this.#commsStationOccupied.delete(entity);
7709
7004
  this.#navigationUnitAutoWarp.delete(entity);
@@ -7719,7 +7014,6 @@
7719
7014
  #readSectionRecords(reader, tag, tableId, spec) {
7720
7015
  const section = { tag, table: tableId, name: spec?.name || null, records: [] };
7721
7016
  let entity = 0;
7722
- const seenEntities = tableId === 78 ? /* @__PURE__ */ new Set() : null;
7723
7017
  while (reader.remaining > 0) {
7724
7018
  const recordOffset = reader.offset;
7725
7019
  let delta;
@@ -7737,7 +7031,6 @@
7737
7031
  throw new Error(`model table ${tableId} tag ${tag} entity ${entity} mask offset ${reader.offset}: ${error.message}`);
7738
7032
  }
7739
7033
  const record = this.#getRecord(tableId, entity);
7740
- const previous = tableId === 78 ? cloneRecord2(record) : null;
7741
7034
  record.lastMask = mask;
7742
7035
  try {
7743
7036
  if (spec) this.#applyRecordSpec(reader, record, mask, spec);
@@ -7746,10 +7039,6 @@
7746
7039
  throw new Error(`model table ${tableId} tag ${tag} entity ${entity} mask ${mask} offset ${reader.offset}: ${error.message}`);
7747
7040
  }
7748
7041
  const changed = { entity, mask };
7749
- if (previous) changed.previous = previous;
7750
- if (seenEntities?.has(entity)) changed.repeatedInSection = true;
7751
- seenEntities?.add(entity);
7752
- if (tableId === 78) changed.record = cloneRecord2(record);
7753
7042
  section.records.push(changed);
7754
7043
  }
7755
7044
  return section;
@@ -7757,7 +7046,7 @@
7757
7046
  #getRecord(tableId, entity) {
7758
7047
  if (!this.tables.has(tableId)) this.tables.set(tableId, /* @__PURE__ */ new Map());
7759
7048
  const records = this.tables.get(tableId);
7760
- if (!records.has(entity)) records.set(entity, {});
7049
+ if (!records.has(entity)) records.set(entity, { ...MODEL_TABLE_SPECS2.get(tableId)?.defaults });
7761
7050
  return records.get(entity);
7762
7051
  }
7763
7052
  #applyRecordSpec(reader, record, mask, spec) {
@@ -8289,6 +7578,7 @@
8289
7578
  var WorldStore = class {
8290
7579
  #overworldId = null;
8291
7580
  #shipWorldId = null;
7581
+ #cameraFocus = null;
8292
7582
  constructor(limits = {}) {
8293
7583
  this.currentWorldId = null;
8294
7584
  this.worlds = /* @__PURE__ */ new Map();
@@ -8305,6 +7595,17 @@
8305
7595
  }
8306
7596
  apply(packet) {
8307
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
+ }
8308
7609
  if (packet.type === 22) return this.#applyMeta(packet);
8309
7610
  if (packet.type === 23) return this.#applyTiles(packet);
8310
7611
  if (packet.type === 20) return this.#applyModel(packet);
@@ -8350,16 +7651,25 @@
8350
7651
  if (!shipWorld || shipWorld.parentWorld == null || shipWorld.parentEntity == null) return null;
8351
7652
  return this.worlds.get(Number(shipWorld.parentWorld))?.entity(shipWorld.parentEntity) || null;
8352
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
+ }
8353
7661
  #applyMeta(packet) {
8354
7662
  if (packet.removed) {
8355
7663
  const world2 = this.worlds.get(Number(packet.world));
8356
7664
  if (world2) world2.readMeta(packet);
8357
7665
  this.worlds.delete(Number(packet.world));
7666
+ if (Number(this.#cameraFocus?.world) === Number(packet.world)) this.#cameraFocus = null;
8358
7667
  if (this.currentWorldId === Number(packet.world)) this.currentWorldId = null;
8359
7668
  return { type: "world-removed", world: world2 || null, packet };
8360
7669
  }
8361
7670
  const world = this.get(packet.world);
8362
7671
  world.readMeta(packet);
7672
+ this.#resolveLocalPlayer(world);
8363
7673
  if (this.currentWorldId == null && !world.isOverworld) this.currentWorldId = world.id;
8364
7674
  return { type: "world", world };
8365
7675
  }
@@ -8407,7 +7717,10 @@
8407
7717
  if (packet.model_data && world.seed != null) {
8408
7718
  const previousModel = world.model;
8409
7719
  try {
8410
- 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
+ }
8411
7724
  result.decoded = decodeModelPayload(packet.model_data, packet.world, world.seed);
8412
7725
  result.model = world.model.apply(result.decoded, { full: Boolean(packet.full) });
8413
7726
  if (result.model?.error && packet.full && previousModel) world.model = previousModel;
@@ -8466,38 +7779,6 @@
8466
7779
  };
8467
7780
  }
8468
7781
 
8469
- // src/protocol/outbound/comms.js
8470
- var COMMS_MESSAGE_TYPE = 3;
8471
- function buildCommsMessage(message = "") {
8472
- return {
8473
- type: COMMS_MESSAGE_TYPE,
8474
- msg: String(message)
8475
- };
8476
- }
8477
- function normalizeCommsEvent(event) {
8478
- const rawMessages = Array.isArray(event?.msgs_text) ? event.msgs_text : [];
8479
- return {
8480
- ...event,
8481
- entity: event?.ent_id != null && Number.isFinite(Number(event.ent_id)) ? Number(event.ent_id) : null,
8482
- rawMessages,
8483
- messages: rawMessages.map((message) => ({
8484
- raw: message,
8485
- text: flattenRichText(message)
8486
- }))
8487
- };
8488
- }
8489
- function flattenRichText(value) {
8490
- if (value == null) return "";
8491
- if (typeof value === "string") return value;
8492
- if (typeof value === "number" || typeof value === "boolean") return String(value);
8493
- if (Array.isArray(value)) return value.map(flattenRichText).join("");
8494
- if (typeof value === "object") {
8495
- if (Object.hasOwn(value, "c")) return flattenRichText(value.c);
8496
- if (Object.hasOwn(value, "text")) return flattenRichText(value.text);
8497
- }
8498
- return "";
8499
- }
8500
-
8501
7782
  // src/protocol/outbound/commands.js
8502
7783
  init_constants();
8503
7784
  var SHA256_K = new Uint32Array([
@@ -8818,11 +8099,11 @@
8818
8099
  }
8819
8100
  return normalized;
8820
8101
  }
8821
- function buildSignTextMessage(text = "", mode = SignDisplayMode.Always) {
8102
+ function buildSignTextMessage(text2 = "", mode = SignDisplayMode.Always) {
8822
8103
  return {
8823
8104
  type: 5,
8824
8105
  cmd: SIGN_TEXT_COMMAND,
8825
- args: [String(text), normalizeSignDisplayMode(mode)]
8106
+ args: [String(text2), normalizeSignDisplayMode(mode)]
8826
8107
  };
8827
8108
  }
8828
8109
 
@@ -9354,6 +8635,9 @@
9354
8635
  get lastPacket() {
9355
8636
  return this.client.lastPacket;
9356
8637
  }
8638
+ get lastDisconnect() {
8639
+ return this.client.lastDisconnect;
8640
+ }
9357
8641
  send(command = {}) {
9358
8642
  return this.client.send(command);
9359
8643
  }
@@ -9475,17 +8759,46 @@
9475
8759
  constructor(client) {
9476
8760
  this.client = client;
9477
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
+ }
9478
8771
  requestPlayerList() {
9479
8772
  return this.client.requestPlayerList();
9480
8773
  }
9481
- resetInvite() {
9482
- return this.client.resetInvite();
8774
+ resetInvite(options = {}) {
8775
+ return this.client.resetInvite(options);
9483
8776
  }
9484
8777
  setPrivacy(privacy) {
9485
8778
  return this.client.setShipPrivacy(privacy);
9486
8779
  }
9487
- recoverStarterItem(itemId) {
9488
- 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);
9489
8802
  }
9490
8803
  setPlayerRank(refId, rank) {
9491
8804
  return this.client.setPlayerRank(refId, rank);
@@ -9496,8 +8809,11 @@
9496
8809
  banPlayer(refId) {
9497
8810
  return this.client.banPlayer(refId);
9498
8811
  }
9499
- demoteSelf() {
9500
- return this.client.demoteSelf();
8812
+ unbanPlayer(refId) {
8813
+ return this.client.unbanPlayer(refId);
8814
+ }
8815
+ demoteSelf(options = {}) {
8816
+ return this.client.demoteSelf(options);
9501
8817
  }
9502
8818
  config() {
9503
8819
  return this.client.shipConfig;
@@ -9508,6 +8824,9 @@
9508
8824
  playerList() {
9509
8825
  return this.client.playerList;
9510
8826
  }
8827
+ crewLog() {
8828
+ return this.client.crewLog;
8829
+ }
9511
8830
  };
9512
8831
  var InventoryDomain = class {
9513
8832
  constructor(client) {
@@ -10228,8 +9547,8 @@
10228
9547
  get state() {
10229
9548
  return summaryForEntity(this.client, this.scope, this.id)?.contents?.sign || null;
10230
9549
  }
10231
- setText(text = "", mode = 0) {
10232
- return this.client.setSignText(text, mode);
9550
+ setText(text2 = "", mode = 0) {
9551
+ return this.client.setSignText(text2, mode);
10233
9552
  }
10234
9553
  get text() {
10235
9554
  return this.state?.text;
@@ -10607,7 +9926,7 @@
10607
9926
  const sid = Number(client?.sid);
10608
9927
  if (!Number.isFinite(sid)) return null;
10609
9928
  const players = worldStateFor(client, scope)?.model.players() || [];
10610
- return players.find((player) => player.state?.q109) || null;
9929
+ return players.find((player) => player.isLocal) || null;
10611
9930
  }
10612
9931
  function currentPlayerRank(client) {
10613
9932
  const player = currentPlayerSummary(client);
@@ -10842,6 +10161,7 @@
10842
10161
  this.ready = false;
10843
10162
  this.packetCount = 0;
10844
10163
  this.lastPacket = null;
10164
+ this.lastDisconnect = null;
10845
10165
  this.packets = [];
10846
10166
  this.packetHistory = normalizeHistoryLimit(packetHistory, DEFAULT_PACKET_HISTORY);
10847
10167
  this.worlds = new WorldStore({ eventHistory, modelPacketHistory, chunkHistory });
@@ -10866,6 +10186,7 @@
10866
10186
  this.shipConfig = null;
10867
10187
  this.captainSubrank = null;
10868
10188
  this.playerList = null;
10189
+ this.crewLog = null;
10869
10190
  this.outfits = /* @__PURE__ */ new Map();
10870
10191
  this.commandAcks = /* @__PURE__ */ new Map();
10871
10192
  this.lastCommandAck = null;
@@ -10874,6 +10195,8 @@
10874
10195
  this.#resolveReady = resolve;
10875
10196
  this.#rejectReady = reject;
10876
10197
  });
10198
+ this.readyPromise.catch(() => {
10199
+ });
10877
10200
  this.attachMode = attachMode;
10878
10201
  this.attached = Boolean(attachMode);
10879
10202
  if (connect) this.#connect().catch((error) => this.#fail(error));
@@ -10895,6 +10218,10 @@
10895
10218
  #attachedOpened = false;
10896
10219
  #resolveReady = null;
10897
10220
  #rejectReady = null;
10221
+ #savePending = false;
10222
+ #renamePending = false;
10223
+ #starterRecoveryPending = false;
10224
+ #managementPending = /* @__PURE__ */ new Set();
10898
10225
  #inputSettings = {
10899
10226
  wrench_mode: 1,
10900
10227
  turret_mode: 0
@@ -10936,17 +10263,170 @@
10936
10263
  sendShipManagement(act, arg = null) {
10937
10264
  return this.sendMessage(buildShipManagementMessage(act, arg));
10938
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
+ }
10939
10365
  requestPlayerList() {
10940
10366
  return this.sendMessage(buildPlayerListMessage());
10941
10367
  }
10942
- resetInvite() {
10943
- 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);
10944
10371
  }
10945
10372
  setShipPrivacy(privacy) {
10946
10373
  return this.sendMessage(buildShipPrivacyMessage(privacy));
10947
10374
  }
10948
- recoverStarterItem(itemId) {
10949
- 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
+ }
10950
10430
  }
10951
10431
  setPlayerRank(refId, rank) {
10952
10432
  return this.sendMessage(buildSetPlayerRankMessage(refId, rank));
@@ -10966,8 +10446,56 @@
10966
10446
  banPlayer(refId) {
10967
10447
  return this.sendMessage(buildBanPlayerMessage(refId));
10968
10448
  }
10969
- demoteSelf() {
10970
- 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
+ }
10971
10499
  }
10972
10500
  sendEntityCommand(cmd, args = [-1, -1, -1]) {
10973
10501
  return this.sendMessage({ type: 5, cmd, args });
@@ -11011,8 +10539,8 @@
11011
10539
  setLauncherPower(power) {
11012
10540
  return this.sendEntityCommand("power", [normalizeLauncherPower(power)]);
11013
10541
  }
11014
- setSignText(text = "", mode = 0) {
11015
- return this.sendMessage(buildSignTextMessage(text, mode));
10542
+ setSignText(text2 = "", mode = 0) {
10543
+ return this.sendMessage(buildSignTextMessage(text2, mode));
11016
10544
  }
11017
10545
  sendUiConfig(data) {
11018
10546
  return this.sendMessage({ type: 8, data });
@@ -11271,12 +10799,14 @@
11271
10799
  shipConfig: this.shipConfig,
11272
10800
  captainSubrank: this.captainSubrank,
11273
10801
  playerList: this.playerList,
10802
+ crewLog: this.crewLog,
11274
10803
  outfits: [...this.outfits.entries()].map(([sid, outfit]) => ({ sid, outfit })),
11275
10804
  commandAcks: [...this.commandAcks.entries()].map(([world, commandNumber]) => ({ world, commandNumber })),
11276
10805
  lastCommandAck: this.lastCommandAck,
11277
10806
  decodeErrors: this.decodeErrors.slice(-50),
11278
10807
  packetCount: this.packetCount,
11279
- lastPacket: this.lastPacket
10808
+ lastPacket: this.lastPacket,
10809
+ lastDisconnect: this.lastDisconnect
11280
10810
  };
11281
10811
  }
11282
10812
  state(options = {}) {
@@ -11401,6 +10931,7 @@
11401
10931
  }
11402
10932
  this.#resolveReady?.(this);
11403
10933
  this.#resolveReady = null;
10934
+ this.#rejectReady = null;
11404
10935
  }
11405
10936
  this.#bindAttachedSocket();
11406
10937
  if (this.connected && this.#ownsSocket) this.#attachedOpen();
@@ -11408,6 +10939,7 @@
11408
10939
  }
11409
10940
  #bindSocket() {
11410
10941
  this.ws.onopen = () => {
10942
+ this.lastDisconnect = null;
11411
10943
  this.connected = true;
11412
10944
  this.ws.send(encodeMsgpack({ type: 1 }));
11413
10945
  this.emit("open", this);
@@ -11421,7 +10953,7 @@
11421
10953
  const open = () => this.#attachedOpen();
11422
10954
  const message = (event) => this.#handleSocketMessage(event?.data ?? event);
11423
10955
  const error = (event) => this.#fail(event?.error || new Error("WebSocket error"));
11424
- const close = (event) => this.#handleSocketClose(event);
10956
+ const close = (event, reason) => this.#handleSocketClose(event, reason);
11425
10957
  if (typeof this.ws.addEventListener === "function") {
11426
10958
  this.ws.addEventListener("open", open);
11427
10959
  this.ws.addEventListener("message", message);
@@ -11436,13 +10968,16 @@
11436
10968
  this.ws.on("close", close);
11437
10969
  return;
11438
10970
  }
11439
- const previousMessage = this.ws.onmessage;
11440
- this.ws.onmessage = (event) => {
11441
- if (typeof previousMessage === "function") previousMessage.call(this.ws, event);
11442
- message(event);
11443
- };
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
+ }
11444
10978
  }
11445
10979
  #attachedOpen() {
10980
+ this.lastDisconnect = null;
11446
10981
  this.connected = true;
11447
10982
  if (this.#attachedOpened) return;
11448
10983
  this.#attachedOpened = true;
@@ -11450,17 +10985,39 @@
11450
10985
  this.emit("open", this);
11451
10986
  if (this.#allowWrites) this.#flushMessages();
11452
10987
  }
11453
- #handleSocketClose(event) {
10988
+ #handleSocketClose(event, reason) {
10989
+ const info = normalizeDisconnect(event, reason);
11454
10990
  this.connected = false;
10991
+ this.ready = false;
10992
+ this.sid = null;
10993
+ this.lastDisconnect = info;
11455
10994
  clearInterval(this.#keepalive);
11456
10995
  this.#keepalive = null;
11457
- 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);
11458
11014
  }
11459
11015
  #socketLooksOpen(websocket) {
11460
11016
  return websocket?.readyState == null || websocket.readyState === 1 || websocket.readyState === websocket.OPEN;
11461
11017
  }
11462
11018
  #assertWritable() {
11463
11019
  if (!this.#allowWrites) throw new Error("DredlessClient is attached in readonly mode");
11020
+ if (this.lastDisconnect) throw new DisconnectError(this.lastDisconnect);
11464
11021
  }
11465
11022
  #handleSocketMessage(data) {
11466
11023
  if (data && typeof data.arrayBuffer === "function") {
@@ -11470,6 +11027,7 @@
11470
11027
  this.#handleMessage(data);
11471
11028
  }
11472
11029
  #handleMessage(data) {
11030
+ if (this.lastDisconnect) return;
11473
11031
  let packet;
11474
11032
  try {
11475
11033
  packet = decodeIncomingFrame(data);
@@ -11496,6 +11054,7 @@
11496
11054
  }
11497
11055
  #markReady(packet) {
11498
11056
  this.sid = packet.sid >>> 0;
11057
+ this.worlds.apply(packet);
11499
11058
  this.worlds.currentWorldId = packet.world ?? this.worlds.currentWorldId;
11500
11059
  this.ready = true;
11501
11060
  if (this.#ownsSocket) {
@@ -11508,6 +11067,7 @@
11508
11067
  }
11509
11068
  this.#resolveReady?.(this);
11510
11069
  this.#resolveReady = null;
11070
+ this.#rejectReady = null;
11511
11071
  this.emit("ready", this);
11512
11072
  }
11513
11073
  #sendBootstrap() {
@@ -11585,6 +11145,32 @@
11585
11145
  #handleSessionSubmessage(packet) {
11586
11146
  const submessage = packet?.submessage;
11587
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
+ }
11588
11174
  if (submessage.type === "config") {
11589
11175
  this.shipConfig = normalizeShipConfig(submessage);
11590
11176
  this.emit("ship-config", this.shipConfig, packet);
@@ -11837,6 +11423,174 @@
11837
11423
  init_cookies();
11838
11424
  init_http();
11839
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
11840
11594
  var Session = class {
11841
11595
  constructor(gameSession = null, gameVersion = null, baseUrl = DEFAULT_BASE_URL) {
11842
11596
  this.baseUrl = baseUrl;
@@ -11884,6 +11638,21 @@
11884
11638
  this.#readStatus(raw);
11885
11639
  return raw;
11886
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
+ }
11887
11656
  async fetchShips(server2) {
11888
11657
  return fetchShips(this, server2);
11889
11658
  }