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.
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Blueprint v1: the `onelobby.json` settings file (docs/design/blueprint.md s.4, spec/blueprint.schema.json).
3
+ * `parseBlueprint` validates by hand (the package has no dependencies), fills the defaults and returns a frozen
4
+ * object. Errors carry the dotted `path` and the `rule` id the shared vectors (spec/vectors/blueprint.json) expect.
5
+ */
6
+ import { OneLobbyError } from "./signaling.js";
7
+ /** Identity function for TypeScript completion in a `.ts` config (like Vite's `defineConfig`). */
8
+ export function defineBlueprint(x) {
9
+ return x;
10
+ }
11
+ /** `bad_config`: the file is not a valid blueprint. `path` is dotted ("" = root), `rule` a vector rule id. */
12
+ export class BlueprintError extends OneLobbyError {
13
+ path;
14
+ rule;
15
+ constructor(path, rule, detail) {
16
+ super("bad_config", `onelobby.json: ${path ? path + ": " : ""}${detail}`);
17
+ this.path = path;
18
+ this.rule = rule;
19
+ }
20
+ }
21
+ const obj = (props, req) => ({ k: "obj", props, req });
22
+ const bool = { k: "bool" };
23
+ const str = (o = {}) => ({ k: "str", ...o });
24
+ const int = (min, max, zeroOr) => ({ k: "int", min, max, zeroOr });
25
+ const en = (...vals) => ({ k: "enum", vals });
26
+ const ROOT = obj({
27
+ version: { k: "const", val: 1 },
28
+ game: obj({ id: str({ pattern: /^[A-Za-z0-9._-]{1,64}$/ }), netVersion: str({ min: 1, max: 32 }), title: str({ max: 64 }) }, ["id"]),
29
+ engine: en("web", "node-browser", "godot-csharp", "unity", "dotnet"),
30
+ hosting: obj({
31
+ direct: bool,
32
+ server: bool,
33
+ lobbyServer: str(),
34
+ directPort: int(1024, 65535),
35
+ openPort: bool,
36
+ relayOnly: bool,
37
+ allowedOrigins: { k: "strarr" },
38
+ }),
39
+ lobby: obj({
40
+ maxPlayers: int(2, 64),
41
+ listed: bool,
42
+ approveJoins: bool,
43
+ password: bool,
44
+ lateJoin: bool,
45
+ rejoin: bool,
46
+ joinLink: bool,
47
+ meta: { k: "meta" },
48
+ }),
49
+ sync: obj({ mode: en("raw", "lockstep", "replication"), tickRate: int(1, 120), hashEveryTicks: int(1, 100000) }),
50
+ players: obj({ identity: en("name", "anonymous", "platform"), maxNameLength: int(1, 24) }),
51
+ chat: obj({ text: bool, voice: bool, voiceMode: en("pushToTalk", "voiceActivity") }),
52
+ session: obj({
53
+ hostLeaves: en("end", "migrate"),
54
+ pause: en("off", "host", "anyone"),
55
+ readyTimeoutMs: int(0, 600000),
56
+ peerTimeoutMs: int(6000, 600000, true),
57
+ rateLimit: { k: "rate" },
58
+ runInBackground: bool,
59
+ }),
60
+ platforms: obj({ steam: { k: "steam" }, epic: { k: "epic" }, discord: bool }),
61
+ ui: obj({ kind: en("drop-in", "generated", "none"), entry: str(), theme: en("light", "dark", "auto") }),
62
+ }, ["game"]);
63
+ /** Allowed (and ignored) anywhere. */
64
+ const IGNORED = new Set(["$schema", "$comment"]);
65
+ class Fail {
66
+ path;
67
+ rule;
68
+ detail;
69
+ constructor(path, rule, detail) {
70
+ this.path = path;
71
+ this.rule = rule;
72
+ this.detail = detail;
73
+ }
74
+ }
75
+ const join = (p, k) => (p ? `${p}.${k}` : String(k));
76
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
77
+ function check(n, v, path) {
78
+ switch (n.k) {
79
+ case "obj": {
80
+ if (!isObj(v))
81
+ throw new Fail(path, "type", "must be an object");
82
+ for (const r of n.req ?? [])
83
+ if (!(r in v))
84
+ throw new Fail(join(path, r), "required", "is required");
85
+ for (const k of Object.keys(v))
86
+ if (!IGNORED.has(k) && !(k in n.props))
87
+ throw new Fail(join(path, k), "unknown_key", "unknown key");
88
+ for (const [k, sub] of Object.entries(n.props))
89
+ if (k in v)
90
+ check(sub, v[k], join(path, k));
91
+ return;
92
+ }
93
+ case "str": {
94
+ if (typeof v !== "string")
95
+ throw new Fail(path, "type", "must be a string");
96
+ if ((n.min !== undefined && v.length < n.min) || (n.max !== undefined && v.length > n.max))
97
+ throw new Fail(path, "length", `must be ${n.min ?? 0} to ${n.max ?? "any"} characters`);
98
+ if (n.pattern && !n.pattern.test(v))
99
+ throw new Fail(path, "pattern", `must match ${n.pattern.source}`);
100
+ return;
101
+ }
102
+ case "int": {
103
+ if (typeof v !== "number" || !Number.isInteger(v))
104
+ throw new Fail(path, "type", "must be an integer");
105
+ if (n.zeroOr && v === 0)
106
+ return;
107
+ if ((n.min !== undefined && v < n.min) || (n.max !== undefined && v > n.max))
108
+ throw new Fail(path, "range", `must be ${n.zeroOr ? "0 or " : ""}${n.min ?? "any"} to ${n.max ?? "any"}`);
109
+ return;
110
+ }
111
+ case "bool":
112
+ if (typeof v !== "boolean")
113
+ throw new Fail(path, "type", "must be true or false");
114
+ return;
115
+ case "enum":
116
+ if (typeof v !== "string" || !n.vals.includes(v))
117
+ throw new Fail(path, "enum", `must be one of ${n.vals.join(", ")}`);
118
+ return;
119
+ case "const":
120
+ if (v !== n.val)
121
+ throw new Fail(path, "const", `must be ${n.val}`);
122
+ return;
123
+ case "strarr": {
124
+ if (!Array.isArray(v))
125
+ throw new Fail(path, "type", "must be an array of strings");
126
+ v.forEach((x, i) => {
127
+ if (typeof x !== "string")
128
+ throw new Fail(join(path, i), "type", "must be a string");
129
+ });
130
+ return;
131
+ }
132
+ case "meta": {
133
+ if (!isObj(v))
134
+ throw new Fail(path, "type", "must be an object of strings");
135
+ const keys = Object.keys(v);
136
+ if (keys.length > 16)
137
+ throw new Fail(path, "length", "at most 16 keys");
138
+ for (const k of keys) {
139
+ if (k.length > 64)
140
+ throw new Fail(join(path, k), "length", "key is longer than 64 characters");
141
+ const x = v[k];
142
+ if (typeof x !== "string")
143
+ throw new Fail(join(path, k), "type", "must be a string");
144
+ if (x.length > 64)
145
+ throw new Fail(join(path, k), "length", "value is longer than 64 characters");
146
+ }
147
+ return;
148
+ }
149
+ case "rate":
150
+ check(obj({ messages: int(1), perMs: int(1) }, ["messages", "perMs"]), v, path);
151
+ return;
152
+ case "steam":
153
+ if (typeof v === "boolean")
154
+ return;
155
+ if (!isObj(v))
156
+ throw new Fail(path, "type", "must be true, false or { appId }");
157
+ check(obj({ appId: int(1) }), v, path);
158
+ return;
159
+ case "epic": {
160
+ if (typeof v === "boolean")
161
+ return;
162
+ if (!isObj(v))
163
+ throw new Fail(path, "type", "must be true, false or { productId }");
164
+ for (const k of Object.keys(v))
165
+ if (!IGNORED.has(k) && typeof v[k] !== "string")
166
+ throw new Fail(join(path, k), "type", "must be a string");
167
+ return;
168
+ }
169
+ }
170
+ }
171
+ // ---- defaults and cross rules -----------------------------------------------------------------------------------
172
+ const sect = (x) => (isObj(x) ? x : {});
173
+ function strip(o) {
174
+ if (Array.isArray(o))
175
+ return o.map(strip);
176
+ if (!isObj(o))
177
+ return o;
178
+ const out = {};
179
+ for (const [k, v] of Object.entries(o))
180
+ if (k !== "$comment" && k !== "$schema")
181
+ out[k] = strip(v);
182
+ return out;
183
+ }
184
+ function deepFreeze(o) {
185
+ if (typeof o === "object" && o !== null && !Object.isFrozen(o)) {
186
+ Object.freeze(o);
187
+ for (const v of Object.values(o))
188
+ deepFreeze(v);
189
+ }
190
+ return o;
191
+ }
192
+ function fill(input) {
193
+ const game = sect(input.game);
194
+ const engine = input.engine;
195
+ const hosting = sect(input.hosting);
196
+ const lobby = sect(input.lobby);
197
+ const sync = sect(input.sync);
198
+ const players = sect(input.players);
199
+ const chat = sect(input.chat);
200
+ const session = sect(input.session);
201
+ const platforms = sect(input.platforms);
202
+ const ui = sect(input.ui);
203
+ const tickRate = sync.tickRate ?? 20;
204
+ const web = engine === "web";
205
+ const out = {
206
+ version: 1,
207
+ game: { id: game.id, netVersion: game.netVersion ?? "1", ...(game.title !== undefined ? { title: game.title } : {}) },
208
+ ...(engine !== undefined ? { engine } : {}),
209
+ hosting: {
210
+ direct: hosting.direct ?? !web,
211
+ server: hosting.server ?? web,
212
+ lobbyServer: hosting.lobbyServer ?? "",
213
+ directPort: hosting.directPort ?? 47810,
214
+ openPort: hosting.openPort ?? true,
215
+ relayOnly: hosting.relayOnly ?? false,
216
+ allowedOrigins: hosting.allowedOrigins ?? [],
217
+ },
218
+ lobby: {
219
+ maxPlayers: lobby.maxPlayers ?? 4,
220
+ listed: lobby.listed ?? false,
221
+ approveJoins: lobby.approveJoins ?? false,
222
+ password: lobby.password ?? false,
223
+ lateJoin: lobby.lateJoin ?? true,
224
+ rejoin: lobby.rejoin ?? true,
225
+ joinLink: lobby.joinLink ?? (web || engine === "node-browser"),
226
+ meta: lobby.meta ?? {},
227
+ },
228
+ sync: { mode: sync.mode ?? "raw", tickRate, hashEveryTicks: sync.hashEveryTicks ?? tickRate },
229
+ players: { identity: players.identity ?? "name", maxNameLength: players.maxNameLength ?? 24 },
230
+ chat: { text: chat.text ?? true, voice: chat.voice ?? false, voiceMode: chat.voiceMode ?? "pushToTalk" },
231
+ session: {
232
+ hostLeaves: session.hostLeaves ?? "end",
233
+ pause: session.pause ?? "off",
234
+ readyTimeoutMs: session.readyTimeoutMs ?? 15000,
235
+ peerTimeoutMs: session.peerTimeoutMs ?? 0,
236
+ ...(session.rateLimit !== undefined ? { rateLimit: session.rateLimit } : {}),
237
+ runInBackground: session.runInBackground ?? true,
238
+ },
239
+ platforms: { steam: platforms.steam ?? false, epic: platforms.epic ?? false, discord: platforms.discord ?? false },
240
+ ui: {
241
+ kind: ui.kind ?? (web ? "drop-in" : "generated"),
242
+ ...(ui.entry !== undefined ? { entry: ui.entry } : {}),
243
+ theme: ui.theme ?? "auto",
244
+ },
245
+ };
246
+ return out;
247
+ }
248
+ function crossRules(b) {
249
+ if (!b.hosting.direct && !b.hosting.server)
250
+ throw new Fail("hosting", "hosting_none", "at least one of direct, server must be true");
251
+ if (b.lobby.listed && b.lobby.approveJoins)
252
+ throw new Fail("lobby", "listed_and_approve", "listed and approveJoins cannot both be true");
253
+ if (b.engine === "web" && b.hosting.direct)
254
+ throw new Fail("hosting.direct", "direct_needs_listener", "engine web cannot host direct (a browser cannot listen on a port)");
255
+ const csharp = b.engine === "godot-csharp" || b.engine === "unity" || b.engine === "dotnet";
256
+ if (b.sync.mode === "replication" && !csharp)
257
+ throw new Fail("sync.mode", "replication_needs_csharp", "replication needs engine godot-csharp, unity or dotnet");
258
+ if (b.sync.mode === "replication" && b.session.hostLeaves === "migrate")
259
+ throw new Fail("session.hostLeaves", "migrate_with_replication", "replication has no host migration; use end");
260
+ if (b.session.hostLeaves === "migrate" && !b.hosting.server)
261
+ throw new Fail("session.hostLeaves", "migrate_needs_server", "migrate needs hosting.server (direct lobbies always end)");
262
+ }
263
+ /**
264
+ * Validate `json` (an already-parsed onelobby.json), fill every default and return the frozen result.
265
+ * Throws a `BlueprintError` (`OneLobbyError` code `bad_config`, with `path` and `rule`) on the first problem.
266
+ */
267
+ export function parseBlueprint(json) {
268
+ let b;
269
+ try {
270
+ check(ROOT, json, "");
271
+ b = fill(strip(json));
272
+ crossRules(b);
273
+ }
274
+ catch (e) {
275
+ if (e instanceof Fail)
276
+ throw new BlueprintError(e.path, e.rule, e.detail);
277
+ throw e;
278
+ }
279
+ return deepFreeze(b);
280
+ }
281
+ //# sourceMappingURL=blueprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blueprint.js","sourceRoot":"","sources":["../src/blueprint.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAmG/C,kGAAkG;AAClG,MAAM,UAAU,eAAe,CAA2B,CAAI;IAC5D,OAAO,CAAC,CAAC;AACX,CAAC;AAED,8GAA8G;AAC9G,MAAM,OAAO,cAAe,SAAQ,aAAa;IAC5B;IAAqB;IAAxC,YAAmB,IAAY,EAAS,IAAY,EAAE,MAAc;QAClE,KAAK,CAAC,YAAY,EAAE,kBAAkB,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;QADzD,SAAI,GAAJ,IAAI,CAAQ;QAAS,SAAI,GAAJ,IAAI,CAAQ;IAEpD,CAAC;CACF;AAiBD,MAAM,GAAG,GAAG,CAAC,KAA2B,EAAE,GAAc,EAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;AAC9F,MAAM,IAAI,GAAS,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;AACjC,MAAM,GAAG,GAAG,CAAC,IAAsD,EAAE,EAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;AACrG,MAAM,GAAG,GAAG,CAAC,GAAY,EAAE,GAAY,EAAE,MAAgB,EAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;AACrG,MAAM,EAAE,GAAG,CAAC,GAAG,IAAc,EAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAE9D,MAAM,IAAI,GAAS,GAAG,CACpB;IACE,OAAO,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE;IAC/B,IAAI,EAAE,GAAG,CACP,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,wBAAwB,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,EACjH,CAAC,IAAI,CAAC,CACP;IACD,MAAM,EAAE,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,OAAO,EAAE,QAAQ,CAAC;IACpE,OAAO,EAAE,GAAG,CAAC;QACX,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,IAAI;QACZ,WAAW,EAAE,GAAG,EAAE;QAClB,UAAU,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC;QAC5B,QAAQ,EAAE,IAAI;QACd,SAAS,EAAE,IAAI;QACf,cAAc,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE;KAChC,CAAC;IACF,KAAK,EAAE,GAAG,CAAC;QACT,UAAU,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC;QACtB,MAAM,EAAE,IAAI;QACZ,YAAY,EAAE,IAAI;QAClB,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE;KACpB,CAAC;IACF,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,aAAa,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,cAAc,EAAE,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC;IAChH,OAAO,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,UAAU,CAAC,EAAE,aAAa,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;IAC1F,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,CAAC,YAAY,EAAE,eAAe,CAAC,EAAE,CAAC;IACpF,OAAO,EAAE,GAAG,CAAC;QACX,UAAU,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC;QAChC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC;QAClC,cAAc,EAAE,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC;QAC9B,aAAa,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC;QACtC,SAAS,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE;QACxB,eAAe,EAAE,IAAI;KACtB,CAAC;IACF,SAAS,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7E,EAAE,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;CACxG,EACD,CAAC,MAAM,CAAC,CACT,CAAC;AAEF,sCAAsC;AACtC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;AAEjD,MAAM,IAAI;IACW;IAAqB;IAAqB;IAA7D,YAAmB,IAAY,EAAS,IAAY,EAAS,MAAc;QAAxD,SAAI,GAAJ,IAAI,CAAQ;QAAS,SAAI,GAAJ,IAAI,CAAQ;QAAS,WAAM,GAAN,MAAM,CAAQ;IAAG,CAAC;CAChF;AACD,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,CAAkB,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9E,MAAM,KAAK,GAAG,CAAC,CAAU,EAAgC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAErH,SAAS,KAAK,CAAC,CAAO,EAAE,CAAU,EAAE,IAAY;IAC9C,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QACZ,KAAK,KAAK,CAAC,CAAC,CAAC;YACX,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAC;YACjE,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE;gBAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;oBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,aAAa,CAAC,CAAC;YACrG,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;gBAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC;oBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,aAAa,CAAC,CAAC;YACpI,KAAK,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC;oBAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAC5F,OAAO;QACT,CAAC;QACD,KAAK,KAAK,CAAC,CAAC,CAAC;YACX,IAAI,OAAO,CAAC,KAAK,QAAQ;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,kBAAkB,CAAC,CAAC;YAC5E,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC;gBACxF,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,KAAK,aAAa,CAAC,CAAC;YAC1F,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;YACvG,OAAO;QACT,CAAC;QACD,KAAK,KAAK,CAAC,CAAC,CAAC;YACX,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,oBAAoB,CAAC,CAAC;YACtG,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC;gBAAE,OAAO;YAChC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC;gBAC1E,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC;YAC5G,OAAO;QACT,CAAC;QACD,KAAK,MAAM;YACT,IAAI,OAAO,CAAC,KAAK,SAAS;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,uBAAuB,CAAC,CAAC;YAClF,OAAO;QACT,KAAK,MAAM;YACT,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,kBAAkB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACtH,OAAO;QACT,KAAK,OAAO;YACV,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;YACnE,OAAO;QACT,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,6BAA6B,CAAC,CAAC;YACnF,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;gBACjB,IAAI,OAAO,CAAC,KAAK,QAAQ;oBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,kBAAkB,CAAC,CAAC;YACvF,CAAC,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,8BAA8B,CAAC,CAAC;YAC5E,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC5B,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC;YACxE,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBACrB,IAAI,CAAC,CAAC,MAAM,GAAG,EAAE;oBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,kCAAkC,CAAC,CAAC;gBAC/F,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACf,IAAI,OAAO,CAAC,KAAK,QAAQ;oBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,kBAAkB,CAAC,CAAC;gBACrF,IAAI,CAAC,CAAC,MAAM,GAAG,EAAE;oBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,oCAAoC,CAAC,CAAC;YACnG,CAAC;YACD,OAAO;QACT,CAAC;QACD,KAAK,MAAM;YACT,KAAK,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;YAChF,OAAO;QACT,KAAK,OAAO;YACV,IAAI,OAAO,CAAC,KAAK,SAAS;gBAAE,OAAO;YACnC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,kCAAkC,CAAC,CAAC;YAChF,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;YACvC,OAAO;QACT,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,IAAI,OAAO,CAAC,KAAK,SAAS;gBAAE,OAAO;YACnC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;gBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,sCAAsC,CAAC,CAAC;YACpF,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;gBAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ;oBAAE,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,kBAAkB,CAAC,CAAC;YAC3I,OAAO;QACT,CAAC;IACH,CAAC;AACH,CAAC;AAED,oHAAoH;AAEpH,MAAM,IAAI,GAAG,CAAC,CAAU,EAA2B,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAC1E,SAAS,KAAK,CAAC,CAAU;IACvB,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACxB,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,IAAI,CAAC,KAAK,UAAU,IAAI,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACnG,OAAO,GAAG,CAAC;AACb,CAAC;AACD,SAAS,UAAU,CAAI,CAAI;IACzB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/D,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACjB,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,IAAI,CAAC,KAA8B;IAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAqC,CAAC;IAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACxC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC1B,MAAM,QAAQ,GAAI,IAAI,CAAC,QAA+B,IAAI,EAAE,CAAC;IAC7D,MAAM,GAAG,GAAG,MAAM,KAAK,KAAK,CAAC;IAC7B,MAAM,GAAG,GAAG;QACV,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;QACrH,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,OAAO,EAAE;YACP,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG;YAC9B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,GAAG;YAC7B,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,EAAE;YACtC,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,KAAK;YACvC,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,IAAI;YAClC,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,KAAK;YACrC,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,EAAE;SAC7C;QACD,KAAK,EAAE;YACL,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,CAAC;YACjC,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,KAAK;YAC7B,YAAY,EAAE,KAAK,CAAC,YAAY,IAAI,KAAK;YACzC,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,KAAK;YACjC,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,IAAI;YAChC,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI;YAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,CAAC,GAAG,IAAI,MAAM,KAAK,cAAc,CAAC;YAC9D,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;SACvB;QACD,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,QAAQ,EAAE;QAC7F,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,MAAM,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,EAAE,EAAE;QAC7F,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,YAAY,EAAE;QACxG,OAAO,EAAE;YACP,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,KAAK;YACvC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,KAAK;YAC7B,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,KAAK;YAC/C,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,CAAC;YACzC,GAAG,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5E,eAAe,EAAE,OAAO,CAAC,eAAe,IAAI,IAAI;SACjD;QACD,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,IAAI,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,IAAI,KAAK,EAAE;QAClH,EAAE,EAAE;YACF,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC;YAChD,GAAG,CAAC,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACtD,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,MAAM;SAC1B;KACF,CAAC;IACF,OAAO,GAA2B,CAAC;AACrC,CAAC;AAED,SAAS,UAAU,CAAC,CAAY;IAC9B,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM;QAAE,MAAM,IAAI,IAAI,CAAC,SAAS,EAAE,cAAc,EAAE,6CAA6C,CAAC,CAAC;IACrI,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,CAAC,YAAY;QAAE,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,oBAAoB,EAAE,6CAA6C,CAAC,CAAC;IACzI,IAAI,CAAC,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM;QAAE,MAAM,IAAI,IAAI,CAAC,gBAAgB,EAAE,uBAAuB,EAAE,mEAAmE,CAAC,CAAC;IAC3K,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,KAAK,cAAc,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC;IAC5F,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,aAAa,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,IAAI,CAAC,WAAW,EAAE,0BAA0B,EAAE,wDAAwD,CAAC,CAAC;IAChK,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,aAAa,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS;QAAE,MAAM,IAAI,IAAI,CAAC,oBAAoB,EAAE,0BAA0B,EAAE,4CAA4C,CAAC,CAAC;IACxL,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM;QAAE,MAAM,IAAI,IAAI,CAAC,oBAAoB,EAAE,sBAAsB,EAAE,0DAA0D,CAAC,CAAC;AACxL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAa;IAC1C,IAAI,CAAY,CAAC;IACjB,IAAI,CAAC;QACH,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QACtB,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC,CAAC;QACjD,UAAU,CAAC,CAAC,CAAC,CAAC;IAChB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,CAAC,YAAY,IAAI;YAAE,MAAM,IAAI,cAAc,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;QAC1E,MAAM,CAAC,CAAC;IACV,CAAC;IACD,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC","sourcesContent":["/**\n * Blueprint v1: the `onelobby.json` settings file (docs/design/blueprint.md s.4, spec/blueprint.schema.json).\n * `parseBlueprint` validates by hand (the package has no dependencies), fills the defaults and returns a frozen\n * object. Errors carry the dotted `path` and the `rule` id the shared vectors (spec/vectors/blueprint.json) expect.\n */\nimport { OneLobbyError } from \"./signaling.js\";\n\nexport type BlueprintEngine = \"web\" | \"node-browser\" | \"godot-csharp\" | \"unity\" | \"dotnet\";\n\nexport interface Blueprint {\n readonly version: 1;\n readonly game: { readonly id: string; readonly netVersion: string; readonly title?: string };\n readonly engine?: BlueprintEngine;\n readonly hosting: {\n readonly direct: boolean;\n readonly server: boolean;\n readonly lobbyServer: string;\n readonly directPort: number;\n readonly openPort: boolean;\n readonly relayOnly: boolean;\n readonly allowedOrigins: readonly string[];\n };\n readonly lobby: {\n readonly maxPlayers: number;\n readonly listed: boolean;\n readonly approveJoins: boolean;\n readonly password: boolean;\n readonly lateJoin: boolean;\n readonly rejoin: boolean;\n readonly joinLink: boolean;\n readonly meta: Readonly<Record<string, string>>;\n };\n readonly sync: {\n readonly mode: \"raw\" | \"lockstep\" | \"replication\";\n readonly tickRate: number;\n readonly hashEveryTicks: number;\n };\n readonly players: { readonly identity: \"name\" | \"anonymous\" | \"platform\"; readonly maxNameLength: number };\n readonly chat: { readonly text: boolean; readonly voice: boolean; readonly voiceMode: \"pushToTalk\" | \"voiceActivity\" };\n readonly session: {\n readonly hostLeaves: \"end\" | \"migrate\";\n readonly pause: \"off\" | \"host\" | \"anyone\";\n readonly readyTimeoutMs: number;\n readonly peerTimeoutMs: number;\n readonly rateLimit?: { readonly messages: number; readonly perMs: number };\n readonly runInBackground: boolean;\n };\n readonly platforms: {\n readonly steam: boolean | { readonly appId?: number };\n readonly epic: boolean | { readonly [k: string]: string };\n readonly discord: boolean;\n };\n readonly ui: { readonly kind: \"drop-in\" | \"generated\" | \"none\"; readonly entry?: string; readonly theme: \"light\" | \"dark\" | \"auto\" };\n}\n\n/** What you may write: only `game.id` is required. */\nexport interface BlueprintInput {\n $schema?: string;\n $comment?: string;\n version?: 1;\n game: { $comment?: string; id: string; netVersion?: string; title?: string };\n engine?: BlueprintEngine;\n hosting?: {\n $comment?: string;\n direct?: boolean;\n server?: boolean;\n lobbyServer?: string;\n directPort?: number;\n openPort?: boolean;\n relayOnly?: boolean;\n allowedOrigins?: string[];\n };\n lobby?: {\n $comment?: string;\n maxPlayers?: number;\n listed?: boolean;\n approveJoins?: boolean;\n password?: boolean;\n lateJoin?: boolean;\n rejoin?: boolean;\n joinLink?: boolean;\n meta?: Record<string, string>;\n };\n sync?: { $comment?: string; mode?: \"raw\" | \"lockstep\" | \"replication\"; tickRate?: number; hashEveryTicks?: number };\n players?: { $comment?: string; identity?: \"name\" | \"anonymous\" | \"platform\"; maxNameLength?: number };\n chat?: { $comment?: string; text?: boolean; voice?: boolean; voiceMode?: \"pushToTalk\" | \"voiceActivity\" };\n session?: {\n $comment?: string;\n hostLeaves?: \"end\" | \"migrate\";\n pause?: \"off\" | \"host\" | \"anyone\";\n readyTimeoutMs?: number;\n peerTimeoutMs?: number;\n rateLimit?: { messages: number; perMs: number };\n runInBackground?: boolean;\n };\n platforms?: {\n $comment?: string;\n steam?: boolean | { appId?: number };\n epic?: boolean | { [k: string]: string };\n discord?: boolean;\n };\n ui?: { $comment?: string; kind?: \"drop-in\" | \"generated\" | \"none\"; entry?: string; theme?: \"light\" | \"dark\" | \"auto\" };\n}\n\n/** Identity function for TypeScript completion in a `.ts` config (like Vite's `defineConfig`). */\nexport function defineBlueprint<T extends BlueprintInput>(x: T): T {\n return x;\n}\n\n/** `bad_config`: the file is not a valid blueprint. `path` is dotted (\"\" = root), `rule` a vector rule id. */\nexport class BlueprintError extends OneLobbyError {\n constructor(public path: string, public rule: string, detail: string) {\n super(\"bad_config\", `onelobby.json: ${path ? path + \": \" : \"\"}${detail}`);\n }\n}\n\n// ---- schema as data (key order = spec/blueprint.schema.json) ----------------------------------------------------\n\ntype Node =\n | { k: \"obj\"; req?: string[]; props: Record<string, Node> }\n | { k: \"str\"; pattern?: RegExp; min?: number; max?: number }\n | { k: \"int\"; min?: number; max?: number; zeroOr?: boolean }\n | { k: \"bool\" }\n | { k: \"enum\"; vals: string[] }\n | { k: \"const\"; val: number }\n | { k: \"strarr\" }\n | { k: \"meta\" }\n | { k: \"rate\" }\n | { k: \"steam\" }\n | { k: \"epic\" };\n\nconst obj = (props: Record<string, Node>, req?: string[]): Node => ({ k: \"obj\", props, req });\nconst bool: Node = { k: \"bool\" };\nconst str = (o: { pattern?: RegExp; min?: number; max?: number } = {}): Node => ({ k: \"str\", ...o });\nconst int = (min?: number, max?: number, zeroOr?: boolean): Node => ({ k: \"int\", min, max, zeroOr });\nconst en = (...vals: string[]): Node => ({ k: \"enum\", vals });\n\nconst ROOT: Node = obj(\n {\n version: { k: \"const\", val: 1 },\n game: obj(\n { id: str({ pattern: /^[A-Za-z0-9._-]{1,64}$/ }), netVersion: str({ min: 1, max: 32 }), title: str({ max: 64 }) },\n [\"id\"],\n ),\n engine: en(\"web\", \"node-browser\", \"godot-csharp\", \"unity\", \"dotnet\"),\n hosting: obj({\n direct: bool,\n server: bool,\n lobbyServer: str(),\n directPort: int(1024, 65535),\n openPort: bool,\n relayOnly: bool,\n allowedOrigins: { k: \"strarr\" },\n }),\n lobby: obj({\n maxPlayers: int(2, 64),\n listed: bool,\n approveJoins: bool,\n password: bool,\n lateJoin: bool,\n rejoin: bool,\n joinLink: bool,\n meta: { k: \"meta\" },\n }),\n sync: obj({ mode: en(\"raw\", \"lockstep\", \"replication\"), tickRate: int(1, 120), hashEveryTicks: int(1, 100000) }),\n players: obj({ identity: en(\"name\", \"anonymous\", \"platform\"), maxNameLength: int(1, 24) }),\n chat: obj({ text: bool, voice: bool, voiceMode: en(\"pushToTalk\", \"voiceActivity\") }),\n session: obj({\n hostLeaves: en(\"end\", \"migrate\"),\n pause: en(\"off\", \"host\", \"anyone\"),\n readyTimeoutMs: int(0, 600000),\n peerTimeoutMs: int(6000, 600000, true),\n rateLimit: { k: \"rate\" },\n runInBackground: bool,\n }),\n platforms: obj({ steam: { k: \"steam\" }, epic: { k: \"epic\" }, discord: bool }),\n ui: obj({ kind: en(\"drop-in\", \"generated\", \"none\"), entry: str(), theme: en(\"light\", \"dark\", \"auto\") }),\n },\n [\"game\"],\n);\n\n/** Allowed (and ignored) anywhere. */\nconst IGNORED = new Set([\"$schema\", \"$comment\"]);\n\nclass Fail {\n constructor(public path: string, public rule: string, public detail: string) {}\n}\nconst join = (p: string, k: string | number) => (p ? `${p}.${k}` : String(k));\nconst isObj = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\nfunction check(n: Node, v: unknown, path: string): void {\n switch (n.k) {\n case \"obj\": {\n if (!isObj(v)) throw new Fail(path, \"type\", \"must be an object\");\n for (const r of n.req ?? []) if (!(r in v)) throw new Fail(join(path, r), \"required\", \"is required\");\n 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\");\n for (const [k, sub] of Object.entries(n.props)) if (k in v) check(sub, v[k], join(path, k));\n return;\n }\n case \"str\": {\n if (typeof v !== \"string\") throw new Fail(path, \"type\", \"must be a string\");\n if ((n.min !== undefined && v.length < n.min) || (n.max !== undefined && v.length > n.max))\n throw new Fail(path, \"length\", `must be ${n.min ?? 0} to ${n.max ?? \"any\"} characters`);\n if (n.pattern && !n.pattern.test(v)) throw new Fail(path, \"pattern\", `must match ${n.pattern.source}`);\n return;\n }\n case \"int\": {\n if (typeof v !== \"number\" || !Number.isInteger(v)) throw new Fail(path, \"type\", \"must be an integer\");\n if (n.zeroOr && v === 0) return;\n if ((n.min !== undefined && v < n.min) || (n.max !== undefined && v > n.max))\n throw new Fail(path, \"range\", `must be ${n.zeroOr ? \"0 or \" : \"\"}${n.min ?? \"any\"} to ${n.max ?? \"any\"}`);\n return;\n }\n case \"bool\":\n if (typeof v !== \"boolean\") throw new Fail(path, \"type\", \"must be true or false\");\n return;\n case \"enum\":\n if (typeof v !== \"string\" || !n.vals.includes(v)) throw new Fail(path, \"enum\", `must be one of ${n.vals.join(\", \")}`);\n return;\n case \"const\":\n if (v !== n.val) throw new Fail(path, \"const\", `must be ${n.val}`);\n return;\n case \"strarr\": {\n if (!Array.isArray(v)) throw new Fail(path, \"type\", \"must be an array of strings\");\n v.forEach((x, i) => {\n if (typeof x !== \"string\") throw new Fail(join(path, i), \"type\", \"must be a string\");\n });\n return;\n }\n case \"meta\": {\n if (!isObj(v)) throw new Fail(path, \"type\", \"must be an object of strings\");\n const keys = Object.keys(v);\n if (keys.length > 16) throw new Fail(path, \"length\", \"at most 16 keys\");\n for (const k of keys) {\n if (k.length > 64) throw new Fail(join(path, k), \"length\", \"key is longer than 64 characters\");\n const x = v[k];\n if (typeof x !== \"string\") throw new Fail(join(path, k), \"type\", \"must be a string\");\n if (x.length > 64) throw new Fail(join(path, k), \"length\", \"value is longer than 64 characters\");\n }\n return;\n }\n case \"rate\":\n check(obj({ messages: int(1), perMs: int(1) }, [\"messages\", \"perMs\"]), v, path);\n return;\n case \"steam\":\n if (typeof v === \"boolean\") return;\n if (!isObj(v)) throw new Fail(path, \"type\", \"must be true, false or { appId }\");\n check(obj({ appId: int(1) }), v, path);\n return;\n case \"epic\": {\n if (typeof v === \"boolean\") return;\n if (!isObj(v)) throw new Fail(path, \"type\", \"must be true, false or { productId }\");\n 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\");\n return;\n }\n }\n}\n\n// ---- defaults and cross rules -----------------------------------------------------------------------------------\n\nconst sect = (x: unknown): Record<string, unknown> => (isObj(x) ? x : {});\nfunction strip(o: unknown): unknown {\n if (Array.isArray(o)) return o.map(strip);\n if (!isObj(o)) return o;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(o)) if (k !== \"$comment\" && k !== \"$schema\") out[k] = strip(v);\n return out;\n}\nfunction deepFreeze<T>(o: T): T {\n if (typeof o === \"object\" && o !== null && !Object.isFrozen(o)) {\n Object.freeze(o);\n for (const v of Object.values(o)) deepFreeze(v);\n }\n return o;\n}\n\nfunction fill(input: Record<string, unknown>): Blueprint {\n const game = sect(input.game);\n const engine = input.engine as BlueprintEngine | undefined;\n const hosting = sect(input.hosting);\n const lobby = sect(input.lobby);\n const sync = sect(input.sync);\n const players = sect(input.players);\n const chat = sect(input.chat);\n const session = sect(input.session);\n const platforms = sect(input.platforms);\n const ui = sect(input.ui);\n const tickRate = (sync.tickRate as number | undefined) ?? 20;\n const web = engine === \"web\";\n const out = {\n version: 1,\n game: { id: game.id, netVersion: game.netVersion ?? \"1\", ...(game.title !== undefined ? { title: game.title } : {}) },\n ...(engine !== undefined ? { engine } : {}),\n hosting: {\n direct: hosting.direct ?? !web,\n server: hosting.server ?? web,\n lobbyServer: hosting.lobbyServer ?? \"\",\n directPort: hosting.directPort ?? 47810,\n openPort: hosting.openPort ?? true,\n relayOnly: hosting.relayOnly ?? false,\n allowedOrigins: hosting.allowedOrigins ?? [],\n },\n lobby: {\n maxPlayers: lobby.maxPlayers ?? 4,\n listed: lobby.listed ?? false,\n approveJoins: lobby.approveJoins ?? false,\n password: lobby.password ?? false,\n lateJoin: lobby.lateJoin ?? true,\n rejoin: lobby.rejoin ?? true,\n joinLink: lobby.joinLink ?? (web || engine === \"node-browser\"),\n meta: lobby.meta ?? {},\n },\n sync: { mode: sync.mode ?? \"raw\", tickRate, hashEveryTicks: sync.hashEveryTicks ?? tickRate },\n players: { identity: players.identity ?? \"name\", maxNameLength: players.maxNameLength ?? 24 },\n chat: { text: chat.text ?? true, voice: chat.voice ?? false, voiceMode: chat.voiceMode ?? \"pushToTalk\" },\n session: {\n hostLeaves: session.hostLeaves ?? \"end\",\n pause: session.pause ?? \"off\",\n readyTimeoutMs: session.readyTimeoutMs ?? 15000,\n peerTimeoutMs: session.peerTimeoutMs ?? 0,\n ...(session.rateLimit !== undefined ? { rateLimit: session.rateLimit } : {}),\n runInBackground: session.runInBackground ?? true,\n },\n platforms: { steam: platforms.steam ?? false, epic: platforms.epic ?? false, discord: platforms.discord ?? false },\n ui: {\n kind: ui.kind ?? (web ? \"drop-in\" : \"generated\"),\n ...(ui.entry !== undefined ? { entry: ui.entry } : {}),\n theme: ui.theme ?? \"auto\",\n },\n };\n return out as unknown as Blueprint;\n}\n\nfunction crossRules(b: Blueprint): void {\n if (!b.hosting.direct && !b.hosting.server) throw new Fail(\"hosting\", \"hosting_none\", \"at least one of direct, server must be true\");\n if (b.lobby.listed && b.lobby.approveJoins) throw new Fail(\"lobby\", \"listed_and_approve\", \"listed and approveJoins cannot both be true\");\n 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)\");\n const csharp = b.engine === \"godot-csharp\" || b.engine === \"unity\" || b.engine === \"dotnet\";\n if (b.sync.mode === \"replication\" && !csharp) throw new Fail(\"sync.mode\", \"replication_needs_csharp\", \"replication needs engine godot-csharp, unity or dotnet\");\n if (b.sync.mode === \"replication\" && b.session.hostLeaves === \"migrate\") throw new Fail(\"session.hostLeaves\", \"migrate_with_replication\", \"replication has no host migration; use end\");\n if (b.session.hostLeaves === \"migrate\" && !b.hosting.server) throw new Fail(\"session.hostLeaves\", \"migrate_needs_server\", \"migrate needs hosting.server (direct lobbies always end)\");\n}\n\n/**\n * Validate `json` (an already-parsed onelobby.json), fill every default and return the frozen result.\n * Throws a `BlueprintError` (`OneLobbyError` code `bad_config`, with `path` and `rule`) on the first problem.\n */\nexport function parseBlueprint(json: unknown): Blueprint {\n let b: Blueprint;\n try {\n check(ROOT, json, \"\");\n b = fill(strip(json) as Record<string, unknown>);\n crossRules(b);\n } catch (e) {\n if (e instanceof Fail) throw new BlueprintError(e.path, e.rule, e.detail);\n throw e;\n }\n return deepFreeze(b);\n}\n"]}
@@ -67,4 +67,4 @@ export type DirectLobby = Lobby & {
67
67
  */
68
68
  /** With `port` left out, how many ports after the default `hostDirect` tries when it is busy. */
69
69
  export declare const DEFAULT_FALLBACK_PORTS = 9;
70
- export declare function hostDirect(kit: OneLobby, o?: DirectHostOptions): Promise<DirectLobby>;
70
+ export declare function hostDirect(kit: OneLobby, opts?: DirectHostOptions): Promise<DirectLobby>;
@@ -17,7 +17,22 @@ export { webRtcLinkFactoryFor } from "../webrtc.js";
17
17
  */
18
18
  /** With `port` left out, how many ports after the default `hostDirect` tries when it is busy. */
19
19
  export const DEFAULT_FALLBACK_PORTS = 9;
20
- export async function hostDirect(kit, o = {}) {
20
+ export async function hostDirect(kit, opts = {}) {
21
+ const bp = kit.blueprint;
22
+ const defined = Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== undefined));
23
+ // a blueprint supplies hosting.* and lobby.* (explicit options win)
24
+ const o = bp
25
+ ? {
26
+ port: bp.hosting.directPort,
27
+ openPort: bp.hosting.openPort,
28
+ allowedOrigins: [...bp.hosting.allowedOrigins],
29
+ maxPlayers: bp.lobby.maxPlayers,
30
+ lateJoin: bp.lobby.lateJoin,
31
+ meta: { ...bp.lobby.meta },
32
+ ...(bp.session.rateLimit ? { rateLimit: { ...bp.session.rateLimit } } : {}),
33
+ ...defined,
34
+ }
35
+ : defined;
21
36
  const port = o.port ?? DEFAULT_DIRECT_PORT;
22
37
  // busy (another program holds it): try the next fallbackPorts; the join code carries the port
23
38
  const last = port + Math.max(0, o.fallbackPorts ?? (o.port === undefined ? DEFAULT_FALLBACK_PORTS : 0));
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/direct/index.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference types=\"node\" />\n/**\n * `onelobby/direct` (Node only): host a direct-IP lobby from a Node process, with no server of anyone's\n * (docs/direct-ip.md). The process runs the signaling hub, listens on TCP `port`, binds its peer links to UDP `port`,\n * asks the router to forward both (UPnP, then NAT-PMP) and hands out a join code. Joiners use `kit.joinDirect(code)`\n * from `onelobby` (Node or browser) or `JoinDirectAsync` in C#; the wire protocol is the same, so they mix.\n *\n * WebRTC in Node needs an RTCPeerConnection: `npm install node-datachannel`, then\n * `advanced: { linkFactory: webRtcLinkFactoryFor(RTCPeerConnection) }` with\n * `import { RTCPeerConnection } from \"node-datachannel/polyfill\"`. Its libdatachannel honours the port pin.\n */\nimport type { HostOptions, OneLobby } from \"../onelobby.js\";\nimport type { Lobby, RateLimit } from \"../lobby.js\";\nimport type { LinkFactory } from \"../transport.js\";\nimport { encodeJoinCode } from \"../joincode.js\";\nimport { DEFAULT_DIRECT_PORT } from \"../direct-address.js\";\nimport { DirectHubHost, PortInUseError, portFailureHint, type HttpHandler } from \"./hub-host.js\";\nimport { directHubOptions } from \"./hub.js\";\nimport { isPublicIPv4, lanIPv4, localAddresses, type PortMapResult, type PortMapperOptions } from \"./portmap.js\";\nimport { OneLobbyError } from \"../signaling.js\";\n\nexport { DirectHubHost, LocalHubSocket, PortInUseError, originAllowed } from \"./hub-host.js\";\nexport type { HttpHandler, HttpReply } from \"./hub-host.js\";\nexport { SignalingHub, Session, RateWindow, sanitize, defaultHubOptions, directHubOptions, PROTO } from \"./hub.js\";\nexport type { HubOptions, HubDeps } from \"./hub.js\";\nexport { PortMapper, isPublicIPv4, lanIPv4, localAddresses } from \"./portmap.js\";\nexport type { PortMapResult, PortMapperOptions, PortMapMethod } from \"./portmap.js\";\nexport { webRtcLinkFactoryFor } from \"../webrtc.js\";\n\nexport interface DirectHostOptions {\n /**\n * TCP and UDP port to listen on (default 47810). When it is busy (another program holds it) the next free one of the\n * following `fallbackPorts` is used; the join code carries the port, so friends never type it.\n */\n port?: number;\n /** How many ports after `port` to try when it is busy; default 9 with `port` left out, else 0. */\n fallbackPorts?: number;\n /** Optional; joiners must send the same one or get `bad_password`. */\n password?: string;\n maxPlayers?: number;\n /** Lobby name; default \"<displayName>'s game\". */\n name?: string;\n lateJoin?: boolean;\n meta?: Record<string, string>;\n /** Ask the router to forward the port so the code works from the internet (s.2a). The host-menu toggle; default on. */\n openPort?: boolean;\n /** Test seam and knobs for the router request (description defaults to \"OneLobby <gameId>\"). */\n portMapping?: PortMapperOptions;\n /**\n * Serves plain-HTTP `GET`s on the same port, typically a join page for browsers (recipe s.4b). The page joins with\n * `kit.joinDirect(location.host)`; the lobby's `joinUrl` is then the link to share.\n */\n http?: HttpHandler;\n /**\n * Browser `Origin` values allowed to open a signaling socket (exact match, e.g. \"https://play.example\"; \"*\" allows\n * every origin). Native clients send no Origin and are always allowed, as are pages served from the host's own address\n * (`http`) and localhost pages, so this is only for a web game hosted somewhere else that joins players to this host.\n */\n allowedOrigins?: string[];\n /** Per-joiner cap on incoming raw messages, e.g. `{ messages: 60, perMs: 1000 }`; the excess is dropped. */\n rateLimit?: RateLimit;\n}\n\n/** A direct host lobby: the normal Lobby, plus the hub behind it. Leaving or ending the lobby stops the hub. */\nexport type DirectLobby = Lobby & {\n directHub: DirectHubHost;\n /** With the `http` option: `http://<the code's address>:<port>/`, the link a browser opens to join. Else null. */\n joinUrl: string | null;\n};\n\n/**\n * Hosts a direct-IP lobby (docs/direct-ip.md s.3): starts the hub and listener on `port`, opens the router port\n * (unless `openPort: false`), connects `kit` to the hub in memory and creates the lobby. Resolves with the lobby;\n * show `lobby.joinCode` with `lobby.joinCodeKind` (\"internet\" or \"same_network\"). Rejects with `port_in_use`.\n * When the lobby ends (leave, or the process exits) the listener closes and the router mapping is removed; joiners\n * get `sessionEnded(\"host_left\")`.\n */\n/** With `port` left out, how many ports after the default `hostDirect` tries when it is busy. */\nexport const DEFAULT_FALLBACK_PORTS = 9;\n\nexport async function hostDirect(kit: OneLobby, o: DirectHostOptions = {}): Promise<DirectLobby> {\n const port = o.port ?? DEFAULT_DIRECT_PORT;\n // busy (another program holds it): try the next fallbackPorts; the join code carries the port\n const last = port + Math.max(0, o.fallbackPorts ?? (o.port === undefined ? DEFAULT_FALLBACK_PORTS : 0));\n const maxPlayers = o.maxPlayers ?? 4;\n let hub: DirectHubHost | undefined;\n let anyBusy = false;\n let anyReserved = false;\n for (let p = port; !hub; p++) {\n try {\n hub = await DirectHubHost.listen(p, directHubOptions(maxPlayers, o.password));\n } catch (e) {\n if (!(e instanceof PortInUseError)) throw e;\n if (e.reserved) anyReserved = true;\n else anyBusy = true;\n if (p < last) continue;\n const range = last > port ? `ports ${port}-${last}` : `port ${port}`;\n throw new OneLobbyError(\"port_in_use\", `cannot listen on ${range}: ${portFailureHint(anyBusy, anyReserved)}`);\n }\n }\n hub.http = o.http ?? null;\n hub.allowedOrigins = o.allowedOrigins ?? [];\n // The router request runs alongside lobby creation; it takes up to a few seconds when nothing answers.\n const mapping: Promise<PortMapResult | null> =\n o.openPort === false\n ? Promise.resolve(null)\n : hub.openPort({ description: `OneLobby ${kit.options.gameId}`, ...o.portMapping }).catch((e: Error) => ({\n mapped: false, method: \"none\" as const, externalIPv4: null, localIPv4: null, detail: e.message,\n }));\n const wrapLinks = (inner: LinkFactory): LinkFactory => ({\n create: (lo) => inner.create({ ...lo, iceServers: [], portRange: { begin: hub.port, end: hub.port } }),\n });\n let lobby: Lobby;\n try {\n const name = o.name ?? `${kit.options.displayName || \"Player\"}'s game`;\n const h: HostOptions = { name, maxPlayers, lateJoin: o.lateJoin ?? true, meta: o.meta, rateLimit: o.rateLimit };\n lobby = await kit.hostDirectWith(h, { socketFactory: () => hub.connectLocal(), wrapLinks });\n } catch (e) {\n await mapping;\n await hub.close();\n throw e;\n }\n lobby.on(\"sessionEnded\", () => void hub.close());\n // leave() resolves once the router mapping is deleted, so a host that exits right after it does not leave the port\n // forwarded until the lease runs out (2 h, or never on routers that only grant permanent leases)\n const leave = lobby.leave.bind(lobby);\n lobby.leave = async () => {\n try {\n await leave();\n } finally {\n await hub.close();\n }\n };\n const map = await mapping;\n lobby.directAddresses = localAddresses(hub.port);\n lobby.portMapping = map ? map.detail : \"off\";\n let codeIp: string | null = null;\n if (map?.mapped && isPublicIPv4(map.externalIPv4)) {\n codeIp = map.externalIPv4!;\n lobby.joinCode = encodeJoinCode(codeIp, hub.port);\n lobby.joinCodeKind = \"internet\";\n } else {\n codeIp = map?.localIPv4 ?? (await lanIPv4());\n if (codeIp) {\n lobby.joinCode = encodeJoinCode(codeIp, hub.port);\n lobby.joinCodeKind = \"same_network\";\n }\n }\n const joinUrl = o.http && codeIp ? `http://${codeIp}:${hub.port}/` : null;\n return Object.assign(lobby, { directHub: hub, joinUrl });\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/direct/index.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference types=\"node\" />\n/**\n * `onelobby/direct` (Node only): host a direct-IP lobby from a Node process, with no server of anyone's\n * (docs/direct-ip.md). The process runs the signaling hub, listens on TCP `port`, binds its peer links to UDP `port`,\n * asks the router to forward both (UPnP, then NAT-PMP) and hands out a join code. Joiners use `kit.joinDirect(code)`\n * from `onelobby` (Node or browser) or `JoinDirectAsync` in C#; the wire protocol is the same, so they mix.\n *\n * WebRTC in Node needs an RTCPeerConnection: `npm install node-datachannel`, then\n * `advanced: { linkFactory: webRtcLinkFactoryFor(RTCPeerConnection) }` with\n * `import { RTCPeerConnection } from \"node-datachannel/polyfill\"`. Its libdatachannel honours the port pin.\n */\nimport type { HostOptions, OneLobby } from \"../onelobby.js\";\nimport type { Lobby, RateLimit } from \"../lobby.js\";\nimport type { LinkFactory } from \"../transport.js\";\nimport { encodeJoinCode } from \"../joincode.js\";\nimport { DEFAULT_DIRECT_PORT } from \"../direct-address.js\";\nimport { DirectHubHost, PortInUseError, portFailureHint, type HttpHandler } from \"./hub-host.js\";\nimport { directHubOptions } from \"./hub.js\";\nimport { isPublicIPv4, lanIPv4, localAddresses, type PortMapResult, type PortMapperOptions } from \"./portmap.js\";\nimport { OneLobbyError } from \"../signaling.js\";\n\nexport { DirectHubHost, LocalHubSocket, PortInUseError, originAllowed } from \"./hub-host.js\";\nexport type { HttpHandler, HttpReply } from \"./hub-host.js\";\nexport { SignalingHub, Session, RateWindow, sanitize, defaultHubOptions, directHubOptions, PROTO } from \"./hub.js\";\nexport type { HubOptions, HubDeps } from \"./hub.js\";\nexport { PortMapper, isPublicIPv4, lanIPv4, localAddresses } from \"./portmap.js\";\nexport type { PortMapResult, PortMapperOptions, PortMapMethod } from \"./portmap.js\";\nexport { webRtcLinkFactoryFor } from \"../webrtc.js\";\n\nexport interface DirectHostOptions {\n /**\n * TCP and UDP port to listen on (default 47810). When it is busy (another program holds it) the next free one of the\n * following `fallbackPorts` is used; the join code carries the port, so friends never type it.\n */\n port?: number;\n /** How many ports after `port` to try when it is busy; default 9 with `port` left out, else 0. */\n fallbackPorts?: number;\n /** Optional; joiners must send the same one or get `bad_password`. */\n password?: string;\n maxPlayers?: number;\n /** Lobby name; default \"<displayName>'s game\". */\n name?: string;\n lateJoin?: boolean;\n meta?: Record<string, string>;\n /** Ask the router to forward the port so the code works from the internet (s.2a). The host-menu toggle; default on. */\n openPort?: boolean;\n /** Test seam and knobs for the router request (description defaults to \"OneLobby <gameId>\"). */\n portMapping?: PortMapperOptions;\n /**\n * Serves plain-HTTP `GET`s on the same port, typically a join page for browsers (recipe s.4b). The page joins with\n * `kit.joinDirect(location.host)`; the lobby's `joinUrl` is then the link to share.\n */\n http?: HttpHandler;\n /**\n * Browser `Origin` values allowed to open a signaling socket (exact match, e.g. \"https://play.example\"; \"*\" allows\n * every origin). Native clients send no Origin and are always allowed, as are pages served from the host's own address\n * (`http`) and localhost pages, so this is only for a web game hosted somewhere else that joins players to this host.\n */\n allowedOrigins?: string[];\n /** Per-joiner cap on incoming raw messages, e.g. `{ messages: 60, perMs: 1000 }`; the excess is dropped. */\n rateLimit?: RateLimit;\n}\n\n/** A direct host lobby: the normal Lobby, plus the hub behind it. Leaving or ending the lobby stops the hub. */\nexport type DirectLobby = Lobby & {\n directHub: DirectHubHost;\n /** With the `http` option: `http://<the code's address>:<port>/`, the link a browser opens to join. Else null. */\n joinUrl: string | null;\n};\n\n/**\n * Hosts a direct-IP lobby (docs/direct-ip.md s.3): starts the hub and listener on `port`, opens the router port\n * (unless `openPort: false`), connects `kit` to the hub in memory and creates the lobby. Resolves with the lobby;\n * show `lobby.joinCode` with `lobby.joinCodeKind` (\"internet\" or \"same_network\"). Rejects with `port_in_use`.\n * When the lobby ends (leave, or the process exits) the listener closes and the router mapping is removed; joiners\n * get `sessionEnded(\"host_left\")`.\n */\n/** With `port` left out, how many ports after the default `hostDirect` tries when it is busy. */\nexport const DEFAULT_FALLBACK_PORTS = 9;\n\nexport async function hostDirect(kit: OneLobby, opts: DirectHostOptions = {}): Promise<DirectLobby> {\n const bp = kit.blueprint;\n const defined = Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== undefined)) as DirectHostOptions;\n // a blueprint supplies hosting.* and lobby.* (explicit options win)\n const o: DirectHostOptions = bp\n ? {\n port: bp.hosting.directPort,\n openPort: bp.hosting.openPort,\n allowedOrigins: [...bp.hosting.allowedOrigins],\n maxPlayers: bp.lobby.maxPlayers,\n lateJoin: bp.lobby.lateJoin,\n meta: { ...bp.lobby.meta },\n ...(bp.session.rateLimit ? { rateLimit: { ...bp.session.rateLimit } } : {}),\n ...defined,\n }\n : defined;\n const port = o.port ?? DEFAULT_DIRECT_PORT;\n // busy (another program holds it): try the next fallbackPorts; the join code carries the port\n const last = port + Math.max(0, o.fallbackPorts ?? (o.port === undefined ? DEFAULT_FALLBACK_PORTS : 0));\n const maxPlayers = o.maxPlayers ?? 4;\n let hub: DirectHubHost | undefined;\n let anyBusy = false;\n let anyReserved = false;\n for (let p = port; !hub; p++) {\n try {\n hub = await DirectHubHost.listen(p, directHubOptions(maxPlayers, o.password));\n } catch (e) {\n if (!(e instanceof PortInUseError)) throw e;\n if (e.reserved) anyReserved = true;\n else anyBusy = true;\n if (p < last) continue;\n const range = last > port ? `ports ${port}-${last}` : `port ${port}`;\n throw new OneLobbyError(\"port_in_use\", `cannot listen on ${range}: ${portFailureHint(anyBusy, anyReserved)}`);\n }\n }\n hub.http = o.http ?? null;\n hub.allowedOrigins = o.allowedOrigins ?? [];\n // The router request runs alongside lobby creation; it takes up to a few seconds when nothing answers.\n const mapping: Promise<PortMapResult | null> =\n o.openPort === false\n ? Promise.resolve(null)\n : hub.openPort({ description: `OneLobby ${kit.options.gameId}`, ...o.portMapping }).catch((e: Error) => ({\n mapped: false, method: \"none\" as const, externalIPv4: null, localIPv4: null, detail: e.message,\n }));\n const wrapLinks = (inner: LinkFactory): LinkFactory => ({\n create: (lo) => inner.create({ ...lo, iceServers: [], portRange: { begin: hub.port, end: hub.port } }),\n });\n let lobby: Lobby;\n try {\n const name = o.name ?? `${kit.options.displayName || \"Player\"}'s game`;\n const h: HostOptions = { name, maxPlayers, lateJoin: o.lateJoin ?? true, meta: o.meta, rateLimit: o.rateLimit };\n lobby = await kit.hostDirectWith(h, { socketFactory: () => hub.connectLocal(), wrapLinks });\n } catch (e) {\n await mapping;\n await hub.close();\n throw e;\n }\n lobby.on(\"sessionEnded\", () => void hub.close());\n // leave() resolves once the router mapping is deleted, so a host that exits right after it does not leave the port\n // forwarded until the lease runs out (2 h, or never on routers that only grant permanent leases)\n const leave = lobby.leave.bind(lobby);\n lobby.leave = async () => {\n try {\n await leave();\n } finally {\n await hub.close();\n }\n };\n const map = await mapping;\n lobby.directAddresses = localAddresses(hub.port);\n lobby.portMapping = map ? map.detail : \"off\";\n let codeIp: string | null = null;\n if (map?.mapped && isPublicIPv4(map.externalIPv4)) {\n codeIp = map.externalIPv4!;\n lobby.joinCode = encodeJoinCode(codeIp, hub.port);\n lobby.joinCodeKind = \"internet\";\n } else {\n codeIp = map?.localIPv4 ?? (await lanIPv4());\n if (codeIp) {\n lobby.joinCode = encodeJoinCode(codeIp, hub.port);\n lobby.joinCodeKind = \"same_network\";\n }\n }\n const joinUrl = o.http && codeIp ? `http://${codeIp}:${hub.port}/` : null;\n return Object.assign(lobby, { directHub: hub, joinUrl });\n}\n"]}
@@ -35,6 +35,11 @@ export declare const ErrorCodes: {
35
35
  readonly ServerLost: "server_lost";
36
36
  readonly InvalidToken: "invalid_token";
37
37
  readonly NoServer: "no_server";
38
+ readonly BadConfig: "bad_config";
39
+ readonly VoiceDisabled: "voice_disabled";
40
+ readonly ChatDisabled: "chat_disabled";
41
+ readonly ListingDisabled: "listing_disabled";
42
+ readonly IdentityAnonymous: "identity_anonymous";
38
43
  };
39
44
  export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
40
45
  /**
package/dist/friendly.js CHANGED
@@ -34,6 +34,12 @@ export const ErrorCodes = {
34
34
  ServerLost: "server_lost",
35
35
  InvalidToken: "invalid_token",
36
36
  NoServer: "no_server",
37
+ // from a blueprint (onelobby.json)
38
+ BadConfig: "bad_config",
39
+ VoiceDisabled: "voice_disabled",
40
+ ChatDisabled: "chat_disabled",
41
+ ListingDisabled: "listing_disabled",
42
+ IdentityAnonymous: "identity_anonymous",
37
43
  };
38
44
  const TEXT = {
39
45
  // server errors (spec/signaling.md)
@@ -56,7 +62,13 @@ const TEXT = {
56
62
  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." },
57
63
  invalid_token: { message: "Your old seat in that lobby is no longer available.", hint: "Join again with the lobby code." },
58
64
  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." },
65
+ 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." },
59
66
  timeout: { message: "The connection took too long.", hint: "Check your internet connection, then try again." },
67
+ voice_disabled: { message: "Voice chat is turned off in this game." },
68
+ chat_disabled: { message: "Text chat is turned off in this game." },
69
+ listing_disabled: { message: "This game has no public lobby list.", hint: "Join with a code or a link from the host." },
70
+ identity_anonymous: { message: "This game doesn't use player names.", hint: "Everyone shows up as Player 1, Player 2, and so on." },
71
+ 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." },
60
72
  left: { message: "You left the lobby." },
61
73
  unknown: { message: "Something went wrong.", hint: "Try again. If it keeps happening, reload the page." },
62
74
  };
@@ -1 +1 @@
1
- {"version":3,"file":"friendly.js","sourceRoot":"","sources":["../src/friendly.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAa3D;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,+CAA+C;IAC/C,WAAW,EAAE,cAAc;IAC3B,OAAO,EAAE,UAAU;IACnB,SAAS,EAAE,YAAY;IACvB,eAAe,EAAE,kBAAkB;IACnC,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,QAAQ;IAChB,WAAW,EAAE,cAAc;IAC3B,OAAO,EAAE,UAAU;IACnB,WAAW,EAAE,cAAc;IAC3B,UAAU,EAAE,aAAa;IACzB,cAAc,EAAE,kBAAkB;IAClC,UAAU,EAAE,aAAa;IACzB,mBAAmB;IACnB,YAAY,EAAE,eAAe;IAC7B,SAAS,EAAE,YAAY;IACvB,OAAO,EAAE,SAAS;IAClB,iBAAiB,EAAE,oBAAoB;IACvC,UAAU,EAAE,aAAa;IACzB,SAAS,EAAE,aAAa;IACxB,aAAa,EAAE,gBAAgB;IAC/B,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,aAAa;IACzB,YAAY,EAAE,eAAe;IAC7B,QAAQ,EAAE,WAAW;CACb,CAAC;AAGX,MAAM,IAAI,GAA0B;IAClC,oCAAoC;IACpC,YAAY,EAAE,EAAE,OAAO,EAAE,0CAA0C,EAAE,IAAI,EAAE,wEAAwE,EAAE;IACrJ,QAAQ,EAAE,EAAE,OAAO,EAAE,sCAAsC,EAAE,IAAI,EAAE,gDAAgD,EAAE;IACrH,UAAU,EAAE,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,qDAAqD,EAAE;IAC3G,gBAAgB,EAAE,EAAE,OAAO,EAAE,8DAA8D,EAAE,IAAI,EAAE,qEAAqE,EAAE;IAC1K,OAAO,EAAE,EAAE,OAAO,EAAE,gCAAgC,EAAE,IAAI,EAAE,uEAAuE,EAAE;IACrI,MAAM,EAAE,EAAE,OAAO,EAAE,sCAAsC,EAAE;IAC3D,MAAM,EAAE,EAAE,OAAO,EAAE,uCAAuC,EAAE,IAAI,EAAE,8CAA8C,EAAE;IAClH,QAAQ,EAAE,EAAE,OAAO,EAAE,4BAA4B,EAAE;IACnD,YAAY,EAAE,EAAE,OAAO,EAAE,qDAAqD,EAAE,IAAI,EAAE,gCAAgC,EAAE;IACxH,WAAW,EAAE,EAAE,OAAO,EAAE,iEAAiE,EAAE,IAAI,EAAE,kDAAkD,EAAE;IACrJ,WAAW,EAAE,EAAE,OAAO,EAAE,oCAAoC,EAAE,IAAI,EAAE,6BAA6B,EAAE;IACnG,gBAAgB,EAAE,EAAE,OAAO,EAAE,0CAA0C,EAAE,IAAI,EAAE,oEAAoE,EAAE;IACrJ,WAAW,EAAE,EAAE,OAAO,EAAE,uDAAuD,EAAE,IAAI,EAAE,oFAAoF,EAAE;IAC7K,gBAAgB;IAChB,cAAc,EAAE,EAAE,OAAO,EAAE,iCAAiC,EAAE,IAAI,EAAE,iDAAiD,EAAE;IACvH,MAAM,EAAE,EAAE,OAAO,EAAE,yCAAyC,EAAE,IAAI,EAAE,iDAAiD,EAAE;IACvH,WAAW,EAAE,EAAE,OAAO,EAAE,yCAAyC,EAAE,IAAI,EAAE,mEAAmE,EAAE;IAC9I,aAAa,EAAE,EAAE,OAAO,EAAE,qDAAqD,EAAE,IAAI,EAAE,iCAAiC,EAAE;IAC1H,UAAU,EAAE,EAAE,OAAO,EAAE,wCAAwC,EAAE,IAAI,EAAE,4GAA4G,EAAE;IACrL,OAAO,EAAE,EAAE,OAAO,EAAE,+BAA+B,EAAE,IAAI,EAAE,iDAAiD,EAAE;IAC9G,IAAI,EAAE,EAAE,OAAO,EAAE,qBAAqB,EAAE;IACxC,OAAO,EAAE,EAAE,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,oDAAoD,EAAE;CAC1G,CAAC;AAEF,MAAM,OAAO,GAAG,mBAAmB,CAAC;AAEpC,SAAS,MAAM,CAAC,GAAY;IAC1B,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,CAAC,GAAG,2BAA2B,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACvD,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACnC,CAAC;IACD,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,GAA4E,CAAC;QACvF,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,IAAgC,CAAC,CAAC,CAAC,SAAS,CAAC;QACpG,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;QACtF,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YAClC,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YAChD,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;YACjD,iFAAiF;YACjF,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,IAAI,oCAAoC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;gBAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC;YACzH,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;gBAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QAC9F,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;IACvD,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,EAAE,MAAM,KAAK,QAAQ;QAAE,GAAG,GAAG,QAAQ,CAAC;IACnE,IAAI,IAAI,KAAK,cAAc,IAAI,IAAI,EAAE,MAAM,KAAK,aAAa;QAAE,GAAG,GAAG,aAAa,CAAC;IACnF,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACpB,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;IACzC,MAAM,GAAG,GAAkB,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IACxD,IAAI,CAAC,CAAC,IAAI;QAAE,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC;IAC9B,IAAI,IAAI,KAAK,cAAc,IAAI,OAAO,IAAI,EAAE,YAAY,KAAK,QAAQ,IAAI,GAAG,KAAK,cAAc,EAAE,CAAC;QAChG,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC;QAC3D,GAAG,CAAC,IAAI,GAAG,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,mBAAmB,CAAC;IACtE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,WAAW;IAClB,IAAI,CAAC;QACH,OAAO,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,IAAa,EAAE,KAAK,GAAG,MAAM;IAClE,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,6BAA6B,CAAC,CAAC;IAClE,MAAM,CAAC,GAAG,IAAI,IAAI,WAAW,EAAE,CAAC;IAChC,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACzE,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,wGAAwG;IACxG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC9D,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;AACtB,CAAC;AAED,mHAAmH;AACnH,MAAM,UAAU,WAAW,CAAC,GAAY,EAAE,KAAK,GAAG,MAAM;IACtD,MAAM,IAAI,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,CAAM,CAAC;IACX,IAAI,CAAC;QACH,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAChG,OAAO,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACrC,CAAC","sourcesContent":["/**\n * Player-facing helpers: plain-language error text and shareable join links.\n * Nothing here talks to the network; it is safe to call from any UI code.\n */\nimport { formatCode, normalizeCode } from \"./crockford.js\";\n\nexport interface FriendlyError {\n /** Stable machine code (the OneLobbyError code, or \"unknown\"). */\n code: string;\n /** One short sentence a player can read. */\n message: string;\n /** What the player can do about it, when there is something. */\n hint?: string;\n}\n\ntype Entry = { message: string; hint?: string };\n\n/**\n * Every `code` a `OneLobbyError` can carry. Compare against these instead of string literals; C# has the shared ones in\n * `LobbyErrorCodes`. `kicked` with `data.reason === \"denied\"` is a refused join request; `rate_limited` with\n * `data.reason === \"max_lobbies\"` is the per-address lobby limit.\n */\nexport const ErrorCodes = {\n // from the lobby server or a direct host's hub\n UnknownCode: \"unknown_code\",\n BadCode: \"bad_code\",\n LobbyFull: \"lobby_full\",\n VersionMismatch: \"version_mismatch\",\n InGame: \"in_game\",\n Kicked: \"kicked\",\n BadPassword: \"bad_password\",\n NotHost: \"not_host\",\n RateLimited: \"rate_limited\",\n ServerFull: \"server_full\",\n GameNotAllowed: \"game_not_allowed\",\n BadRequest: \"bad_request\",\n // from this client\n ServerClosed: \"server_closed\",\n IceFailed: \"ice_failed\",\n Timeout: \"timeout\",\n DirectUnreachable: \"direct_unreachable\",\n BadAddress: \"bad_address\",\n PortInUse: \"port_in_use\",\n ConnectFailed: \"connect_failed\",\n Closed: \"closed\",\n ServerLost: \"server_lost\",\n InvalidToken: \"invalid_token\",\n NoServer: \"no_server\",\n} as const;\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\nconst TEXT: Record<string, Entry> = {\n // server errors (spec/signaling.md)\n 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.\" },\n bad_code: { message: \"That doesn't look like a lobby code.\", hint: \"Codes are 6 letters and numbers, like 7HQ 4K2.\" },\n lobby_full: { message: \"That lobby is full.\", hint: \"Ask the host to make room, or start your own lobby.\" },\n version_mismatch: { message: \"You and the host are running different versions of the game.\", hint: \"Reload the page (or update the game) on both sides, then try again.\" },\n in_game: { message: \"That game has already started.\", hint: \"Ask the host to let people join mid-game, or wait for the next round.\" },\n kicked: { message: \"The host removed you from the lobby.\" },\n denied: { message: \"The host didn't let you in this time.\", hint: \"Ask the host to approve you, then try again.\" },\n not_host: { message: \"Only the host can do that.\" },\n rate_limited: { message: \"Slow down a little: too many tries in a short time.\", hint: \"Wait a moment, then try again.\" },\n max_lobbies: { message: \"You already have the most lobbies open that this server allows.\", hint: \"Close one of your other lobbies, then try again.\" },\n server_full: { message: \"The game server is full right now.\", hint: \"Try again in a few minutes.\" },\n game_not_allowed: { message: \"This game server doesn't host this game.\", hint: \"The game's server setting may be wrong; let the game's owner know.\" },\n bad_request: { message: \"The game sent something the server didn't understand.\", hint: \"Reload the page and try again. If it keeps happening, the game may need an update.\" },\n // client errors\n connect_failed: { message: \"Couldn't reach the game server.\", hint: \"Check your internet connection, then try again.\" },\n closed: { message: \"Lost the connection to the game server.\", hint: \"Check your internet connection, then try again.\" },\n 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.\" },\n invalid_token: { message: \"Your old seat in that lobby is no longer available.\", hint: \"Join again with the lobby code.\" },\n 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.\" },\n timeout: { message: \"The connection took too long.\", hint: \"Check your internet connection, then try again.\" },\n left: { message: \"You left the lobby.\" },\n unknown: { message: \"Something went wrong.\", hint: \"Try again. If it keeps happening, reload the page.\" },\n};\n\nconst CODE_RE = /^[a-z][a-z0-9_]*$/;\n\nfunction codeOf(err: unknown): { code: string | null; data?: Record<string, unknown> } {\n if (typeof err === \"string\") {\n const m = /^([a-z][a-z0-9_]*)(?::|$)/.exec(err.trim());\n return { code: m ? m[1] : null };\n }\n if (err && typeof err === \"object\") {\n const o = err as { code?: unknown; data?: unknown; message?: unknown; name?: unknown };\n const data = o.data && typeof o.data === \"object\" ? (o.data as Record<string, unknown>) : undefined;\n if (typeof o.code === \"string\" && CODE_RE.test(o.code)) return { code: o.code, data };\n if (typeof o.message === \"string\") {\n const m = /^([a-z][a-z0-9_]*):/.exec(o.message);\n if (m && TEXT[m[1]]) return { code: m[1], data };\n // browser network failures (fetch/WebSocket) surface as TypeError / NetworkError\n if (o.name === \"NetworkError\" || /network|failed to fetch|websocket/i.test(o.message)) return { code: \"connect_failed\" };\n if (o.name === \"TimeoutError\" || /timed? ?out/i.test(o.message)) return { code: \"timeout\" };\n }\n }\n return { code: null };\n}\n\n/**\n * Plain-language, player-facing text for any error the kit (or its server) can produce.\n * Never returns a stack trace or raw technical message; unknown errors get a generic friendly line.\n */\nexport function describeError(err: unknown): FriendlyError {\n const { code, data } = codeOf(err);\n if (!code) return { code: \"unknown\", ...TEXT.unknown };\n let key = code;\n if (code === \"kicked\" && data?.reason === \"denied\") key = \"denied\";\n if (code === \"rate_limited\" && data?.reason === \"max_lobbies\") key = \"max_lobbies\";\n const e = TEXT[key];\n if (!e) return { code, ...TEXT.unknown };\n const out: FriendlyError = { code, message: e.message };\n if (e.hint) out.hint = e.hint;\n if (code === \"rate_limited\" && typeof data?.retryAfterMs === \"number\" && key === \"rate_limited\") {\n const s = Math.max(1, Math.ceil(data.retryAfterMs / 1000));\n out.hint = `Wait ${s} second${s === 1 ? \"\" : \"s\"}, then try again.`;\n }\n return out;\n}\n\nfunction currentHref(): string | undefined {\n try {\n return typeof location !== \"undefined\" ? location.href : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * A link that opens the game straight into lobby `code`, e.g. `https://game.example/?join=7HQ-4K2`.\n * `base` defaults to the current page; any existing `param` is replaced. Throws on an invalid code.\n */\nexport function shareUrl(code: string, base?: string, param = \"join\"): string {\n const norm = normalizeCode(code);\n if (!norm) throw new Error(`\"${code}\" is not a valid lobby code`);\n const b = base ?? currentHref();\n if (!b) throw new Error(\"shareUrl needs a base URL outside the browser\");\n const u = new URL(b);\n u.searchParams.delete(param);\n // URLSearchParams would encode the display space as \"+\": use a hyphen, which normalizeCode also accepts\n u.searchParams.set(param, formatCode(norm).replace(\" \", \"-\"));\n return u.toString();\n}\n\n/** The lobby code carried by a share link (see shareUrl), or null when there is none or it is not a valid code. */\nexport function codeFromUrl(url?: string, param = \"join\"): string | null {\n const href = url ?? currentHref();\n if (!href) return null;\n let u: URL;\n try {\n u = new URL(href, \"http://localhost/\");\n } catch {\n return null;\n }\n const v = u.searchParams.get(param) ?? new URLSearchParams(u.hash.replace(/^#/, \"\")).get(param);\n return v ? normalizeCode(v) : null;\n}\n"]}
1
+ {"version":3,"file":"friendly.js","sourceRoot":"","sources":["../src/friendly.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Player-facing helpers: plain-language error text and shareable join links.\n * Nothing here talks to the network; it is safe to call from any UI code.\n */\nimport { formatCode, normalizeCode } from \"./crockford.js\";\n\nexport interface FriendlyError {\n /** Stable machine code (the OneLobbyError code, or \"unknown\"). */\n code: string;\n /** One short sentence a player can read. */\n message: string;\n /** What the player can do about it, when there is something. */\n hint?: string;\n}\n\ntype Entry = { message: string; hint?: string };\n\n/**\n * Every `code` a `OneLobbyError` can carry. Compare against these instead of string literals; C# has the shared ones in\n * `LobbyErrorCodes`. `kicked` with `data.reason === \"denied\"` is a refused join request; `rate_limited` with\n * `data.reason === \"max_lobbies\"` is the per-address lobby limit.\n */\nexport const ErrorCodes = {\n // from the lobby server or a direct host's hub\n UnknownCode: \"unknown_code\",\n BadCode: \"bad_code\",\n LobbyFull: \"lobby_full\",\n VersionMismatch: \"version_mismatch\",\n InGame: \"in_game\",\n Kicked: \"kicked\",\n BadPassword: \"bad_password\",\n NotHost: \"not_host\",\n RateLimited: \"rate_limited\",\n ServerFull: \"server_full\",\n GameNotAllowed: \"game_not_allowed\",\n BadRequest: \"bad_request\",\n // from this client\n ServerClosed: \"server_closed\",\n IceFailed: \"ice_failed\",\n Timeout: \"timeout\",\n DirectUnreachable: \"direct_unreachable\",\n BadAddress: \"bad_address\",\n PortInUse: \"port_in_use\",\n ConnectFailed: \"connect_failed\",\n Closed: \"closed\",\n ServerLost: \"server_lost\",\n InvalidToken: \"invalid_token\",\n NoServer: \"no_server\",\n // from a blueprint (onelobby.json)\n BadConfig: \"bad_config\",\n VoiceDisabled: \"voice_disabled\",\n ChatDisabled: \"chat_disabled\",\n ListingDisabled: \"listing_disabled\",\n IdentityAnonymous: \"identity_anonymous\",\n} as const;\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\nconst TEXT: Record<string, Entry> = {\n // server errors (spec/signaling.md)\n 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.\" },\n bad_code: { message: \"That doesn't look like a lobby code.\", hint: \"Codes are 6 letters and numbers, like 7HQ 4K2.\" },\n lobby_full: { message: \"That lobby is full.\", hint: \"Ask the host to make room, or start your own lobby.\" },\n version_mismatch: { message: \"You and the host are running different versions of the game.\", hint: \"Reload the page (or update the game) on both sides, then try again.\" },\n in_game: { message: \"That game has already started.\", hint: \"Ask the host to let people join mid-game, or wait for the next round.\" },\n kicked: { message: \"The host removed you from the lobby.\" },\n denied: { message: \"The host didn't let you in this time.\", hint: \"Ask the host to approve you, then try again.\" },\n not_host: { message: \"Only the host can do that.\" },\n rate_limited: { message: \"Slow down a little: too many tries in a short time.\", hint: \"Wait a moment, then try again.\" },\n max_lobbies: { message: \"You already have the most lobbies open that this server allows.\", hint: \"Close one of your other lobbies, then try again.\" },\n server_full: { message: \"The game server is full right now.\", hint: \"Try again in a few minutes.\" },\n game_not_allowed: { message: \"This game server doesn't host this game.\", hint: \"The game's server setting may be wrong; let the game's owner know.\" },\n bad_request: { message: \"The game sent something the server didn't understand.\", hint: \"Reload the page and try again. If it keeps happening, the game may need an update.\" },\n // client errors\n connect_failed: { message: \"Couldn't reach the game server.\", hint: \"Check your internet connection, then try again.\" },\n closed: { message: \"Lost the connection to the game server.\", hint: \"Check your internet connection, then try again.\" },\n 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.\" },\n invalid_token: { message: \"Your old seat in that lobby is no longer available.\", hint: \"Join again with the lobby code.\" },\n 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.\" },\n 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.\" },\n timeout: { message: \"The connection took too long.\", hint: \"Check your internet connection, then try again.\" },\n voice_disabled: { message: \"Voice chat is turned off in this game.\" },\n chat_disabled: { message: \"Text chat is turned off in this game.\" },\n listing_disabled: { message: \"This game has no public lobby list.\", hint: \"Join with a code or a link from the host.\" },\n identity_anonymous: { message: \"This game doesn't use player names.\", hint: \"Everyone shows up as Player 1, Player 2, and so on.\" },\n 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.\" },\n left: { message: \"You left the lobby.\" },\n unknown: { message: \"Something went wrong.\", hint: \"Try again. If it keeps happening, reload the page.\" },\n};\n\nconst CODE_RE = /^[a-z][a-z0-9_]*$/;\n\nfunction codeOf(err: unknown): { code: string | null; data?: Record<string, unknown> } {\n if (typeof err === \"string\") {\n const m = /^([a-z][a-z0-9_]*)(?::|$)/.exec(err.trim());\n return { code: m ? m[1] : null };\n }\n if (err && typeof err === \"object\") {\n const o = err as { code?: unknown; data?: unknown; message?: unknown; name?: unknown };\n const data = o.data && typeof o.data === \"object\" ? (o.data as Record<string, unknown>) : undefined;\n if (typeof o.code === \"string\" && CODE_RE.test(o.code)) return { code: o.code, data };\n if (typeof o.message === \"string\") {\n const m = /^([a-z][a-z0-9_]*):/.exec(o.message);\n if (m && TEXT[m[1]]) return { code: m[1], data };\n // browser network failures (fetch/WebSocket) surface as TypeError / NetworkError\n if (o.name === \"NetworkError\" || /network|failed to fetch|websocket/i.test(o.message)) return { code: \"connect_failed\" };\n if (o.name === \"TimeoutError\" || /timed? ?out/i.test(o.message)) return { code: \"timeout\" };\n }\n }\n return { code: null };\n}\n\n/**\n * Plain-language, player-facing text for any error the kit (or its server) can produce.\n * Never returns a stack trace or raw technical message; unknown errors get a generic friendly line.\n */\nexport function describeError(err: unknown): FriendlyError {\n const { code, data } = codeOf(err);\n if (!code) return { code: \"unknown\", ...TEXT.unknown };\n let key = code;\n if (code === \"kicked\" && data?.reason === \"denied\") key = \"denied\";\n if (code === \"rate_limited\" && data?.reason === \"max_lobbies\") key = \"max_lobbies\";\n const e = TEXT[key];\n if (!e) return { code, ...TEXT.unknown };\n const out: FriendlyError = { code, message: e.message };\n if (e.hint) out.hint = e.hint;\n if (code === \"rate_limited\" && typeof data?.retryAfterMs === \"number\" && key === \"rate_limited\") {\n const s = Math.max(1, Math.ceil(data.retryAfterMs / 1000));\n out.hint = `Wait ${s} second${s === 1 ? \"\" : \"s\"}, then try again.`;\n }\n return out;\n}\n\nfunction currentHref(): string | undefined {\n try {\n return typeof location !== \"undefined\" ? location.href : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * A link that opens the game straight into lobby `code`, e.g. `https://game.example/?join=7HQ-4K2`.\n * `base` defaults to the current page; any existing `param` is replaced. Throws on an invalid code.\n */\nexport function shareUrl(code: string, base?: string, param = \"join\"): string {\n const norm = normalizeCode(code);\n if (!norm) throw new Error(`\"${code}\" is not a valid lobby code`);\n const b = base ?? currentHref();\n if (!b) throw new Error(\"shareUrl needs a base URL outside the browser\");\n const u = new URL(b);\n u.searchParams.delete(param);\n // URLSearchParams would encode the display space as \"+\": use a hyphen, which normalizeCode also accepts\n u.searchParams.set(param, formatCode(norm).replace(\" \", \"-\"));\n return u.toString();\n}\n\n/** The lobby code carried by a share link (see shareUrl), or null when there is none or it is not a valid code. */\nexport function codeFromUrl(url?: string, param = \"join\"): string | null {\n const href = url ?? currentHref();\n if (!href) return null;\n let u: URL;\n try {\n u = new URL(href, \"http://localhost/\");\n } catch {\n return null;\n }\n const v = u.searchParams.get(param) ?? new URLSearchParams(u.hash.replace(/^#/, \"\")).get(param);\n return v ? normalizeCode(v) : null;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -4,9 +4,11 @@
4
4
  * WebRTC/signaling classes, clocks, the gridpaint demo game) live in `onelobby/internals`.
5
5
  */
6
6
  export { OneLobby, LIB_NAME, LIB_VERSION } from "./onelobby.js";
7
- export type { OneLobbyOptions, HostOptions, ListOptions, OneLobbyEvents, JoinDirectOptions } from "./onelobby.js";
7
+ export type { OneLobbyOptions, FromConfigOverrides, HostOptions, ListOptions, OneLobbyEvents, JoinDirectOptions } from "./onelobby.js";
8
8
  export { DEFAULT_READY_TIMEOUT_MS } from "./onelobby.js";
9
9
  export type { AdvancedOptions, SeatStorage } from "./onelobby.js";
10
+ export { parseBlueprint, defineBlueprint, BlueprintError } from "./blueprint.js";
11
+ export type { Blueprint, BlueprintInput, BlueprintEngine } from "./blueprint.js";
10
12
  export { Lobby } from "./lobby.js";
11
13
  export type { LobbyEvents, LobbyConfig, Peer, Phase, RateLimit } from "./lobby.js";
12
14
  export { MIN_PEER_TIMEOUT_MS } from "./lobby.js";
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@
5
5
  */
6
6
  export { OneLobby, LIB_NAME, LIB_VERSION } from "./onelobby.js";
7
7
  export { DEFAULT_READY_TIMEOUT_MS } from "./onelobby.js";
8
+ // Blueprint v1: onelobby.json (docs/design/blueprint.md). OneLobby.fromConfig(json) builds a kit from it.
9
+ export { parseBlueprint, defineBlueprint, BlueprintError } from "./blueprint.js";
8
10
  export { Lobby } from "./lobby.js";
9
11
  export { MIN_PEER_TIMEOUT_MS } from "./lobby.js";
10
12
  // WebSocket-shaped sockets over a lobby: lobby.asWebSocket() (joiner), lobby.onConnection(conn => ...) (host).
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,+GAA+G;AAC/G,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAInC,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC3D,iHAAiH;AACjH,8GAA8G;AAC9G,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAK/C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEjF,yEAAyE;AACzE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * Public API of `onelobby`: create a `OneLobby`, `host()` / `join(code)` / `list()` lobbies, then use the\n * `Lobby`'s lockstep, raw messages, chat and voice. Lower-level pieces (codec, chunking, migration, jitter buffer,\n * WebRTC/signaling classes, clocks, the gridpaint demo game) live in `onelobby/internals`.\n */\nexport { OneLobby, LIB_NAME, LIB_VERSION } from \"./onelobby.js\";\nexport type { OneLobbyOptions, HostOptions, ListOptions, OneLobbyEvents, JoinDirectOptions } from \"./onelobby.js\";\nexport { DEFAULT_READY_TIMEOUT_MS } from \"./onelobby.js\";\nexport type { AdvancedOptions, SeatStorage } from \"./onelobby.js\";\nexport { Lobby } from \"./lobby.js\";\nexport type { LobbyEvents, LobbyConfig, Peer, Phase, RateLimit } from \"./lobby.js\";\nexport { MIN_PEER_TIMEOUT_MS } from \"./lobby.js\";\n// WebSocket-shaped sockets over a lobby: lobby.asWebSocket() (joiner), lobby.onConnection(conn => ...) (host).\nexport { closeCodeFor } from \"./socket.js\";\nexport type { LobbySocket, LobbySocketOptions } from \"./socket.js\";\nexport { Lockstep } from \"./lockstep.js\";\nexport type { Frame, DesyncInfo, LockstepEvents } from \"./lockstep.js\";\nexport { Chat, CHAT_MAX_BYTES } from \"./chat.js\";\nexport type { ChatMessage, ChatEvents } from \"./chat.js\";\nexport { Voice } from \"./voice.js\";\nexport type { VoiceEvents, VoiceDeps } from \"./voice.js\";\nexport type { VoiceMode } from \"./voice-core.js\";\nexport type { VoiceCodec, VoiceEncoderHandle, VoiceDecoderHandle, FrameSource, AudioSink, VoiceError } from \"./voice-types.js\";\nexport { webCodecsOpusAvailable } from \"./voice-browser.js\";\nexport { normalizeCode, formatCode } from \"./crockford.js\";\n// Direct IP (docs/direct-ip.md): kit.joinDirect(code); the code is the host's address in disguise. Hosting is in\n// onelobby/direct (Node). webRtcLinkFactoryFor gives Node joiners a WebRTC stack (node-datachannel/polyfill).\nexport { decodeJoinCode, looksLikeJoinCode } from \"./joincode.js\";\nexport type { DecodedJoinCode, JoinCodeError } from \"./joincode.js\";\nexport { webRtcLinkFactoryFor } from \"./webrtc.js\";\nexport { OneLobbyError } from \"./signaling.js\";\nexport type { LobbySummary, LobbyState, PeerInfo } from \"./signaling.js\";\n// Types for the seams in OneLobbyOptions (ChannelName also tags raw messages).\nexport type { PeerLink, LinkFactory, SocketFactory, SocketLike, IceServer, ChannelName } from \"./transport.js\";\nexport type { Clock } from \"./clock.js\";\nexport { describeError, shareUrl, codeFromUrl, ErrorCodes } from \"./friendly.js\";\nexport type { FriendlyError, ErrorCode } from \"./friendly.js\";\n// Pre-rename names, kept for one version so existing games keep working.\nexport { Keepalive, KeepaliveError } from \"./deprecated.js\";\nexport type { KeepaliveOptions, KeepaliveEvents } from \"./deprecated.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAEzD,0GAA0G;AAC1G,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEjF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,+GAA+G;AAC/G,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAInC,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC3D,iHAAiH;AACjH,8GAA8G;AAC9G,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAK/C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEjF,yEAAyE;AACzE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * Public API of `onelobby`: create a `OneLobby`, `host()` / `join(code)` / `list()` lobbies, then use the\n * `Lobby`'s lockstep, raw messages, chat and voice. Lower-level pieces (codec, chunking, migration, jitter buffer,\n * WebRTC/signaling classes, clocks, the gridpaint demo game) live in `onelobby/internals`.\n */\nexport { OneLobby, LIB_NAME, LIB_VERSION } from \"./onelobby.js\";\nexport type { OneLobbyOptions, FromConfigOverrides, HostOptions, ListOptions, OneLobbyEvents, JoinDirectOptions } from \"./onelobby.js\";\nexport { DEFAULT_READY_TIMEOUT_MS } from \"./onelobby.js\";\nexport type { AdvancedOptions, SeatStorage } from \"./onelobby.js\";\n// Blueprint v1: onelobby.json (docs/design/blueprint.md). OneLobby.fromConfig(json) builds a kit from it.\nexport { parseBlueprint, defineBlueprint, BlueprintError } from \"./blueprint.js\";\nexport type { Blueprint, BlueprintInput, BlueprintEngine } from \"./blueprint.js\";\nexport { Lobby } from \"./lobby.js\";\nexport type { LobbyEvents, LobbyConfig, Peer, Phase, RateLimit } from \"./lobby.js\";\nexport { MIN_PEER_TIMEOUT_MS } from \"./lobby.js\";\n// WebSocket-shaped sockets over a lobby: lobby.asWebSocket() (joiner), lobby.onConnection(conn => ...) (host).\nexport { closeCodeFor } from \"./socket.js\";\nexport type { LobbySocket, LobbySocketOptions } from \"./socket.js\";\nexport { Lockstep } from \"./lockstep.js\";\nexport type { Frame, DesyncInfo, LockstepEvents } from \"./lockstep.js\";\nexport { Chat, CHAT_MAX_BYTES } from \"./chat.js\";\nexport type { ChatMessage, ChatEvents } from \"./chat.js\";\nexport { Voice } from \"./voice.js\";\nexport type { VoiceEvents, VoiceDeps } from \"./voice.js\";\nexport type { VoiceMode } from \"./voice-core.js\";\nexport type { VoiceCodec, VoiceEncoderHandle, VoiceDecoderHandle, FrameSource, AudioSink, VoiceError } from \"./voice-types.js\";\nexport { webCodecsOpusAvailable } from \"./voice-browser.js\";\nexport { normalizeCode, formatCode } from \"./crockford.js\";\n// Direct IP (docs/direct-ip.md): kit.joinDirect(code); the code is the host's address in disguise. Hosting is in\n// onelobby/direct (Node). webRtcLinkFactoryFor gives Node joiners a WebRTC stack (node-datachannel/polyfill).\nexport { decodeJoinCode, looksLikeJoinCode } from \"./joincode.js\";\nexport type { DecodedJoinCode, JoinCodeError } from \"./joincode.js\";\nexport { webRtcLinkFactoryFor } from \"./webrtc.js\";\nexport { OneLobbyError } from \"./signaling.js\";\nexport type { LobbySummary, LobbyState, PeerInfo } from \"./signaling.js\";\n// Types for the seams in OneLobbyOptions (ChannelName also tags raw messages).\nexport type { PeerLink, LinkFactory, SocketFactory, SocketLike, IceServer, ChannelName } from \"./transport.js\";\nexport type { Clock } from \"./clock.js\";\nexport { describeError, shareUrl, codeFromUrl, ErrorCodes } from \"./friendly.js\";\nexport type { FriendlyError, ErrorCode } from \"./friendly.js\";\n// Pre-rename names, kept for one version so existing games keep working.\nexport { Keepalive, KeepaliveError } from \"./deprecated.js\";\nexport type { KeepaliveOptions, KeepaliveEvents } from \"./deprecated.js\";\n"]}