@fabi-play/protocol 0.0.0-stage → 0.2.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/LICENSE +76 -0
- package/README.md +75 -2
- package/dist/index.d.ts +741 -0
- package/dist/index.js +605 -0
- package/fabi-play.schema.json +176 -0
- package/package.json +52 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
// src/sha256.ts
|
|
2
|
+
function utf8(s) {
|
|
3
|
+
const out = [];
|
|
4
|
+
for (let i = 0; i < s.length; i++) {
|
|
5
|
+
let c = s.charCodeAt(i);
|
|
6
|
+
if (c >= 55296 && c <= 56319 && i + 1 < s.length) {
|
|
7
|
+
const d = s.charCodeAt(i + 1);
|
|
8
|
+
if (d >= 56320 && d <= 57343) {
|
|
9
|
+
c = 65536 + (c - 55296 << 10) + (d - 56320);
|
|
10
|
+
i++;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (c >= 55296 && c <= 57343) c = 65533;
|
|
14
|
+
if (c < 128) out.push(c);
|
|
15
|
+
else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
|
|
16
|
+
else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
|
|
17
|
+
else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
|
|
18
|
+
}
|
|
19
|
+
return Uint8Array.from(out);
|
|
20
|
+
}
|
|
21
|
+
var K = Uint32Array.from([
|
|
22
|
+
1116352408,
|
|
23
|
+
1899447441,
|
|
24
|
+
3049323471,
|
|
25
|
+
3921009573,
|
|
26
|
+
961987163,
|
|
27
|
+
1508970993,
|
|
28
|
+
2453635748,
|
|
29
|
+
2870763221,
|
|
30
|
+
3624381080,
|
|
31
|
+
310598401,
|
|
32
|
+
607225278,
|
|
33
|
+
1426881987,
|
|
34
|
+
1925078388,
|
|
35
|
+
2162078206,
|
|
36
|
+
2614888103,
|
|
37
|
+
3248222580,
|
|
38
|
+
3835390401,
|
|
39
|
+
4022224774,
|
|
40
|
+
264347078,
|
|
41
|
+
604807628,
|
|
42
|
+
770255983,
|
|
43
|
+
1249150122,
|
|
44
|
+
1555081692,
|
|
45
|
+
1996064986,
|
|
46
|
+
2554220882,
|
|
47
|
+
2821834349,
|
|
48
|
+
2952996808,
|
|
49
|
+
3210313671,
|
|
50
|
+
3336571891,
|
|
51
|
+
3584528711,
|
|
52
|
+
113926993,
|
|
53
|
+
338241895,
|
|
54
|
+
666307205,
|
|
55
|
+
773529912,
|
|
56
|
+
1294757372,
|
|
57
|
+
1396182291,
|
|
58
|
+
1695183700,
|
|
59
|
+
1986661051,
|
|
60
|
+
2177026350,
|
|
61
|
+
2456956037,
|
|
62
|
+
2730485921,
|
|
63
|
+
2820302411,
|
|
64
|
+
3259730800,
|
|
65
|
+
3345764771,
|
|
66
|
+
3516065817,
|
|
67
|
+
3600352804,
|
|
68
|
+
4094571909,
|
|
69
|
+
275423344,
|
|
70
|
+
430227734,
|
|
71
|
+
506948616,
|
|
72
|
+
659060556,
|
|
73
|
+
883997877,
|
|
74
|
+
958139571,
|
|
75
|
+
1322822218,
|
|
76
|
+
1537002063,
|
|
77
|
+
1747873779,
|
|
78
|
+
1955562222,
|
|
79
|
+
2024104815,
|
|
80
|
+
2227730452,
|
|
81
|
+
2361852424,
|
|
82
|
+
2428436474,
|
|
83
|
+
2756734187,
|
|
84
|
+
3204031479,
|
|
85
|
+
3329325298
|
|
86
|
+
]);
|
|
87
|
+
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
88
|
+
function sha256(bytes) {
|
|
89
|
+
const blocks = Math.ceil((bytes.length + 9) / 64);
|
|
90
|
+
const data = new Uint8Array(blocks * 64);
|
|
91
|
+
data.set(bytes);
|
|
92
|
+
data[bytes.length] = 128;
|
|
93
|
+
const view = new DataView(data.buffer);
|
|
94
|
+
const bits = bytes.length * 8;
|
|
95
|
+
view.setUint32(data.length - 8, Math.floor(bits / 4294967296));
|
|
96
|
+
view.setUint32(data.length - 4, bits >>> 0);
|
|
97
|
+
const h = Uint32Array.from([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
|
|
98
|
+
const w = new Uint32Array(64);
|
|
99
|
+
for (let off = 0; off < data.length; off += 64) {
|
|
100
|
+
for (let t = 0; t < 16; t++) w[t] = view.getUint32(off + t * 4);
|
|
101
|
+
for (let t = 16; t < 64; t++) {
|
|
102
|
+
const a2 = w[t - 15];
|
|
103
|
+
const b2 = w[t - 2];
|
|
104
|
+
const s0 = rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3;
|
|
105
|
+
const s1 = rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10;
|
|
106
|
+
w[t] = w[t - 16] + s0 + w[t - 7] + s1 | 0;
|
|
107
|
+
}
|
|
108
|
+
let [a, b, c, d, e, f, g, hh] = [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]];
|
|
109
|
+
for (let t = 0; t < 64; t++) {
|
|
110
|
+
const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[t] + w[t] | 0;
|
|
111
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
|
|
112
|
+
hh = g;
|
|
113
|
+
g = f;
|
|
114
|
+
f = e;
|
|
115
|
+
e = d + t1 | 0;
|
|
116
|
+
d = c;
|
|
117
|
+
c = b;
|
|
118
|
+
b = a;
|
|
119
|
+
a = t1 + t2 | 0;
|
|
120
|
+
}
|
|
121
|
+
h[0] = h[0] + a;
|
|
122
|
+
h[1] = h[1] + b;
|
|
123
|
+
h[2] = h[2] + c;
|
|
124
|
+
h[3] = h[3] + d;
|
|
125
|
+
h[4] = h[4] + e;
|
|
126
|
+
h[5] = h[5] + f;
|
|
127
|
+
h[6] = h[6] + g;
|
|
128
|
+
h[7] = h[7] + hh;
|
|
129
|
+
}
|
|
130
|
+
const out = new Uint8Array(32);
|
|
131
|
+
const outView = new DataView(out.buffer);
|
|
132
|
+
for (let i = 0; i < 8; i++) outView.setUint32(i * 4, h[i]);
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/index.ts
|
|
137
|
+
var PROTOCOL_VERSION = 1;
|
|
138
|
+
var LANDMARKS = 33;
|
|
139
|
+
var P = {
|
|
140
|
+
nose: 0,
|
|
141
|
+
leftShoulder: 11,
|
|
142
|
+
rightShoulder: 12,
|
|
143
|
+
leftElbow: 13,
|
|
144
|
+
rightElbow: 14,
|
|
145
|
+
leftWrist: 15,
|
|
146
|
+
rightWrist: 16,
|
|
147
|
+
leftHip: 23,
|
|
148
|
+
rightHip: 24,
|
|
149
|
+
leftKnee: 25,
|
|
150
|
+
rightKnee: 26,
|
|
151
|
+
leftAnkle: 27,
|
|
152
|
+
rightAnkle: 28
|
|
153
|
+
};
|
|
154
|
+
var AGE_RANGES = ["3-5", "6-8", "9-12"];
|
|
155
|
+
var isAgeRange = (v) => AGE_RANGES.includes(v);
|
|
156
|
+
var youngest = (ranges) => AGE_RANGES.find((r) => ranges.includes(r)) ?? AGE_RANGES[0];
|
|
157
|
+
var MAX_PLAYERS = 4;
|
|
158
|
+
var PLAYER_COLORS = ["#EE6A43", "#6FA0E0", "#8CC9A0", "#C49BE0"];
|
|
159
|
+
var MAX_AVATAR_LENGTH = 32;
|
|
160
|
+
var isAvatarName = (v) => typeof v === "string" && v.length <= MAX_AVATAR_LENGTH && /^[a-z0-9]+(-[a-z0-9]+)*$/.test(v);
|
|
161
|
+
var DEFAULT_AVATARS_BY_AGE = {
|
|
162
|
+
"3-5": ["bunny", "bear", "panda", "penguin"],
|
|
163
|
+
"6-8": ["fox", "lion", "dragon", "unicorn"],
|
|
164
|
+
"9-12": ["robot", "astronaut", "ninja", "skater"]
|
|
165
|
+
};
|
|
166
|
+
var DEFAULT_AVATARS = DEFAULT_AVATARS_BY_AGE["6-8"];
|
|
167
|
+
var defaultAvatarFor = (slot, ageRange) => {
|
|
168
|
+
const list = isAgeRange(ageRange) ? DEFAULT_AVATARS_BY_AGE[ageRange] : DEFAULT_AVATARS;
|
|
169
|
+
return list[Number.isInteger(slot) && slot >= 0 ? slot % list.length : 0];
|
|
170
|
+
};
|
|
171
|
+
var MIRROR_ENABLED = true;
|
|
172
|
+
var VIDEO_STATES = ["on", "off", "blocked"];
|
|
173
|
+
var VIDEO_BLOCKS = ["not-local", "game", "idle"];
|
|
174
|
+
function parseFov(v) {
|
|
175
|
+
if (!isObject(v)) return void 0;
|
|
176
|
+
const ok = (n) => isNumber(n) && n >= 10 && n <= 170;
|
|
177
|
+
return ok(v.h) && ok(v.v) ? { h: v.h, v: v.v } : void 0;
|
|
178
|
+
}
|
|
179
|
+
function parseZone(v) {
|
|
180
|
+
if (!isObject(v)) return void 0;
|
|
181
|
+
const { min, max } = v;
|
|
182
|
+
return isNumber(min) && isNumber(max) && min > 0 && min < max && max <= 10 ? { min, max } : void 0;
|
|
183
|
+
}
|
|
184
|
+
var SCREEN_SIZES = [
|
|
185
|
+
{ id: "32", kind: "tv", inches: 32, label: "32\u2033", cm: "81 cm", range: [24, 36] },
|
|
186
|
+
{ id: "40-43", kind: "tv", inches: 43, label: "40\u201343\u2033", cm: "102\u2013109 cm", range: [36, 46] },
|
|
187
|
+
{ id: "50-55", kind: "tv", inches: 55, label: "50\u201355\u2033", cm: "127\u2013140 cm", range: [46, 60] },
|
|
188
|
+
{ id: "65", kind: "tv", inches: 65, label: "65\u2033", cm: "165 cm", range: [60, 70] },
|
|
189
|
+
{ id: "75+", kind: "tv", inches: 75, label: "75\u2033+", cm: "190+ cm", range: [70, 120] },
|
|
190
|
+
{ id: "phone", kind: "tablet", inches: 7, label: "6\u20137\u2033", cm: "15\u201318 cm", range: [4, 7.5] },
|
|
191
|
+
{ id: "tablet-8", kind: "tablet", inches: 8.5, label: "8\u2033", cm: "20 cm", range: [7.5, 9.5] },
|
|
192
|
+
{ id: "tablet-11", kind: "tablet", inches: 11, label: "10\u201311\u2033", cm: "25\u201328 cm", range: [9.5, 11.5] },
|
|
193
|
+
{ id: "tablet-13", kind: "tablet", inches: 13, label: "12\u201313\u2033", cm: "30\u201333 cm", range: [11.5, 18] }
|
|
194
|
+
];
|
|
195
|
+
var isScreenSizeId = (v) => SCREEN_SIZES.some((s) => s.id === v);
|
|
196
|
+
var screenSize = (id) => SCREEN_SIZES.find((s) => s.id === id);
|
|
197
|
+
var screenSizesOf = (kind) => SCREEN_SIZES.filter((s) => s.kind === kind);
|
|
198
|
+
function sizeForInches(inches, kind) {
|
|
199
|
+
if (!isNumber(inches)) return null;
|
|
200
|
+
return screenSizesOf(kind).find((s) => inches >= s.range[0] && inches < s.range[1])?.id ?? null;
|
|
201
|
+
}
|
|
202
|
+
var SCREEN_SIZE_KEY = "fabi-play.screen-size";
|
|
203
|
+
var TOO_CLOSE_KEY = "fabi-play.too-close-pause";
|
|
204
|
+
var PHASES = ["setup", "intro", "calibrate", "play", "pause", "break", "end"];
|
|
205
|
+
var NS = "fabi-play";
|
|
206
|
+
var wire = (m) => ({ ns: NS, ...m });
|
|
207
|
+
var BREAK_REASONS = ["level", "gameover"];
|
|
208
|
+
function parseView(v) {
|
|
209
|
+
if (!isObject(v)) return void 0;
|
|
210
|
+
const { x, y, w, h } = v;
|
|
211
|
+
return isNumber(x) && isNumber(y) && isNumber(w) && isNumber(h) && w > 0 && h > 0 ? { x, y, w, h } : void 0;
|
|
212
|
+
}
|
|
213
|
+
var toScreen = (view, p) => view ? { x: view.x + p.x * view.w, y: view.y + p.y * view.h } : { x: p.x, y: p.y };
|
|
214
|
+
var DEVICE_TIERS = ["high", "medium", "low"];
|
|
215
|
+
function parseDeviceHint(v) {
|
|
216
|
+
if (!isObject(v)) return void 0;
|
|
217
|
+
const out = {};
|
|
218
|
+
if (DEVICE_TIERS.includes(v.tier)) out.tier = v.tier;
|
|
219
|
+
if (isNumber(v.hz) && v.hz >= 20 && v.hz <= 240) out.hz = Math.round(v.hz);
|
|
220
|
+
return out.tier || out.hz ? out : void 0;
|
|
221
|
+
}
|
|
222
|
+
function parseGameStats(v) {
|
|
223
|
+
const out = {};
|
|
224
|
+
if (!isObject(v)) return out;
|
|
225
|
+
if (isNumber(v.fps) && v.fps >= 0 && v.fps < 1e3) out.fps = v.fps;
|
|
226
|
+
if (typeof v.renderer === "string" && /^[a-z0-9-]{1,16}$/.test(v.renderer)) out.renderer = v.renderer;
|
|
227
|
+
if (DEVICE_TIERS.includes(v.tier)) out.tier = v.tier;
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
var PAUSE_REASONS = ["user", "distance"];
|
|
231
|
+
var GAME_BACKDROPS = ["video"];
|
|
232
|
+
var MOVEMENTS = ["hands", "jump", "squat", "clap", "freeze", "lanes", "shapes"];
|
|
233
|
+
function parseManifest(data) {
|
|
234
|
+
const errors = [];
|
|
235
|
+
const m = isObject(data) ? data : {};
|
|
236
|
+
const text = (v, max) => typeof v === "string" && v.trim().length > 0 && v.length <= max;
|
|
237
|
+
const texts = (v, max) => isObject(v) && text(v.it, max) && text(v.en, max);
|
|
238
|
+
if (!(typeof m.id === "string" && isGameId(m.id))) errors.push("id: lowercase letters, digits and hyphens");
|
|
239
|
+
if (!(typeof m.version === "string" && /^\d+\.\d+(\.\d+)?$/.test(m.version))) errors.push("version: e.g. 1.0 or 1.0.2");
|
|
240
|
+
if (!texts(m.title, 60)) errors.push("title: in Italian and English, up to 60 characters");
|
|
241
|
+
if (!texts(m.description, 300)) errors.push("description: in Italian and English, up to 300 characters");
|
|
242
|
+
if (!(Array.isArray(m.ageRanges) && m.ageRanges.length > 0 && m.ageRanges.every(isAgeRange))) errors.push(`ageRanges: at least one of ${AGE_RANGES.join(", ")}`);
|
|
243
|
+
if (!(Array.isArray(m.movements) && m.movements.length > 0 && m.movements.every((x) => MOVEMENTS.includes(x)))) errors.push(`movements: from ${MOVEMENTS.join(", ")}`);
|
|
244
|
+
const pl = isObject(m.players) ? m.players : {};
|
|
245
|
+
if (!(isNumber(pl.min) && isNumber(pl.max) && pl.min >= 1 && pl.max <= 4 && pl.min <= pl.max)) errors.push("players: min and max between 1 and 4");
|
|
246
|
+
if (!(isNumber(m.minutes) && m.minutes >= 1 && m.minutes <= 20)) errors.push("minutes: from 1 to 20");
|
|
247
|
+
if (!isGamePath(m.entry, "html")) errors.push("entry: an .html page of the game, a path relative to its address (no scheme, no leading /)");
|
|
248
|
+
if (m.sdk !== PROTOCOL_VERSION) errors.push(`sdk: this version of Fabi Play uses protocol ${PROTOCOL_VERSION}`);
|
|
249
|
+
if (m.backdrop !== void 0 && !(Array.isArray(m.backdrop) && m.backdrop.every((b) => GAME_BACKDROPS.includes(b)) && new Set(m.backdrop).size === m.backdrop.length))
|
|
250
|
+
errors.push(`backdrop: optional, from ${GAME_BACKDROPS.join(", ")}, without repeats`);
|
|
251
|
+
if (m.intro !== void 0) errors.push(...introErrors(m.intro));
|
|
252
|
+
return errors.length ? { errors } : { manifest: m };
|
|
253
|
+
}
|
|
254
|
+
var INTRO_MOVES = ["jump", "squat", "clap", "lane-left", "lane-right", "arms-up", "arms-wide", "arms-v", "star"];
|
|
255
|
+
var INTRO_MAX_STEPS = 4;
|
|
256
|
+
var INTRO_MAX_CHARS = 90;
|
|
257
|
+
var LOCALES = ["it", "en"];
|
|
258
|
+
var STEP_KEYS = ["say", "image", "move", "practice"];
|
|
259
|
+
var UNSPEAKABLE = /[\p{Cc}\p{Cs}\u202A-\u202E\u2066-\u2069]/u;
|
|
260
|
+
var sentence = (v) => typeof v === "string" && v.length > 0 && v === v.trim() && [...v.normalize("NFC")].length <= INTRO_MAX_CHARS && !UNSPEAKABLE.test(v);
|
|
261
|
+
var sentences = (v) => isObject(v) && Object.keys(v).every((k) => LOCALES.includes(k)) && LOCALES.every((l) => sentence(v[l]));
|
|
262
|
+
var isPlainText = (say) => LOCALES.some((l) => l in say);
|
|
263
|
+
function sayErrors(say, path) {
|
|
264
|
+
const plain = `Italian and English, up to ${INTRO_MAX_CHARS} characters, no spaces around, no control characters`;
|
|
265
|
+
if (!isObject(say)) return [`${path}: ${plain}`];
|
|
266
|
+
const keys = Object.keys(say);
|
|
267
|
+
if (keys.some((k) => LOCALES.includes(k))) return sentences(say) ? [] : [`${path}: ${plain}`];
|
|
268
|
+
if (keys.length === 0 || !keys.every(isAgeRange)) return [`${path}: ${plain}, or that for one or more of ${AGE_RANGES.join(", ")}`];
|
|
269
|
+
return keys.flatMap((k) => sentences(say[k]) ? [] : [`${path}.${k}: ${plain}`]);
|
|
270
|
+
}
|
|
271
|
+
function introErrors(intro) {
|
|
272
|
+
const steps = isObject(intro) ? intro.steps : void 0;
|
|
273
|
+
if (!(Array.isArray(steps) && steps.length >= 1 && steps.length <= INTRO_MAX_STEPS)) return [`intro: 1 to ${INTRO_MAX_STEPS} steps`];
|
|
274
|
+
const unknown = Object.keys(intro).filter((k) => k !== "steps").map((k) => `intro.${k}: not a field of intro (only steps)`);
|
|
275
|
+
return unknown.concat(steps.flatMap((step, i) => {
|
|
276
|
+
const path = `intro.steps[${i}]`;
|
|
277
|
+
if (!isObject(step)) return [`${path}: an object with say, and an image or a move`];
|
|
278
|
+
const errors = Object.keys(step).filter((k) => !STEP_KEYS.includes(k)).map((k) => `${path}.${k}: not a field of a step (${STEP_KEYS.join(", ")})`);
|
|
279
|
+
errors.push(...sayErrors(step.say, `${path}.say`));
|
|
280
|
+
const { image, move, practice } = step;
|
|
281
|
+
if (image !== void 0 && move !== void 0) errors.push(`${path}: an image or a move, not both`);
|
|
282
|
+
else if (image === void 0 && move === void 0) errors.push(`${path}: an image or a move`);
|
|
283
|
+
if (image !== void 0 && !isGamePath(image, "jpg|png"))
|
|
284
|
+
errors.push(`${path}.image: a .jpg or .png of the game, a path relative to its address (no scheme, no leading /)`);
|
|
285
|
+
if (move !== void 0 && !INTRO_MOVES.includes(move)) errors.push(`${path}.move: one of ${INTRO_MOVES.join(", ")}`);
|
|
286
|
+
if (practice !== void 0 && typeof practice !== "boolean") errors.push(`${path}.practice: true or false`);
|
|
287
|
+
else if (practice === true && move === void 0) errors.push(`${path}.practice: only with a move`);
|
|
288
|
+
return errors;
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
function parseIntro(data) {
|
|
292
|
+
if (data === void 0 || introErrors(data).length) return null;
|
|
293
|
+
const steps = data.steps.map((s) => {
|
|
294
|
+
const say = s.say;
|
|
295
|
+
const copy = Object.fromEntries(Object.entries(say).map(([k, v]) => [k, typeof v === "string" ? v : { it: v.it, en: v.en }]));
|
|
296
|
+
return {
|
|
297
|
+
say: copy,
|
|
298
|
+
...typeof s.image === "string" ? { image: s.image } : { move: s.move },
|
|
299
|
+
...s.practice === true ? { practice: true } : {}
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
return { steps };
|
|
303
|
+
}
|
|
304
|
+
function introText(say, locale, ageRange) {
|
|
305
|
+
if (isPlainText(say)) return say[locale] ?? "";
|
|
306
|
+
const at = AGE_RANGES.indexOf(ageRange);
|
|
307
|
+
const present = AGE_RANGES.filter((r) => say[r]);
|
|
308
|
+
const closest = present.reduce((best, r) => best === void 0 || Math.abs(AGE_RANGES.indexOf(r) - at) < Math.abs(AGE_RANGES.indexOf(best) - at) ? r : best, void 0);
|
|
309
|
+
return closest ? say[closest]?.[locale] ?? "" : "";
|
|
310
|
+
}
|
|
311
|
+
var FABI_VOICE = "fabi-v1";
|
|
312
|
+
function voiceKeySync(text, locale, voice = FABI_VOICE) {
|
|
313
|
+
const hash = sha256(utf8(`${voice}
|
|
314
|
+
${locale}
|
|
315
|
+
${text.normalize("NFC").trim()}`));
|
|
316
|
+
return Array.from(hash.subarray(0, 16), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
317
|
+
}
|
|
318
|
+
var voiceKey = (text, locale, voice = FABI_VOICE) => Promise.resolve(voiceKeySync(text, locale, voice));
|
|
319
|
+
var voiceUrl = (base, key, voice = FABI_VOICE) => `${base.replace(/\/+$/, "")}/voice/${voice}/${key}.mp3`;
|
|
320
|
+
var gamesFor = (games, range, players = 1) => games.filter((g) => g.ageRanges.includes(range) && players >= g.players.min && players <= g.players.max);
|
|
321
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
322
|
+
var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
323
|
+
var SEGMENT = "[A-Za-z0-9_][A-Za-z0-9_-]*(?:\\.[A-Za-z0-9_-]+)*";
|
|
324
|
+
var isGamePath = (v, ext) => typeof v === "string" && v.length <= 200 && new RegExp(`^(?:${SEGMENT}/)*${SEGMENT}\\.(?:${ext})$`).test(v);
|
|
325
|
+
var isGameId = (v) => /^[a-z0-9]+(-[a-z0-9]+)*$/.test(v) && v.length <= 60;
|
|
326
|
+
var isBody = (v) => Array.isArray(v) && v.length === LANDMARKS * 3 && v.every(isNumber);
|
|
327
|
+
function parseGameMessage(data) {
|
|
328
|
+
if (!isObject(data) || data.ns !== NS) return null;
|
|
329
|
+
switch (data.type) {
|
|
330
|
+
case "ready":
|
|
331
|
+
return { type: "ready" };
|
|
332
|
+
case "break":
|
|
333
|
+
return { type: "break", reason: data.reason === void 0 ? "level" : BREAK_REASONS.includes(data.reason) ? data.reason : "unknown" };
|
|
334
|
+
case "score":
|
|
335
|
+
return isNumber(data.player) && isNumber(data.score) ? { type: "score", player: data.player, score: data.score } : null;
|
|
336
|
+
case "end": {
|
|
337
|
+
if (!Array.isArray(data.scores) || data.scores.length > 8) return null;
|
|
338
|
+
const scores = data.scores.flatMap(
|
|
339
|
+
(s) => isObject(s) && isNumber(s.player) && isNumber(s.score) && [0, 1, 2, 3].includes(s.stars) ? [{ player: s.player, score: s.score, stars: s.stars }] : []
|
|
340
|
+
);
|
|
341
|
+
return scores.length === data.scores.length ? { type: "end", scores } : null;
|
|
342
|
+
}
|
|
343
|
+
case "stats":
|
|
344
|
+
return isNumber(data.renderFps) && data.renderFps >= 0 && data.renderFps < 1e3 ? { type: "stats", renderFps: data.renderFps, ...parseGameStats(data) } : null;
|
|
345
|
+
default:
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
var COMMANDS = ["pause", "resume", "quit"];
|
|
350
|
+
var INTRO_ACTIONS = ["next", "repeat", "skip", "replay"];
|
|
351
|
+
function parseControllerMessage(data) {
|
|
352
|
+
if (!isObject(data)) return null;
|
|
353
|
+
switch (data.type) {
|
|
354
|
+
case "hello":
|
|
355
|
+
if (!(isNumber(data.v) && (data.camera === "back" || data.camera === "front") && typeof data.device === "string")) return null;
|
|
356
|
+
{
|
|
357
|
+
const fov = parseFov(data.fov);
|
|
358
|
+
return {
|
|
359
|
+
type: "hello",
|
|
360
|
+
v: data.v,
|
|
361
|
+
camera: data.camera,
|
|
362
|
+
device: data.device.slice(0, 80),
|
|
363
|
+
aspect: isNumber(data.aspect) ? Math.min(Math.max(data.aspect, 0.3), 3) : 0.75,
|
|
364
|
+
...fov ? { fov } : {}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
case "pose":
|
|
368
|
+
return isNumber(data.t) && Array.isArray(data.bodies) && data.bodies.length <= MAX_PLAYERS && data.bodies.every(isBody) ? { type: "pose", t: data.t, bodies: data.bodies } : null;
|
|
369
|
+
case "players": {
|
|
370
|
+
if (!Array.isArray(data.players) || data.players.length < 1 || data.players.length > MAX_PLAYERS) return null;
|
|
371
|
+
const players = data.players.flatMap(
|
|
372
|
+
(p) => isObject(p) && typeof p.name === "string" && isAgeRange(p.ageRange) ? [{ name: p.name.trim().slice(0, 20), ageRange: p.ageRange, ...isAvatarName(p.avatar) ? { avatar: p.avatar } : {} }] : []
|
|
373
|
+
);
|
|
374
|
+
if (players.length !== data.players.length) return null;
|
|
375
|
+
return typeof data.tooClosePause === "boolean" ? { type: "players", players, tooClosePause: data.tooClosePause } : { type: "players", players };
|
|
376
|
+
}
|
|
377
|
+
case "screen-size":
|
|
378
|
+
if (!isScreenSizeId(data.size)) return null;
|
|
379
|
+
return data.change === true ? { type: "screen-size", size: data.size, change: true } : { type: "screen-size", size: data.size };
|
|
380
|
+
case "pick":
|
|
381
|
+
return typeof data.game === "string" && isGameId(data.game) ? { type: "pick", game: data.game } : null;
|
|
382
|
+
case "command":
|
|
383
|
+
return COMMANDS.includes(data.command) ? { type: "command", command: data.command } : null;
|
|
384
|
+
case "intro":
|
|
385
|
+
return INTRO_ACTIONS.includes(data.action) ? { type: "intro", action: data.action } : null;
|
|
386
|
+
case "video":
|
|
387
|
+
return VIDEO_STATES.includes(data.state) ? { type: "video", state: data.state, ...VIDEO_BLOCKS.includes(data.reason) ? { reason: data.reason } : {} } : null;
|
|
388
|
+
default:
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function parseScreenMessage(data) {
|
|
393
|
+
if (!isObject(data)) return null;
|
|
394
|
+
switch (data.type) {
|
|
395
|
+
case "welcome":
|
|
396
|
+
if (!(isNumber(data.v) && typeof data.screen === "string")) return null;
|
|
397
|
+
{
|
|
398
|
+
const binaryPose = Array.isArray(data.binaryPose) ? data.binaryPose.slice(0, 8).filter((n) => Number.isInteger(n) && n >= 1 && n <= 255) : [];
|
|
399
|
+
return { type: "welcome", v: data.v, screen: data.screen, ...binaryPose.length ? { binaryPose } : {} };
|
|
400
|
+
}
|
|
401
|
+
case "phase":
|
|
402
|
+
return PHASES.includes(data.phase) ? {
|
|
403
|
+
type: "phase",
|
|
404
|
+
phase: data.phase,
|
|
405
|
+
...typeof data.game === "string" ? { game: data.game } : {},
|
|
406
|
+
...data.intro === true ? { intro: true } : {}
|
|
407
|
+
} : null;
|
|
408
|
+
case "catalogue": {
|
|
409
|
+
if (!Array.isArray(data.games) || data.games.length > 50) return null;
|
|
410
|
+
const games = data.games.flatMap(
|
|
411
|
+
(g) => isObject(g) && typeof g.id === "string" && isGameId(g.id) && typeof g.title === "string" ? [{ id: g.id, title: g.title.slice(0, 60) }] : []
|
|
412
|
+
);
|
|
413
|
+
return games.length === data.games.length ? { type: "catalogue", games } : null;
|
|
414
|
+
}
|
|
415
|
+
case "vibrate":
|
|
416
|
+
return isNumber(data.ms) ? { type: "vibrate", ms: Math.min(Math.max(data.ms, 0), 500) } : null;
|
|
417
|
+
case "video-allowed":
|
|
418
|
+
return typeof data.allowed === "boolean" ? { type: "video-allowed", allowed: data.allowed, ...VIDEO_BLOCKS.includes(data.reason) ? { reason: data.reason } : {} } : null;
|
|
419
|
+
case "intro": {
|
|
420
|
+
const { step, steps, text, voice } = data;
|
|
421
|
+
if (!(Number.isInteger(step) && Number.isInteger(steps) && typeof text === "string" && typeof voice === "boolean")) return null;
|
|
422
|
+
const [s, n] = [step, steps];
|
|
423
|
+
return s >= 0 && s < n && n <= INTRO_MAX_STEPS + 1 ? { type: "intro", step: s, steps: n, text: Array.from(text.trim()).slice(0, 200).join(""), voice } : null;
|
|
424
|
+
}
|
|
425
|
+
case "screen-size": {
|
|
426
|
+
if (typeof data.ask !== "boolean") return null;
|
|
427
|
+
const zone = parseZone(data.zone);
|
|
428
|
+
return {
|
|
429
|
+
type: "screen-size",
|
|
430
|
+
ask: data.ask,
|
|
431
|
+
...isScreenSizeId(data.size) ? { size: data.size } : {},
|
|
432
|
+
...isScreenSizeId(data.detected) ? { detected: data.detected } : {},
|
|
433
|
+
...typeof data.model === "string" && data.model.trim() ? { model: data.model.trim().slice(0, 40) } : {},
|
|
434
|
+
...zone ? { zone } : {}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
default:
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function toPlayerSpace(points, mirrored = false) {
|
|
442
|
+
const r = (n) => Math.round(Math.min(Math.max(n, 0), 1) * 1e3) / 1e3;
|
|
443
|
+
return points.flatMap((p) => [r(mirrored ? p.x : 1 - p.x), r(p.y), r(p.visibility ?? 1)]);
|
|
444
|
+
}
|
|
445
|
+
var POSE_CODEC_VERSION = 1;
|
|
446
|
+
var POSE_CODEC_VERSIONS = [POSE_CODEC_VERSION];
|
|
447
|
+
var POSE_MAGIC = 251;
|
|
448
|
+
var POSE_KIND = 1;
|
|
449
|
+
var POSE_HEADER = 12;
|
|
450
|
+
var POSE_BODY_BYTES = LANDMARKS * 4;
|
|
451
|
+
var poseBytes = (bodies) => POSE_HEADER + bodies * POSE_BODY_BYTES;
|
|
452
|
+
var POSE_MAX_BYTES = poseBytes(MAX_PLAYERS);
|
|
453
|
+
var acceptsBinaryPose = (welcome) => welcome.binaryPose?.includes(POSE_CODEC_VERSION) ?? false;
|
|
454
|
+
var milli = (v) => {
|
|
455
|
+
if (!Number.isFinite(v)) throw new RangeError("pose: a landmark is not a finite number");
|
|
456
|
+
return v <= 0 ? 0 : v >= 1 ? 1e3 : Math.round(v * 1e3);
|
|
457
|
+
};
|
|
458
|
+
var lastOut = null;
|
|
459
|
+
var lastView = null;
|
|
460
|
+
var viewOf = (b) => {
|
|
461
|
+
if (b !== lastOut || !lastView) {
|
|
462
|
+
lastOut = b;
|
|
463
|
+
lastView = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
464
|
+
}
|
|
465
|
+
return lastView;
|
|
466
|
+
};
|
|
467
|
+
function encodePose(msg, out) {
|
|
468
|
+
const { t, bodies } = msg;
|
|
469
|
+
if (!Number.isFinite(t)) throw new RangeError("pose: t is not a finite number");
|
|
470
|
+
if (bodies.length > MAX_PLAYERS) throw new RangeError(`pose: more than ${MAX_PLAYERS} bodies`);
|
|
471
|
+
const size = poseBytes(bodies.length);
|
|
472
|
+
const bytes = out && out.byteLength >= size ? out : new Uint8Array(size);
|
|
473
|
+
const view = viewOf(bytes);
|
|
474
|
+
view.setUint8(0, POSE_MAGIC);
|
|
475
|
+
view.setUint8(1, POSE_CODEC_VERSION);
|
|
476
|
+
view.setUint8(2, POSE_KIND);
|
|
477
|
+
view.setUint8(3, bodies.length);
|
|
478
|
+
view.setFloat64(4, t, true);
|
|
479
|
+
let at = POSE_HEADER;
|
|
480
|
+
for (const body of bodies) {
|
|
481
|
+
if (body.length !== LANDMARKS * 3) throw new RangeError(`pose: a body is not ${LANDMARKS} \xD7 3 numbers`);
|
|
482
|
+
for (let i = 0; i < body.length; i += 3) {
|
|
483
|
+
view.setUint32(at, (milli(body[i]) | milli(body[i + 1]) << 10 | milli(body[i + 2]) << 20) >>> 0, true);
|
|
484
|
+
at += 4;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return bytes.byteLength === size ? bytes : bytes.subarray(0, size);
|
|
488
|
+
}
|
|
489
|
+
function decodePose(data) {
|
|
490
|
+
let view;
|
|
491
|
+
if (data instanceof ArrayBuffer) view = new DataView(data);
|
|
492
|
+
else if (ArrayBuffer.isView(data)) view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
493
|
+
else return null;
|
|
494
|
+
if (view.byteLength < POSE_HEADER) return null;
|
|
495
|
+
if (view.getUint8(0) !== POSE_MAGIC || !POSE_CODEC_VERSIONS.includes(view.getUint8(1)) || view.getUint8(2) !== POSE_KIND) return null;
|
|
496
|
+
const count = view.getUint8(3);
|
|
497
|
+
if (count > MAX_PLAYERS || view.byteLength !== poseBytes(count)) return null;
|
|
498
|
+
const t = view.getFloat64(4, true);
|
|
499
|
+
if (!Number.isFinite(t)) return null;
|
|
500
|
+
const bodies = [];
|
|
501
|
+
let at = POSE_HEADER;
|
|
502
|
+
for (let b = 0; b < count; b++) {
|
|
503
|
+
const body = new Array(LANDMARKS * 3);
|
|
504
|
+
for (let i = 0; i < LANDMARKS * 3; i += 3) {
|
|
505
|
+
const word = view.getUint32(at, true);
|
|
506
|
+
at += 4;
|
|
507
|
+
const x = word & 1023;
|
|
508
|
+
const y = word >>> 10 & 1023;
|
|
509
|
+
const v = word >>> 20 & 1023;
|
|
510
|
+
if (x > 1e3 || y > 1e3 || v > 1e3 || word >>> 30 !== 0) return null;
|
|
511
|
+
body[i] = x / 1e3;
|
|
512
|
+
body[i + 1] = y / 1e3;
|
|
513
|
+
body[i + 2] = v / 1e3;
|
|
514
|
+
}
|
|
515
|
+
bodies.push(body);
|
|
516
|
+
}
|
|
517
|
+
return { type: "pose", t, bodies };
|
|
518
|
+
}
|
|
519
|
+
var legacyStorageKey = (key) => key.startsWith("fabi-play.") ? `fablee-play.${key.slice("fabi-play.".length)}` : null;
|
|
520
|
+
function migrateStorageKey(store, newKey, oldKey = legacyStorageKey(newKey)) {
|
|
521
|
+
if (!store || !oldKey) return;
|
|
522
|
+
try {
|
|
523
|
+
const old = store.getItem(oldKey);
|
|
524
|
+
if (old === null) return;
|
|
525
|
+
if (store.getItem(newKey) === null) store.setItem(newKey, old);
|
|
526
|
+
store.removeItem(oldKey);
|
|
527
|
+
} catch {
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function migrateStorageKeyAsync(store, newKey, oldKey = legacyStorageKey(newKey)) {
|
|
531
|
+
if (!oldKey) return;
|
|
532
|
+
try {
|
|
533
|
+
const old = await store.getItem(oldKey);
|
|
534
|
+
if (old === null) return;
|
|
535
|
+
if (await store.getItem(newKey) === null) await store.setItem(newKey, old);
|
|
536
|
+
await store.removeItem(oldKey);
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
export {
|
|
541
|
+
AGE_RANGES,
|
|
542
|
+
BREAK_REASONS,
|
|
543
|
+
DEFAULT_AVATARS,
|
|
544
|
+
DEFAULT_AVATARS_BY_AGE,
|
|
545
|
+
DEVICE_TIERS,
|
|
546
|
+
FABI_VOICE,
|
|
547
|
+
GAME_BACKDROPS,
|
|
548
|
+
INTRO_MAX_CHARS,
|
|
549
|
+
INTRO_MAX_STEPS,
|
|
550
|
+
INTRO_MOVES,
|
|
551
|
+
LANDMARKS,
|
|
552
|
+
MAX_AVATAR_LENGTH,
|
|
553
|
+
MAX_PLAYERS,
|
|
554
|
+
MIRROR_ENABLED,
|
|
555
|
+
MOVEMENTS,
|
|
556
|
+
NS,
|
|
557
|
+
P,
|
|
558
|
+
PAUSE_REASONS,
|
|
559
|
+
PHASES,
|
|
560
|
+
PLAYER_COLORS,
|
|
561
|
+
POSE_CODEC_VERSION,
|
|
562
|
+
POSE_CODEC_VERSIONS,
|
|
563
|
+
POSE_MAX_BYTES,
|
|
564
|
+
PROTOCOL_VERSION,
|
|
565
|
+
SCREEN_SIZES,
|
|
566
|
+
SCREEN_SIZE_KEY,
|
|
567
|
+
TOO_CLOSE_KEY,
|
|
568
|
+
VIDEO_BLOCKS,
|
|
569
|
+
VIDEO_STATES,
|
|
570
|
+
acceptsBinaryPose,
|
|
571
|
+
decodePose,
|
|
572
|
+
defaultAvatarFor,
|
|
573
|
+
encodePose,
|
|
574
|
+
gamesFor,
|
|
575
|
+
introText,
|
|
576
|
+
isAgeRange,
|
|
577
|
+
isAvatarName,
|
|
578
|
+
isBody,
|
|
579
|
+
isGameId,
|
|
580
|
+
isScreenSizeId,
|
|
581
|
+
legacyStorageKey,
|
|
582
|
+
migrateStorageKey,
|
|
583
|
+
migrateStorageKeyAsync,
|
|
584
|
+
parseControllerMessage,
|
|
585
|
+
parseDeviceHint,
|
|
586
|
+
parseFov,
|
|
587
|
+
parseGameMessage,
|
|
588
|
+
parseGameStats,
|
|
589
|
+
parseIntro,
|
|
590
|
+
parseManifest,
|
|
591
|
+
parseScreenMessage,
|
|
592
|
+
parseView,
|
|
593
|
+
parseZone,
|
|
594
|
+
poseBytes,
|
|
595
|
+
screenSize,
|
|
596
|
+
screenSizesOf,
|
|
597
|
+
sizeForInches,
|
|
598
|
+
toPlayerSpace,
|
|
599
|
+
toScreen,
|
|
600
|
+
voiceKey,
|
|
601
|
+
voiceKeySync,
|
|
602
|
+
voiceUrl,
|
|
603
|
+
wire,
|
|
604
|
+
youngest
|
|
605
|
+
};
|