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.
@@ -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.4.1";
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
- host(h: HostOptions): Promise<Lobby>;
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
@@ -1,4 +1,4 @@
1
- /*! onelobby 0.4.1 | MIT license */
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.4.1";
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
- async host(h) {
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
  };