dredless 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,6 +37,20 @@ const ship = client.currentShip();
37
37
  const loader = ship?.machines.loaders()[0];
38
38
  loader?.configure({ cycle: 5, stack: 12 });
39
39
 
40
+ const door = ship?.machines.doors()[0];
41
+ if (door) await door.setRank("crew");
42
+ const spawn = ship?.machines.spawnPoints()[0];
43
+ if (spawn) await spawn.setRank("captain");
44
+
45
+ // Other context-menu actions can be selected by their displayed text.
46
+ const cannon = ship?.machines.cannons()[0];
47
+ if (cannon) await client.tips.choose(cannon, "Unload Cannon");
48
+
49
+ if (door) {
50
+ console.log((await door.openTips()).options);
51
+ await client.tips.close();
52
+ }
53
+
40
54
  const sign = ship?.machines.signs()[0];
41
55
  sign?.open();
42
56
  sign?.setText("Dock here", SignDisplayMode.WhenNear);
@@ -588,6 +588,367 @@
588
588
  }
589
589
  };
590
590
 
591
+ // src/protocol/outbound/ship-management.js
592
+ var SHIP_MANAGEMENT_TYPE = 4;
593
+ var SET_PRIVACY_ACTION = "set_privacy";
594
+ var STARTER_RECOVERY_ACTION = "starter_recovery";
595
+ var PLAYER_LIST_ACTION = "player_list";
596
+ var INVITE_RESET_ACTION = "invite_reset";
597
+ var SET_RANK_ACTION = "set_rank";
598
+ var KICK_ACTION = "kick";
599
+ var BAN_ACTION = "ban";
600
+ var DEMOTE_SELF_ACTION = "demote_self";
601
+ function requireNonNegativeInteger(value, name) {
602
+ const number = Number(value);
603
+ if (!Number.isInteger(number) || number < 0) throw new RangeError(`${name} must be a non-negative integer`);
604
+ return number;
605
+ }
606
+ function normalizePrivacy(value) {
607
+ if (value === true || value === 1 || value === "private") return 1;
608
+ if (value === false || value === 0 || value === "public") return 0;
609
+ throw new RangeError(`privacy must be 0, 1, true, false, "public", or "private"`);
610
+ }
611
+ function normalizePlayerRef(value) {
612
+ return requireNonNegativeInteger(value, "refId");
613
+ }
614
+ function normalizePlayerRank(value) {
615
+ if (value === 0 || value === "guest") return 0;
616
+ if (value === 1 || value === "crew") return 1;
617
+ if (value === 3 || value === "captain") return 3;
618
+ throw new RangeError(`rank must be 0, 1, 3, "guest", "crew", or "captain"`);
619
+ }
620
+ function buildShipManagementMessage(act, arg = null, extra = null) {
621
+ if (typeof act !== "string" || !act) throw new TypeError(`act must be a non-empty string`);
622
+ const message = { type: SHIP_MANAGEMENT_TYPE, act, arg };
623
+ if (extra && typeof extra === "object") Object.assign(message, extra);
624
+ return message;
625
+ }
626
+ function buildShipPrivacyMessage(privacy) {
627
+ return buildShipManagementMessage(SET_PRIVACY_ACTION, normalizePrivacy(privacy));
628
+ }
629
+ function buildStarterRecoveryMessage(itemId) {
630
+ return buildShipManagementMessage(STARTER_RECOVERY_ACTION, requireNonNegativeInteger(itemId, "itemId"));
631
+ }
632
+ function buildPlayerListMessage() {
633
+ return buildShipManagementMessage(PLAYER_LIST_ACTION);
634
+ }
635
+ function buildInviteResetMessage() {
636
+ return buildShipManagementMessage(INVITE_RESET_ACTION);
637
+ }
638
+ function buildSetPlayerRankMessage(refId, rank) {
639
+ return buildShipManagementMessage(SET_RANK_ACTION, normalizePlayerRef(refId), { rank: normalizePlayerRank(rank) });
640
+ }
641
+ function buildKickPlayerMessage(refId) {
642
+ return buildShipManagementMessage(KICK_ACTION, normalizePlayerRef(refId));
643
+ }
644
+ function buildBanPlayerMessage(refId) {
645
+ return buildShipManagementMessage(BAN_ACTION, normalizePlayerRef(refId));
646
+ }
647
+ function buildDemoteSelfMessage() {
648
+ return buildShipManagementMessage(DEMOTE_SELF_ACTION);
649
+ }
650
+ function normalizeShipConfig(event = {}) {
651
+ const source = event && typeof event === "object" ? event : {};
652
+ const config = source.config && typeof source.config === "object" ? source.config : {};
653
+ return {
654
+ privacy: config.privacy ?? null,
655
+ privacyName: config.privacy === 0 ? "public" : config.privacy === 1 ? "private" : null,
656
+ inviteKey: config.invite_key ?? null,
657
+ teamId: source.team_id ?? null,
658
+ patronPerks: Array.isArray(source.patron_perks) ? source.patron_perks.slice() : []
659
+ };
660
+ }
661
+ function normalizeCaptainSubrank(event = {}) {
662
+ const source = event && typeof event === "object" ? event : {};
663
+ return {
664
+ subrank: source.subrank ?? null,
665
+ enableCheats: Boolean(source.enable_cheats)
666
+ };
667
+ }
668
+ function normalizeShipPlayerList(event = {}, previous = null, currentCaptainSubrank = null) {
669
+ const source = event && typeof event === "object" ? event : {};
670
+ const changes = Array.isArray(source.player_list) ? source.player_list.map(normalizePlayerListChange) : [];
671
+ const removedPlayers = changes.filter((change) => change.remove && Number.isFinite(Number(change.refId))).map((change) => Number(change.refId));
672
+ const changedPlayers = changes.filter((change) => !change.remove).map(({ remove: _remove, ...change }) => change);
673
+ const players = mergePlayerListChanges(previous?.players, changes);
674
+ const ownerCaptainRank = ownerRankForPlayers(players);
675
+ applyPlayerControlState(players, currentCaptainSubrank);
676
+ applyPlayerControlState(changedPlayers, currentCaptainSubrank);
677
+ return {
678
+ ownerCaptainRank,
679
+ shipOwners: players.filter((player) => player.isShipOwner),
680
+ players,
681
+ changes: changedPlayers,
682
+ removedPlayers
683
+ };
684
+ }
685
+ function mergePlayerListChanges(previousPlayers, changes) {
686
+ if (!Array.isArray(previousPlayers) || !previousPlayers.length) return changes.filter((change) => !change.remove).map(({
687
+ remove: _remove,
688
+ ...player
689
+ }) => ({ ...player }));
690
+ const existing = /* @__PURE__ */ new Map();
691
+ for (const player of previousPlayers) {
692
+ if (player?.refId == null) continue;
693
+ existing.set(player.refId, { ...player, isShipOwner: false });
694
+ }
695
+ const changedRefs = /* @__PURE__ */ new Set();
696
+ const merged = [];
697
+ for (const change of changes) {
698
+ if (change.refId == null) {
699
+ if (!change.remove) {
700
+ const { remove: _remove2, ...player2 } = change;
701
+ merged.push({ ...player2, isShipOwner: false });
702
+ }
703
+ continue;
704
+ }
705
+ changedRefs.add(change.refId);
706
+ if (change.remove) {
707
+ existing.delete(change.refId);
708
+ continue;
709
+ }
710
+ const { remove: _remove, ...changePlayer } = change;
711
+ const player = { ...existing.get(change.refId) || {}, ...changePlayer, isShipOwner: false };
712
+ existing.set(change.refId, player);
713
+ merged.push(player);
714
+ }
715
+ for (const player of previousPlayers) {
716
+ if (player?.refId == null || changedRefs.has(player.refId)) continue;
717
+ const retained = existing.get(player.refId);
718
+ if (retained) merged.push(retained);
719
+ }
720
+ return merged;
721
+ }
722
+ function normalizePlayerListChange(entry = {}) {
723
+ const source = entry && typeof entry === "object" ? entry : {};
724
+ if (source._removed) return { refId: source.ref_id ?? null, remove: true };
725
+ return {
726
+ refId: source.ref_id ?? null,
727
+ remove: false,
728
+ discrim: source.discrim ?? null,
729
+ discrimColor: source.discrim_color ?? null,
730
+ teamRank: source.team_rank ?? null,
731
+ captainRank: source.captain_rank ?? null,
732
+ isCaptain: Number(source.captain_rank) > 0,
733
+ isShipOwner: false,
734
+ canBeManaged: false,
735
+ time: source.time ?? null,
736
+ items: Array.isArray(source.items) ? source.items.slice() : [],
737
+ aliasDiscrims: Array.isArray(source.alias_discrims) ? source.alias_discrims.slice() : [],
738
+ extraAliasCount: normalizeNonNegativeNumber(source.extra_aliases),
739
+ onlineCount: source.online_count ?? null
740
+ };
741
+ }
742
+ function normalizeNonNegativeNumber(value) {
743
+ const number = Number(value);
744
+ return Number.isFinite(number) && number > 0 ? number : 0;
745
+ }
746
+ function ownerRankForPlayers(players) {
747
+ const ranks = players.filter((player) => Number(player.captainRank) > 0).map((player) => Number(player.captainRank)).filter(Number.isFinite);
748
+ if (!ranks.length) return null;
749
+ const ownerRank = Math.min(...ranks);
750
+ for (const player of players) player.isShipOwner = Number(player.captainRank) === ownerRank;
751
+ return ownerRank;
752
+ }
753
+ function applyPlayerControlState(players, currentCaptainSubrank) {
754
+ const currentRank = currentCaptainRank(currentCaptainSubrank);
755
+ for (const player of players) player.canBeManaged = playerCanBeManagedBy(player, currentRank);
756
+ }
757
+ function currentCaptainRank(currentCaptainSubrank) {
758
+ const rank = Number(currentCaptainSubrank?.subrank);
759
+ return Number.isFinite(rank) && rank > 0 ? rank : null;
760
+ }
761
+ function playerCanBeManagedBy(player, currentRank) {
762
+ if (!Number.isFinite(currentRank) || !player) return false;
763
+ const targetCaptainRank = Number(player.captainRank);
764
+ if (Number.isFinite(targetCaptainRank) && targetCaptainRank > 0) return currentRank < targetCaptainRank;
765
+ const targetTeamRank = Number(player.teamRank);
766
+ return targetTeamRank === 0 || targetTeamRank === 1;
767
+ }
768
+
769
+ // src/client/domains/tips.js
770
+ function timeoutValue(options) {
771
+ const timeout = options.timeout ?? 5e3;
772
+ if (!Number.isFinite(timeout) || timeout <= 0) throw new RangeError("timeout must be positive milliseconds");
773
+ return timeout;
774
+ }
775
+ function waitFor(client, eventType, read, timeout, label, send = null) {
776
+ return new Promise((resolve, reject) => {
777
+ const cleanup = () => {
778
+ clearTimeout(timer);
779
+ client.off(eventType, check);
780
+ client.off("close", closed);
781
+ };
782
+ const check = (...args) => {
783
+ const value = read(...args);
784
+ if (value) {
785
+ cleanup();
786
+ resolve(value);
787
+ }
788
+ };
789
+ const closed = () => {
790
+ cleanup();
791
+ reject(new Error(`Disconnected while waiting for ${label}`));
792
+ };
793
+ const timer = setTimeout(() => {
794
+ cleanup();
795
+ reject(new Error(`Timed out waiting for ${label}`));
796
+ }, timeout);
797
+ client.on(eventType, check);
798
+ client.on("close", closed);
799
+ if (send) {
800
+ try {
801
+ send();
802
+ } catch (error) {
803
+ cleanup();
804
+ reject(error);
805
+ return;
806
+ }
807
+ }
808
+ check();
809
+ });
810
+ }
811
+ var TipDomain = class {
812
+ #busy = false;
813
+ #target = null;
814
+ #current = null;
815
+ constructor(client) {
816
+ this.client = client;
817
+ client.on("close", () => {
818
+ this.#current = null;
819
+ this.#target = null;
820
+ });
821
+ }
822
+ get current() {
823
+ return this.#current;
824
+ }
825
+ receive(event, world) {
826
+ const options = Array.isArray(event.options) ? event.options.map(String) : [];
827
+ const entity = this.#target?.world === world?.id ? this.#target.entity : null;
828
+ const menu = Object.freeze({
829
+ entity,
830
+ world: world?.id ?? null,
831
+ filter: event.filter ?? null,
832
+ x: event.x ?? 0,
833
+ y: event.y ?? 0,
834
+ options: Object.freeze(options)
835
+ });
836
+ this.#current = options.length ? menu : null;
837
+ this.client.emit("tip-list", menu, world);
838
+ }
839
+ async #exclusive(action) {
840
+ if (this.#busy) throw new Error("A tip menu operation is already in progress");
841
+ this.#busy = true;
842
+ try {
843
+ return await action();
844
+ } catch (error) {
845
+ this.#current = null;
846
+ this.#target = null;
847
+ throw error;
848
+ } finally {
849
+ this.#busy = false;
850
+ }
851
+ }
852
+ async #open(entity, options) {
853
+ if (!this.client.ready || !this.client.connected) throw new Error("Client must be connected and ready to open tips");
854
+ const timeout = timeoutValue(options);
855
+ const handle = typeof entity === "number" ? this.client.currentShip()?.entities.get(entity) : entity;
856
+ if (!handle || handle.client !== this.client || !handle.exists()) throw new Error("Tip target must be an existing entity on this client");
857
+ const world = this.client.worlds.currentWorldId;
858
+ const scopeWorld = handle.scope === "ship" ? this.client.worlds.shipWorld() : handle.scope === "current" ? this.client.worlds.worlds.get(world) : handle.scope === "overworld" ? this.client.worlds.overworld() : this.client.worlds.worlds.get(Number(handle.scope));
859
+ if (scopeWorld?.id !== world) {
860
+ throw new Error("Tip target must be in the current world");
861
+ }
862
+ const position = handle.position;
863
+ if (!position) throw new Error("Tip target has no position");
864
+ this.#current = null;
865
+ this.#target = { entity: handle.id, world, command: {
866
+ ...options.command,
867
+ focus_ent: handle.id,
868
+ mx: position.x,
869
+ my: position.y
870
+ } };
871
+ return waitFor(this.client, "tip-list", () => {
872
+ return this.#current?.entity === handle.id && this.#current.world === world ? this.#current : null;
873
+ }, timeout, "tip menu", () => {
874
+ this.client.send({ ...this.#target.command, act1: false, act2: true, tip_select: null });
875
+ });
876
+ }
877
+ open(entity, options = {}) {
878
+ return this.#exclusive(() => this.#open(entity, options));
879
+ }
880
+ close(options = {}) {
881
+ return this.#exclusive(async () => {
882
+ const timeout = timeoutValue(options);
883
+ const menu = this.#current;
884
+ if (!menu) return this.client;
885
+ if (!this.client.connected || !this.client.ready) throw new Error("Client must be connected and ready to close tips");
886
+ if (menu.world !== this.client.worlds.currentWorldId) throw new Error("Tip menu belongs to another world");
887
+ await waitFor(this.client, "tip-list", (update) => update?.world === menu.world && update.options.length === 0, timeout, "tip menu dismissal", () => {
888
+ this.client.send({
889
+ ...options.command,
890
+ focus_ent: null,
891
+ config_ent: null,
892
+ act1: true,
893
+ act1_held: true,
894
+ act2: false,
895
+ tip_select: null
896
+ });
897
+ });
898
+ this.#target = null;
899
+ return this.client;
900
+ });
901
+ }
902
+ select(option, command = {}) {
903
+ if (this.#busy) throw new Error("A tip menu operation is already in progress");
904
+ return this.#select(option, command);
905
+ }
906
+ #select(option, command = {}) {
907
+ if (!this.client.connected || !this.client.ready) throw new Error("Client must be connected and ready to select tips");
908
+ const menu = this.#current;
909
+ if (!menu || menu.entity == null || !this.#target) throw new Error("Open an entity's tip menu before selecting an option");
910
+ if (menu.world !== this.client.worlds.currentWorldId) throw new Error("Tip menu belongs to another world");
911
+ const index = typeof option === "string" ? menu.options.indexOf(option) : option;
912
+ if (!Number.isInteger(index) || index < 0 || index >= menu.options.length) throw new RangeError("Tip option is not in the current menu");
913
+ this.client.send({
914
+ ...this.#target.command,
915
+ ...command,
916
+ focus_ent: menu.entity,
917
+ act1: false,
918
+ act2: false,
919
+ tip_select: index
920
+ });
921
+ this.#current = null;
922
+ this.#target = null;
923
+ return this.client;
924
+ }
925
+ choose(entity, option, options = {}) {
926
+ return this.#exclusive(async () => {
927
+ await this.#open(entity, options);
928
+ return this.#select(option);
929
+ });
930
+ }
931
+ setRank(handle, rank, options = {}) {
932
+ return this.#exclusive(async () => {
933
+ const value = normalizePlayerRank(rank);
934
+ const timeout = timeoutValue(options);
935
+ if (!handle.exists()) throw new Error("Rank target no longer exists");
936
+ if (handle.rank === value) return this.client;
937
+ const text = { 0: "Restrict to Guests", 1: "Restrict to Crew", 3: "Restrict to Captains" }[value];
938
+ await this.#open(handle, options);
939
+ await waitFor(
940
+ this.client,
941
+ "model",
942
+ () => handle.rank === value,
943
+ timeout,
944
+ "access rank update",
945
+ () => this.#select(text)
946
+ );
947
+ return this.client;
948
+ });
949
+ }
950
+ };
951
+
591
952
  // src/client/core.js
592
953
  init_cookies();
593
954
  init_servers();
@@ -8433,184 +8794,6 @@
8433
8794
  return number === 0 && count === 0 ? null : number;
8434
8795
  }
8435
8796
 
8436
- // src/protocol/outbound/ship-management.js
8437
- var SHIP_MANAGEMENT_TYPE = 4;
8438
- var SET_PRIVACY_ACTION = "set_privacy";
8439
- var STARTER_RECOVERY_ACTION = "starter_recovery";
8440
- var PLAYER_LIST_ACTION = "player_list";
8441
- var INVITE_RESET_ACTION = "invite_reset";
8442
- var SET_RANK_ACTION = "set_rank";
8443
- var KICK_ACTION = "kick";
8444
- var BAN_ACTION = "ban";
8445
- var DEMOTE_SELF_ACTION = "demote_self";
8446
- function requireNonNegativeInteger(value, name) {
8447
- const number = Number(value);
8448
- if (!Number.isInteger(number) || number < 0) throw new RangeError(`${name} must be a non-negative integer`);
8449
- return number;
8450
- }
8451
- function normalizePrivacy(value) {
8452
- if (value === true || value === 1 || value === "private") return 1;
8453
- if (value === false || value === 0 || value === "public") return 0;
8454
- throw new RangeError(`privacy must be 0, 1, true, false, "public", or "private"`);
8455
- }
8456
- function normalizePlayerRef(value) {
8457
- return requireNonNegativeInteger(value, "refId");
8458
- }
8459
- function normalizePlayerRank(value) {
8460
- if (value === 0 || value === "guest") return 0;
8461
- if (value === 1 || value === "crew") return 1;
8462
- if (value === 3 || value === "captain") return 3;
8463
- throw new RangeError(`rank must be 0, 1, 3, "guest", "crew", or "captain"`);
8464
- }
8465
- function buildShipManagementMessage(act, arg = null, extra = null) {
8466
- if (typeof act !== "string" || !act) throw new TypeError(`act must be a non-empty string`);
8467
- const message = { type: SHIP_MANAGEMENT_TYPE, act, arg };
8468
- if (extra && typeof extra === "object") Object.assign(message, extra);
8469
- return message;
8470
- }
8471
- function buildShipPrivacyMessage(privacy) {
8472
- return buildShipManagementMessage(SET_PRIVACY_ACTION, normalizePrivacy(privacy));
8473
- }
8474
- function buildStarterRecoveryMessage(itemId) {
8475
- return buildShipManagementMessage(STARTER_RECOVERY_ACTION, requireNonNegativeInteger(itemId, "itemId"));
8476
- }
8477
- function buildPlayerListMessage() {
8478
- return buildShipManagementMessage(PLAYER_LIST_ACTION);
8479
- }
8480
- function buildInviteResetMessage() {
8481
- return buildShipManagementMessage(INVITE_RESET_ACTION);
8482
- }
8483
- function buildSetPlayerRankMessage(refId, rank) {
8484
- return buildShipManagementMessage(SET_RANK_ACTION, normalizePlayerRef(refId), { rank: normalizePlayerRank(rank) });
8485
- }
8486
- function buildKickPlayerMessage(refId) {
8487
- return buildShipManagementMessage(KICK_ACTION, normalizePlayerRef(refId));
8488
- }
8489
- function buildBanPlayerMessage(refId) {
8490
- return buildShipManagementMessage(BAN_ACTION, normalizePlayerRef(refId));
8491
- }
8492
- function buildDemoteSelfMessage() {
8493
- return buildShipManagementMessage(DEMOTE_SELF_ACTION);
8494
- }
8495
- function normalizeShipConfig(event = {}) {
8496
- const source = event && typeof event === "object" ? event : {};
8497
- const config = source.config && typeof source.config === "object" ? source.config : {};
8498
- return {
8499
- privacy: config.privacy ?? null,
8500
- privacyName: config.privacy === 0 ? "public" : config.privacy === 1 ? "private" : null,
8501
- inviteKey: config.invite_key ?? null,
8502
- teamId: source.team_id ?? null,
8503
- patronPerks: Array.isArray(source.patron_perks) ? source.patron_perks.slice() : []
8504
- };
8505
- }
8506
- function normalizeCaptainSubrank(event = {}) {
8507
- const source = event && typeof event === "object" ? event : {};
8508
- return {
8509
- subrank: source.subrank ?? null,
8510
- enableCheats: Boolean(source.enable_cheats)
8511
- };
8512
- }
8513
- function normalizeShipPlayerList(event = {}, previous = null, currentCaptainSubrank = null) {
8514
- const source = event && typeof event === "object" ? event : {};
8515
- const changes = Array.isArray(source.player_list) ? source.player_list.map(normalizePlayerListChange) : [];
8516
- const removedPlayers = changes.filter((change) => change.remove && Number.isFinite(Number(change.refId))).map((change) => Number(change.refId));
8517
- const changedPlayers = changes.filter((change) => !change.remove).map(({ remove: _remove, ...change }) => change);
8518
- const players = mergePlayerListChanges(previous?.players, changes);
8519
- const ownerCaptainRank = ownerRankForPlayers(players);
8520
- applyPlayerControlState(players, currentCaptainSubrank);
8521
- applyPlayerControlState(changedPlayers, currentCaptainSubrank);
8522
- return {
8523
- ownerCaptainRank,
8524
- shipOwners: players.filter((player) => player.isShipOwner),
8525
- players,
8526
- changes: changedPlayers,
8527
- removedPlayers
8528
- };
8529
- }
8530
- function mergePlayerListChanges(previousPlayers, changes) {
8531
- if (!Array.isArray(previousPlayers) || !previousPlayers.length) return changes.filter((change) => !change.remove).map(({
8532
- remove: _remove,
8533
- ...player
8534
- }) => ({ ...player }));
8535
- const existing = /* @__PURE__ */ new Map();
8536
- for (const player of previousPlayers) {
8537
- if (player?.refId == null) continue;
8538
- existing.set(player.refId, { ...player, isShipOwner: false });
8539
- }
8540
- const changedRefs = /* @__PURE__ */ new Set();
8541
- const merged = [];
8542
- for (const change of changes) {
8543
- if (change.refId == null) {
8544
- if (!change.remove) {
8545
- const { remove: _remove2, ...player2 } = change;
8546
- merged.push({ ...player2, isShipOwner: false });
8547
- }
8548
- continue;
8549
- }
8550
- changedRefs.add(change.refId);
8551
- if (change.remove) {
8552
- existing.delete(change.refId);
8553
- continue;
8554
- }
8555
- const { remove: _remove, ...changePlayer } = change;
8556
- const player = { ...existing.get(change.refId) || {}, ...changePlayer, isShipOwner: false };
8557
- existing.set(change.refId, player);
8558
- merged.push(player);
8559
- }
8560
- for (const player of previousPlayers) {
8561
- if (player?.refId == null || changedRefs.has(player.refId)) continue;
8562
- const retained = existing.get(player.refId);
8563
- if (retained) merged.push(retained);
8564
- }
8565
- return merged;
8566
- }
8567
- function normalizePlayerListChange(entry = {}) {
8568
- const source = entry && typeof entry === "object" ? entry : {};
8569
- if (source._removed) return { refId: source.ref_id ?? null, remove: true };
8570
- return {
8571
- refId: source.ref_id ?? null,
8572
- remove: false,
8573
- discrim: source.discrim ?? null,
8574
- discrimColor: source.discrim_color ?? null,
8575
- teamRank: source.team_rank ?? null,
8576
- captainRank: source.captain_rank ?? null,
8577
- isCaptain: Number(source.captain_rank) > 0,
8578
- isShipOwner: false,
8579
- canBeManaged: false,
8580
- time: source.time ?? null,
8581
- items: Array.isArray(source.items) ? source.items.slice() : [],
8582
- aliasDiscrims: Array.isArray(source.alias_discrims) ? source.alias_discrims.slice() : [],
8583
- extraAliasCount: normalizeNonNegativeNumber(source.extra_aliases),
8584
- onlineCount: source.online_count ?? null
8585
- };
8586
- }
8587
- function normalizeNonNegativeNumber(value) {
8588
- const number = Number(value);
8589
- return Number.isFinite(number) && number > 0 ? number : 0;
8590
- }
8591
- function ownerRankForPlayers(players) {
8592
- const ranks = players.filter((player) => Number(player.captainRank) > 0).map((player) => Number(player.captainRank)).filter(Number.isFinite);
8593
- if (!ranks.length) return null;
8594
- const ownerRank = Math.min(...ranks);
8595
- for (const player of players) player.isShipOwner = Number(player.captainRank) === ownerRank;
8596
- return ownerRank;
8597
- }
8598
- function applyPlayerControlState(players, currentCaptainSubrank) {
8599
- const currentRank = currentCaptainRank(currentCaptainSubrank);
8600
- for (const player of players) player.canBeManaged = playerCanBeManagedBy(player, currentRank);
8601
- }
8602
- function currentCaptainRank(currentCaptainSubrank) {
8603
- const rank = Number(currentCaptainSubrank?.subrank);
8604
- return Number.isFinite(rank) && rank > 0 ? rank : null;
8605
- }
8606
- function playerCanBeManagedBy(player, currentRank) {
8607
- if (!Number.isFinite(currentRank) || !player) return false;
8608
- const targetCaptainRank = Number(player.captainRank);
8609
- if (Number.isFinite(targetCaptainRank) && targetCaptainRank > 0) return currentRank < targetCaptainRank;
8610
- const targetTeamRank = Number(player.teamRank);
8611
- return targetTeamRank === 0 || targetTeamRank === 1;
8612
- }
8613
-
8614
8797
  // src/protocol/outbound/sign.js
8615
8798
  var SIGN_TEXT_COMMAND = "sign_text";
8616
8799
  var SignDisplayMode = Object.freeze({
@@ -9800,6 +9983,9 @@
9800
9983
  use(options = {}, command = {}) {
9801
9984
  return this.client.player.useEntity(this.id, options, command);
9802
9985
  }
9986
+ openTips(options = {}) {
9987
+ return this.client.tips.open(this, options);
9988
+ }
9803
9989
  };
9804
9990
  var MachineHandle = class extends EntityHandle {
9805
9991
  open(options = {}, command = {}) {
@@ -10102,6 +10288,9 @@
10102
10288
  }
10103
10289
  };
10104
10290
  var DoorHandle = class extends MachineHandle {
10291
+ setRank(rank, options = {}) {
10292
+ return this.client.tips.setRank(this, rank, options);
10293
+ }
10105
10294
  get state() {
10106
10295
  return summaryForEntity(this.client, this.scope, this.id)?.contents?.door || null;
10107
10296
  }
@@ -10113,6 +10302,9 @@
10113
10302
  }
10114
10303
  };
10115
10304
  var SpawnPointHandle = class extends MachineHandle {
10305
+ setRank(rank, options = {}) {
10306
+ return this.client.tips.setRank(this, rank, options);
10307
+ }
10116
10308
  get state() {
10117
10309
  return summaryForEntity(this.client, this.scope, this.id)?.contents?.spawnPoint || null;
10118
10310
  }
@@ -10657,6 +10849,7 @@
10657
10849
  this.debug = new ClientDebugDomain(this);
10658
10850
  this.player = new PlayerDomain(this);
10659
10851
  this.inventory = new InventoryDomain(this);
10852
+ this.tips = new TipDomain(this);
10660
10853
  this.management = new ShipManagementDomain(this);
10661
10854
  this.cpuLoad = null;
10662
10855
  this.inventoryState = normalizeInventoryEvent({ type: "inventory", general_slots: 5 });
@@ -11410,6 +11603,7 @@
11410
11603
  }
11411
11604
  #handleSideEvent(event, world) {
11412
11605
  if (!event || typeof event !== "object") return;
11606
+ if (event.type === "tip_list") this.tips.receive(event, world);
11413
11607
  if (event.type === "inventory") {
11414
11608
  this.inventoryState = normalizeInventoryEvent(event);
11415
11609
  this.emit("inventory", this.inventoryState, world);