onelobby 0.4.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/direct/index.d.ts +1 -1
- package/dist/direct/index.js +16 -1
- package/dist/direct/index.js.map +1 -1
- package/dist/friendly.d.ts +5 -0
- package/dist/friendly.js +12 -0
- package/dist/friendly.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/onelobby.bundle.mjs +5 -5
- package/dist/onelobby.bundle.mjs.map +4 -4
- package/dist/onelobby.d.ts +24 -2
- package/dist/onelobby.global.js +335 -5
- 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 +91 -2
- package/dist/onelobby.js.map +1 -1
- package/package.json +4 -2
package/dist/onelobby.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { Lobby, type RateLimit } from "./lobby.js";
|
|
|
4
4
|
import { type LobbySummary } from "./signaling.js";
|
|
5
5
|
import { type LinkFactory, type SocketFactory } from "./transport.js";
|
|
6
6
|
import type { VoiceCodec, FrameSource, AudioSink } from "./voice-types.js";
|
|
7
|
+
import { type Blueprint } from "./blueprint.js";
|
|
7
8
|
export declare const LIB_NAME = "onelobby";
|
|
8
|
-
export declare const LIB_VERSION = "0.
|
|
9
|
+
export declare const LIB_VERSION = "0.5.0";
|
|
9
10
|
/** Where `rememberSeat` keeps the resume token: the subset of the Web Storage API it uses. */
|
|
10
11
|
export interface SeatStorage {
|
|
11
12
|
getItem(key: string): string | null;
|
|
@@ -73,6 +74,16 @@ export interface OneLobbyOptions {
|
|
|
73
74
|
/** @deprecated use `advanced.createSink` */
|
|
74
75
|
createSink?: (() => AudioSink) | null;
|
|
75
76
|
}
|
|
77
|
+
/** Per-launch settings for `OneLobby.fromConfig`; everything else comes from the blueprint. */
|
|
78
|
+
export interface FromConfigOverrides {
|
|
79
|
+
/** The player's name. Checked against `players.identity` and `players.maxNameLength`. */
|
|
80
|
+
displayName?: string;
|
|
81
|
+
/** Wins over `hosting.lobbyServer` (the file is the bottom layer: put `?lobby=` / storage / `<meta>` lookups here). */
|
|
82
|
+
lobbyServer?: string;
|
|
83
|
+
/** Default: `lobby.rejoin` from the file. */
|
|
84
|
+
rememberSeat?: boolean;
|
|
85
|
+
advanced?: AdvancedOptions;
|
|
86
|
+
}
|
|
76
87
|
export interface HostOptions {
|
|
77
88
|
name: string;
|
|
78
89
|
maxPlayers: number;
|
|
@@ -120,6 +131,16 @@ export declare class OneLobby extends Emitter<OneLobbyEvents> {
|
|
|
120
131
|
private createMic;
|
|
121
132
|
private createSink;
|
|
122
133
|
private storage;
|
|
134
|
+
/** The validated, frozen onelobby.json this kit was built from (`OneLobby.fromConfig`); undefined for `new OneLobby`. */
|
|
135
|
+
readonly blueprint?: Blueprint;
|
|
136
|
+
/**
|
|
137
|
+
* Build a kit from an onelobby.json (already parsed; docs/design/blueprint.md s.7). Throws `bad_config` for an
|
|
138
|
+
* invalid file or name, `identity_anonymous` for a name when `players.identity` is "anonymous". Then `host()` and
|
|
139
|
+
* `hostDirect()` default their options from the file and the file's chat/listing switches are enforced.
|
|
140
|
+
*/
|
|
141
|
+
static fromConfig(json: unknown, overrides?: FromConfigOverrides): OneLobby;
|
|
142
|
+
/** @internal Host options the blueprint implies (empty without one). */
|
|
143
|
+
blueprintHostDefaults(): Partial<HostOptions>;
|
|
123
144
|
constructor(o: OneLobbyOptions);
|
|
124
145
|
private get seatKey();
|
|
125
146
|
private loadSeat;
|
|
@@ -137,7 +158,8 @@ export declare class OneLobby extends Emitter<OneLobbyEvents> {
|
|
|
137
158
|
/** The signaling socket only (no hello yet); with a timeout (direct IP), no answer is `direct_unreachable`. */
|
|
138
159
|
private open;
|
|
139
160
|
private makeLobby;
|
|
140
|
-
|
|
161
|
+
/** With a blueprint (`fromConfig`) every option defaults from `lobby.*`; explicit options win. */
|
|
162
|
+
host(opts?: Partial<HostOptions>): Promise<Lobby>;
|
|
141
163
|
join(code: string): Promise<Lobby>;
|
|
142
164
|
/**
|
|
143
165
|
* Join a direct-IP host (docs/direct-ip.md): a join code from the host's screen (`6B01-RGFE-P2B`, any case, spaces
|
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,6 +22,7 @@ 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,
|
|
@@ -42,12 +43,14 @@ var OneLobby = (() => {
|
|
|
42
43
|
closeCodeFor: () => closeCodeFor,
|
|
43
44
|
codeFromUrl: () => codeFromUrl,
|
|
44
45
|
decodeJoinCode: () => decodeJoinCode,
|
|
46
|
+
defineBlueprint: () => defineBlueprint,
|
|
45
47
|
defineLobbyElement: () => defineLobbyElement,
|
|
46
48
|
describeError: () => describeError,
|
|
47
49
|
formatCode: () => formatCode,
|
|
48
50
|
lobbyServerUrl: () => lobbyServerUrl,
|
|
49
51
|
looksLikeJoinCode: () => looksLikeJoinCode,
|
|
50
52
|
normalizeCode: () => normalizeCode,
|
|
53
|
+
parseBlueprint: () => parseBlueprint,
|
|
51
54
|
shareUrl: () => shareUrl,
|
|
52
55
|
webCodecsOpusAvailable: () => webCodecsOpusAvailable,
|
|
53
56
|
webRtcLinkFactoryFor: () => webRtcLinkFactoryFor
|
|
@@ -3014,9 +3017,243 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3014
3017
|
return { host, port, isIPv6: v6, display, url: `ws://${display}/`, candidate: `candidate:1 1 UDP 2122317823 ${host} ${port} typ host` };
|
|
3015
3018
|
}
|
|
3016
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
|
+
|
|
3017
3254
|
// src/onelobby.ts
|
|
3018
3255
|
var LIB_NAME = "onelobby";
|
|
3019
|
-
var LIB_VERSION = "0.
|
|
3256
|
+
var LIB_VERSION = "0.5.0";
|
|
3020
3257
|
var DEFAULT_READY_TIMEOUT_MS = 15e3;
|
|
3021
3258
|
function hasMic() {
|
|
3022
3259
|
return typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof AudioWorkletNode !== "undefined";
|
|
@@ -3034,7 +3271,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3034
3271
|
for (const l of liveSeats) if (l.resumeToken === token) return true;
|
|
3035
3272
|
return false;
|
|
3036
3273
|
}
|
|
3037
|
-
var OneLobby = class extends Emitter {
|
|
3274
|
+
var OneLobby = class _OneLobby extends Emitter {
|
|
3038
3275
|
options;
|
|
3039
3276
|
socketFactory;
|
|
3040
3277
|
linkFactory;
|
|
@@ -3044,6 +3281,53 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3044
3281
|
createMic;
|
|
3045
3282
|
createSink;
|
|
3046
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
|
+
}
|
|
3047
3331
|
constructor(o) {
|
|
3048
3332
|
super();
|
|
3049
3333
|
const tickRate = o.tickRate ?? 20;
|
|
@@ -3152,6 +3436,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3152
3436
|
}
|
|
3153
3437
|
/** The signaling socket only (no hello yet); with a timeout (direct IP), no answer is `direct_unreachable`. */
|
|
3154
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)");
|
|
3155
3440
|
const sig = new SignalingClient(url, factory, this.clock);
|
|
3156
3441
|
if (timeoutMs > 0) {
|
|
3157
3442
|
let cancel = () => {
|
|
@@ -3189,6 +3474,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3189
3474
|
rateLimit
|
|
3190
3475
|
});
|
|
3191
3476
|
lobby.errorSink = (e) => this.emit("error", e);
|
|
3477
|
+
if (this.blueprint) guardLobby(lobby, this.blueprint);
|
|
3192
3478
|
if (this.storage && !direct) {
|
|
3193
3479
|
liveSeats.add(lobby);
|
|
3194
3480
|
this.saveSeat(lobby.code, lobby.resumeToken);
|
|
@@ -3200,7 +3486,9 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3200
3486
|
}
|
|
3201
3487
|
return lobby;
|
|
3202
3488
|
}
|
|
3203
|
-
|
|
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) };
|
|
3204
3492
|
const { sig, welcome } = await this.connect();
|
|
3205
3493
|
try {
|
|
3206
3494
|
const state = await sig.request(
|
|
@@ -3331,6 +3619,7 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3331
3619
|
}
|
|
3332
3620
|
}
|
|
3333
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)");
|
|
3334
3623
|
const { sig } = await this.connect();
|
|
3335
3624
|
try {
|
|
3336
3625
|
const res = await sig.request(
|
|
@@ -3351,6 +3640,35 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3351
3640
|
return addr.display;
|
|
3352
3641
|
}
|
|
3353
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
|
+
}
|
|
3354
3672
|
|
|
3355
3673
|
// src/friendly.ts
|
|
3356
3674
|
var ErrorCodes = {
|
|
@@ -3378,7 +3696,13 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3378
3696
|
Closed: "closed",
|
|
3379
3697
|
ServerLost: "server_lost",
|
|
3380
3698
|
InvalidToken: "invalid_token",
|
|
3381
|
-
NoServer: "no_server"
|
|
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"
|
|
3382
3706
|
};
|
|
3383
3707
|
var TEXT = {
|
|
3384
3708
|
// server errors (spec/signaling.md)
|
|
@@ -3401,7 +3725,13 @@ registerProcessor('onelobby-capture', OneLobbyCapture);
|
|
|
3401
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." },
|
|
3402
3726
|
invalid_token: { message: "Your old seat in that lobby is no longer available.", hint: "Join again with the lobby code." },
|
|
3403
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." },
|
|
3404
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." },
|
|
3405
3735
|
left: { message: "You left the lobby." },
|
|
3406
3736
|
unknown: { message: "Something went wrong.", hint: "Try again. If it keeps happening, reload the page." }
|
|
3407
3737
|
};
|