@bountyboard/arcade-sdk 1.1.0 → 1.3.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/AGENTS.md +22 -10
- package/CHANGELOG.md +73 -0
- package/README.md +378 -85
- package/dist/{chunk-OWOESH4X.js → chunk-PYW5F45R.js} +384 -10
- package/dist/chunk-PYW5F45R.js.map +1 -0
- package/dist/global-sdk.d.ts +100 -3
- package/dist/global.js +482 -48
- package/dist/index.cjs +383 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/multiplayer.cjs +481 -48
- package/dist/multiplayer.cjs.map +1 -1
- package/dist/multiplayer.d.cts +2 -2
- package/dist/multiplayer.d.ts +2 -2
- package/dist/multiplayer.js +88 -29
- package/dist/multiplayer.js.map +1 -1
- package/dist/{types-Cq8p7ZkC.d.cts → types-B5bqWnHH.d.cts} +99 -4
- package/dist/{types-Cq8p7ZkC.d.ts → types-B5bqWnHH.d.ts} +99 -4
- package/docs/external-authoritative-servers.md +265 -0
- package/docs/llms.txt +800 -0
- package/docs/relay-rooms.md +134 -0
- package/package.json +2 -1
- package/dist/chunk-OWOESH4X.js.map +0 -1
package/dist/global.js
CHANGED
|
@@ -5,6 +5,316 @@
|
|
|
5
5
|
var SDK_MESSAGE_SOURCE = "bb-arcade";
|
|
6
6
|
var HOST_MESSAGE_SOURCE = "bb-arcade-host";
|
|
7
7
|
|
|
8
|
+
// src/storage.ts
|
|
9
|
+
var FLUSH_DELAY_MS = 400;
|
|
10
|
+
var ENVELOPE_TAG = "__bbArcadeStorage";
|
|
11
|
+
var ENVELOPE_VERSION = 1;
|
|
12
|
+
function hasOwn(target, key) {
|
|
13
|
+
return Object.prototype.hasOwnProperty.call(target, key);
|
|
14
|
+
}
|
|
15
|
+
function emptyMap() {
|
|
16
|
+
return /* @__PURE__ */ Object.create(null);
|
|
17
|
+
}
|
|
18
|
+
function storageError(code) {
|
|
19
|
+
const err = new Error(code);
|
|
20
|
+
err.code = code;
|
|
21
|
+
return err;
|
|
22
|
+
}
|
|
23
|
+
function parseEnvelope(raw) {
|
|
24
|
+
if (raw == null) return { ls: {}, app: null };
|
|
25
|
+
if (raw.charAt(0) !== "{") return { ls: {}, app: raw };
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(raw);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return { ls: {}, app: raw };
|
|
31
|
+
}
|
|
32
|
+
if (!parsed || typeof parsed !== "object") return { ls: {}, app: raw };
|
|
33
|
+
const record = parsed;
|
|
34
|
+
if (record[ENVELOPE_TAG] !== ENVELOPE_VERSION) return { ls: {}, app: raw };
|
|
35
|
+
const ls = emptyMap();
|
|
36
|
+
const source = record.ls;
|
|
37
|
+
if (source && typeof source === "object") {
|
|
38
|
+
const entries = source;
|
|
39
|
+
for (const key in entries) {
|
|
40
|
+
if (hasOwn(entries, key) && typeof entries[key] === "string") {
|
|
41
|
+
ls[key] = entries[key];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { ls, app: typeof record.app === "string" ? record.app : null };
|
|
46
|
+
}
|
|
47
|
+
function buildEnvelope(ls, app) {
|
|
48
|
+
const envelope = {};
|
|
49
|
+
envelope[ENVELOPE_TAG] = ENVELOPE_VERSION;
|
|
50
|
+
envelope.ls = ls;
|
|
51
|
+
envelope.app = app;
|
|
52
|
+
return JSON.stringify(envelope);
|
|
53
|
+
}
|
|
54
|
+
function quotaError() {
|
|
55
|
+
try {
|
|
56
|
+
return new DOMException("Cloud save quota exceeded", "QuotaExceededError");
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const fallback = new Error("Cloud save quota exceeded");
|
|
59
|
+
fallback.name = "QuotaExceededError";
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
var RESERVED = {
|
|
64
|
+
getItem: true,
|
|
65
|
+
setItem: true,
|
|
66
|
+
removeItem: true,
|
|
67
|
+
clear: true,
|
|
68
|
+
key: true,
|
|
69
|
+
length: true
|
|
70
|
+
};
|
|
71
|
+
function createStorageShim(backend = {}) {
|
|
72
|
+
const map = emptyMap();
|
|
73
|
+
const localWrites = emptyMap();
|
|
74
|
+
function keys() {
|
|
75
|
+
return Object.keys(map);
|
|
76
|
+
}
|
|
77
|
+
function changed() {
|
|
78
|
+
if (backend.onChange) backend.onChange();
|
|
79
|
+
}
|
|
80
|
+
const api = {
|
|
81
|
+
getItem: function(key) {
|
|
82
|
+
const name = String(key);
|
|
83
|
+
return hasOwn(map, name) ? map[name] : null;
|
|
84
|
+
},
|
|
85
|
+
setItem: function(key, value) {
|
|
86
|
+
const name = String(key);
|
|
87
|
+
const next = String(value);
|
|
88
|
+
if (backend.wouldOverflow && backend.wouldOverflow(name, next)) throw quotaError();
|
|
89
|
+
map[name] = next;
|
|
90
|
+
localWrites[name] = true;
|
|
91
|
+
changed();
|
|
92
|
+
},
|
|
93
|
+
removeItem: function(key) {
|
|
94
|
+
const name = String(key);
|
|
95
|
+
localWrites[name] = true;
|
|
96
|
+
if (!hasOwn(map, name)) return;
|
|
97
|
+
delete map[name];
|
|
98
|
+
changed();
|
|
99
|
+
},
|
|
100
|
+
clear: function() {
|
|
101
|
+
const existing = keys();
|
|
102
|
+
for (let i = 0; i < existing.length; i++) {
|
|
103
|
+
delete map[existing[i]];
|
|
104
|
+
localWrites[existing[i]] = true;
|
|
105
|
+
}
|
|
106
|
+
changed();
|
|
107
|
+
},
|
|
108
|
+
key: function(index) {
|
|
109
|
+
const existing = keys();
|
|
110
|
+
const at = Number(index);
|
|
111
|
+
return at >= 0 && at < existing.length ? existing[at] : null;
|
|
112
|
+
},
|
|
113
|
+
get length() {
|
|
114
|
+
return keys().length;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const storage = new Proxy(api, {
|
|
118
|
+
get(target, prop, receiver) {
|
|
119
|
+
if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
|
|
120
|
+
if (hasOwn(RESERVED, prop)) return Reflect.get(target, prop, receiver);
|
|
121
|
+
return hasOwn(map, prop) ? map[prop] : void 0;
|
|
122
|
+
},
|
|
123
|
+
set(target, prop, value) {
|
|
124
|
+
if (typeof prop !== "string" || hasOwn(RESERVED, prop)) return true;
|
|
125
|
+
api.setItem(prop, value);
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
has(target, prop) {
|
|
129
|
+
if (typeof prop !== "string") return Reflect.has(target, prop);
|
|
130
|
+
return hasOwn(RESERVED, prop) || hasOwn(map, prop);
|
|
131
|
+
},
|
|
132
|
+
deleteProperty(target, prop) {
|
|
133
|
+
if (typeof prop === "string" && !hasOwn(RESERVED, prop)) api.removeItem(prop);
|
|
134
|
+
return true;
|
|
135
|
+
},
|
|
136
|
+
ownKeys() {
|
|
137
|
+
return keys();
|
|
138
|
+
},
|
|
139
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
140
|
+
if (typeof prop !== "string" || !hasOwn(map, prop)) return void 0;
|
|
141
|
+
return { value: map[prop], writable: true, enumerable: true, configurable: true };
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
storage,
|
|
146
|
+
entries: function() {
|
|
147
|
+
const copy = emptyMap();
|
|
148
|
+
const existing = keys();
|
|
149
|
+
for (let i = 0; i < existing.length; i++) copy[existing[i]] = map[existing[i]];
|
|
150
|
+
return copy;
|
|
151
|
+
},
|
|
152
|
+
hydrate: function(values) {
|
|
153
|
+
for (const key in values) {
|
|
154
|
+
if (hasOwn(values, key) && !hasOwn(localWrites, key)) map[key] = values[key];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function settled(promise) {
|
|
160
|
+
return promise.then(
|
|
161
|
+
function() {
|
|
162
|
+
},
|
|
163
|
+
function() {
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
function installSessionStorageShim() {
|
|
168
|
+
if (nativeStorageWorks("sessionStorage")) return;
|
|
169
|
+
try {
|
|
170
|
+
Object.defineProperty(window, "sessionStorage", {
|
|
171
|
+
value: createStorageShim().storage,
|
|
172
|
+
configurable: true
|
|
173
|
+
});
|
|
174
|
+
} catch (err) {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function nativeStorageWorks(area) {
|
|
178
|
+
try {
|
|
179
|
+
const store = window[area];
|
|
180
|
+
if (!store) return false;
|
|
181
|
+
const probe = "__bb_arcade_probe__";
|
|
182
|
+
store.setItem(probe, "1");
|
|
183
|
+
store.removeItem(probe);
|
|
184
|
+
return true;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function createStorageController(options) {
|
|
190
|
+
let shim = null;
|
|
191
|
+
let mode = "native";
|
|
192
|
+
let appBlob = null;
|
|
193
|
+
let hydration = null;
|
|
194
|
+
let flushTimer = null;
|
|
195
|
+
let inFlight = Promise.resolve();
|
|
196
|
+
let dirty = false;
|
|
197
|
+
let hydrated = false;
|
|
198
|
+
function serialize() {
|
|
199
|
+
return buildEnvelope(shim ? shim.entries() : {}, appBlob);
|
|
200
|
+
}
|
|
201
|
+
function hydrateFromCloud() {
|
|
202
|
+
hydration = options.load().then(
|
|
203
|
+
function(raw) {
|
|
204
|
+
const envelope = parseEnvelope(raw);
|
|
205
|
+
if (shim) shim.hydrate(envelope.ls);
|
|
206
|
+
if (appBlob === null) appBlob = envelope.app;
|
|
207
|
+
hydrated = true;
|
|
208
|
+
return mode;
|
|
209
|
+
},
|
|
210
|
+
function(err) {
|
|
211
|
+
const code = err && err.code;
|
|
212
|
+
if (code === "unsupported" || code === "unauthenticated") mode = "memory";
|
|
213
|
+
return mode;
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
return hydration;
|
|
217
|
+
}
|
|
218
|
+
function wouldOverflow(key, value) {
|
|
219
|
+
if (!shim) return false;
|
|
220
|
+
const next = shim.entries();
|
|
221
|
+
next[key] = value;
|
|
222
|
+
return options.byteLength(buildEnvelope(next, appBlob)) > options.maxBytes;
|
|
223
|
+
}
|
|
224
|
+
function persist() {
|
|
225
|
+
return whenHydrated().then(function() {
|
|
226
|
+
if (mode === "memory") return;
|
|
227
|
+
if (hydrated) return options.save(serialize());
|
|
228
|
+
return hydrateFromCloud().then(function() {
|
|
229
|
+
if (mode === "memory") return;
|
|
230
|
+
if (!hydrated) throw storageError("error");
|
|
231
|
+
return options.save(serialize());
|
|
232
|
+
});
|
|
233
|
+
}).then(void 0, function(err) {
|
|
234
|
+
dirty = true;
|
|
235
|
+
throw err;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function flushNow() {
|
|
239
|
+
if (flushTimer !== null) {
|
|
240
|
+
clearTimeout(flushTimer);
|
|
241
|
+
flushTimer = null;
|
|
242
|
+
}
|
|
243
|
+
dirty = false;
|
|
244
|
+
const next = inFlight.then(persist, persist);
|
|
245
|
+
inFlight = settled(next);
|
|
246
|
+
return next;
|
|
247
|
+
}
|
|
248
|
+
function scheduleFlush() {
|
|
249
|
+
dirty = true;
|
|
250
|
+
if (flushTimer !== null) return;
|
|
251
|
+
flushTimer = setTimeout(function() {
|
|
252
|
+
flushTimer = null;
|
|
253
|
+
void settled(flushNow());
|
|
254
|
+
}, FLUSH_DELAY_MS);
|
|
255
|
+
}
|
|
256
|
+
function whenHydrated() {
|
|
257
|
+
return hydration ? hydration : Promise.resolve(mode);
|
|
258
|
+
}
|
|
259
|
+
function attachUnloadFlush() {
|
|
260
|
+
const flushIfDirty = function() {
|
|
261
|
+
if (dirty) void settled(flushNow());
|
|
262
|
+
};
|
|
263
|
+
try {
|
|
264
|
+
window.addEventListener("pagehide", flushIfDirty);
|
|
265
|
+
window.addEventListener("visibilitychange", function() {
|
|
266
|
+
if (document.visibilityState === "hidden") flushIfDirty();
|
|
267
|
+
});
|
|
268
|
+
} catch (err) {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function install() {
|
|
272
|
+
if (shim) return mode;
|
|
273
|
+
installSessionStorageShim();
|
|
274
|
+
if (nativeStorageWorks("localStorage")) {
|
|
275
|
+
mode = "native";
|
|
276
|
+
return mode;
|
|
277
|
+
}
|
|
278
|
+
const handle = createStorageShim({ onChange: scheduleFlush, wouldOverflow });
|
|
279
|
+
try {
|
|
280
|
+
Object.defineProperty(window, "localStorage", {
|
|
281
|
+
value: handle.storage,
|
|
282
|
+
configurable: true
|
|
283
|
+
});
|
|
284
|
+
} catch (err) {
|
|
285
|
+
return mode;
|
|
286
|
+
}
|
|
287
|
+
shim = handle;
|
|
288
|
+
mode = "cloud";
|
|
289
|
+
attachUnloadFlush();
|
|
290
|
+
void hydrateFromCloud();
|
|
291
|
+
return mode;
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
install,
|
|
295
|
+
active: function() {
|
|
296
|
+
return shim !== null;
|
|
297
|
+
},
|
|
298
|
+
ready: function() {
|
|
299
|
+
return whenHydrated();
|
|
300
|
+
},
|
|
301
|
+
flush: function() {
|
|
302
|
+
if (!shim && appBlob === null) return Promise.resolve();
|
|
303
|
+
return flushNow();
|
|
304
|
+
},
|
|
305
|
+
writeApp: function(blob) {
|
|
306
|
+
if (options.byteLength(buildEnvelope(shim ? shim.entries() : {}, blob)) > options.maxBytes) {
|
|
307
|
+
return Promise.reject(storageError("too_large"));
|
|
308
|
+
}
|
|
309
|
+
appBlob = blob;
|
|
310
|
+
return flushNow();
|
|
311
|
+
},
|
|
312
|
+
get mode() {
|
|
313
|
+
return mode;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
8
318
|
// src/core.ts
|
|
9
319
|
var BOUNTY_BOARD_URL = "https://www.bountyboard.gg/arcade";
|
|
10
320
|
var ADSENSE_CLIENT_ID = "ca-pub-7727110228190547";
|
|
@@ -12,6 +322,8 @@
|
|
|
12
322
|
var AD_PLACEMENT_SCRIPT_ID = "bb-arcade-h5-ads";
|
|
13
323
|
var DEFAULT_AD_LOAD_TIMEOUT_MS = 6e3;
|
|
14
324
|
var SAVE_TIMEOUT_MS = 15e3;
|
|
325
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
326
|
+
var MAX_SAVE_BYTES = 1024 * 1024;
|
|
15
327
|
var INIT_CONFIG_GRACE_MS = 1500;
|
|
16
328
|
var DEFAULT_ALLOW = [
|
|
17
329
|
"bountyboard.gg",
|
|
@@ -36,6 +348,14 @@
|
|
|
36
348
|
if (typeof value !== "string") return fallback || "";
|
|
37
349
|
return value.replace(/[^\w:.-]+/g, "_").slice(0, max || 80) || fallback || "";
|
|
38
350
|
}
|
|
351
|
+
var cachedTextEncoder = null;
|
|
352
|
+
function utf8ByteLength(value) {
|
|
353
|
+
if (typeof TextEncoder !== "undefined") {
|
|
354
|
+
cachedTextEncoder = cachedTextEncoder || new TextEncoder();
|
|
355
|
+
return cachedTextEncoder.encode(value).length;
|
|
356
|
+
}
|
|
357
|
+
return new Blob([value]).size;
|
|
358
|
+
}
|
|
39
359
|
function toBoolean(value) {
|
|
40
360
|
return value === true || value === "true" || value === "1";
|
|
41
361
|
}
|
|
@@ -155,13 +475,14 @@
|
|
|
155
475
|
};
|
|
156
476
|
let hostConfigReceived = false;
|
|
157
477
|
let hostPlayer = null;
|
|
478
|
+
const playerChangeHandlers = [];
|
|
158
479
|
let blocked = false;
|
|
159
480
|
const pending = {};
|
|
160
481
|
let seq = 0;
|
|
161
482
|
function post(type, score, extra) {
|
|
162
483
|
if (blocked) return;
|
|
163
484
|
const message = { source: SDK_MESSAGE_SOURCE, v: VERSION, type };
|
|
164
|
-
if (typeof score === "number" && isFinite(score)) message.score = Math.
|
|
485
|
+
if (typeof score === "number" && isFinite(score)) message.score = Math.trunc(score);
|
|
165
486
|
if (extra && typeof extra === "object") {
|
|
166
487
|
const fields = extra;
|
|
167
488
|
for (const key in fields) {
|
|
@@ -182,7 +503,7 @@
|
|
|
182
503
|
return true;
|
|
183
504
|
}
|
|
184
505
|
}
|
|
185
|
-
function request(type, payload) {
|
|
506
|
+
function request(type, payload, timeoutMs = SAVE_TIMEOUT_MS) {
|
|
186
507
|
return new Promise(function(resolve, reject) {
|
|
187
508
|
if (blocked) return reject(sdkError("error"));
|
|
188
509
|
if (!isEmbedded()) return reject(sdkError("unsupported"));
|
|
@@ -192,7 +513,7 @@
|
|
|
192
513
|
delete pending[requestId];
|
|
193
514
|
reject(sdkError("error"));
|
|
194
515
|
}
|
|
195
|
-
},
|
|
516
|
+
}, timeoutMs);
|
|
196
517
|
pending[requestId] = { resolve, reject, timer };
|
|
197
518
|
try {
|
|
198
519
|
window.parent.postMessage(
|
|
@@ -284,7 +605,7 @@
|
|
|
284
605
|
if (!hostnameAllowed(origin, allow)) blockHost(options);
|
|
285
606
|
return;
|
|
286
607
|
}
|
|
287
|
-
request("host").then(
|
|
608
|
+
request("host", void 0, LOCK_TIMEOUT_MS).then(
|
|
288
609
|
function(res) {
|
|
289
610
|
const r = res;
|
|
290
611
|
if (!hostnameAllowed(r && r.origin, allow)) blockHost(options);
|
|
@@ -308,7 +629,7 @@
|
|
|
308
629
|
lockToHostBestEffort(opts, allow);
|
|
309
630
|
return;
|
|
310
631
|
}
|
|
311
|
-
request("host_attest").then(
|
|
632
|
+
request("host_attest", void 0, LOCK_TIMEOUT_MS).then(
|
|
312
633
|
function(res) {
|
|
313
634
|
const r = res;
|
|
314
635
|
if (!r || !r.attestation) {
|
|
@@ -370,7 +691,15 @@
|
|
|
370
691
|
if (msg.source !== HOST_MESSAGE_SOURCE || msg.type !== "config") return;
|
|
371
692
|
hostConfigReceived = true;
|
|
372
693
|
if (msg.player !== void 0) {
|
|
373
|
-
|
|
694
|
+
const nextPlayer = sanitizePlayer(msg.player);
|
|
695
|
+
const changed = nextPlayer === null ? hostPlayer !== null : hostPlayer === null || nextPlayer.name !== hostPlayer.name || nextPlayer.avatarUrl !== hostPlayer.avatarUrl;
|
|
696
|
+
hostPlayer = nextPlayer;
|
|
697
|
+
if (changed) {
|
|
698
|
+
const snapshot = nextPlayer ? { name: nextPlayer.name, avatarUrl: nextPlayer.avatarUrl } : null;
|
|
699
|
+
for (let i = 0; i < playerChangeHandlers.length; i++) {
|
|
700
|
+
safeCall(playerChangeHandlers[i], snapshot);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
374
703
|
}
|
|
375
704
|
const rewardedAds = msg.rewardedAds;
|
|
376
705
|
if (rewardedAds && typeof rewardedAds === "object") {
|
|
@@ -502,6 +831,7 @@
|
|
|
502
831
|
let resolved = false;
|
|
503
832
|
let status = "unavailable";
|
|
504
833
|
let rewardOffered = false;
|
|
834
|
+
let adShown = false;
|
|
505
835
|
const waitTimer = setTimeout(function() {
|
|
506
836
|
if (rewardOffered || resolved) return;
|
|
507
837
|
const timeout = Object.assign(
|
|
@@ -546,6 +876,7 @@
|
|
|
546
876
|
}
|
|
547
877
|
post("rewarded_ad", void 0, Object.assign({ event: "started" }, base));
|
|
548
878
|
try {
|
|
879
|
+
adShown = true;
|
|
549
880
|
showAdFn();
|
|
550
881
|
} catch (err) {
|
|
551
882
|
const showError = Object.assign(
|
|
@@ -595,7 +926,13 @@
|
|
|
595
926
|
},
|
|
596
927
|
adBreakDone: function(placementInfo) {
|
|
597
928
|
if (resolved) return;
|
|
598
|
-
const result = rewardOffered || status !== "unavailable" ? {
|
|
929
|
+
const result = rewardOffered || status !== "unavailable" ? {
|
|
930
|
+
// A reward was offered but the ad never reached the screen
|
|
931
|
+
// (the game retained show() and let the break lapse): the
|
|
932
|
+
// break is over without a view, so resolve 'dismissed',
|
|
933
|
+
// not the non-terminal-looking 'ready'.
|
|
934
|
+
status: status === "ready" && !adShown ? "dismissed" : status
|
|
935
|
+
} : { status: "unavailable", error: "no_rewarded_ad" };
|
|
599
936
|
if (placementInfo && typeof placementInfo === "object") {
|
|
600
937
|
result.breakStatus = cleanText(
|
|
601
938
|
placementInfo.breakStatus
|
|
@@ -766,6 +1103,16 @@
|
|
|
766
1103
|
if (!hostnameAllowed(event.origin, DEFAULT_ALLOW)) return;
|
|
767
1104
|
applyHostConfig(event.data);
|
|
768
1105
|
});
|
|
1106
|
+
const storage = createStorageController({
|
|
1107
|
+
save: function(blob) {
|
|
1108
|
+
return request("save", blob);
|
|
1109
|
+
},
|
|
1110
|
+
load: function() {
|
|
1111
|
+
return request("load");
|
|
1112
|
+
},
|
|
1113
|
+
byteLength: utf8ByteLength,
|
|
1114
|
+
maxBytes: MAX_SAVE_BYTES
|
|
1115
|
+
});
|
|
769
1116
|
const sdk = {
|
|
770
1117
|
lockToHost,
|
|
771
1118
|
init,
|
|
@@ -798,12 +1145,29 @@
|
|
|
798
1145
|
prepareRewardedBreak: prepareRewardedAd,
|
|
799
1146
|
rewardedBreak,
|
|
800
1147
|
save: function(blob) {
|
|
801
|
-
|
|
1148
|
+
const data = String(blob);
|
|
1149
|
+
if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
|
|
1150
|
+
return Promise.reject(sdkError("too_large"));
|
|
1151
|
+
}
|
|
1152
|
+
if (storage.active()) return storage.writeApp(data);
|
|
1153
|
+
return request("save", data);
|
|
802
1154
|
},
|
|
803
1155
|
load: function() {
|
|
804
|
-
return request("load")
|
|
1156
|
+
return request("load").then(function(raw) {
|
|
1157
|
+
return parseEnvelope(raw).app;
|
|
1158
|
+
});
|
|
805
1159
|
},
|
|
1160
|
+
storage,
|
|
806
1161
|
getPlayer,
|
|
1162
|
+
onPlayerChange: function(handler) {
|
|
1163
|
+
if (typeof handler !== "function") return function() {
|
|
1164
|
+
};
|
|
1165
|
+
playerChangeHandlers.push(handler);
|
|
1166
|
+
return function() {
|
|
1167
|
+
const idx = playerChangeHandlers.indexOf(handler);
|
|
1168
|
+
if (idx !== -1) playerChangeHandlers.splice(idx, 1);
|
|
1169
|
+
};
|
|
1170
|
+
},
|
|
807
1171
|
getVariant: function(key, variants) {
|
|
808
1172
|
const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
|
|
809
1173
|
const fallback = sorted.length > 0 ? sorted[0] : null;
|
|
@@ -869,7 +1233,17 @@
|
|
|
869
1233
|
rewardedBreak: () => Promise.resolve(false),
|
|
870
1234
|
save: () => Promise.reject(sdkError2("unsupported")),
|
|
871
1235
|
load: () => Promise.reject(sdkError2("unsupported")),
|
|
1236
|
+
// No window to shim and nothing to persist to: report the honest mode
|
|
1237
|
+
// rather than pretending an install succeeded.
|
|
1238
|
+
storage: {
|
|
1239
|
+
install: () => "memory",
|
|
1240
|
+
ready: () => Promise.resolve("memory"),
|
|
1241
|
+
flush: () => Promise.resolve(),
|
|
1242
|
+
mode: "memory"
|
|
1243
|
+
},
|
|
872
1244
|
getPlayer: () => Promise.resolve(null),
|
|
1245
|
+
onPlayerChange: () => () => {
|
|
1246
|
+
},
|
|
873
1247
|
getVariant: (key, variants) => {
|
|
874
1248
|
const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
|
|
875
1249
|
return Promise.resolve(sorted.length > 0 ? sorted[0] : null);
|
|
@@ -883,13 +1257,17 @@
|
|
|
883
1257
|
|
|
884
1258
|
// src/multiplayer.ts
|
|
885
1259
|
var WELCOME_TIMEOUT_MS = 1e4;
|
|
1260
|
+
var MIN_WELCOME_TIMEOUT_MS = 1e3;
|
|
1261
|
+
var MAX_WELCOME_TIMEOUT_MS = 6e4;
|
|
886
1262
|
var MAX_RECONNECT_ATTEMPTS = 3;
|
|
887
1263
|
var MAX_JOIN_DATA_BYTES = 1024;
|
|
1264
|
+
var SEAT_FULL_CLOSE_CODE = 4005;
|
|
888
1265
|
var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
889
1266
|
var CODE_LENGTH = 4;
|
|
890
|
-
function sdkError3(code) {
|
|
1267
|
+
function sdkError3(code, detail) {
|
|
891
1268
|
const err = new Error(code);
|
|
892
1269
|
err.code = code;
|
|
1270
|
+
if (detail) err.detail = detail;
|
|
893
1271
|
return err;
|
|
894
1272
|
}
|
|
895
1273
|
function randomChars(length) {
|
|
@@ -906,23 +1284,13 @@
|
|
|
906
1284
|
function generateCode() {
|
|
907
1285
|
return randomChars(CODE_LENGTH);
|
|
908
1286
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
else if (code < 2048) bytes += 2;
|
|
915
|
-
else if (code >= 55296 && code <= 56319 && i + 1 < value.length) {
|
|
916
|
-
const next = value.charCodeAt(i + 1);
|
|
917
|
-
if (next >= 56320 && next <= 57343) {
|
|
918
|
-
bytes += 4;
|
|
919
|
-
i += 1;
|
|
920
|
-
} else {
|
|
921
|
-
bytes += 3;
|
|
922
|
-
}
|
|
923
|
-
} else bytes += 3;
|
|
1287
|
+
var cachedTextEncoder2 = null;
|
|
1288
|
+
function utf8ByteLength2(value) {
|
|
1289
|
+
if (typeof TextEncoder !== "undefined") {
|
|
1290
|
+
cachedTextEncoder2 = cachedTextEncoder2 || new TextEncoder();
|
|
1291
|
+
return cachedTextEncoder2.encode(value).length;
|
|
924
1292
|
}
|
|
925
|
-
return
|
|
1293
|
+
return new Blob([value]).size;
|
|
926
1294
|
}
|
|
927
1295
|
function serializeJoinData(data) {
|
|
928
1296
|
if (data === void 0) return null;
|
|
@@ -933,28 +1301,34 @@
|
|
|
933
1301
|
} catch (e) {
|
|
934
1302
|
throw sdkError3("rejected");
|
|
935
1303
|
}
|
|
936
|
-
if (!serialized ||
|
|
1304
|
+
if (!serialized || utf8ByteLength2(serialized) > MAX_JOIN_DATA_BYTES) {
|
|
937
1305
|
throw sdkError3("rejected");
|
|
938
1306
|
}
|
|
939
1307
|
return serialized;
|
|
940
1308
|
}
|
|
941
|
-
function requestTicket(
|
|
1309
|
+
function requestTicket(payload) {
|
|
942
1310
|
const request = BBArcade._request;
|
|
943
1311
|
if (typeof request !== "function") return Promise.reject(sdkError3("unsupported"));
|
|
944
|
-
return request("mp_ticket",
|
|
1312
|
+
return request("mp_ticket", payload).then((grant) => {
|
|
945
1313
|
const g = grant;
|
|
946
1314
|
if (!g || typeof g.ticket !== "string" || typeof g.roomUrl !== "string") {
|
|
947
1315
|
throw sdkError3("error");
|
|
948
1316
|
}
|
|
949
|
-
return {
|
|
1317
|
+
return {
|
|
1318
|
+
ticket: g.ticket,
|
|
1319
|
+
roomUrl: g.roomUrl,
|
|
1320
|
+
expiresAt: Number(g.expiresAt) || 0,
|
|
1321
|
+
roomId: typeof g.roomId === "string" && g.roomId ? g.roomId : null
|
|
1322
|
+
};
|
|
950
1323
|
});
|
|
951
1324
|
}
|
|
952
1325
|
var RoomConnection = class {
|
|
953
|
-
constructor(code, devGrant, joinData) {
|
|
1326
|
+
constructor(code, devGrant, joinData, welcomeTimeoutMs = WELCOME_TIMEOUT_MS) {
|
|
954
1327
|
this.playerId = "";
|
|
955
1328
|
this.players = [];
|
|
956
1329
|
this.state = null;
|
|
957
1330
|
this.connected = false;
|
|
1331
|
+
this.latencyMs = null;
|
|
958
1332
|
this.ws = null;
|
|
959
1333
|
// Keyed by event name; values are erased to (unknown) => void internally and
|
|
960
1334
|
// retyped at the on()/emit() boundary, which keeps the public API strictly
|
|
@@ -967,15 +1341,20 @@
|
|
|
967
1341
|
// or the welcome timed out — permanent outcomes that must not reconnect.
|
|
968
1342
|
this.terminal = false;
|
|
969
1343
|
this.reconnectAttempts = 0;
|
|
1344
|
+
// The in-flight connect()'s settlement, so leave() can reject a join that
|
|
1345
|
+
// is still waiting on its welcome instead of stranding it on the timeout.
|
|
1346
|
+
this.pendingConnect = null;
|
|
970
1347
|
this.code = code;
|
|
971
1348
|
this.devGrant = devGrant;
|
|
972
1349
|
this.joinData = joinData;
|
|
1350
|
+
this.welcomeTimeoutMs = welcomeTimeoutMs;
|
|
973
1351
|
}
|
|
974
1352
|
/** Connect and resolve on the server's welcome (post-ticket-verification). */
|
|
975
1353
|
connect(grant) {
|
|
976
1354
|
return new Promise((resolve, reject) => {
|
|
977
|
-
let
|
|
1355
|
+
let settled2 = false;
|
|
978
1356
|
const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket) + (this.joinData === null ? "" : "&join=" + encodeURIComponent(this.joinData));
|
|
1357
|
+
const connectStartedAt = Date.now();
|
|
979
1358
|
let ws;
|
|
980
1359
|
try {
|
|
981
1360
|
ws = new WebSocket(url);
|
|
@@ -984,15 +1363,17 @@
|
|
|
984
1363
|
}
|
|
985
1364
|
this.ws = ws;
|
|
986
1365
|
const welcomeTimer = setTimeout(() => {
|
|
987
|
-
if (
|
|
988
|
-
|
|
1366
|
+
if (settled2) return;
|
|
1367
|
+
settled2 = true;
|
|
1368
|
+
this.pendingConnect = null;
|
|
989
1369
|
this.terminal = true;
|
|
990
1370
|
try {
|
|
991
1371
|
ws.close();
|
|
992
1372
|
} catch (err) {
|
|
993
1373
|
}
|
|
994
1374
|
reject(sdkError3("error"));
|
|
995
|
-
},
|
|
1375
|
+
}, this.welcomeTimeoutMs);
|
|
1376
|
+
this.pendingConnect = { reject, timer: welcomeTimer };
|
|
996
1377
|
ws.onmessage = (event) => {
|
|
997
1378
|
var _a, _b, _c, _d, _e;
|
|
998
1379
|
let msg;
|
|
@@ -1003,14 +1384,23 @@
|
|
|
1003
1384
|
}
|
|
1004
1385
|
if (!msg || typeof msg.t !== "string") return;
|
|
1005
1386
|
if (msg.t === "welcome") {
|
|
1387
|
+
if (this.intentionalClose) {
|
|
1388
|
+
try {
|
|
1389
|
+
ws.close();
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
}
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1006
1394
|
const reconnected = this.reconnectAttempts > 0;
|
|
1007
1395
|
this.playerId = String((_a = msg.playerId) != null ? _a : "");
|
|
1008
1396
|
this.players = Array.isArray(msg.players) ? msg.players : [];
|
|
1009
1397
|
this.state = (_b = msg.state) != null ? _b : null;
|
|
1010
1398
|
this.connected = true;
|
|
1011
1399
|
this.reconnectAttempts = 0;
|
|
1012
|
-
|
|
1013
|
-
|
|
1400
|
+
this.latencyMs = Math.max(0, Date.now() - connectStartedAt);
|
|
1401
|
+
if (!settled2) {
|
|
1402
|
+
settled2 = true;
|
|
1403
|
+
this.pendingConnect = null;
|
|
1014
1404
|
clearTimeout(welcomeTimer);
|
|
1015
1405
|
resolve(this);
|
|
1016
1406
|
}
|
|
@@ -1049,24 +1439,27 @@
|
|
|
1049
1439
|
}
|
|
1050
1440
|
if (msg.t === "error") {
|
|
1051
1441
|
const code = String((_e = msg.code) != null ? _e : "error");
|
|
1052
|
-
if (!
|
|
1053
|
-
|
|
1442
|
+
if (!settled2) {
|
|
1443
|
+
settled2 = true;
|
|
1054
1444
|
this.terminal = true;
|
|
1445
|
+
this.pendingConnect = null;
|
|
1055
1446
|
clearTimeout(welcomeTimer);
|
|
1056
1447
|
try {
|
|
1057
1448
|
ws.close();
|
|
1058
1449
|
} catch (err) {
|
|
1059
1450
|
}
|
|
1060
|
-
reject(sdkError3(code === "unauthenticated" ? "unauthenticated" : "rejected"));
|
|
1451
|
+
reject(sdkError3(code === "unauthenticated" ? "unauthenticated" : "rejected", code));
|
|
1061
1452
|
return;
|
|
1062
1453
|
}
|
|
1063
1454
|
this.emit("error", { code });
|
|
1064
1455
|
}
|
|
1065
1456
|
};
|
|
1066
|
-
ws.onclose = () => {
|
|
1457
|
+
ws.onclose = (event) => {
|
|
1067
1458
|
this.connected = false;
|
|
1068
|
-
if (
|
|
1069
|
-
|
|
1459
|
+
if (event && event.code === SEAT_FULL_CLOSE_CODE) this.terminal = true;
|
|
1460
|
+
if (!settled2) {
|
|
1461
|
+
settled2 = true;
|
|
1462
|
+
this.pendingConnect = null;
|
|
1070
1463
|
clearTimeout(welcomeTimer);
|
|
1071
1464
|
reject(sdkError3("error"));
|
|
1072
1465
|
return;
|
|
@@ -1095,7 +1488,7 @@
|
|
|
1095
1488
|
await new Promise((r) => setTimeout(r, 400 * this.reconnectAttempts));
|
|
1096
1489
|
if (this.intentionalClose || this.ended || this.terminal) return;
|
|
1097
1490
|
try {
|
|
1098
|
-
const grant = (_a = this.devGrant) != null ? _a : await requestTicket(this.code);
|
|
1491
|
+
const grant = (_a = this.devGrant) != null ? _a : await requestTicket({ roomId: this.code });
|
|
1099
1492
|
if (this.intentionalClose || this.ended || this.terminal) return;
|
|
1100
1493
|
await this.connect(grant);
|
|
1101
1494
|
if (this.intentionalClose) this.leave();
|
|
@@ -1130,6 +1523,12 @@
|
|
|
1130
1523
|
var _a;
|
|
1131
1524
|
this.intentionalClose = true;
|
|
1132
1525
|
this.connected = false;
|
|
1526
|
+
if (this.pendingConnect) {
|
|
1527
|
+
const pending = this.pendingConnect;
|
|
1528
|
+
this.pendingConnect = null;
|
|
1529
|
+
clearTimeout(pending.timer);
|
|
1530
|
+
pending.reject(sdkError3("error"));
|
|
1531
|
+
}
|
|
1133
1532
|
try {
|
|
1134
1533
|
(_a = this.ws) == null ? void 0 : _a.close(1e3);
|
|
1135
1534
|
} catch (err) {
|
|
@@ -1149,27 +1548,62 @@
|
|
|
1149
1548
|
}
|
|
1150
1549
|
}
|
|
1151
1550
|
};
|
|
1551
|
+
var MATCH_ATTEMPTS = 3;
|
|
1552
|
+
function normalizeWelcomeTimeoutMs(value) {
|
|
1553
|
+
if (typeof value !== "number" || !isFinite(value)) return WELCOME_TIMEOUT_MS;
|
|
1554
|
+
return Math.min(MAX_WELCOME_TIMEOUT_MS, Math.max(MIN_WELCOME_TIMEOUT_MS, Math.floor(value)));
|
|
1555
|
+
}
|
|
1556
|
+
async function matchJoin(joinData, timeoutMs) {
|
|
1557
|
+
let lastError = sdkError3("error");
|
|
1558
|
+
for (let attempt = 0; attempt < MATCH_ATTEMPTS; attempt++) {
|
|
1559
|
+
const grant = await requestTicket({ matchmake: true });
|
|
1560
|
+
if (!grant.roomId) throw sdkError3("error");
|
|
1561
|
+
try {
|
|
1562
|
+
return await new RoomConnection(grant.roomId, null, joinData, timeoutMs).connect(grant);
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
lastError = error;
|
|
1565
|
+
const detail = error == null ? void 0 : error.detail;
|
|
1566
|
+
if (detail !== "room_full" && detail !== "room_over") throw error;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
throw lastError;
|
|
1570
|
+
}
|
|
1152
1571
|
function joinRoom(options) {
|
|
1153
1572
|
const opts = options != null ? options : {};
|
|
1154
1573
|
if (typeof window === "undefined" || typeof WebSocket === "undefined") {
|
|
1155
1574
|
return Promise.reject(sdkError3("unsupported"));
|
|
1156
1575
|
}
|
|
1157
|
-
const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
|
|
1158
1576
|
let joinData;
|
|
1159
1577
|
try {
|
|
1160
1578
|
joinData = serializeJoinData(opts.joinData);
|
|
1161
1579
|
} catch (err) {
|
|
1162
1580
|
return Promise.reject(err);
|
|
1163
1581
|
}
|
|
1582
|
+
const timeoutMs = normalizeWelcomeTimeoutMs(opts.timeoutMs);
|
|
1583
|
+
if (opts.match) {
|
|
1584
|
+
if (opts.code || opts.create || opts.roomUrl || opts.ticket) {
|
|
1585
|
+
return Promise.reject(sdkError3("rejected"));
|
|
1586
|
+
}
|
|
1587
|
+
return matchJoin(joinData, timeoutMs);
|
|
1588
|
+
}
|
|
1589
|
+
const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
|
|
1164
1590
|
if (opts.roomUrl && opts.ticket) {
|
|
1165
|
-
const devGrant = {
|
|
1166
|
-
|
|
1591
|
+
const devGrant = {
|
|
1592
|
+
ticket: opts.ticket,
|
|
1593
|
+
roomUrl: opts.roomUrl,
|
|
1594
|
+
expiresAt: 0,
|
|
1595
|
+
roomId: code
|
|
1596
|
+
};
|
|
1597
|
+
return new RoomConnection(code, devGrant, joinData, timeoutMs).connect(devGrant);
|
|
1167
1598
|
}
|
|
1168
|
-
return requestTicket(
|
|
1599
|
+
return requestTicket({ roomId: code }).then(
|
|
1600
|
+
(grant) => new RoomConnection(code, null, joinData, timeoutMs).connect(grant)
|
|
1601
|
+
);
|
|
1169
1602
|
}
|
|
1170
1603
|
var multiplayer = { joinRoom };
|
|
1171
1604
|
|
|
1172
1605
|
// src/global.ts
|
|
1173
1606
|
BBArcade.multiplayer = multiplayer;
|
|
1174
1607
|
window.BBArcade = BBArcade;
|
|
1608
|
+
BBArcade.storage.install();
|
|
1175
1609
|
})();
|