onelobby 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/blueprint.schema.json +244 -0
- package/dist/blueprint.d.ts +170 -0
- package/dist/blueprint.js +286 -0
- package/dist/blueprint.js.map +1 -0
- package/dist/codec.d.ts +23 -0
- package/dist/codec.js +28 -0
- package/dist/codec.js.map +1 -1
- package/dist/direct/browser-host.d.ts +46 -0
- package/dist/direct/browser-host.js +93 -0
- package/dist/direct/browser-host.js.map +1 -0
- package/dist/direct/hub-host.d.ts +34 -23
- package/dist/direct/hub-host.js +75 -59
- package/dist/direct/hub-host.js.map +1 -1
- package/dist/direct/hub-local.d.ts +22 -0
- package/dist/direct/hub-local.js +46 -0
- package/dist/direct/hub-local.js.map +1 -0
- package/dist/direct/hub.d.ts +37 -10
- package/dist/direct/hub.js +114 -23
- package/dist/direct/hub.js.map +1 -1
- package/dist/direct/index.d.ts +37 -4
- package/dist/direct/index.js +162 -21
- package/dist/direct/index.js.map +1 -1
- package/dist/direct/metrics.d.ts +12 -0
- package/dist/direct/metrics.js +45 -0
- package/dist/direct/metrics.js.map +1 -0
- package/dist/direct/portmap.d.ts +9 -2
- package/dist/direct/portmap.js +7 -2
- package/dist/direct/portmap.js.map +1 -1
- package/dist/direct/sha1.d.ts +16 -0
- package/dist/direct/sha1.js +111 -0
- package/dist/direct/sha1.js.map +1 -0
- package/dist/direct-address.d.ts +2 -2
- package/dist/direct-address.js.map +1 -1
- package/dist/friendly.d.ts +28 -0
- package/dist/friendly.js +40 -0
- package/dist/friendly.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/internals.d.ts +9 -0
- package/dist/internals.js +9 -0
- package/dist/internals.js.map +1 -1
- package/dist/join-code-kind.d.ts +10 -0
- package/dist/join-code-kind.js +10 -0
- package/dist/join-code-kind.js.map +1 -0
- package/dist/joincode.d.ts +10 -1
- package/dist/joincode.js +23 -3
- package/dist/joincode.js.map +1 -1
- package/dist/lobby.d.ts +65 -4
- package/dist/lobby.js +151 -12
- package/dist/lobby.js.map +1 -1
- package/dist/lockstep.d.ts +8 -0
- package/dist/lockstep.js +23 -2
- package/dist/lockstep.js.map +1 -1
- package/dist/nostr/crypto.d.ts +20 -0
- package/dist/nostr/crypto.js +78 -0
- package/dist/nostr/crypto.js.map +1 -0
- package/dist/nostr/host.d.ts +66 -0
- package/dist/nostr/host.js +196 -0
- package/dist/nostr/host.js.map +1 -0
- package/dist/nostr/limits.d.ts +31 -0
- package/dist/nostr/limits.js +36 -0
- package/dist/nostr/limits.js.map +1 -0
- package/dist/nostr/link.d.ts +70 -0
- package/dist/nostr/link.js +180 -0
- package/dist/nostr/link.js.map +1 -0
- package/dist/nostr/relays.d.ts +79 -0
- package/dist/nostr/relays.js +315 -0
- package/dist/nostr/relays.js.map +1 -0
- package/dist/nostr/room.d.ts +43 -0
- package/dist/nostr/room.js +104 -0
- package/dist/nostr/room.js.map +1 -0
- package/dist/nostr/secp256k1.d.ts +10 -0
- package/dist/nostr/secp256k1.js +171 -0
- package/dist/nostr/secp256k1.js.map +1 -0
- package/dist/nostr/socket.d.ts +48 -0
- package/dist/nostr/socket.js +141 -0
- package/dist/nostr/socket.js.map +1 -0
- package/dist/onelobby.bundle.js +146 -0
- package/dist/onelobby.bundle.js.map +7 -0
- package/dist/onelobby.bundle.mjs +11 -5
- package/dist/onelobby.bundle.mjs.map +4 -4
- package/dist/onelobby.d.ts +54 -4
- package/dist/onelobby.global.js +2932 -49
- package/dist/onelobby.global.js.map +4 -4
- package/dist/onelobby.global.min.js +11 -5
- package/dist/onelobby.global.min.js.map +4 -4
- package/dist/onelobby.js +158 -15
- package/dist/onelobby.js.map +1 -1
- package/dist/pause.d.ts +85 -0
- package/dist/pause.js +160 -0
- package/dist/pause.js.map +1 -0
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js.map +1 -1
- package/dist/webrtc.d.ts +1 -0
- package/dist/webrtc.js +6 -1
- package/dist/webrtc.js.map +1 -1
- package/package.json +4 -2
package/dist/lobby.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Reassembler, splitChunks } from "./chunking.js";
|
|
|
4
4
|
import { Emitter } from "./emitter.js";
|
|
5
5
|
import { Lockstep } from "./lockstep.js";
|
|
6
6
|
import { claimWinsWrapped, pickNewHost } from "./migration.js";
|
|
7
|
+
import { Pause } from "./pause.js";
|
|
7
8
|
import { OneLobbyError } from "./signaling.js";
|
|
8
9
|
import { Voice } from "./voice.js";
|
|
9
10
|
import { hostConnections, joinerSocket } from "./socket.js";
|
|
@@ -11,6 +12,8 @@ import { hostConnections, joinerSocket } from "./socket.js";
|
|
|
11
12
|
export const MIN_PEER_TIMEOUT_MS = 6000;
|
|
12
13
|
/** Host: how long a kicked / disconnected peer's link stays open for the hub's notice to reach it first. */
|
|
13
14
|
export const KICK_LINGER_MS = 3000;
|
|
15
|
+
/** How long an offer held during a possible migration stays answerable. */
|
|
16
|
+
const HELD_OFFER_MS = 20_000;
|
|
14
17
|
/** The name shown for a peer that chose none: "Player N", N = its seat (peer id, join order). */
|
|
15
18
|
export function seatName(p) {
|
|
16
19
|
const n = typeof p.name === "string" ? p.name.trim() : "";
|
|
@@ -31,31 +34,62 @@ export class Lobby extends Emitter {
|
|
|
31
34
|
tickRate;
|
|
32
35
|
lockstep;
|
|
33
36
|
chat;
|
|
37
|
+
/** The shared co-op pause (docs/recipes/pause.md). */
|
|
38
|
+
pause;
|
|
34
39
|
voice;
|
|
35
40
|
localPeerId;
|
|
36
41
|
hostPeerId;
|
|
37
42
|
phase;
|
|
38
43
|
meta;
|
|
44
|
+
/** The lobby's name as the server keeps it (host()'s `name`, sanitised); "" if the server sends none. */
|
|
45
|
+
name;
|
|
39
46
|
resumeToken;
|
|
40
47
|
/** Last measured round trip to the host in ms (joiners), or 0. */
|
|
41
48
|
rtt = 0;
|
|
42
49
|
/**
|
|
43
|
-
* Direct-IP host only (onelobby/direct): the code friends paste to join, e.g. "6B01-RGFE-P2B" (docs/direct-ip.md
|
|
50
|
+
* Direct-IP host only (onelobby/direct): the code friends paste to join, e.g. "6B01-RGFE-P2B" (docs/design/direct-ip.md
|
|
44
51
|
* s.2a). Show it with joinCodeKind. Empty when the machine has no IPv4 address, and on joiners.
|
|
45
52
|
*/
|
|
46
53
|
joinCode = "";
|
|
47
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* "nostr": a version 1 code that works through public Nostr relays (spec/nostr-signaling.md; the default for a direct
|
|
56
|
+
* host). "internet": the router forwarded the port and the code carries its public address; "same_network": the LAN
|
|
57
|
+
* address. "none": no code.
|
|
58
|
+
*/
|
|
48
59
|
joinCodeKind = "none";
|
|
60
|
+
/**
|
|
61
|
+
* Direct-IP host only: the IP code (address and port, version 0), with `directCodeKind` "internet" or "same_network".
|
|
62
|
+
* Equal to `joinCode` when Nostr is off; otherwise the code to hand out for LAN play or when the router port is open.
|
|
63
|
+
*/
|
|
64
|
+
directCode = "";
|
|
65
|
+
directCodeKind = "none";
|
|
49
66
|
/** Direct-IP host only: this machine's addresses with the port, for an "Advanced" fold-out. */
|
|
50
67
|
directAddresses = [];
|
|
51
68
|
/** Direct-IP host only: how the router port-open went ("UPnP", "no UPnP gateway answered; ...", "off"). */
|
|
52
69
|
portMapping = "";
|
|
70
|
+
/** Direct-IP host only: the one startup line (bound port, join code, port mapping); see `formatHostLog` in onelobby/direct. */
|
|
71
|
+
directHostLog = "";
|
|
72
|
+
/** Browser host only (`kit.hostNostr`): true while the host tab is hidden (see the `hostHiddenChanged` event). */
|
|
73
|
+
hostHidden = false;
|
|
74
|
+
/** @internal kit.hostNostr: the tab's visibility changed. */
|
|
75
|
+
setHostHidden(hidden) {
|
|
76
|
+
if (this.hostHidden === hidden)
|
|
77
|
+
return;
|
|
78
|
+
this.hostHidden = hidden;
|
|
79
|
+
this.emit("hostHiddenChanged", hidden);
|
|
80
|
+
}
|
|
81
|
+
/** @internal onelobby/direct: fires `joinCodeChanged` with the current code. */
|
|
82
|
+
notifyJoinCodeChanged() {
|
|
83
|
+
this.emit("joinCodeChanged", this.joinCode, this.joinCodeKind);
|
|
84
|
+
}
|
|
53
85
|
signaling;
|
|
54
86
|
cfg;
|
|
55
87
|
roster = new Map();
|
|
56
88
|
links = new Map();
|
|
57
89
|
/** ICE from a peer that has no link yet: its candidates can overtake its offer (node-datachannel). */
|
|
58
90
|
earlyIce = new Map();
|
|
91
|
+
/** Offers from roster members that arrived while the host link was open (see offerVerdict), by sender. */
|
|
92
|
+
heldOffers = new Map();
|
|
59
93
|
connected = new Set(); // host: peers that said hi
|
|
60
94
|
hiSentTo = new Set();
|
|
61
95
|
epoch = 0;
|
|
@@ -77,6 +111,8 @@ export class Lobby extends Emitter {
|
|
|
77
111
|
lastHeard = new Map();
|
|
78
112
|
/** Host: links of kicked / disconnected peers, kept open briefly (removePeer `linger`). */
|
|
79
113
|
lingering = new Set();
|
|
114
|
+
/** Host: joiners that joined but are not ready yet, with the cancel of their readyTimeoutMs timer (peerFailed). */
|
|
115
|
+
notReady = new Map();
|
|
80
116
|
/** Host: per-peer fixed window for rateLimit. */
|
|
81
117
|
rate = new Map();
|
|
82
118
|
/**
|
|
@@ -107,6 +143,7 @@ export class Lobby extends Emitter {
|
|
|
107
143
|
this.hostPeerId = st.hostId;
|
|
108
144
|
this.phase = st.phase;
|
|
109
145
|
this.meta = { ...st.meta };
|
|
146
|
+
this.name = st.name ?? "";
|
|
110
147
|
this.resumeToken = st.resumeToken;
|
|
111
148
|
for (const p of st.peers)
|
|
112
149
|
this.roster.set(p.peerId, seatName(p));
|
|
@@ -131,6 +168,11 @@ export class Lobby extends Emitter {
|
|
|
131
168
|
this.lockstep = new Lockstep(this.hub, { hashEveryTicks: cfg.hashEveryTicks });
|
|
132
169
|
this.chat = new Chat(this.hub, cfg.wall, (id) => this.voice.isMutedPeer(id));
|
|
133
170
|
this.voice = new Voice(this.hub, cfg.voice);
|
|
171
|
+
this.pause = new Pause(this.hub, {
|
|
172
|
+
phase: () => this.phase,
|
|
173
|
+
hold: (held) => this.lockstep.hold(held),
|
|
174
|
+
hasPeer: (id) => this.roster.has(id),
|
|
175
|
+
});
|
|
134
176
|
this.lockstep.on("started", () => this.setPhase("in_game"));
|
|
135
177
|
this.lockstep.on("error", (e) => this.reportError(e));
|
|
136
178
|
this.signaling.onMessage = (m) => this.onSignaling(m);
|
|
@@ -377,6 +419,8 @@ export class Lobby extends Emitter {
|
|
|
377
419
|
this.teardown("ice_failed");
|
|
378
420
|
}
|
|
379
421
|
// ---- links -----------------------------------------------------------------------------------------------------
|
|
422
|
+
/** @internal A direct host's hub learns where each joiner's link comes from (Nostr kick bans the address too). */
|
|
423
|
+
linkAddressSink = null;
|
|
380
424
|
makeLink(peerId, relayOnly = this.cfg.relayOnly) {
|
|
381
425
|
const old = this.links.get(peerId);
|
|
382
426
|
if (old) {
|
|
@@ -391,6 +435,7 @@ export class Lobby extends Emitter {
|
|
|
391
435
|
link.onClose = (reason) => this.onLinkClosed(peerId, link, reason);
|
|
392
436
|
link.onMessage = (ch, bytes) => this.onData(peerId, ch, bytes);
|
|
393
437
|
link.onPath = (path) => this.signaling.send({ t: "path", path });
|
|
438
|
+
link.onRemoteAddress = (ip) => this.linkAddressSink?.(peerId, ip);
|
|
394
439
|
return link;
|
|
395
440
|
}
|
|
396
441
|
/** Host side: offer to a joiner. */
|
|
@@ -454,6 +499,10 @@ export class Lobby extends Emitter {
|
|
|
454
499
|
}
|
|
455
500
|
if (!d)
|
|
456
501
|
return;
|
|
502
|
+
// The topology is a star: a joiner hears only its host. Anything else over some other link (a peer that offered to
|
|
503
|
+
// us, or a member that dialled us) is not host traffic and must not reach raw / lockstep / pause / chat handling.
|
|
504
|
+
if (!this.isHost && from !== this.hostPeerId && d.msg.type !== "host_claim")
|
|
505
|
+
return;
|
|
457
506
|
if (!reassembled)
|
|
458
507
|
this.lastHeard.set(from, this.cfg.clock.now());
|
|
459
508
|
const { msg, route } = d;
|
|
@@ -505,6 +554,11 @@ export class Lobby extends Emitter {
|
|
|
505
554
|
case "chat_reject":
|
|
506
555
|
this.chat.handle(from, route, msg);
|
|
507
556
|
break;
|
|
557
|
+
case "pause_request":
|
|
558
|
+
case "pause_state":
|
|
559
|
+
case "pause_refuse":
|
|
560
|
+
this.pause.handle(from, msg);
|
|
561
|
+
break;
|
|
508
562
|
case "voice":
|
|
509
563
|
if (channel === "voice")
|
|
510
564
|
this.voice.handle(from, route, msg); // a voice frame on another channel is ignored
|
|
@@ -525,6 +579,8 @@ export class Lobby extends Emitter {
|
|
|
525
579
|
}
|
|
526
580
|
onHi(from, lastTick) {
|
|
527
581
|
this.connected.add(from);
|
|
582
|
+
this.notReady.get(from)?.();
|
|
583
|
+
this.notReady.delete(from);
|
|
528
584
|
this.sendTo(from, {
|
|
529
585
|
type: "welcome_dc",
|
|
530
586
|
// the tick the snapshot will carry (the game's applied tick), so welcome_dc.tick == snapshot.tick even when
|
|
@@ -535,6 +591,7 @@ export class Lobby extends Emitter {
|
|
|
535
591
|
hostId: this.localPeerId,
|
|
536
592
|
}, 0);
|
|
537
593
|
this.chat.sendHistory(from);
|
|
594
|
+
this.pause.sendState(from);
|
|
538
595
|
if (this.mode === "lockstep")
|
|
539
596
|
this.lockstep.onPeerHi(from, lastTick);
|
|
540
597
|
this.emit("peerReady", this.roster.get(from) ?? { peerId: from, name: "" });
|
|
@@ -583,6 +640,12 @@ export class Lobby extends Emitter {
|
|
|
583
640
|
return;
|
|
584
641
|
this.hostPeerId = hostId;
|
|
585
642
|
this.emit("hostChanged", hostId);
|
|
643
|
+
const held = this.heldOffers.get(hostId);
|
|
644
|
+
if (held) {
|
|
645
|
+
this.heldOffers.delete(hostId);
|
|
646
|
+
if (this.cfg.clock.now() - held.at <= HELD_OFFER_MS)
|
|
647
|
+
void this.acceptOffer(hostId, held.payload);
|
|
648
|
+
}
|
|
586
649
|
}
|
|
587
650
|
/** The host is gone (link closed, no frames for 3 s, or the server said so): run the s.9 rule. */
|
|
588
651
|
handleHostLost(reason) {
|
|
@@ -610,6 +673,16 @@ export class Lobby extends Emitter {
|
|
|
610
673
|
if (candidate === this.localPeerId)
|
|
611
674
|
this.becomeHost();
|
|
612
675
|
// else: wait for the candidate's host_claim (or the server's host_changed); it offers to us and we answer with hi
|
|
676
|
+
this.replayHeldOffers();
|
|
677
|
+
}
|
|
678
|
+
/** Offers that came from roster members while the host link was still open: now the host is lost, answer them. */
|
|
679
|
+
replayHeldOffers() {
|
|
680
|
+
const now = this.cfg.clock.now();
|
|
681
|
+
for (const [from, held] of [...this.heldOffers]) {
|
|
682
|
+
this.heldOffers.delete(from);
|
|
683
|
+
if (now - held.at <= HELD_OFFER_MS && this.roster.has(from))
|
|
684
|
+
void this.acceptOffer(from, held.payload);
|
|
685
|
+
}
|
|
613
686
|
}
|
|
614
687
|
becomeHost() {
|
|
615
688
|
this.epoch = (this.epoch + 1) & 0xffff;
|
|
@@ -619,6 +692,7 @@ export class Lobby extends Emitter {
|
|
|
619
692
|
this.hiSentTo.clear();
|
|
620
693
|
this.lastHeard.clear(); // joiners spoke to the old host; their clocks start when they say hi to us
|
|
621
694
|
this.lockstep.becomeHost();
|
|
695
|
+
this.pause.onBecameHost();
|
|
622
696
|
this.emit("hostChanged", this.localPeerId);
|
|
623
697
|
if (this.serverUp)
|
|
624
698
|
this.signaling.send({ t: "host_changed", hostId: this.localPeerId, epoch: this.epoch });
|
|
@@ -628,6 +702,8 @@ export class Lobby extends Emitter {
|
|
|
628
702
|
}
|
|
629
703
|
}
|
|
630
704
|
onHostClaim(from, epoch, hostId) {
|
|
705
|
+
if (hostId !== from)
|
|
706
|
+
return; // a claim is a peer claiming itself; nobody claims for someone else
|
|
631
707
|
const mine = { epoch: this.epoch, hostId: this.hostPeerId };
|
|
632
708
|
const theirs = { epoch, hostId };
|
|
633
709
|
if (hostId === this.hostPeerId && epoch === this.epoch && !this.isHost) {
|
|
@@ -667,8 +743,10 @@ export class Lobby extends Emitter {
|
|
|
667
743
|
const peer = seatName(m.peer);
|
|
668
744
|
this.roster.set(peer.peerId, peer);
|
|
669
745
|
this.emit("peerJoined", peer);
|
|
670
|
-
if (this.isHost)
|
|
746
|
+
if (this.isHost) {
|
|
747
|
+
this.watchReady(peer.peerId);
|
|
671
748
|
this.offerTo(peer.peerId);
|
|
749
|
+
}
|
|
672
750
|
break;
|
|
673
751
|
}
|
|
674
752
|
case "peer_left":
|
|
@@ -702,6 +780,8 @@ export class Lobby extends Emitter {
|
|
|
702
780
|
case "lobby": {
|
|
703
781
|
const st = m;
|
|
704
782
|
this.meta = { ...st.meta };
|
|
783
|
+
if (st.name)
|
|
784
|
+
this.name = st.name;
|
|
705
785
|
if (st.resumeToken && st.resumeToken !== this.resumeToken) {
|
|
706
786
|
this.resumeToken = st.resumeToken;
|
|
707
787
|
this.seatSink?.(this.resumeToken);
|
|
@@ -745,20 +825,21 @@ export class Lobby extends Emitter {
|
|
|
745
825
|
if (s.ice)
|
|
746
826
|
payload.ice = s.ice;
|
|
747
827
|
if (s.sdp?.type === "offer") {
|
|
748
|
-
// We are the answerer
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
await done;
|
|
828
|
+
// We are the answerer. An offer comes from the (new) host, so a peer may dial us only as that rule allows.
|
|
829
|
+
const verdict = this.offerVerdict(from);
|
|
830
|
+
if (verdict === "hold") {
|
|
831
|
+
if (this.heldOffers.size < 64 || this.heldOffers.has(from))
|
|
832
|
+
this.heldOffers.set(from, { payload, at: this.cfg.clock.now() });
|
|
833
|
+
}
|
|
834
|
+
else if (verdict === "accept")
|
|
835
|
+
await this.acceptOffer(from, payload);
|
|
757
836
|
return;
|
|
758
837
|
}
|
|
759
838
|
if (!link) {
|
|
760
839
|
if (!payload.ice || this.ended || !this.roster.has(from))
|
|
761
840
|
return;
|
|
841
|
+
if (this.cfg.direct && from !== this.hostPeerId)
|
|
842
|
+
return;
|
|
762
843
|
const q = this.earlyIce.get(from) ?? [];
|
|
763
844
|
if (q.length < 64)
|
|
764
845
|
q.push(payload);
|
|
@@ -767,6 +848,35 @@ export class Lobby extends Emitter {
|
|
|
767
848
|
}
|
|
768
849
|
await link.handleSignal(payload);
|
|
769
850
|
}
|
|
851
|
+
/**
|
|
852
|
+
* Whether an SDP offer from `from` may be answered. The host (or a joiner's host) always may. Direct lobbies have no
|
|
853
|
+
* migration, so nobody else. In a server lobby a roster member may dial us when we are the host (a claim race decides
|
|
854
|
+
* who wins) or when our own link to the host is gone (the migration window); while that link is open the offer is
|
|
855
|
+
* held, to be answered if the host turns out to be lost a moment later.
|
|
856
|
+
*/
|
|
857
|
+
offerVerdict(from) {
|
|
858
|
+
if (this.ended || from === this.localPeerId)
|
|
859
|
+
return "drop";
|
|
860
|
+
if (!this.isHost && from === this.hostPeerId)
|
|
861
|
+
return "accept";
|
|
862
|
+
if (this.cfg.direct || !this.roster.has(from))
|
|
863
|
+
return "drop";
|
|
864
|
+
if (this.isHost)
|
|
865
|
+
return "accept";
|
|
866
|
+
if (!this.isReady)
|
|
867
|
+
return "drop"; // still joining: only the host's own offer is expected
|
|
868
|
+
return this.links.get(this.hostPeerId)?.isOpen ? "hold" : "accept";
|
|
869
|
+
}
|
|
870
|
+
async acceptOffer(from, payload) {
|
|
871
|
+
const link = this.makeLink(from);
|
|
872
|
+
const early = this.earlyIce.get(from) ?? [];
|
|
873
|
+
this.earlyIce.delete(from);
|
|
874
|
+
// the link holds candidates until the offer is set, so hand them over right behind it
|
|
875
|
+
const done = link.handleSignal(payload);
|
|
876
|
+
for (const e of early)
|
|
877
|
+
void link.handleSignal(e);
|
|
878
|
+
await done;
|
|
879
|
+
}
|
|
770
880
|
/**
|
|
771
881
|
* `linger` (host kick / disconnect): stop using the peer's link at once but leave it open until that peer closes it
|
|
772
882
|
* or KICK_LINGER_MS pass. The hub tells the peer over its WebSocket; closing the data link first would make it look
|
|
@@ -781,6 +891,13 @@ export class Lobby extends Emitter {
|
|
|
781
891
|
if (peer)
|
|
782
892
|
this.roster.delete(peerId);
|
|
783
893
|
this.earlyIce.delete(peerId);
|
|
894
|
+
const cancelWatch = this.notReady.get(peerId);
|
|
895
|
+
if (cancelWatch) {
|
|
896
|
+
cancelWatch();
|
|
897
|
+
this.notReady.delete(peerId);
|
|
898
|
+
if (peer && reason !== "kicked" && reason !== "disconnected")
|
|
899
|
+
this.emit("peerFailed", peer, reason);
|
|
900
|
+
}
|
|
784
901
|
const link = this.links.get(peerId);
|
|
785
902
|
if (link && peerId !== this.hostPeerId) {
|
|
786
903
|
link.onMessage = null;
|
|
@@ -810,6 +927,7 @@ export class Lobby extends Emitter {
|
|
|
810
927
|
this.lockstep.onPeerLeft(peerId);
|
|
811
928
|
this.chat.onPeerLeft(peerId);
|
|
812
929
|
this.voice.onPeerLeft(peerId);
|
|
930
|
+
this.pause.onPeerLeft(peerId);
|
|
813
931
|
if (peer)
|
|
814
932
|
this.emit("peerLeft", peer, reason, close);
|
|
815
933
|
if (peerId === this.hostPeerId && !this.isHost) {
|
|
@@ -827,6 +945,8 @@ export class Lobby extends Emitter {
|
|
|
827
945
|
if (this.phase === p)
|
|
828
946
|
return;
|
|
829
947
|
this.phase = p;
|
|
948
|
+
if (p === "lobby")
|
|
949
|
+
this.pause.onLobbyPhase();
|
|
830
950
|
if (p === "in_game" && !this.lockstep.started)
|
|
831
951
|
this.lockstep.markStarted();
|
|
832
952
|
this.emit("phase", p);
|
|
@@ -882,11 +1002,29 @@ export class Lobby extends Emitter {
|
|
|
882
1002
|
return;
|
|
883
1003
|
this.sendTo(this.hostPeerId, { type: "ping", n: ++this.pingN, t: this.cfg.clock.now() >>> 0 }, 0, "fast");
|
|
884
1004
|
}
|
|
1005
|
+
/** Host: report a joiner that is not ready `readyTimeoutMs` after joining as peerFailed("timeout"), once. */
|
|
1006
|
+
watchReady(peerId) {
|
|
1007
|
+
const ms = this.cfg.readyTimeoutMs ?? 0;
|
|
1008
|
+
let cancel = () => { };
|
|
1009
|
+
if (ms > 0) {
|
|
1010
|
+
cancel = this.cfg.clock.setTimeout(() => {
|
|
1011
|
+
if (!this.notReady.delete(peerId))
|
|
1012
|
+
return;
|
|
1013
|
+
const peer = this.roster.get(peerId);
|
|
1014
|
+
if (peer)
|
|
1015
|
+
this.emit("peerFailed", peer, "timeout");
|
|
1016
|
+
}, ms);
|
|
1017
|
+
}
|
|
1018
|
+
this.notReady.set(peerId, cancel);
|
|
1019
|
+
}
|
|
885
1020
|
teardown(reason, close) {
|
|
886
1021
|
if (this.ended)
|
|
887
1022
|
return;
|
|
888
1023
|
this.ended = true;
|
|
889
1024
|
this.cancelReadyTimer?.();
|
|
1025
|
+
for (const cancel of this.notReady.values())
|
|
1026
|
+
cancel();
|
|
1027
|
+
this.notReady.clear();
|
|
890
1028
|
if (!this.isReady) {
|
|
891
1029
|
const code = reason === "ice_failed" || reason === "kicked" ? reason : "closed";
|
|
892
1030
|
this.failReady(new OneLobbyError(code, `the lobby ended before connecting (${reason})`));
|
|
@@ -902,6 +1040,7 @@ export class Lobby extends Emitter {
|
|
|
902
1040
|
}
|
|
903
1041
|
this.links.clear();
|
|
904
1042
|
this.earlyIce.clear();
|
|
1043
|
+
this.heldOffers.clear();
|
|
905
1044
|
this.lingering.clear();
|
|
906
1045
|
this.signaling.onClose = null;
|
|
907
1046
|
this.signaling.onMessage = null;
|