onelobby 0.3.1 → 0.5.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 +237 -0
- package/dist/blueprint.d.ts +166 -0
- package/dist/blueprint.js +281 -0
- package/dist/blueprint.js.map +1 -0
- package/dist/chunking.d.ts +4 -0
- package/dist/chunking.js +23 -2
- package/dist/chunking.js.map +1 -1
- package/dist/deflate.d.ts +2 -1
- package/dist/deflate.js +10 -4
- package/dist/deflate.js.map +1 -1
- package/dist/direct/hub-host.d.ts +16 -0
- package/dist/direct/hub-host.js +45 -1
- package/dist/direct/hub-host.js.map +1 -1
- package/dist/direct/hub.d.ts +3 -0
- package/dist/direct/hub.js +32 -6
- package/dist/direct/hub.js.map +1 -1
- package/dist/direct/index.d.ts +8 -2
- package/dist/direct/index.js +26 -4
- package/dist/direct/index.js.map +1 -1
- package/dist/friendly.d.ts +36 -0
- package/dist/friendly.js +44 -0
- package/dist/friendly.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lockstep.d.ts +5 -0
- package/dist/lockstep.js +17 -2
- package/dist/lockstep.js.map +1 -1
- package/dist/onelobby.bundle.mjs +5 -5
- package/dist/onelobby.bundle.mjs.map +4 -4
- package/dist/onelobby.d.ts +34 -2
- package/dist/onelobby.global.js +445 -30
- package/dist/onelobby.global.js.map +3 -3
- package/dist/onelobby.global.min.js +5 -5
- package/dist/onelobby.global.min.js.map +4 -4
- package/dist/onelobby.js +136 -25
- package/dist/onelobby.js.map +1 -1
- package/package.json +4 -2
package/dist/onelobby.global.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! onelobby 0.
|
|
1
|
+
/*! onelobby 0.5.0 | MIT license */
|
|
2
2
|
"use strict";
|
|
3
3
|
var OneLobby = (() => {
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -22,9 +22,11 @@ var OneLobby = (() => {
|
|
|
22
22
|
// src/global.ts
|
|
23
23
|
var global_exports = {};
|
|
24
24
|
__export(global_exports, {
|
|
25
|
+
BlueprintError: () => BlueprintError,
|
|
25
26
|
CHAT_MAX_BYTES: () => CHAT_MAX_BYTES,
|
|
26
27
|
Chat: () => Chat,
|
|
27
28
|
DEFAULT_READY_TIMEOUT_MS: () => DEFAULT_READY_TIMEOUT_MS,
|
|
29
|
+
ErrorCodes: () => ErrorCodes,
|
|
28
30
|
Keepalive: () => Keepalive,
|
|
29
31
|
KeepaliveError: () => KeepaliveError,
|
|
30
32
|
LIB_NAME: () => LIB_NAME,
|
|
@@ -41,12 +43,14 @@ var OneLobby = (() => {
|
|
|
41
43
|
closeCodeFor: () => closeCodeFor,
|
|
42
44
|
codeFromUrl: () => codeFromUrl,
|
|
43
45
|
decodeJoinCode: () => decodeJoinCode,
|
|
46
|
+
defineBlueprint: () => defineBlueprint,
|
|
44
47
|
defineLobbyElement: () => defineLobbyElement,
|
|
45
48
|
describeError: () => describeError,
|
|
46
49
|
formatCode: () => formatCode,
|
|
47
50
|
lobbyServerUrl: () => lobbyServerUrl,
|
|
48
51
|
looksLikeJoinCode: () => looksLikeJoinCode,
|
|
49
52
|
normalizeCode: () => normalizeCode,
|
|
53
|
+
parseBlueprint: () => parseBlueprint,
|
|
50
54
|
shareUrl: () => shareUrl,
|
|
51
55
|
webCodecsOpusAvailable: () => webCodecsOpusAvailable,
|
|
52
56
|
webRtcLinkFactoryFor: () => webRtcLinkFactoryFor
|
|
@@ -629,13 +633,17 @@ var OneLobby = (() => {
|
|
|
629
633
|
}
|
|
630
634
|
return out;
|
|
631
635
|
}
|
|
636
|
+
var MAX_PARTIAL_SETS = 8;
|
|
632
637
|
var Reassembler = class {
|
|
633
638
|
parts = /* @__PURE__ */ new Map();
|
|
639
|
+
// insertion order = oldest first
|
|
634
640
|
/** Returns the full payload when the last chunk arrives, otherwise null. */
|
|
635
641
|
push(chunk) {
|
|
636
642
|
if (chunk.count === 0 || chunk.index >= chunk.count) return null;
|
|
637
643
|
let e = this.parts.get(chunk.id);
|
|
638
644
|
if (!e || e.count !== chunk.count) {
|
|
645
|
+
this.parts.delete(chunk.id);
|
|
646
|
+
while (this.parts.size >= MAX_PARTIAL_SETS) this.parts.delete(this.parts.keys().next().value);
|
|
639
647
|
e = { count: chunk.count, got: 0, parts: new Array(chunk.count), bytes: 0 };
|
|
640
648
|
this.parts.set(chunk.id, e);
|
|
641
649
|
}
|
|
@@ -647,7 +655,15 @@ var OneLobby = (() => {
|
|
|
647
655
|
this.parts.delete(chunk.id);
|
|
648
656
|
throw new CodecError("too_long", `chunk set over ${MAX_REASSEMBLED_BYTES} bytes`);
|
|
649
657
|
}
|
|
650
|
-
if (e.got < e.count)
|
|
658
|
+
if (e.got < e.count) {
|
|
659
|
+
while (this.pendingBytes > MAX_REASSEMBLED_BYTES && this.parts.size > 1) {
|
|
660
|
+
for (const id of this.parts.keys()) if (id !== chunk.id) {
|
|
661
|
+
this.parts.delete(id);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
651
667
|
this.parts.delete(chunk.id);
|
|
652
668
|
const out = new Uint8Array(e.bytes);
|
|
653
669
|
let pos = 0;
|
|
@@ -660,6 +676,12 @@ var OneLobby = (() => {
|
|
|
660
676
|
get pending() {
|
|
661
677
|
return this.parts.size;
|
|
662
678
|
}
|
|
679
|
+
/** Bytes held across incomplete sets. */
|
|
680
|
+
get pendingBytes() {
|
|
681
|
+
let n = 0;
|
|
682
|
+
for (const p of this.parts.values()) n += p.bytes;
|
|
683
|
+
return n;
|
|
684
|
+
}
|
|
663
685
|
clear() {
|
|
664
686
|
this.parts.clear();
|
|
665
687
|
}
|
|
@@ -669,9 +691,10 @@ var OneLobby = (() => {
|
|
|
669
691
|
function deflateAvailable() {
|
|
670
692
|
return typeof CompressionStream !== "undefined" && typeof DecompressionStream !== "undefined";
|
|
671
693
|
}
|
|
672
|
-
async function pipe(bytes, stream) {
|
|
694
|
+
async function pipe(bytes, stream, maxBytes = Infinity) {
|
|
673
695
|
const w = stream.writable.getWriter();
|
|
674
|
-
void w.write(bytes).then(() => w.close())
|
|
696
|
+
void w.write(bytes).then(() => w.close()).catch(() => {
|
|
697
|
+
});
|
|
675
698
|
const chunks = [];
|
|
676
699
|
const r = stream.readable.getReader();
|
|
677
700
|
let total = 0;
|
|
@@ -680,6 +703,11 @@ var OneLobby = (() => {
|
|
|
680
703
|
if (done) break;
|
|
681
704
|
chunks.push(value);
|
|
682
705
|
total += value.length;
|
|
706
|
+
if (total > maxBytes) {
|
|
707
|
+
void r.cancel().catch(() => {
|
|
708
|
+
});
|
|
709
|
+
throw new CodecError("too_long", `snapshot inflates to more than ${maxBytes} bytes`);
|
|
710
|
+
}
|
|
683
711
|
}
|
|
684
712
|
const out = new Uint8Array(total);
|
|
685
713
|
let pos = 0;
|
|
@@ -692,8 +720,8 @@ var OneLobby = (() => {
|
|
|
692
720
|
function deflate(bytes) {
|
|
693
721
|
return pipe(bytes, new CompressionStream("deflate"));
|
|
694
722
|
}
|
|
695
|
-
function inflate(bytes) {
|
|
696
|
-
return pipe(bytes, new DecompressionStream("deflate"));
|
|
723
|
+
function inflate(bytes, maxBytes = MAX_REASSEMBLED_BYTES) {
|
|
724
|
+
return pipe(bytes, new DecompressionStream("deflate"), maxBytes);
|
|
697
725
|
}
|
|
698
726
|
|
|
699
727
|
// src/lockstep.ts
|
|
@@ -701,6 +729,7 @@ var OneLobby = (() => {
|
|
|
701
729
|
var MAX_RESTORE_RETRIES = 3;
|
|
702
730
|
var HOST_SILENCE_MS = 3e3;
|
|
703
731
|
var PEER_HASH_SILENCE_MS = 1e4;
|
|
732
|
+
var MAX_PENDING_PER_PEER = 256;
|
|
704
733
|
var SNAPSHOT_COMPRESS_MIN = 1024;
|
|
705
734
|
var Lockstep = class extends Emitter {
|
|
706
735
|
constructor(hub, opts) {
|
|
@@ -732,6 +761,7 @@ var OneLobby = (() => {
|
|
|
732
761
|
hosting = false;
|
|
733
762
|
_started = false;
|
|
734
763
|
pending = [];
|
|
764
|
+
_droppedCommands = 0;
|
|
735
765
|
ready = /* @__PURE__ */ new Set();
|
|
736
766
|
snapshotting = /* @__PURE__ */ new Set();
|
|
737
767
|
cancelTicker = null;
|
|
@@ -746,6 +776,10 @@ var OneLobby = (() => {
|
|
|
746
776
|
// joiner side
|
|
747
777
|
pendingSnapshot = null;
|
|
748
778
|
// ---- public game-facing surface -------------------------------------------------------------------------------
|
|
779
|
+
/** Remote commands the host dropped because that peer already had MAX_PENDING_PER_PEER waiting. */
|
|
780
|
+
get droppedCommands() {
|
|
781
|
+
return this._droppedCommands;
|
|
782
|
+
}
|
|
749
783
|
get started() {
|
|
750
784
|
return this._started;
|
|
751
785
|
}
|
|
@@ -891,7 +925,12 @@ var OneLobby = (() => {
|
|
|
891
925
|
handle(from, msg) {
|
|
892
926
|
switch (msg.type) {
|
|
893
927
|
case "cmd":
|
|
894
|
-
if (this.hosting)
|
|
928
|
+
if (this.hosting) {
|
|
929
|
+
let waiting = 0;
|
|
930
|
+
for (const c of this.pending) if (c.peerId === from) waiting++;
|
|
931
|
+
if (waiting >= MAX_PENDING_PER_PEER) this._droppedCommands++;
|
|
932
|
+
else this.pending.push({ peerId: from, bytes: msg.bytes });
|
|
933
|
+
}
|
|
895
934
|
break;
|
|
896
935
|
case "frame":
|
|
897
936
|
if (!this.hosting) this.onFrame(msg.tick, msg.commands);
|
|
@@ -2978,9 +3017,243 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2978
3017
|
return { host, port, isIPv6: v6, display, url: `ws://${display}/`, candidate: `candidate:1 1 UDP 2122317823 ${host} ${port} typ host` };
|
|
2979
3018
|
}
|
|
2980
3019
|
|
|
3020
|
+
// src/blueprint.ts
|
|
3021
|
+
function defineBlueprint(x) {
|
|
3022
|
+
return x;
|
|
3023
|
+
}
|
|
3024
|
+
var BlueprintError = class extends OneLobbyError {
|
|
3025
|
+
constructor(path, rule, detail) {
|
|
3026
|
+
super("bad_config", `onelobby.json: ${path ? path + ": " : ""}${detail}`);
|
|
3027
|
+
this.path = path;
|
|
3028
|
+
this.rule = rule;
|
|
3029
|
+
}
|
|
3030
|
+
path;
|
|
3031
|
+
rule;
|
|
3032
|
+
};
|
|
3033
|
+
var obj = (props, req) => ({ k: "obj", props, req });
|
|
3034
|
+
var bool = { k: "bool" };
|
|
3035
|
+
var str = (o = {}) => ({ k: "str", ...o });
|
|
3036
|
+
var int = (min, max, zeroOr) => ({ k: "int", min, max, zeroOr });
|
|
3037
|
+
var en = (...vals) => ({ k: "enum", vals });
|
|
3038
|
+
var ROOT = obj(
|
|
3039
|
+
{
|
|
3040
|
+
version: { k: "const", val: 1 },
|
|
3041
|
+
game: obj(
|
|
3042
|
+
{ id: str({ pattern: /^[A-Za-z0-9._-]{1,64}$/ }), netVersion: str({ min: 1, max: 32 }), title: str({ max: 64 }) },
|
|
3043
|
+
["id"]
|
|
3044
|
+
),
|
|
3045
|
+
engine: en("web", "node-browser", "godot-csharp", "unity", "dotnet"),
|
|
3046
|
+
hosting: obj({
|
|
3047
|
+
direct: bool,
|
|
3048
|
+
server: bool,
|
|
3049
|
+
lobbyServer: str(),
|
|
3050
|
+
directPort: int(1024, 65535),
|
|
3051
|
+
openPort: bool,
|
|
3052
|
+
relayOnly: bool,
|
|
3053
|
+
allowedOrigins: { k: "strarr" }
|
|
3054
|
+
}),
|
|
3055
|
+
lobby: obj({
|
|
3056
|
+
maxPlayers: int(2, 64),
|
|
3057
|
+
listed: bool,
|
|
3058
|
+
approveJoins: bool,
|
|
3059
|
+
password: bool,
|
|
3060
|
+
lateJoin: bool,
|
|
3061
|
+
rejoin: bool,
|
|
3062
|
+
joinLink: bool,
|
|
3063
|
+
meta: { k: "meta" }
|
|
3064
|
+
}),
|
|
3065
|
+
sync: obj({ mode: en("raw", "lockstep", "replication"), tickRate: int(1, 120), hashEveryTicks: int(1, 1e5) }),
|
|
3066
|
+
players: obj({ identity: en("name", "anonymous", "platform"), maxNameLength: int(1, 24) }),
|
|
3067
|
+
chat: obj({ text: bool, voice: bool, voiceMode: en("pushToTalk", "voiceActivity") }),
|
|
3068
|
+
session: obj({
|
|
3069
|
+
hostLeaves: en("end", "migrate"),
|
|
3070
|
+
pause: en("off", "host", "anyone"),
|
|
3071
|
+
readyTimeoutMs: int(0, 6e5),
|
|
3072
|
+
peerTimeoutMs: int(6e3, 6e5, true),
|
|
3073
|
+
rateLimit: { k: "rate" },
|
|
3074
|
+
runInBackground: bool
|
|
3075
|
+
}),
|
|
3076
|
+
platforms: obj({ steam: { k: "steam" }, epic: { k: "epic" }, discord: bool }),
|
|
3077
|
+
ui: obj({ kind: en("drop-in", "generated", "none"), entry: str(), theme: en("light", "dark", "auto") })
|
|
3078
|
+
},
|
|
3079
|
+
["game"]
|
|
3080
|
+
);
|
|
3081
|
+
var IGNORED = /* @__PURE__ */ new Set(["$schema", "$comment"]);
|
|
3082
|
+
var Fail = class {
|
|
3083
|
+
constructor(path, rule, detail) {
|
|
3084
|
+
this.path = path;
|
|
3085
|
+
this.rule = rule;
|
|
3086
|
+
this.detail = detail;
|
|
3087
|
+
}
|
|
3088
|
+
path;
|
|
3089
|
+
rule;
|
|
3090
|
+
detail;
|
|
3091
|
+
};
|
|
3092
|
+
var join = (p, k) => p ? `${p}.${k}` : String(k);
|
|
3093
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3094
|
+
function check(n, v, path) {
|
|
3095
|
+
switch (n.k) {
|
|
3096
|
+
case "obj": {
|
|
3097
|
+
if (!isObj(v)) throw new Fail(path, "type", "must be an object");
|
|
3098
|
+
for (const r of n.req ?? []) if (!(r in v)) throw new Fail(join(path, r), "required", "is required");
|
|
3099
|
+
for (const k of Object.keys(v)) if (!IGNORED.has(k) && !(k in n.props)) throw new Fail(join(path, k), "unknown_key", "unknown key");
|
|
3100
|
+
for (const [k, sub] of Object.entries(n.props)) if (k in v) check(sub, v[k], join(path, k));
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
case "str": {
|
|
3104
|
+
if (typeof v !== "string") throw new Fail(path, "type", "must be a string");
|
|
3105
|
+
if (n.min !== void 0 && v.length < n.min || n.max !== void 0 && v.length > n.max)
|
|
3106
|
+
throw new Fail(path, "length", `must be ${n.min ?? 0} to ${n.max ?? "any"} characters`);
|
|
3107
|
+
if (n.pattern && !n.pattern.test(v)) throw new Fail(path, "pattern", `must match ${n.pattern.source}`);
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
case "int": {
|
|
3111
|
+
if (typeof v !== "number" || !Number.isInteger(v)) throw new Fail(path, "type", "must be an integer");
|
|
3112
|
+
if (n.zeroOr && v === 0) return;
|
|
3113
|
+
if (n.min !== void 0 && v < n.min || n.max !== void 0 && v > n.max)
|
|
3114
|
+
throw new Fail(path, "range", `must be ${n.zeroOr ? "0 or " : ""}${n.min ?? "any"} to ${n.max ?? "any"}`);
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
case "bool":
|
|
3118
|
+
if (typeof v !== "boolean") throw new Fail(path, "type", "must be true or false");
|
|
3119
|
+
return;
|
|
3120
|
+
case "enum":
|
|
3121
|
+
if (typeof v !== "string" || !n.vals.includes(v)) throw new Fail(path, "enum", `must be one of ${n.vals.join(", ")}`);
|
|
3122
|
+
return;
|
|
3123
|
+
case "const":
|
|
3124
|
+
if (v !== n.val) throw new Fail(path, "const", `must be ${n.val}`);
|
|
3125
|
+
return;
|
|
3126
|
+
case "strarr": {
|
|
3127
|
+
if (!Array.isArray(v)) throw new Fail(path, "type", "must be an array of strings");
|
|
3128
|
+
v.forEach((x, i) => {
|
|
3129
|
+
if (typeof x !== "string") throw new Fail(join(path, i), "type", "must be a string");
|
|
3130
|
+
});
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
case "meta": {
|
|
3134
|
+
if (!isObj(v)) throw new Fail(path, "type", "must be an object of strings");
|
|
3135
|
+
const keys = Object.keys(v);
|
|
3136
|
+
if (keys.length > 16) throw new Fail(path, "length", "at most 16 keys");
|
|
3137
|
+
for (const k of keys) {
|
|
3138
|
+
if (k.length > 64) throw new Fail(join(path, k), "length", "key is longer than 64 characters");
|
|
3139
|
+
const x = v[k];
|
|
3140
|
+
if (typeof x !== "string") throw new Fail(join(path, k), "type", "must be a string");
|
|
3141
|
+
if (x.length > 64) throw new Fail(join(path, k), "length", "value is longer than 64 characters");
|
|
3142
|
+
}
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
case "rate":
|
|
3146
|
+
check(obj({ messages: int(1), perMs: int(1) }, ["messages", "perMs"]), v, path);
|
|
3147
|
+
return;
|
|
3148
|
+
case "steam":
|
|
3149
|
+
if (typeof v === "boolean") return;
|
|
3150
|
+
if (!isObj(v)) throw new Fail(path, "type", "must be true, false or { appId }");
|
|
3151
|
+
check(obj({ appId: int(1) }), v, path);
|
|
3152
|
+
return;
|
|
3153
|
+
case "epic": {
|
|
3154
|
+
if (typeof v === "boolean") return;
|
|
3155
|
+
if (!isObj(v)) throw new Fail(path, "type", "must be true, false or { productId }");
|
|
3156
|
+
for (const k of Object.keys(v)) if (!IGNORED.has(k) && typeof v[k] !== "string") throw new Fail(join(path, k), "type", "must be a string");
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
var sect = (x) => isObj(x) ? x : {};
|
|
3162
|
+
function strip(o) {
|
|
3163
|
+
if (Array.isArray(o)) return o.map(strip);
|
|
3164
|
+
if (!isObj(o)) return o;
|
|
3165
|
+
const out = {};
|
|
3166
|
+
for (const [k, v] of Object.entries(o)) if (k !== "$comment" && k !== "$schema") out[k] = strip(v);
|
|
3167
|
+
return out;
|
|
3168
|
+
}
|
|
3169
|
+
function deepFreeze(o) {
|
|
3170
|
+
if (typeof o === "object" && o !== null && !Object.isFrozen(o)) {
|
|
3171
|
+
Object.freeze(o);
|
|
3172
|
+
for (const v of Object.values(o)) deepFreeze(v);
|
|
3173
|
+
}
|
|
3174
|
+
return o;
|
|
3175
|
+
}
|
|
3176
|
+
function fill(input) {
|
|
3177
|
+
const game = sect(input.game);
|
|
3178
|
+
const engine = input.engine;
|
|
3179
|
+
const hosting = sect(input.hosting);
|
|
3180
|
+
const lobby = sect(input.lobby);
|
|
3181
|
+
const sync = sect(input.sync);
|
|
3182
|
+
const players = sect(input.players);
|
|
3183
|
+
const chat = sect(input.chat);
|
|
3184
|
+
const session = sect(input.session);
|
|
3185
|
+
const platforms = sect(input.platforms);
|
|
3186
|
+
const ui = sect(input.ui);
|
|
3187
|
+
const tickRate = sync.tickRate ?? 20;
|
|
3188
|
+
const web = engine === "web";
|
|
3189
|
+
const out = {
|
|
3190
|
+
version: 1,
|
|
3191
|
+
game: { id: game.id, netVersion: game.netVersion ?? "1", ...game.title !== void 0 ? { title: game.title } : {} },
|
|
3192
|
+
...engine !== void 0 ? { engine } : {},
|
|
3193
|
+
hosting: {
|
|
3194
|
+
direct: hosting.direct ?? !web,
|
|
3195
|
+
server: hosting.server ?? web,
|
|
3196
|
+
lobbyServer: hosting.lobbyServer ?? "",
|
|
3197
|
+
directPort: hosting.directPort ?? 47810,
|
|
3198
|
+
openPort: hosting.openPort ?? true,
|
|
3199
|
+
relayOnly: hosting.relayOnly ?? false,
|
|
3200
|
+
allowedOrigins: hosting.allowedOrigins ?? []
|
|
3201
|
+
},
|
|
3202
|
+
lobby: {
|
|
3203
|
+
maxPlayers: lobby.maxPlayers ?? 4,
|
|
3204
|
+
listed: lobby.listed ?? false,
|
|
3205
|
+
approveJoins: lobby.approveJoins ?? false,
|
|
3206
|
+
password: lobby.password ?? false,
|
|
3207
|
+
lateJoin: lobby.lateJoin ?? true,
|
|
3208
|
+
rejoin: lobby.rejoin ?? true,
|
|
3209
|
+
joinLink: lobby.joinLink ?? (web || engine === "node-browser"),
|
|
3210
|
+
meta: lobby.meta ?? {}
|
|
3211
|
+
},
|
|
3212
|
+
sync: { mode: sync.mode ?? "raw", tickRate, hashEveryTicks: sync.hashEveryTicks ?? tickRate },
|
|
3213
|
+
players: { identity: players.identity ?? "name", maxNameLength: players.maxNameLength ?? 24 },
|
|
3214
|
+
chat: { text: chat.text ?? true, voice: chat.voice ?? false, voiceMode: chat.voiceMode ?? "pushToTalk" },
|
|
3215
|
+
session: {
|
|
3216
|
+
hostLeaves: session.hostLeaves ?? "end",
|
|
3217
|
+
pause: session.pause ?? "off",
|
|
3218
|
+
readyTimeoutMs: session.readyTimeoutMs ?? 15e3,
|
|
3219
|
+
peerTimeoutMs: session.peerTimeoutMs ?? 0,
|
|
3220
|
+
...session.rateLimit !== void 0 ? { rateLimit: session.rateLimit } : {},
|
|
3221
|
+
runInBackground: session.runInBackground ?? true
|
|
3222
|
+
},
|
|
3223
|
+
platforms: { steam: platforms.steam ?? false, epic: platforms.epic ?? false, discord: platforms.discord ?? false },
|
|
3224
|
+
ui: {
|
|
3225
|
+
kind: ui.kind ?? (web ? "drop-in" : "generated"),
|
|
3226
|
+
...ui.entry !== void 0 ? { entry: ui.entry } : {},
|
|
3227
|
+
theme: ui.theme ?? "auto"
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
return out;
|
|
3231
|
+
}
|
|
3232
|
+
function crossRules(b) {
|
|
3233
|
+
if (!b.hosting.direct && !b.hosting.server) throw new Fail("hosting", "hosting_none", "at least one of direct, server must be true");
|
|
3234
|
+
if (b.lobby.listed && b.lobby.approveJoins) throw new Fail("lobby", "listed_and_approve", "listed and approveJoins cannot both be true");
|
|
3235
|
+
if (b.engine === "web" && b.hosting.direct) throw new Fail("hosting.direct", "direct_needs_listener", "engine web cannot host direct (a browser cannot listen on a port)");
|
|
3236
|
+
const csharp = b.engine === "godot-csharp" || b.engine === "unity" || b.engine === "dotnet";
|
|
3237
|
+
if (b.sync.mode === "replication" && !csharp) throw new Fail("sync.mode", "replication_needs_csharp", "replication needs engine godot-csharp, unity or dotnet");
|
|
3238
|
+
if (b.sync.mode === "replication" && b.session.hostLeaves === "migrate") throw new Fail("session.hostLeaves", "migrate_with_replication", "replication has no host migration; use end");
|
|
3239
|
+
if (b.session.hostLeaves === "migrate" && !b.hosting.server) throw new Fail("session.hostLeaves", "migrate_needs_server", "migrate needs hosting.server (direct lobbies always end)");
|
|
3240
|
+
}
|
|
3241
|
+
function parseBlueprint(json) {
|
|
3242
|
+
let b;
|
|
3243
|
+
try {
|
|
3244
|
+
check(ROOT, json, "");
|
|
3245
|
+
b = fill(strip(json));
|
|
3246
|
+
crossRules(b);
|
|
3247
|
+
} catch (e) {
|
|
3248
|
+
if (e instanceof Fail) throw new BlueprintError(e.path, e.rule, e.detail);
|
|
3249
|
+
throw e;
|
|
3250
|
+
}
|
|
3251
|
+
return deepFreeze(b);
|
|
3252
|
+
}
|
|
3253
|
+
|
|
2981
3254
|
// src/onelobby.ts
|
|
2982
3255
|
var LIB_NAME = "onelobby";
|
|
2983
|
-
var LIB_VERSION = "0.
|
|
3256
|
+
var LIB_VERSION = "0.5.0";
|
|
2984
3257
|
var DEFAULT_READY_TIMEOUT_MS = 15e3;
|
|
2985
3258
|
function hasMic() {
|
|
2986
3259
|
return typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof AudioWorkletNode !== "undefined";
|
|
@@ -2998,7 +3271,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
2998
3271
|
for (const l of liveSeats) if (l.resumeToken === token) return true;
|
|
2999
3272
|
return false;
|
|
3000
3273
|
}
|
|
3001
|
-
var OneLobby = class extends Emitter {
|
|
3274
|
+
var OneLobby = class _OneLobby extends Emitter {
|
|
3002
3275
|
options;
|
|
3003
3276
|
socketFactory;
|
|
3004
3277
|
linkFactory;
|
|
@@ -3008,6 +3281,53 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3008
3281
|
createMic;
|
|
3009
3282
|
createSink;
|
|
3010
3283
|
storage;
|
|
3284
|
+
/** The validated, frozen onelobby.json this kit was built from (`OneLobby.fromConfig`); undefined for `new OneLobby`. */
|
|
3285
|
+
blueprint;
|
|
3286
|
+
/**
|
|
3287
|
+
* Build a kit from an onelobby.json (already parsed; docs/design/blueprint.md s.7). Throws `bad_config` for an
|
|
3288
|
+
* invalid file or name, `identity_anonymous` for a name when `players.identity` is "anonymous". Then `host()` and
|
|
3289
|
+
* `hostDirect()` default their options from the file and the file's chat/listing switches are enforced.
|
|
3290
|
+
*/
|
|
3291
|
+
static fromConfig(json, overrides = {}) {
|
|
3292
|
+
const bp = parseBlueprint(json);
|
|
3293
|
+
const name = (overrides.displayName ?? "").trim();
|
|
3294
|
+
if (name) {
|
|
3295
|
+
if (bp.players.identity === "anonymous") throw new OneLobbyError("identity_anonymous", "this game uses anonymous players: no display names");
|
|
3296
|
+
if (name.length > bp.players.maxNameLength)
|
|
3297
|
+
throw new BlueprintError("displayName", "length", `must be at most ${bp.players.maxNameLength} characters`);
|
|
3298
|
+
}
|
|
3299
|
+
const kit = new _OneLobby({
|
|
3300
|
+
gameId: bp.game.id,
|
|
3301
|
+
netVersion: bp.game.netVersion,
|
|
3302
|
+
lobbyServer: overrides.lobbyServer ?? bp.hosting.lobbyServer,
|
|
3303
|
+
mode: bp.sync.mode === "lockstep" ? "lockstep" : "raw",
|
|
3304
|
+
// replication is C# only; JS speaks raw
|
|
3305
|
+
tickRate: bp.sync.tickRate,
|
|
3306
|
+
hashEveryTicks: bp.sync.hashEveryTicks,
|
|
3307
|
+
displayName: name,
|
|
3308
|
+
relayOnly: bp.hosting.relayOnly,
|
|
3309
|
+
readyTimeoutMs: bp.session.readyTimeoutMs,
|
|
3310
|
+
peerTimeoutMs: bp.session.peerTimeoutMs,
|
|
3311
|
+
rememberSeat: overrides.rememberSeat ?? bp.lobby.rejoin,
|
|
3312
|
+
advanced: overrides.advanced
|
|
3313
|
+
});
|
|
3314
|
+
kit.blueprint = bp;
|
|
3315
|
+
return kit;
|
|
3316
|
+
}
|
|
3317
|
+
/** @internal Host options the blueprint implies (empty without one). */
|
|
3318
|
+
blueprintHostDefaults() {
|
|
3319
|
+
const bp = this.blueprint;
|
|
3320
|
+
if (!bp) return {};
|
|
3321
|
+
const name = this.options.displayName;
|
|
3322
|
+
return {
|
|
3323
|
+
name: name ? `${name}'s game` : "Game",
|
|
3324
|
+
maxPlayers: bp.lobby.maxPlayers,
|
|
3325
|
+
visibility: bp.lobby.listed ? "public" : bp.lobby.approveJoins ? "private" : "unlisted",
|
|
3326
|
+
lateJoin: bp.lobby.lateJoin,
|
|
3327
|
+
meta: { ...bp.lobby.meta },
|
|
3328
|
+
...bp.session.rateLimit ? { rateLimit: { ...bp.session.rateLimit } } : {}
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3011
3331
|
constructor(o) {
|
|
3012
3332
|
super();
|
|
3013
3333
|
const tickRate = o.tickRate ?? 20;
|
|
@@ -3105,6 +3425,18 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3105
3425
|
};
|
|
3106
3426
|
}
|
|
3107
3427
|
async connect(url = this.options.lobbyServer, factory = this.socketFactory, timeoutMs = 0) {
|
|
3428
|
+
const sig = await this.open(url, factory, timeoutMs);
|
|
3429
|
+
try {
|
|
3430
|
+
const welcome = await sig.request(this.helloMsg(), "welcome");
|
|
3431
|
+
return { sig, welcome };
|
|
3432
|
+
} catch (e) {
|
|
3433
|
+
sig.close();
|
|
3434
|
+
throw e;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
/** The signaling socket only (no hello yet); with a timeout (direct IP), no answer is `direct_unreachable`. */
|
|
3438
|
+
async open(url, factory, timeoutMs) {
|
|
3439
|
+
if (!url.trim() && factory === defaultSocketFactory) throw new OneLobbyError("no_server", "no lobby server address (set hosting.lobbyServer or pass lobbyServer)");
|
|
3108
3440
|
const sig = new SignalingClient(url, factory, this.clock);
|
|
3109
3441
|
if (timeoutMs > 0) {
|
|
3110
3442
|
let cancel = () => {
|
|
@@ -3122,13 +3454,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3122
3454
|
cancel();
|
|
3123
3455
|
}
|
|
3124
3456
|
} else await sig.connect();
|
|
3125
|
-
|
|
3126
|
-
const welcome = await sig.request(this.helloMsg(), "welcome");
|
|
3127
|
-
return { sig, welcome };
|
|
3128
|
-
} catch (e) {
|
|
3129
|
-
sig.close();
|
|
3130
|
-
throw e;
|
|
3131
|
-
}
|
|
3457
|
+
return sig;
|
|
3132
3458
|
}
|
|
3133
3459
|
makeLobby(sig, welcome, state, direct, rateLimit) {
|
|
3134
3460
|
const lobby = new Lobby({
|
|
@@ -3148,6 +3474,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3148
3474
|
rateLimit
|
|
3149
3475
|
});
|
|
3150
3476
|
lobby.errorSink = (e) => this.emit("error", e);
|
|
3477
|
+
if (this.blueprint) guardLobby(lobby, this.blueprint);
|
|
3151
3478
|
if (this.storage && !direct) {
|
|
3152
3479
|
liveSeats.add(lobby);
|
|
3153
3480
|
this.saveSeat(lobby.code, lobby.resumeToken);
|
|
@@ -3159,7 +3486,9 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3159
3486
|
}
|
|
3160
3487
|
return lobby;
|
|
3161
3488
|
}
|
|
3162
|
-
|
|
3489
|
+
/** With a blueprint (`fromConfig`) every option defaults from `lobby.*`; explicit options win. */
|
|
3490
|
+
async host(opts) {
|
|
3491
|
+
const h = { name: "Game", maxPlayers: 4, ...this.blueprintHostDefaults(), ...definedOnly(opts) };
|
|
3163
3492
|
const { sig, welcome } = await this.connect();
|
|
3164
3493
|
try {
|
|
3165
3494
|
const state = await sig.request(
|
|
@@ -3210,6 +3539,30 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3210
3539
|
* The address is also given to ICE as the host's UDP candidate. When the host leaves, `sessionEnded("host_left")`.
|
|
3211
3540
|
*/
|
|
3212
3541
|
async joinDirect(codeOrAddress, opts = {}) {
|
|
3542
|
+
const { addr, links } = this.directTarget(codeOrAddress, opts);
|
|
3543
|
+
const { sig, welcome } = await this.connect(addr.url, this.socketFactory, opts.connectTimeoutMs ?? 5e3);
|
|
3544
|
+
try {
|
|
3545
|
+
const state = await sig.request({ t: "join", ...opts.password ? { password: opts.password } : {} }, "lobby");
|
|
3546
|
+
const lobby = this.makeLobby(sig, welcome, state, { links });
|
|
3547
|
+
lobby.joinCode = joinedWith(addr);
|
|
3548
|
+
return lobby;
|
|
3549
|
+
} catch (e) {
|
|
3550
|
+
sig.close();
|
|
3551
|
+
throw e;
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* Take a direct-IP seat back after a drop or a restart: the code (or address) joined before and that lobby's
|
|
3556
|
+
* `resumeToken` (save it after every join; it changes on each resume). The host keeps a dropped seat for 60 s and the
|
|
3557
|
+
* same peer id comes back. Rejects with `invalid_token` when the seat is gone: offer `joinDirect` then.
|
|
3558
|
+
*/
|
|
3559
|
+
async rejoinDirect(codeOrAddress, resumeToken, opts = {}) {
|
|
3560
|
+
const { addr, links } = this.directTarget(codeOrAddress, opts);
|
|
3561
|
+
const lobby = await this.resumeAt(resumeToken, addr.url, opts.connectTimeoutMs ?? 5e3, { links });
|
|
3562
|
+
lobby.joinCode = joinedWith(addr);
|
|
3563
|
+
return lobby;
|
|
3564
|
+
}
|
|
3565
|
+
directTarget(codeOrAddress, opts) {
|
|
3213
3566
|
let input = codeOrAddress.trim();
|
|
3214
3567
|
if (looksLikeJoinCode(input)) {
|
|
3215
3568
|
const d = decodeJoinCode(input);
|
|
@@ -3221,16 +3574,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3221
3574
|
if (!addr) throw new OneLobbyError("bad_address", `"${codeOrAddress}" is not a join code or an address (try 6B01-RGFE-P2B or 192.168.1.20:47810)`);
|
|
3222
3575
|
const inner = this.linkFactory;
|
|
3223
3576
|
const links = { create: (o) => inner.create({ ...o, iceServers: [], extraRemoteCandidates: [addr.candidate] }) };
|
|
3224
|
-
|
|
3225
|
-
try {
|
|
3226
|
-
const state = await sig.request({ t: "join", ...opts.password ? { password: opts.password } : {} }, "lobby");
|
|
3227
|
-
const lobby = this.makeLobby(sig, welcome, state, { links });
|
|
3228
|
-
lobby.joinCode = joinedWith(addr);
|
|
3229
|
-
return lobby;
|
|
3230
|
-
} catch (e) {
|
|
3231
|
-
sig.close();
|
|
3232
|
-
throw e;
|
|
3233
|
-
}
|
|
3577
|
+
return { addr, links };
|
|
3234
3578
|
}
|
|
3235
3579
|
/** @internal onelobby/direct's host: create the one lobby on the embedded hub, in direct mode. */
|
|
3236
3580
|
async hostDirectWith(h, hooks) {
|
|
@@ -3256,8 +3600,10 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3256
3600
|
}
|
|
3257
3601
|
}
|
|
3258
3602
|
async rejoin(resumeToken) {
|
|
3259
|
-
|
|
3260
|
-
|
|
3603
|
+
return this.resumeAt(resumeToken, this.options.lobbyServer, 0);
|
|
3604
|
+
}
|
|
3605
|
+
async resumeAt(resumeToken, url, timeoutMs, direct) {
|
|
3606
|
+
const sig = await this.open(url, this.socketFactory, timeoutMs);
|
|
3261
3607
|
try {
|
|
3262
3608
|
const lobbyP = sig.expect("lobby");
|
|
3263
3609
|
const welcomeP = sig.request(this.helloMsg(resumeToken), "welcome");
|
|
@@ -3266,13 +3612,14 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3266
3612
|
const welcome = await welcomeP;
|
|
3267
3613
|
if (welcome.peerId === void 0) throw new OneLobbyError("invalid_token", "resume token unknown or expired");
|
|
3268
3614
|
const state = await lobbyP;
|
|
3269
|
-
return this.makeLobby(sig, welcome, state);
|
|
3615
|
+
return this.makeLobby(sig, welcome, state, direct);
|
|
3270
3616
|
} catch (e) {
|
|
3271
3617
|
sig.close();
|
|
3272
3618
|
throw e;
|
|
3273
3619
|
}
|
|
3274
3620
|
}
|
|
3275
3621
|
async list(opts = {}) {
|
|
3622
|
+
if (this.blueprint && !this.blueprint.hosting.server) throw new OneLobbyError("listing_disabled", "this game has no lobby server (hosting.server is false)");
|
|
3276
3623
|
const { sig } = await this.connect();
|
|
3277
3624
|
try {
|
|
3278
3625
|
const res = await sig.request(
|
|
@@ -3293,8 +3640,70 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3293
3640
|
return addr.display;
|
|
3294
3641
|
}
|
|
3295
3642
|
}
|
|
3643
|
+
function definedOnly(o) {
|
|
3644
|
+
const out = {};
|
|
3645
|
+
if (o) {
|
|
3646
|
+
for (const [k, v] of Object.entries(o)) if (v !== void 0) out[k] = v;
|
|
3647
|
+
}
|
|
3648
|
+
return out;
|
|
3649
|
+
}
|
|
3650
|
+
function guardLobby(lobby, bp) {
|
|
3651
|
+
lobby.voice.mode = bp.chat.voiceMode;
|
|
3652
|
+
if (!bp.chat.voice) {
|
|
3653
|
+
const voice = lobby.voice;
|
|
3654
|
+
const d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(voice), "enabled");
|
|
3655
|
+
if (d?.get && d.set) {
|
|
3656
|
+
Object.defineProperty(voice, "enabled", {
|
|
3657
|
+
configurable: true,
|
|
3658
|
+
get: () => d.get.call(voice),
|
|
3659
|
+
set: (v) => {
|
|
3660
|
+
if (v) throw new OneLobbyError("voice_disabled", "this game has voice chat turned off (chat.voice is false)");
|
|
3661
|
+
d.set.call(voice, v);
|
|
3662
|
+
}
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
if (!bp.chat.text) {
|
|
3667
|
+
lobby.chat.send = () => {
|
|
3668
|
+
throw new OneLobbyError("chat_disabled", "this game has text chat turned off (chat.text is false)");
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3296
3672
|
|
|
3297
3673
|
// src/friendly.ts
|
|
3674
|
+
var ErrorCodes = {
|
|
3675
|
+
// from the lobby server or a direct host's hub
|
|
3676
|
+
UnknownCode: "unknown_code",
|
|
3677
|
+
BadCode: "bad_code",
|
|
3678
|
+
LobbyFull: "lobby_full",
|
|
3679
|
+
VersionMismatch: "version_mismatch",
|
|
3680
|
+
InGame: "in_game",
|
|
3681
|
+
Kicked: "kicked",
|
|
3682
|
+
BadPassword: "bad_password",
|
|
3683
|
+
NotHost: "not_host",
|
|
3684
|
+
RateLimited: "rate_limited",
|
|
3685
|
+
ServerFull: "server_full",
|
|
3686
|
+
GameNotAllowed: "game_not_allowed",
|
|
3687
|
+
BadRequest: "bad_request",
|
|
3688
|
+
// from this client
|
|
3689
|
+
ServerClosed: "server_closed",
|
|
3690
|
+
IceFailed: "ice_failed",
|
|
3691
|
+
Timeout: "timeout",
|
|
3692
|
+
DirectUnreachable: "direct_unreachable",
|
|
3693
|
+
BadAddress: "bad_address",
|
|
3694
|
+
PortInUse: "port_in_use",
|
|
3695
|
+
ConnectFailed: "connect_failed",
|
|
3696
|
+
Closed: "closed",
|
|
3697
|
+
ServerLost: "server_lost",
|
|
3698
|
+
InvalidToken: "invalid_token",
|
|
3699
|
+
NoServer: "no_server",
|
|
3700
|
+
// from a blueprint (onelobby.json)
|
|
3701
|
+
BadConfig: "bad_config",
|
|
3702
|
+
VoiceDisabled: "voice_disabled",
|
|
3703
|
+
ChatDisabled: "chat_disabled",
|
|
3704
|
+
ListingDisabled: "listing_disabled",
|
|
3705
|
+
IdentityAnonymous: "identity_anonymous"
|
|
3706
|
+
};
|
|
3298
3707
|
var TEXT = {
|
|
3299
3708
|
// server errors (spec/signaling.md)
|
|
3300
3709
|
unknown_code: { message: "We couldn't find a lobby with that code.", hint: "Check the code with the host. Codes stop working once everyone leaves." },
|
|
@@ -3316,7 +3725,13 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3316
3725
|
server_lost: { message: "Lost the connection to the game server.", hint: "Your game may keep going; new players can't join until it's back." },
|
|
3317
3726
|
invalid_token: { message: "Your old seat in that lobby is no longer available.", hint: "Join again with the lobby code." },
|
|
3318
3727
|
ice_failed: { message: "Couldn't connect to the other players.", hint: "Some networks (school, work, public Wi-Fi) block game connections. Try another network or a phone hotspot." },
|
|
3728
|
+
no_server: { message: "This game has no lobby server address yet.", hint: "Enter one in the multiplayer settings, or host from your own computer with a join code." },
|
|
3319
3729
|
timeout: { message: "The connection took too long.", hint: "Check your internet connection, then try again." },
|
|
3730
|
+
voice_disabled: { message: "Voice chat is turned off in this game." },
|
|
3731
|
+
chat_disabled: { message: "Text chat is turned off in this game." },
|
|
3732
|
+
listing_disabled: { message: "This game has no public lobby list.", hint: "Join with a code or a link from the host." },
|
|
3733
|
+
identity_anonymous: { message: "This game doesn't use player names.", hint: "Everyone shows up as Player 1, Player 2, and so on." },
|
|
3734
|
+
bad_config: { message: "The game's multiplayer settings file is not valid.", hint: "Let the game's owner know; the details are in the error message." },
|
|
3320
3735
|
left: { message: "You left the lobby." },
|
|
3321
3736
|
unknown: { message: "Something went wrong.", hint: "Try again. If it keeps happening, reload the page." }
|
|
3322
3737
|
};
|