@onceworlds/cli 0.9.5 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -18
- package/dist/cli.js +4608 -559
- package/dist/emulator.js +13753 -0
- package/dist/sdk.js +12 -0
- package/dist/v1/bots.js +193 -0
- package/dist/v1/host.js +547 -0
- package/dist/v1/match.js +1263 -0
- package/package.json +3 -3
package/dist/v1/host.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// ../sdk/src/helpers/random.ts
|
|
6
|
+
function hashString(text) {
|
|
7
|
+
let h1 = 3735928559;
|
|
8
|
+
let h2 = 1103547991;
|
|
9
|
+
for (let i = 0; i < text.length; i++) {
|
|
10
|
+
const c = text.charCodeAt(i);
|
|
11
|
+
h1 = Math.imul(h1 ^ c, 2654435761);
|
|
12
|
+
h2 = Math.imul(h2 ^ c, 1597334677);
|
|
13
|
+
}
|
|
14
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
15
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
16
|
+
return (h2 ^ h1) >>> 0;
|
|
17
|
+
}
|
|
18
|
+
function createRandom(seed) {
|
|
19
|
+
const s = typeof seed === "number" && Number.isFinite(seed) ? Math.floor(seed) >>> 0 : hashString(String(seed));
|
|
20
|
+
let a = 2654435769;
|
|
21
|
+
let b = 608135816;
|
|
22
|
+
let c = 3084996962;
|
|
23
|
+
let d = s;
|
|
24
|
+
const next = () => {
|
|
25
|
+
a >>>= 0;
|
|
26
|
+
b >>>= 0;
|
|
27
|
+
c >>>= 0;
|
|
28
|
+
d >>>= 0;
|
|
29
|
+
let t2 = a + b | 0;
|
|
30
|
+
a = b ^ b >>> 9;
|
|
31
|
+
b = c + (c << 3) | 0;
|
|
32
|
+
c = c << 21 | c >>> 11;
|
|
33
|
+
d = d + 1 | 0;
|
|
34
|
+
t2 = t2 + d | 0;
|
|
35
|
+
c = c + t2 | 0;
|
|
36
|
+
return (t2 >>> 0) / 4294967296;
|
|
37
|
+
};
|
|
38
|
+
for (let i = 0; i < 12; i++) next();
|
|
39
|
+
const int = (n) => Number.isFinite(n) && n > 0 ? Math.floor(next() * Math.floor(n)) : 0;
|
|
40
|
+
return {
|
|
41
|
+
next,
|
|
42
|
+
int,
|
|
43
|
+
range: (min, max) => min + next() * (max - min),
|
|
44
|
+
chance: (p) => next() < p,
|
|
45
|
+
pick: (items) => Array.isArray(items) && items.length > 0 ? items[int(items.length)] : void 0,
|
|
46
|
+
shuffle(items) {
|
|
47
|
+
const out = Array.isArray(items) ? [...items] : [];
|
|
48
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
49
|
+
const j = int(i + 1);
|
|
50
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ../sdk/src/helpers/util.ts
|
|
58
|
+
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
59
|
+
function jsonSize(value) {
|
|
60
|
+
try {
|
|
61
|
+
const text = JSON.stringify(value);
|
|
62
|
+
return text === void 0 ? 0 : text.length;
|
|
63
|
+
} catch {
|
|
64
|
+
return Infinity;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function copy(value) {
|
|
68
|
+
try {
|
|
69
|
+
return value === void 0 ? void 0 : JSON.parse(JSON.stringify(value));
|
|
70
|
+
} catch {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
var said = /* @__PURE__ */ new Map();
|
|
75
|
+
function warn(key, message, level = "warn", error) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
if (now - (said.get(key) ?? -Infinity) < 5e3) return;
|
|
78
|
+
said.set(key, now);
|
|
79
|
+
if (said.size > 500) said.delete(said.keys().next().value);
|
|
80
|
+
try {
|
|
81
|
+
const text = `[onceworlds] ${message}`;
|
|
82
|
+
if (level === "error") console.error(text, error);
|
|
83
|
+
else if (level === "debug") console.debug(text);
|
|
84
|
+
else console.warn(text);
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function guard(where, fn, fallback) {
|
|
89
|
+
try {
|
|
90
|
+
return fn();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
warn(`threw:${where}`, `${where} threw (the helper carries on)`, "error", error);
|
|
93
|
+
return fallback;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
var Bucket = class {
|
|
97
|
+
constructor(perSecond, burst, now) {
|
|
98
|
+
__publicField(this, "perSecond", perSecond);
|
|
99
|
+
__publicField(this, "burst", burst);
|
|
100
|
+
__publicField(this, "tokens");
|
|
101
|
+
__publicField(this, "last");
|
|
102
|
+
this.tokens = burst;
|
|
103
|
+
this.last = now;
|
|
104
|
+
}
|
|
105
|
+
take(now) {
|
|
106
|
+
this.tokens = Math.min(this.burst, this.tokens + Math.max(0, now - this.last) / 1e3 * this.perSecond);
|
|
107
|
+
this.last = now;
|
|
108
|
+
if (this.tokens < 1) return false;
|
|
109
|
+
this.tokens -= 1;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var Limits = class {
|
|
114
|
+
constructor(perSecond, burst) {
|
|
115
|
+
__publicField(this, "perSecond", perSecond);
|
|
116
|
+
__publicField(this, "burst", burst);
|
|
117
|
+
__publicField(this, "buckets", /* @__PURE__ */ new Map());
|
|
118
|
+
}
|
|
119
|
+
take(id, now) {
|
|
120
|
+
let bucket = this.buckets.get(id);
|
|
121
|
+
if (!bucket) {
|
|
122
|
+
if (this.buckets.size > 256) this.buckets.clear();
|
|
123
|
+
this.buckets.set(id, bucket = new Bucket(this.perSecond, this.burst, now));
|
|
124
|
+
}
|
|
125
|
+
return bucket.take(now);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
function token() {
|
|
129
|
+
const words = new Uint32Array(2);
|
|
130
|
+
const crypto = globalThis.crypto;
|
|
131
|
+
if (crypto?.getRandomValues) crypto.getRandomValues(words);
|
|
132
|
+
else for (let i = 0; i < words.length; i++) words[i] = Math.floor(Math.random() * 4294967296);
|
|
133
|
+
return words[0].toString(36) + words[1].toString(36);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ../sdk/src/helpers/schema.ts
|
|
137
|
+
var schema = (check2, optional = false) => ({ check: check2, optional });
|
|
138
|
+
function toSchema(like) {
|
|
139
|
+
if (like === void 0 || like === null) return null;
|
|
140
|
+
if (typeof like === "function") {
|
|
141
|
+
return schema((value, path) => {
|
|
142
|
+
let verdict;
|
|
143
|
+
try {
|
|
144
|
+
verdict = like(value);
|
|
145
|
+
} catch {
|
|
146
|
+
return `${path} failed its check`;
|
|
147
|
+
}
|
|
148
|
+
if (verdict === false) return `${path} failed its check`;
|
|
149
|
+
return typeof verdict === "string" ? `${path}: ${verdict}` : null;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (typeof like.check === "function") return like;
|
|
153
|
+
return t.object(like);
|
|
154
|
+
}
|
|
155
|
+
function check(like, value, path = "value") {
|
|
156
|
+
const s = toSchema(like);
|
|
157
|
+
if (!s) return null;
|
|
158
|
+
try {
|
|
159
|
+
return s.check(value, path);
|
|
160
|
+
} catch {
|
|
161
|
+
return `${path} could not be checked`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
var DANGEROUS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
165
|
+
var t = {
|
|
166
|
+
/** A finite number in [min, max] (default: any finite number). */
|
|
167
|
+
number(min = -1e15, max = 1e15) {
|
|
168
|
+
return schema((v, path) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max ? null : `${path} must be a number from ${min} to ${max}`);
|
|
169
|
+
},
|
|
170
|
+
/** A whole number in [min, max]. */
|
|
171
|
+
int(min = -2147483648, max = 2147483647) {
|
|
172
|
+
return schema((v, path) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max ? null : `${path} must be a whole number from ${min} to ${max}`);
|
|
173
|
+
},
|
|
174
|
+
/** A string of at most `max` characters (default 200), matching `pattern` when given. */
|
|
175
|
+
string(max = 200, pattern) {
|
|
176
|
+
return schema((v, path) => {
|
|
177
|
+
if (typeof v !== "string" || v.length > max) return `${path} must be text of at most ${max} characters`;
|
|
178
|
+
return pattern && !pattern.test(v) ? `${path} is not in the right form` : null;
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
boolean() {
|
|
182
|
+
return schema((v, path) => typeof v === "boolean" ? null : `${path} must be true or false`);
|
|
183
|
+
},
|
|
184
|
+
/** One of these exact values (strings, numbers, booleans or null). */
|
|
185
|
+
oneOf(...values) {
|
|
186
|
+
return schema((v, path) => values.includes(v) ? null : `${path} must be one of ${values.map((x) => JSON.stringify(x)).join(", ")}`);
|
|
187
|
+
},
|
|
188
|
+
/** A player id (or a bot's seat id): text of up to 128 characters. Whether they are in the room is for `validate` to say. */
|
|
189
|
+
id() {
|
|
190
|
+
return schema((v, path) => typeof v === "string" && v.length > 0 && v.length <= 128 ? null : `${path} must be a player id`);
|
|
191
|
+
},
|
|
192
|
+
/** A list of at most `max` items (default 64), each passing `item`. */
|
|
193
|
+
array(item, max = 64) {
|
|
194
|
+
const inner = toSchema(item);
|
|
195
|
+
return schema((v, path) => {
|
|
196
|
+
if (!Array.isArray(v) || v.length > max) return `${path} must be a list of at most ${max}`;
|
|
197
|
+
for (let i = 0; i < v.length; i++) {
|
|
198
|
+
const problem = inner.check(v[i], `${path}[${i}]`);
|
|
199
|
+
if (problem) return problem;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
/** An object with exactly these fields (unknown fields are refused; t.optional ones may be left out). */
|
|
205
|
+
object(shape) {
|
|
206
|
+
const fields = Object.entries(shape).map(([key, like]) => [key, toSchema(like)]);
|
|
207
|
+
const known = new Set(fields.map(([key]) => key));
|
|
208
|
+
return schema((v, path) => {
|
|
209
|
+
if (!isObject(v)) return `${path} must be an object`;
|
|
210
|
+
for (const key of Object.keys(v)) if (!known.has(key) || DANGEROUS.has(key)) return `${path} has an unknown field '${key.slice(0, 40)}'`;
|
|
211
|
+
for (const [key, inner] of fields) {
|
|
212
|
+
if (!Object.hasOwn(v, key) || v[key] === void 0) {
|
|
213
|
+
if (inner.optional) continue;
|
|
214
|
+
return `${path}.${key} is missing`;
|
|
215
|
+
}
|
|
216
|
+
const problem = inner.check(v[key], `${path}.${key}`);
|
|
217
|
+
if (problem) return problem;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
/** An object used as a map: at most `maxKeys` keys (default 64) of up to 128 characters, each value passing `value`. */
|
|
223
|
+
record(value, maxKeys = 64) {
|
|
224
|
+
const inner = toSchema(value);
|
|
225
|
+
return schema((v, path) => {
|
|
226
|
+
if (!isObject(v)) return `${path} must be an object`;
|
|
227
|
+
const keys = Object.keys(v);
|
|
228
|
+
if (keys.length > maxKeys) return `${path} has more than ${maxKeys} keys`;
|
|
229
|
+
for (const key of keys) {
|
|
230
|
+
if (key.length > 128 || DANGEROUS.has(key)) return `${path} has a bad key`;
|
|
231
|
+
const problem = inner.check(v[key], `${path}.${key}`);
|
|
232
|
+
if (problem) return problem;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
/** The field may be left out (or undefined). */
|
|
238
|
+
optional(inner) {
|
|
239
|
+
const s = toSchema(inner);
|
|
240
|
+
return schema((v, path) => v === void 0 ? null : s.check(v, path), true);
|
|
241
|
+
},
|
|
242
|
+
/** null, or a value passing `inner`. */
|
|
243
|
+
nullable(inner) {
|
|
244
|
+
const s = toSchema(inner);
|
|
245
|
+
return schema((v, path) => v === null ? null : s.check(v, path), s.optional);
|
|
246
|
+
},
|
|
247
|
+
/** Any plain data of up to `maxBytes` as JSON (default 1024). Prefer a real shape: this only bounds the size. */
|
|
248
|
+
any(maxBytes = 1024) {
|
|
249
|
+
return schema((v, path) => jsonSize(v) <= maxBytes ? null : `${path} is bigger than ${maxBytes} bytes`);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// ../sdk/src/helpers/host.ts
|
|
254
|
+
var MESSAGE_TAG = "$ow";
|
|
255
|
+
var NAME_RE = /^[A-Za-z0-9_.:-]{1,48}$/;
|
|
256
|
+
var STATE_NAME_RE = /^[A-Za-z0-9_.:-]{1,59}$/;
|
|
257
|
+
var BOT_ID_RE = /^bot:[A-Za-z0-9_.-]{1,24}$/;
|
|
258
|
+
var CLAIM_TIMEOUT_MS = 5e3;
|
|
259
|
+
var STATE_MAX_BYTES = 15e3;
|
|
260
|
+
function platformNow() {
|
|
261
|
+
try {
|
|
262
|
+
const now = globalThis.onceworlds?.now?.();
|
|
263
|
+
if (typeof now === "number" && Number.isFinite(now)) return now;
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
return Date.now();
|
|
267
|
+
}
|
|
268
|
+
var callerOf = (player) => ({ id: player.id, name: player.name, team: player.team ?? 0, bot: false });
|
|
269
|
+
function createHost(room, options = {}) {
|
|
270
|
+
const rpcs = /* @__PURE__ */ new Map();
|
|
271
|
+
const claims = /* @__PURE__ */ new Map();
|
|
272
|
+
const open = /* @__PURE__ */ new Map();
|
|
273
|
+
const seen = /* @__PURE__ */ new Set();
|
|
274
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
275
|
+
const offs = [];
|
|
276
|
+
let disposed = false;
|
|
277
|
+
const now = () => {
|
|
278
|
+
try {
|
|
279
|
+
return options.now ? options.now() : platformNow();
|
|
280
|
+
} catch {
|
|
281
|
+
return Date.now();
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const reject = (playerId, reason) => {
|
|
285
|
+
if (options.onReject) guard("onReject", () => options.onReject(playerId, reason), void 0);
|
|
286
|
+
else warn(`reject:${playerId}`, `refused a message from ${playerId}: ${reason}`, "debug");
|
|
287
|
+
};
|
|
288
|
+
const mayCall = (from, spectators) => {
|
|
289
|
+
if (from.bot || spectators || room.match.phase === "lobby") return true;
|
|
290
|
+
return from.id === room.host || room.isParticipant(from.id);
|
|
291
|
+
};
|
|
292
|
+
function runRpc(name, entry, from, raw, info) {
|
|
293
|
+
if (!mayCall(from, entry.def.spectators)) return reject(from.id, `'${name}' from a player who is only watching`), false;
|
|
294
|
+
if (!entry.limits.take(from.id, now())) return reject(from.id, `'${name}' called too often`), false;
|
|
295
|
+
if (jsonSize(raw) > (entry.def.maxBytes ?? 2048)) return reject(from.id, `'${name}' arguments are too big`), false;
|
|
296
|
+
const args = raw === void 0 && entry.schema ? {} : raw;
|
|
297
|
+
if (entry.schema) {
|
|
298
|
+
const problem = entry.schema.check(args, `'${name}' args`);
|
|
299
|
+
if (problem) return reject(from.id, problem), false;
|
|
300
|
+
} else if (args !== void 0 && args !== null && !(isObject(args) && Object.keys(args).length === 0)) {
|
|
301
|
+
return reject(from.id, `'${name}' takes no arguments`), false;
|
|
302
|
+
}
|
|
303
|
+
const verdict = guard(`rpc '${name}' validate`, () => entry.def.validate?.(from, args, info), false);
|
|
304
|
+
if (verdict === false || typeof verdict === "string") return reject(from.id, typeof verdict === "string" ? verdict : `'${name}' refused`), false;
|
|
305
|
+
guard(`rpc '${name}' run`, () => entry.def.run(from, args, info), void 0);
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
function rpc(name, def) {
|
|
309
|
+
if (typeof name !== "string" || !NAME_RE.test(name) || !def || typeof def.run !== "function") {
|
|
310
|
+
warn(`rpc-def:${String(name)}`, `host.rpc('${String(name)}', { run(from, args) {...} }) needs a name of 1-48 letters, digits or _.:- and a run function`, "error");
|
|
311
|
+
return () => false;
|
|
312
|
+
}
|
|
313
|
+
if (rpcs.has(name)) warn(`rpc-twice:${name}`, `host.rpc('${name}') was defined twice: the second replaces the first. Define each once, where the game starts.`);
|
|
314
|
+
const rate = def.rate && def.rate > 0 ? def.rate : 10;
|
|
315
|
+
const entry = { def, schema: toSchema(def.args), limits: new Limits(rate, Math.max(4, rate * 2)) };
|
|
316
|
+
rpcs.set(name, entry);
|
|
317
|
+
return (args) => {
|
|
318
|
+
if (disposed || room.closed) return false;
|
|
319
|
+
const wire = args === void 0 ? void 0 : copy(args);
|
|
320
|
+
if (args !== void 0 && wire === void 0) return warn(`rpc-args:${name}`, `rpc '${name}': arguments must be plain data`), false;
|
|
321
|
+
if (entry.schema) {
|
|
322
|
+
const problem = check(entry.schema, wire === void 0 ? {} : wire, `'${name}' args`);
|
|
323
|
+
if (problem) return warn(`rpc-args:${name}`, `rpc '${name}' not sent: ${problem}`), false;
|
|
324
|
+
}
|
|
325
|
+
if (room.isHost) {
|
|
326
|
+
const me = room.players.get(room.me.id) ?? room.me;
|
|
327
|
+
return runRpc(name, entry, callerOf(me), wire, { at: now(), ...room.running ? { matchTime: room.matchNow() } : {} });
|
|
328
|
+
}
|
|
329
|
+
room.send({ [MESSAGE_TAG]: "rpc", n: name, ...wire === void 0 ? {} : { a: wire } }, { to: room.host });
|
|
330
|
+
return true;
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function claim(name, def) {
|
|
334
|
+
if (typeof name !== "string" || !NAME_RE.test(name) || !def || typeof def.apply !== "function") {
|
|
335
|
+
warn(`claim-def:${String(name)}`, `host.claim('${String(name)}', { apply(winner, rivals) {...} }) needs a name of 1-48 letters, digits or _.:- and an apply function`, "error");
|
|
336
|
+
return async () => "rejected";
|
|
337
|
+
}
|
|
338
|
+
const rate = def.rate && def.rate > 0 ? def.rate : 10;
|
|
339
|
+
const entry = { def, schema: toSchema(def.args), limits: new Limits(rate, Math.max(4, rate * 2)) };
|
|
340
|
+
claims.set(name, entry);
|
|
341
|
+
return (args) => {
|
|
342
|
+
if (disposed || room.closed) return Promise.resolve("rejected");
|
|
343
|
+
const wire = copy(args === void 0 ? {} : args);
|
|
344
|
+
const problem = wire === void 0 ? "arguments must be plain data" : entry.schema ? check(entry.schema, wire, `'${name}' args`) : null;
|
|
345
|
+
if (problem) {
|
|
346
|
+
warn(`claim-args:${name}`, `claim '${name}' not sent: ${problem}`);
|
|
347
|
+
return Promise.resolve("rejected");
|
|
348
|
+
}
|
|
349
|
+
const id = token();
|
|
350
|
+
const message = { [MESSAGE_TAG]: "claim", n: name, i: id, a: wire };
|
|
351
|
+
return new Promise((resolve) => {
|
|
352
|
+
const timer = setTimeout(() => settleMine(id, "timeout"), CLAIM_TIMEOUT_MS);
|
|
353
|
+
waiting.set(id, { resolve, message, sentTo: [room.host], timer });
|
|
354
|
+
if (room.isHost) receiveClaim(callerOf(room.players.get(room.me.id) ?? room.me), name, id, wire, { at: now(), ...room.running ? { matchTime: room.matchNow() } : {} });
|
|
355
|
+
else room.send(message, { to: room.host });
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function settleMine(id, result) {
|
|
360
|
+
const w = waiting.get(id);
|
|
361
|
+
if (!w) return;
|
|
362
|
+
waiting.delete(id);
|
|
363
|
+
clearTimeout(w.timer);
|
|
364
|
+
w.resolve(result);
|
|
365
|
+
}
|
|
366
|
+
function answer(playerId, id, result) {
|
|
367
|
+
if (playerId === room.me.id) settleMine(id, result);
|
|
368
|
+
else if (!playerId.startsWith("bot:")) room.send({ [MESSAGE_TAG]: "ans", i: id, r: result }, { to: playerId });
|
|
369
|
+
}
|
|
370
|
+
function receiveClaim(from, name, id, raw, info) {
|
|
371
|
+
const entry = claims.get(name);
|
|
372
|
+
if (!entry) return false;
|
|
373
|
+
const seenKey = `${from.id}:${id}`;
|
|
374
|
+
if (seen.has(seenKey)) return false;
|
|
375
|
+
seen.add(seenKey);
|
|
376
|
+
if (seen.size > 512) seen.delete(seen.values().next().value);
|
|
377
|
+
const refuse = (reason) => (reject(from.id, reason), answer(from.id, id, "rejected"), false);
|
|
378
|
+
if (!mayCall(from, entry.def.spectators)) return refuse(`claim '${name}' from a player who is only watching`);
|
|
379
|
+
if (!entry.limits.take(from.id, now())) return refuse(`claim '${name}' made too often`);
|
|
380
|
+
if (jsonSize(raw) > (entry.def.maxBytes ?? 2048)) return refuse(`claim '${name}' arguments are too big`);
|
|
381
|
+
const args = raw === void 0 ? {} : raw;
|
|
382
|
+
const problem = entry.schema?.check(args, `'${name}' args`);
|
|
383
|
+
if (problem) return refuse(problem);
|
|
384
|
+
const made = { id, from, args, at: info.at, ...info.matchTime !== void 0 ? { matchTime: info.matchTime } : {} };
|
|
385
|
+
const keyed = entry.def.key ? guard(`claim '${name}' key`, () => String(entry.def.key(args)), "") : "";
|
|
386
|
+
const key = keyed ? `${name}|${keyed}` : `${name}|#${seenKey}`;
|
|
387
|
+
const group = open.get(key);
|
|
388
|
+
if (group) {
|
|
389
|
+
group.claims.push(made);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
const window = Math.max(0, Math.min(5e3, entry.def.window ?? (keyed ? 150 : 0)));
|
|
393
|
+
open.set(key, { entry, claims: [made], timer: setTimeout(() => settle(key), window) });
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
function settle(key) {
|
|
397
|
+
const group = open.get(key);
|
|
398
|
+
if (!group) return;
|
|
399
|
+
open.delete(key);
|
|
400
|
+
if (!room.isHost || disposed) return;
|
|
401
|
+
const sorted = group.claims.sort((a, b) => a.at - b.at || (a.from.id < b.from.id ? -1 : a.from.id > b.from.id ? 1 : 0));
|
|
402
|
+
let winner;
|
|
403
|
+
const rivals = [];
|
|
404
|
+
for (const c of sorted) {
|
|
405
|
+
if (winner) {
|
|
406
|
+
rivals.push(c);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const verdict = guard("claim validate", () => group.entry.def.validate?.(c), false);
|
|
410
|
+
if (verdict === false || typeof verdict === "string") {
|
|
411
|
+
reject(c.from.id, typeof verdict === "string" ? verdict : "claim refused");
|
|
412
|
+
answer(c.from.id, c.id, "rejected");
|
|
413
|
+
} else winner = c;
|
|
414
|
+
}
|
|
415
|
+
if (!winner) return;
|
|
416
|
+
guard("claim apply", () => group.entry.def.apply(winner, rivals), void 0);
|
|
417
|
+
for (const rival of rivals) answer(rival.from.id, rival.id, "lost");
|
|
418
|
+
answer(winner.from.id, winner.id, "won");
|
|
419
|
+
}
|
|
420
|
+
offs.push(
|
|
421
|
+
room.on("message", (data, player, at, matchTime) => {
|
|
422
|
+
if (disposed || !isObject(data) || typeof data[MESSAGE_TAG] !== "string") return;
|
|
423
|
+
const kind = data[MESSAGE_TAG];
|
|
424
|
+
const info = { at: typeof at === "number" ? at : now(), ...typeof matchTime === "number" ? { matchTime } : {} };
|
|
425
|
+
if (kind === "ans") {
|
|
426
|
+
const id = data.i;
|
|
427
|
+
if (typeof id !== "string") return;
|
|
428
|
+
const w = waiting.get(id);
|
|
429
|
+
if (!w) return;
|
|
430
|
+
if (!w.sentTo.includes(player.id)) return reject(player.id, "an answer to a claim that was not sent to them");
|
|
431
|
+
if (data.r === "won" || data.r === "lost" || data.r === "rejected") settleMine(id, data.r);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (!room.isHost || typeof data.n !== "string" || data.n.length > 48) return;
|
|
435
|
+
if (!room.players.has(player.id)) return reject(player.id, "a message from somebody who is not in the room");
|
|
436
|
+
const from = callerOf(player);
|
|
437
|
+
if (kind === "rpc") {
|
|
438
|
+
const entry = rpcs.get(data.n);
|
|
439
|
+
if (entry) runRpc(data.n, entry, from, data.a, info);
|
|
440
|
+
} else if (kind === "claim") {
|
|
441
|
+
if (typeof data.i !== "string" || data.i.length > 32) return reject(player.id, "a claim without an id");
|
|
442
|
+
if (claims.has(data.n)) receiveClaim(from, data.n, data.i, data.a, info);
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
offs.push(
|
|
447
|
+
room.on("host", (hostId) => {
|
|
448
|
+
for (const [id, w] of waiting) {
|
|
449
|
+
if (w.sentTo[w.sentTo.length - 1] === hostId) continue;
|
|
450
|
+
w.sentTo.push(hostId);
|
|
451
|
+
const message = w.message;
|
|
452
|
+
if (hostId === room.me.id && room.isHost) receiveClaim(callerOf(room.players.get(room.me.id) ?? room.me), message.n, id, message.a, { at: now() });
|
|
453
|
+
else room.send(w.message, { to: hostId });
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
);
|
|
457
|
+
offs.push(room.on("close", () => dispose()));
|
|
458
|
+
function state(name, opts = {}) {
|
|
459
|
+
const ok = typeof name === "string" && STATE_NAME_RE.test(name);
|
|
460
|
+
if (!ok) warn(`state-name:${String(name)}`, `host.state('${String(name)}'): a name is 1-59 letters, digits or _.:-`, "error");
|
|
461
|
+
const key = `host:${ok ? name : "invalid"}`;
|
|
462
|
+
const schema2 = toSchema(opts.schema);
|
|
463
|
+
let lastRaw = void 0;
|
|
464
|
+
let lastGood = void 0;
|
|
465
|
+
const initial = () => copy(opts.initial);
|
|
466
|
+
const get = () => {
|
|
467
|
+
const raw = room.state[key];
|
|
468
|
+
if (raw === void 0 || raw === null) return lastGood ?? initial();
|
|
469
|
+
if (raw === lastRaw) return lastGood ?? initial();
|
|
470
|
+
lastRaw = raw;
|
|
471
|
+
const problem = schema2 ? check(schema2, raw, key) : null;
|
|
472
|
+
if (problem) {
|
|
473
|
+
warn(`state-bad:${key}`, `ignored ${key} from the host: ${problem}`);
|
|
474
|
+
return lastGood ?? initial();
|
|
475
|
+
}
|
|
476
|
+
lastGood = raw;
|
|
477
|
+
return lastGood;
|
|
478
|
+
};
|
|
479
|
+
const set = (value) => {
|
|
480
|
+
if (!ok || disposed || room.closed) return false;
|
|
481
|
+
if (!room.isHost) return warn(`state-host:${key}`, `${key}.set() did nothing: only the host writes it (check host.isHost)`), false;
|
|
482
|
+
const clean = value === void 0 ? null : copy(value);
|
|
483
|
+
if (value !== void 0 && value !== null && clean === void 0) return warn(`state-data:${key}`, `${key}.set(): the value must be plain data`), false;
|
|
484
|
+
if (schema2 && clean !== null) {
|
|
485
|
+
const problem = check(schema2, clean, key);
|
|
486
|
+
if (problem) return warn(`state-schema:${key}`, `${key}.set() did nothing: ${problem}`), false;
|
|
487
|
+
}
|
|
488
|
+
if (jsonSize(clean) > STATE_MAX_BYTES) return warn(`state-size:${key}`, `${key}.set() did nothing: the value is over ${STATE_MAX_BYTES} bytes (split it over several states)`), false;
|
|
489
|
+
room.setState(key, clean);
|
|
490
|
+
return true;
|
|
491
|
+
};
|
|
492
|
+
return {
|
|
493
|
+
key,
|
|
494
|
+
get: () => guard(`${key}.get`, get, initial()),
|
|
495
|
+
set: (value) => guard(`${key}.set`, () => set(value), false),
|
|
496
|
+
update(fn) {
|
|
497
|
+
if (!room.isHost) return warn(`state-host:${key}`, `${key}.update() did nothing: only the host writes it (check host.isHost)`), false;
|
|
498
|
+
const draft = copy(get());
|
|
499
|
+
const out = guard(`${key}.update`, () => fn(draft), void 0);
|
|
500
|
+
return set(out === void 0 ? draft : out);
|
|
501
|
+
},
|
|
502
|
+
on(listener) {
|
|
503
|
+
return room.on("state", (k) => {
|
|
504
|
+
if (k === key) guard(`${key} listener`, () => listener(get()), void 0);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function callAs(bot, name, args) {
|
|
510
|
+
const id = typeof bot === "string" ? bot : bot?.id;
|
|
511
|
+
if (!room.isHost || disposed || typeof id !== "string" || !BOT_ID_RE.test(id)) return false;
|
|
512
|
+
const from = { id, name: typeof bot === "object" && typeof bot.name === "string" ? bot.name : id, team: 0, bot: true };
|
|
513
|
+
const info = { at: now(), ...room.running ? { matchTime: room.matchNow() } : {} };
|
|
514
|
+
const wire = args === void 0 ? void 0 : copy(args);
|
|
515
|
+
const entry = rpcs.get(name);
|
|
516
|
+
if (entry) return runRpc(name, entry, from, wire, info);
|
|
517
|
+
if (claims.has(name)) return receiveClaim(from, name, token(), wire, info);
|
|
518
|
+
warn(`callAs:${name}`, `callAs: no rpc or claim named '${name}'`);
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
function dispose() {
|
|
522
|
+
if (disposed) return;
|
|
523
|
+
disposed = true;
|
|
524
|
+
for (const off of offs) off();
|
|
525
|
+
for (const group of open.values()) clearTimeout(group.timer);
|
|
526
|
+
open.clear();
|
|
527
|
+
for (const id of [...waiting.keys()]) settleMine(id, "timeout");
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
get isHost() {
|
|
531
|
+
return room.isHost;
|
|
532
|
+
},
|
|
533
|
+
state,
|
|
534
|
+
rpc,
|
|
535
|
+
claim,
|
|
536
|
+
callAs: (bot, name, args) => guard("callAs", () => callAs(bot, name, args), false),
|
|
537
|
+
random: (name) => createRandom(`${room.match.seed ?? 0}:${String(name)}`),
|
|
538
|
+
dispose
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
export {
|
|
542
|
+
MESSAGE_TAG,
|
|
543
|
+
check,
|
|
544
|
+
createHost,
|
|
545
|
+
createRandom,
|
|
546
|
+
t
|
|
547
|
+
};
|