@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/multiplayer.cjs
CHANGED
|
@@ -30,6 +30,316 @@ var VERSION = 1;
|
|
|
30
30
|
var SDK_MESSAGE_SOURCE = "bb-arcade";
|
|
31
31
|
var HOST_MESSAGE_SOURCE = "bb-arcade-host";
|
|
32
32
|
|
|
33
|
+
// src/storage.ts
|
|
34
|
+
var FLUSH_DELAY_MS = 400;
|
|
35
|
+
var ENVELOPE_TAG = "__bbArcadeStorage";
|
|
36
|
+
var ENVELOPE_VERSION = 1;
|
|
37
|
+
function hasOwn(target, key) {
|
|
38
|
+
return Object.prototype.hasOwnProperty.call(target, key);
|
|
39
|
+
}
|
|
40
|
+
function emptyMap() {
|
|
41
|
+
return /* @__PURE__ */ Object.create(null);
|
|
42
|
+
}
|
|
43
|
+
function storageError(code) {
|
|
44
|
+
const err = new Error(code);
|
|
45
|
+
err.code = code;
|
|
46
|
+
return err;
|
|
47
|
+
}
|
|
48
|
+
function parseEnvelope(raw) {
|
|
49
|
+
if (raw == null) return { ls: {}, app: null };
|
|
50
|
+
if (raw.charAt(0) !== "{") return { ls: {}, app: raw };
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(raw);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return { ls: {}, app: raw };
|
|
56
|
+
}
|
|
57
|
+
if (!parsed || typeof parsed !== "object") return { ls: {}, app: raw };
|
|
58
|
+
const record = parsed;
|
|
59
|
+
if (record[ENVELOPE_TAG] !== ENVELOPE_VERSION) return { ls: {}, app: raw };
|
|
60
|
+
const ls = emptyMap();
|
|
61
|
+
const source = record.ls;
|
|
62
|
+
if (source && typeof source === "object") {
|
|
63
|
+
const entries = source;
|
|
64
|
+
for (const key in entries) {
|
|
65
|
+
if (hasOwn(entries, key) && typeof entries[key] === "string") {
|
|
66
|
+
ls[key] = entries[key];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { ls, app: typeof record.app === "string" ? record.app : null };
|
|
71
|
+
}
|
|
72
|
+
function buildEnvelope(ls, app) {
|
|
73
|
+
const envelope = {};
|
|
74
|
+
envelope[ENVELOPE_TAG] = ENVELOPE_VERSION;
|
|
75
|
+
envelope.ls = ls;
|
|
76
|
+
envelope.app = app;
|
|
77
|
+
return JSON.stringify(envelope);
|
|
78
|
+
}
|
|
79
|
+
function quotaError() {
|
|
80
|
+
try {
|
|
81
|
+
return new DOMException("Cloud save quota exceeded", "QuotaExceededError");
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const fallback = new Error("Cloud save quota exceeded");
|
|
84
|
+
fallback.name = "QuotaExceededError";
|
|
85
|
+
return fallback;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
var RESERVED = {
|
|
89
|
+
getItem: true,
|
|
90
|
+
setItem: true,
|
|
91
|
+
removeItem: true,
|
|
92
|
+
clear: true,
|
|
93
|
+
key: true,
|
|
94
|
+
length: true
|
|
95
|
+
};
|
|
96
|
+
function createStorageShim(backend = {}) {
|
|
97
|
+
const map = emptyMap();
|
|
98
|
+
const localWrites = emptyMap();
|
|
99
|
+
function keys() {
|
|
100
|
+
return Object.keys(map);
|
|
101
|
+
}
|
|
102
|
+
function changed() {
|
|
103
|
+
if (backend.onChange) backend.onChange();
|
|
104
|
+
}
|
|
105
|
+
const api = {
|
|
106
|
+
getItem: function(key) {
|
|
107
|
+
const name = String(key);
|
|
108
|
+
return hasOwn(map, name) ? map[name] : null;
|
|
109
|
+
},
|
|
110
|
+
setItem: function(key, value) {
|
|
111
|
+
const name = String(key);
|
|
112
|
+
const next = String(value);
|
|
113
|
+
if (backend.wouldOverflow && backend.wouldOverflow(name, next)) throw quotaError();
|
|
114
|
+
map[name] = next;
|
|
115
|
+
localWrites[name] = true;
|
|
116
|
+
changed();
|
|
117
|
+
},
|
|
118
|
+
removeItem: function(key) {
|
|
119
|
+
const name = String(key);
|
|
120
|
+
localWrites[name] = true;
|
|
121
|
+
if (!hasOwn(map, name)) return;
|
|
122
|
+
delete map[name];
|
|
123
|
+
changed();
|
|
124
|
+
},
|
|
125
|
+
clear: function() {
|
|
126
|
+
const existing = keys();
|
|
127
|
+
for (let i = 0; i < existing.length; i++) {
|
|
128
|
+
delete map[existing[i]];
|
|
129
|
+
localWrites[existing[i]] = true;
|
|
130
|
+
}
|
|
131
|
+
changed();
|
|
132
|
+
},
|
|
133
|
+
key: function(index) {
|
|
134
|
+
const existing = keys();
|
|
135
|
+
const at = Number(index);
|
|
136
|
+
return at >= 0 && at < existing.length ? existing[at] : null;
|
|
137
|
+
},
|
|
138
|
+
get length() {
|
|
139
|
+
return keys().length;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const storage = new Proxy(api, {
|
|
143
|
+
get(target, prop, receiver) {
|
|
144
|
+
if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
|
|
145
|
+
if (hasOwn(RESERVED, prop)) return Reflect.get(target, prop, receiver);
|
|
146
|
+
return hasOwn(map, prop) ? map[prop] : void 0;
|
|
147
|
+
},
|
|
148
|
+
set(target, prop, value) {
|
|
149
|
+
if (typeof prop !== "string" || hasOwn(RESERVED, prop)) return true;
|
|
150
|
+
api.setItem(prop, value);
|
|
151
|
+
return true;
|
|
152
|
+
},
|
|
153
|
+
has(target, prop) {
|
|
154
|
+
if (typeof prop !== "string") return Reflect.has(target, prop);
|
|
155
|
+
return hasOwn(RESERVED, prop) || hasOwn(map, prop);
|
|
156
|
+
},
|
|
157
|
+
deleteProperty(target, prop) {
|
|
158
|
+
if (typeof prop === "string" && !hasOwn(RESERVED, prop)) api.removeItem(prop);
|
|
159
|
+
return true;
|
|
160
|
+
},
|
|
161
|
+
ownKeys() {
|
|
162
|
+
return keys();
|
|
163
|
+
},
|
|
164
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
165
|
+
if (typeof prop !== "string" || !hasOwn(map, prop)) return void 0;
|
|
166
|
+
return { value: map[prop], writable: true, enumerable: true, configurable: true };
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
storage,
|
|
171
|
+
entries: function() {
|
|
172
|
+
const copy = emptyMap();
|
|
173
|
+
const existing = keys();
|
|
174
|
+
for (let i = 0; i < existing.length; i++) copy[existing[i]] = map[existing[i]];
|
|
175
|
+
return copy;
|
|
176
|
+
},
|
|
177
|
+
hydrate: function(values) {
|
|
178
|
+
for (const key in values) {
|
|
179
|
+
if (hasOwn(values, key) && !hasOwn(localWrites, key)) map[key] = values[key];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function settled(promise) {
|
|
185
|
+
return promise.then(
|
|
186
|
+
function() {
|
|
187
|
+
},
|
|
188
|
+
function() {
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
function installSessionStorageShim() {
|
|
193
|
+
if (nativeStorageWorks("sessionStorage")) return;
|
|
194
|
+
try {
|
|
195
|
+
Object.defineProperty(window, "sessionStorage", {
|
|
196
|
+
value: createStorageShim().storage,
|
|
197
|
+
configurable: true
|
|
198
|
+
});
|
|
199
|
+
} catch (err) {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function nativeStorageWorks(area) {
|
|
203
|
+
try {
|
|
204
|
+
const store = window[area];
|
|
205
|
+
if (!store) return false;
|
|
206
|
+
const probe = "__bb_arcade_probe__";
|
|
207
|
+
store.setItem(probe, "1");
|
|
208
|
+
store.removeItem(probe);
|
|
209
|
+
return true;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function createStorageController(options) {
|
|
215
|
+
let shim = null;
|
|
216
|
+
let mode = "native";
|
|
217
|
+
let appBlob = null;
|
|
218
|
+
let hydration = null;
|
|
219
|
+
let flushTimer = null;
|
|
220
|
+
let inFlight = Promise.resolve();
|
|
221
|
+
let dirty = false;
|
|
222
|
+
let hydrated = false;
|
|
223
|
+
function serialize() {
|
|
224
|
+
return buildEnvelope(shim ? shim.entries() : {}, appBlob);
|
|
225
|
+
}
|
|
226
|
+
function hydrateFromCloud() {
|
|
227
|
+
hydration = options.load().then(
|
|
228
|
+
function(raw) {
|
|
229
|
+
const envelope = parseEnvelope(raw);
|
|
230
|
+
if (shim) shim.hydrate(envelope.ls);
|
|
231
|
+
if (appBlob === null) appBlob = envelope.app;
|
|
232
|
+
hydrated = true;
|
|
233
|
+
return mode;
|
|
234
|
+
},
|
|
235
|
+
function(err) {
|
|
236
|
+
const code = err && err.code;
|
|
237
|
+
if (code === "unsupported" || code === "unauthenticated") mode = "memory";
|
|
238
|
+
return mode;
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
return hydration;
|
|
242
|
+
}
|
|
243
|
+
function wouldOverflow(key, value) {
|
|
244
|
+
if (!shim) return false;
|
|
245
|
+
const next = shim.entries();
|
|
246
|
+
next[key] = value;
|
|
247
|
+
return options.byteLength(buildEnvelope(next, appBlob)) > options.maxBytes;
|
|
248
|
+
}
|
|
249
|
+
function persist() {
|
|
250
|
+
return whenHydrated().then(function() {
|
|
251
|
+
if (mode === "memory") return;
|
|
252
|
+
if (hydrated) return options.save(serialize());
|
|
253
|
+
return hydrateFromCloud().then(function() {
|
|
254
|
+
if (mode === "memory") return;
|
|
255
|
+
if (!hydrated) throw storageError("error");
|
|
256
|
+
return options.save(serialize());
|
|
257
|
+
});
|
|
258
|
+
}).then(void 0, function(err) {
|
|
259
|
+
dirty = true;
|
|
260
|
+
throw err;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function flushNow() {
|
|
264
|
+
if (flushTimer !== null) {
|
|
265
|
+
clearTimeout(flushTimer);
|
|
266
|
+
flushTimer = null;
|
|
267
|
+
}
|
|
268
|
+
dirty = false;
|
|
269
|
+
const next = inFlight.then(persist, persist);
|
|
270
|
+
inFlight = settled(next);
|
|
271
|
+
return next;
|
|
272
|
+
}
|
|
273
|
+
function scheduleFlush() {
|
|
274
|
+
dirty = true;
|
|
275
|
+
if (flushTimer !== null) return;
|
|
276
|
+
flushTimer = setTimeout(function() {
|
|
277
|
+
flushTimer = null;
|
|
278
|
+
void settled(flushNow());
|
|
279
|
+
}, FLUSH_DELAY_MS);
|
|
280
|
+
}
|
|
281
|
+
function whenHydrated() {
|
|
282
|
+
return hydration ? hydration : Promise.resolve(mode);
|
|
283
|
+
}
|
|
284
|
+
function attachUnloadFlush() {
|
|
285
|
+
const flushIfDirty = function() {
|
|
286
|
+
if (dirty) void settled(flushNow());
|
|
287
|
+
};
|
|
288
|
+
try {
|
|
289
|
+
window.addEventListener("pagehide", flushIfDirty);
|
|
290
|
+
window.addEventListener("visibilitychange", function() {
|
|
291
|
+
if (document.visibilityState === "hidden") flushIfDirty();
|
|
292
|
+
});
|
|
293
|
+
} catch (err) {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function install() {
|
|
297
|
+
if (shim) return mode;
|
|
298
|
+
installSessionStorageShim();
|
|
299
|
+
if (nativeStorageWorks("localStorage")) {
|
|
300
|
+
mode = "native";
|
|
301
|
+
return mode;
|
|
302
|
+
}
|
|
303
|
+
const handle = createStorageShim({ onChange: scheduleFlush, wouldOverflow });
|
|
304
|
+
try {
|
|
305
|
+
Object.defineProperty(window, "localStorage", {
|
|
306
|
+
value: handle.storage,
|
|
307
|
+
configurable: true
|
|
308
|
+
});
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return mode;
|
|
311
|
+
}
|
|
312
|
+
shim = handle;
|
|
313
|
+
mode = "cloud";
|
|
314
|
+
attachUnloadFlush();
|
|
315
|
+
void hydrateFromCloud();
|
|
316
|
+
return mode;
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
install,
|
|
320
|
+
active: function() {
|
|
321
|
+
return shim !== null;
|
|
322
|
+
},
|
|
323
|
+
ready: function() {
|
|
324
|
+
return whenHydrated();
|
|
325
|
+
},
|
|
326
|
+
flush: function() {
|
|
327
|
+
if (!shim && appBlob === null) return Promise.resolve();
|
|
328
|
+
return flushNow();
|
|
329
|
+
},
|
|
330
|
+
writeApp: function(blob) {
|
|
331
|
+
if (options.byteLength(buildEnvelope(shim ? shim.entries() : {}, blob)) > options.maxBytes) {
|
|
332
|
+
return Promise.reject(storageError("too_large"));
|
|
333
|
+
}
|
|
334
|
+
appBlob = blob;
|
|
335
|
+
return flushNow();
|
|
336
|
+
},
|
|
337
|
+
get mode() {
|
|
338
|
+
return mode;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
33
343
|
// src/core.ts
|
|
34
344
|
var BOUNTY_BOARD_URL = "https://www.bountyboard.gg/arcade";
|
|
35
345
|
var ADSENSE_CLIENT_ID = "ca-pub-7727110228190547";
|
|
@@ -37,6 +347,8 @@ var AD_PLACEMENT_SRC = "https://pagead2.googlesyndication.com/pagead/js/adsbygoo
|
|
|
37
347
|
var AD_PLACEMENT_SCRIPT_ID = "bb-arcade-h5-ads";
|
|
38
348
|
var DEFAULT_AD_LOAD_TIMEOUT_MS = 6e3;
|
|
39
349
|
var SAVE_TIMEOUT_MS = 15e3;
|
|
350
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
351
|
+
var MAX_SAVE_BYTES = 1024 * 1024;
|
|
40
352
|
var INIT_CONFIG_GRACE_MS = 1500;
|
|
41
353
|
var DEFAULT_ALLOW = [
|
|
42
354
|
"bountyboard.gg",
|
|
@@ -61,6 +373,14 @@ function cleanText(value, fallback, max) {
|
|
|
61
373
|
if (typeof value !== "string") return fallback || "";
|
|
62
374
|
return value.replace(/[^\w:.-]+/g, "_").slice(0, max || 80) || fallback || "";
|
|
63
375
|
}
|
|
376
|
+
var cachedTextEncoder = null;
|
|
377
|
+
function utf8ByteLength(value) {
|
|
378
|
+
if (typeof TextEncoder !== "undefined") {
|
|
379
|
+
cachedTextEncoder = cachedTextEncoder || new TextEncoder();
|
|
380
|
+
return cachedTextEncoder.encode(value).length;
|
|
381
|
+
}
|
|
382
|
+
return new Blob([value]).size;
|
|
383
|
+
}
|
|
64
384
|
function toBoolean(value) {
|
|
65
385
|
return value === true || value === "true" || value === "1";
|
|
66
386
|
}
|
|
@@ -180,13 +500,14 @@ function createBBArcade() {
|
|
|
180
500
|
};
|
|
181
501
|
let hostConfigReceived = false;
|
|
182
502
|
let hostPlayer = null;
|
|
503
|
+
const playerChangeHandlers = [];
|
|
183
504
|
let blocked = false;
|
|
184
505
|
const pending = {};
|
|
185
506
|
let seq = 0;
|
|
186
507
|
function post(type, score, extra) {
|
|
187
508
|
if (blocked) return;
|
|
188
509
|
const message = { source: SDK_MESSAGE_SOURCE, v: VERSION, type };
|
|
189
|
-
if (typeof score === "number" && isFinite(score)) message.score = Math.
|
|
510
|
+
if (typeof score === "number" && isFinite(score)) message.score = Math.trunc(score);
|
|
190
511
|
if (extra && typeof extra === "object") {
|
|
191
512
|
const fields = extra;
|
|
192
513
|
for (const key in fields) {
|
|
@@ -207,7 +528,7 @@ function createBBArcade() {
|
|
|
207
528
|
return true;
|
|
208
529
|
}
|
|
209
530
|
}
|
|
210
|
-
function request(type, payload) {
|
|
531
|
+
function request(type, payload, timeoutMs = SAVE_TIMEOUT_MS) {
|
|
211
532
|
return new Promise(function(resolve, reject) {
|
|
212
533
|
if (blocked) return reject(sdkError("error"));
|
|
213
534
|
if (!isEmbedded()) return reject(sdkError("unsupported"));
|
|
@@ -217,7 +538,7 @@ function createBBArcade() {
|
|
|
217
538
|
delete pending[requestId];
|
|
218
539
|
reject(sdkError("error"));
|
|
219
540
|
}
|
|
220
|
-
},
|
|
541
|
+
}, timeoutMs);
|
|
221
542
|
pending[requestId] = { resolve, reject, timer };
|
|
222
543
|
try {
|
|
223
544
|
window.parent.postMessage(
|
|
@@ -309,7 +630,7 @@ function createBBArcade() {
|
|
|
309
630
|
if (!hostnameAllowed(origin, allow)) blockHost(options);
|
|
310
631
|
return;
|
|
311
632
|
}
|
|
312
|
-
request("host").then(
|
|
633
|
+
request("host", void 0, LOCK_TIMEOUT_MS).then(
|
|
313
634
|
function(res) {
|
|
314
635
|
const r = res;
|
|
315
636
|
if (!hostnameAllowed(r && r.origin, allow)) blockHost(options);
|
|
@@ -333,7 +654,7 @@ function createBBArcade() {
|
|
|
333
654
|
lockToHostBestEffort(opts, allow);
|
|
334
655
|
return;
|
|
335
656
|
}
|
|
336
|
-
request("host_attest").then(
|
|
657
|
+
request("host_attest", void 0, LOCK_TIMEOUT_MS).then(
|
|
337
658
|
function(res) {
|
|
338
659
|
const r = res;
|
|
339
660
|
if (!r || !r.attestation) {
|
|
@@ -395,7 +716,15 @@ function createBBArcade() {
|
|
|
395
716
|
if (msg.source !== HOST_MESSAGE_SOURCE || msg.type !== "config") return;
|
|
396
717
|
hostConfigReceived = true;
|
|
397
718
|
if (msg.player !== void 0) {
|
|
398
|
-
|
|
719
|
+
const nextPlayer = sanitizePlayer(msg.player);
|
|
720
|
+
const changed = nextPlayer === null ? hostPlayer !== null : hostPlayer === null || nextPlayer.name !== hostPlayer.name || nextPlayer.avatarUrl !== hostPlayer.avatarUrl;
|
|
721
|
+
hostPlayer = nextPlayer;
|
|
722
|
+
if (changed) {
|
|
723
|
+
const snapshot = nextPlayer ? { name: nextPlayer.name, avatarUrl: nextPlayer.avatarUrl } : null;
|
|
724
|
+
for (let i = 0; i < playerChangeHandlers.length; i++) {
|
|
725
|
+
safeCall(playerChangeHandlers[i], snapshot);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
399
728
|
}
|
|
400
729
|
const rewardedAds = msg.rewardedAds;
|
|
401
730
|
if (rewardedAds && typeof rewardedAds === "object") {
|
|
@@ -527,6 +856,7 @@ function createBBArcade() {
|
|
|
527
856
|
let resolved = false;
|
|
528
857
|
let status = "unavailable";
|
|
529
858
|
let rewardOffered = false;
|
|
859
|
+
let adShown = false;
|
|
530
860
|
const waitTimer = setTimeout(function() {
|
|
531
861
|
if (rewardOffered || resolved) return;
|
|
532
862
|
const timeout = Object.assign(
|
|
@@ -571,6 +901,7 @@ function createBBArcade() {
|
|
|
571
901
|
}
|
|
572
902
|
post("rewarded_ad", void 0, Object.assign({ event: "started" }, base));
|
|
573
903
|
try {
|
|
904
|
+
adShown = true;
|
|
574
905
|
showAdFn();
|
|
575
906
|
} catch (err) {
|
|
576
907
|
const showError = Object.assign(
|
|
@@ -620,7 +951,13 @@ function createBBArcade() {
|
|
|
620
951
|
},
|
|
621
952
|
adBreakDone: function(placementInfo) {
|
|
622
953
|
if (resolved) return;
|
|
623
|
-
const result = rewardOffered || status !== "unavailable" ? {
|
|
954
|
+
const result = rewardOffered || status !== "unavailable" ? {
|
|
955
|
+
// A reward was offered but the ad never reached the screen
|
|
956
|
+
// (the game retained show() and let the break lapse): the
|
|
957
|
+
// break is over without a view, so resolve 'dismissed',
|
|
958
|
+
// not the non-terminal-looking 'ready'.
|
|
959
|
+
status: status === "ready" && !adShown ? "dismissed" : status
|
|
960
|
+
} : { status: "unavailable", error: "no_rewarded_ad" };
|
|
624
961
|
if (placementInfo && typeof placementInfo === "object") {
|
|
625
962
|
result.breakStatus = cleanText(
|
|
626
963
|
placementInfo.breakStatus
|
|
@@ -791,6 +1128,16 @@ function createBBArcade() {
|
|
|
791
1128
|
if (!hostnameAllowed(event.origin, DEFAULT_ALLOW)) return;
|
|
792
1129
|
applyHostConfig(event.data);
|
|
793
1130
|
});
|
|
1131
|
+
const storage = createStorageController({
|
|
1132
|
+
save: function(blob) {
|
|
1133
|
+
return request("save", blob);
|
|
1134
|
+
},
|
|
1135
|
+
load: function() {
|
|
1136
|
+
return request("load");
|
|
1137
|
+
},
|
|
1138
|
+
byteLength: utf8ByteLength,
|
|
1139
|
+
maxBytes: MAX_SAVE_BYTES
|
|
1140
|
+
});
|
|
794
1141
|
const sdk = {
|
|
795
1142
|
lockToHost,
|
|
796
1143
|
init,
|
|
@@ -823,12 +1170,29 @@ function createBBArcade() {
|
|
|
823
1170
|
prepareRewardedBreak: prepareRewardedAd,
|
|
824
1171
|
rewardedBreak,
|
|
825
1172
|
save: function(blob) {
|
|
826
|
-
|
|
1173
|
+
const data = String(blob);
|
|
1174
|
+
if (utf8ByteLength(data) > MAX_SAVE_BYTES) {
|
|
1175
|
+
return Promise.reject(sdkError("too_large"));
|
|
1176
|
+
}
|
|
1177
|
+
if (storage.active()) return storage.writeApp(data);
|
|
1178
|
+
return request("save", data);
|
|
827
1179
|
},
|
|
828
1180
|
load: function() {
|
|
829
|
-
return request("load")
|
|
1181
|
+
return request("load").then(function(raw) {
|
|
1182
|
+
return parseEnvelope(raw).app;
|
|
1183
|
+
});
|
|
830
1184
|
},
|
|
1185
|
+
storage,
|
|
831
1186
|
getPlayer,
|
|
1187
|
+
onPlayerChange: function(handler) {
|
|
1188
|
+
if (typeof handler !== "function") return function() {
|
|
1189
|
+
};
|
|
1190
|
+
playerChangeHandlers.push(handler);
|
|
1191
|
+
return function() {
|
|
1192
|
+
const idx = playerChangeHandlers.indexOf(handler);
|
|
1193
|
+
if (idx !== -1) playerChangeHandlers.splice(idx, 1);
|
|
1194
|
+
};
|
|
1195
|
+
},
|
|
832
1196
|
getVariant: function(key, variants) {
|
|
833
1197
|
const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
|
|
834
1198
|
const fallback = sorted.length > 0 ? sorted[0] : null;
|
|
@@ -894,7 +1258,17 @@ function createInertBBArcade() {
|
|
|
894
1258
|
rewardedBreak: () => Promise.resolve(false),
|
|
895
1259
|
save: () => Promise.reject(sdkError2("unsupported")),
|
|
896
1260
|
load: () => Promise.reject(sdkError2("unsupported")),
|
|
1261
|
+
// No window to shim and nothing to persist to: report the honest mode
|
|
1262
|
+
// rather than pretending an install succeeded.
|
|
1263
|
+
storage: {
|
|
1264
|
+
install: () => "memory",
|
|
1265
|
+
ready: () => Promise.resolve("memory"),
|
|
1266
|
+
flush: () => Promise.resolve(),
|
|
1267
|
+
mode: "memory"
|
|
1268
|
+
},
|
|
897
1269
|
getPlayer: () => Promise.resolve(null),
|
|
1270
|
+
onPlayerChange: () => () => {
|
|
1271
|
+
},
|
|
898
1272
|
getVariant: (key, variants) => {
|
|
899
1273
|
const sorted = Array.isArray(variants) ? variants.slice().sort() : [];
|
|
900
1274
|
return Promise.resolve(sorted.length > 0 ? sorted[0] : null);
|
|
@@ -908,13 +1282,17 @@ var BBArcade = typeof window === "undefined" ? createInertBBArcade() : createBBA
|
|
|
908
1282
|
|
|
909
1283
|
// src/multiplayer.ts
|
|
910
1284
|
var WELCOME_TIMEOUT_MS = 1e4;
|
|
1285
|
+
var MIN_WELCOME_TIMEOUT_MS = 1e3;
|
|
1286
|
+
var MAX_WELCOME_TIMEOUT_MS = 6e4;
|
|
911
1287
|
var MAX_RECONNECT_ATTEMPTS = 3;
|
|
912
1288
|
var MAX_JOIN_DATA_BYTES = 1024;
|
|
1289
|
+
var SEAT_FULL_CLOSE_CODE = 4005;
|
|
913
1290
|
var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
914
1291
|
var CODE_LENGTH = 4;
|
|
915
|
-
function sdkError3(code) {
|
|
1292
|
+
function sdkError3(code, detail) {
|
|
916
1293
|
const err = new Error(code);
|
|
917
1294
|
err.code = code;
|
|
1295
|
+
if (detail) err.detail = detail;
|
|
918
1296
|
return err;
|
|
919
1297
|
}
|
|
920
1298
|
function randomChars(length) {
|
|
@@ -931,23 +1309,13 @@ function randomChars(length) {
|
|
|
931
1309
|
function generateCode() {
|
|
932
1310
|
return randomChars(CODE_LENGTH);
|
|
933
1311
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
else if (code < 2048) bytes += 2;
|
|
940
|
-
else if (code >= 55296 && code <= 56319 && i + 1 < value.length) {
|
|
941
|
-
const next = value.charCodeAt(i + 1);
|
|
942
|
-
if (next >= 56320 && next <= 57343) {
|
|
943
|
-
bytes += 4;
|
|
944
|
-
i += 1;
|
|
945
|
-
} else {
|
|
946
|
-
bytes += 3;
|
|
947
|
-
}
|
|
948
|
-
} else bytes += 3;
|
|
1312
|
+
var cachedTextEncoder2 = null;
|
|
1313
|
+
function utf8ByteLength2(value) {
|
|
1314
|
+
if (typeof TextEncoder !== "undefined") {
|
|
1315
|
+
cachedTextEncoder2 = cachedTextEncoder2 || new TextEncoder();
|
|
1316
|
+
return cachedTextEncoder2.encode(value).length;
|
|
949
1317
|
}
|
|
950
|
-
return
|
|
1318
|
+
return new Blob([value]).size;
|
|
951
1319
|
}
|
|
952
1320
|
function serializeJoinData(data) {
|
|
953
1321
|
if (data === void 0) return null;
|
|
@@ -958,28 +1326,34 @@ function serializeJoinData(data) {
|
|
|
958
1326
|
} catch (e) {
|
|
959
1327
|
throw sdkError3("rejected");
|
|
960
1328
|
}
|
|
961
|
-
if (!serialized ||
|
|
1329
|
+
if (!serialized || utf8ByteLength2(serialized) > MAX_JOIN_DATA_BYTES) {
|
|
962
1330
|
throw sdkError3("rejected");
|
|
963
1331
|
}
|
|
964
1332
|
return serialized;
|
|
965
1333
|
}
|
|
966
|
-
function requestTicket(
|
|
1334
|
+
function requestTicket(payload) {
|
|
967
1335
|
const request = BBArcade._request;
|
|
968
1336
|
if (typeof request !== "function") return Promise.reject(sdkError3("unsupported"));
|
|
969
|
-
return request("mp_ticket",
|
|
1337
|
+
return request("mp_ticket", payload).then((grant) => {
|
|
970
1338
|
const g = grant;
|
|
971
1339
|
if (!g || typeof g.ticket !== "string" || typeof g.roomUrl !== "string") {
|
|
972
1340
|
throw sdkError3("error");
|
|
973
1341
|
}
|
|
974
|
-
return {
|
|
1342
|
+
return {
|
|
1343
|
+
ticket: g.ticket,
|
|
1344
|
+
roomUrl: g.roomUrl,
|
|
1345
|
+
expiresAt: Number(g.expiresAt) || 0,
|
|
1346
|
+
roomId: typeof g.roomId === "string" && g.roomId ? g.roomId : null
|
|
1347
|
+
};
|
|
975
1348
|
});
|
|
976
1349
|
}
|
|
977
1350
|
var RoomConnection = class {
|
|
978
|
-
constructor(code, devGrant, joinData) {
|
|
1351
|
+
constructor(code, devGrant, joinData, welcomeTimeoutMs = WELCOME_TIMEOUT_MS) {
|
|
979
1352
|
this.playerId = "";
|
|
980
1353
|
this.players = [];
|
|
981
1354
|
this.state = null;
|
|
982
1355
|
this.connected = false;
|
|
1356
|
+
this.latencyMs = null;
|
|
983
1357
|
this.ws = null;
|
|
984
1358
|
// Keyed by event name; values are erased to (unknown) => void internally and
|
|
985
1359
|
// retyped at the on()/emit() boundary, which keeps the public API strictly
|
|
@@ -992,15 +1366,20 @@ var RoomConnection = class {
|
|
|
992
1366
|
// or the welcome timed out — permanent outcomes that must not reconnect.
|
|
993
1367
|
this.terminal = false;
|
|
994
1368
|
this.reconnectAttempts = 0;
|
|
1369
|
+
// The in-flight connect()'s settlement, so leave() can reject a join that
|
|
1370
|
+
// is still waiting on its welcome instead of stranding it on the timeout.
|
|
1371
|
+
this.pendingConnect = null;
|
|
995
1372
|
this.code = code;
|
|
996
1373
|
this.devGrant = devGrant;
|
|
997
1374
|
this.joinData = joinData;
|
|
1375
|
+
this.welcomeTimeoutMs = welcomeTimeoutMs;
|
|
998
1376
|
}
|
|
999
1377
|
/** Connect and resolve on the server's welcome (post-ticket-verification). */
|
|
1000
1378
|
connect(grant) {
|
|
1001
1379
|
return new Promise((resolve, reject) => {
|
|
1002
|
-
let
|
|
1380
|
+
let settled2 = false;
|
|
1003
1381
|
const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket) + (this.joinData === null ? "" : "&join=" + encodeURIComponent(this.joinData));
|
|
1382
|
+
const connectStartedAt = Date.now();
|
|
1004
1383
|
let ws;
|
|
1005
1384
|
try {
|
|
1006
1385
|
ws = new WebSocket(url);
|
|
@@ -1009,15 +1388,17 @@ var RoomConnection = class {
|
|
|
1009
1388
|
}
|
|
1010
1389
|
this.ws = ws;
|
|
1011
1390
|
const welcomeTimer = setTimeout(() => {
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1391
|
+
if (settled2) return;
|
|
1392
|
+
settled2 = true;
|
|
1393
|
+
this.pendingConnect = null;
|
|
1014
1394
|
this.terminal = true;
|
|
1015
1395
|
try {
|
|
1016
1396
|
ws.close();
|
|
1017
1397
|
} catch (err) {
|
|
1018
1398
|
}
|
|
1019
1399
|
reject(sdkError3("error"));
|
|
1020
|
-
},
|
|
1400
|
+
}, this.welcomeTimeoutMs);
|
|
1401
|
+
this.pendingConnect = { reject, timer: welcomeTimer };
|
|
1021
1402
|
ws.onmessage = (event) => {
|
|
1022
1403
|
var _a, _b, _c, _d, _e;
|
|
1023
1404
|
let msg;
|
|
@@ -1028,14 +1409,23 @@ var RoomConnection = class {
|
|
|
1028
1409
|
}
|
|
1029
1410
|
if (!msg || typeof msg.t !== "string") return;
|
|
1030
1411
|
if (msg.t === "welcome") {
|
|
1412
|
+
if (this.intentionalClose) {
|
|
1413
|
+
try {
|
|
1414
|
+
ws.close();
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
}
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1031
1419
|
const reconnected = this.reconnectAttempts > 0;
|
|
1032
1420
|
this.playerId = String((_a = msg.playerId) != null ? _a : "");
|
|
1033
1421
|
this.players = Array.isArray(msg.players) ? msg.players : [];
|
|
1034
1422
|
this.state = (_b = msg.state) != null ? _b : null;
|
|
1035
1423
|
this.connected = true;
|
|
1036
1424
|
this.reconnectAttempts = 0;
|
|
1037
|
-
|
|
1038
|
-
|
|
1425
|
+
this.latencyMs = Math.max(0, Date.now() - connectStartedAt);
|
|
1426
|
+
if (!settled2) {
|
|
1427
|
+
settled2 = true;
|
|
1428
|
+
this.pendingConnect = null;
|
|
1039
1429
|
clearTimeout(welcomeTimer);
|
|
1040
1430
|
resolve(this);
|
|
1041
1431
|
}
|
|
@@ -1074,24 +1464,27 @@ var RoomConnection = class {
|
|
|
1074
1464
|
}
|
|
1075
1465
|
if (msg.t === "error") {
|
|
1076
1466
|
const code = String((_e = msg.code) != null ? _e : "error");
|
|
1077
|
-
if (!
|
|
1078
|
-
|
|
1467
|
+
if (!settled2) {
|
|
1468
|
+
settled2 = true;
|
|
1079
1469
|
this.terminal = true;
|
|
1470
|
+
this.pendingConnect = null;
|
|
1080
1471
|
clearTimeout(welcomeTimer);
|
|
1081
1472
|
try {
|
|
1082
1473
|
ws.close();
|
|
1083
1474
|
} catch (err) {
|
|
1084
1475
|
}
|
|
1085
|
-
reject(sdkError3(code === "unauthenticated" ? "unauthenticated" : "rejected"));
|
|
1476
|
+
reject(sdkError3(code === "unauthenticated" ? "unauthenticated" : "rejected", code));
|
|
1086
1477
|
return;
|
|
1087
1478
|
}
|
|
1088
1479
|
this.emit("error", { code });
|
|
1089
1480
|
}
|
|
1090
1481
|
};
|
|
1091
|
-
ws.onclose = () => {
|
|
1482
|
+
ws.onclose = (event) => {
|
|
1092
1483
|
this.connected = false;
|
|
1093
|
-
if (
|
|
1094
|
-
|
|
1484
|
+
if (event && event.code === SEAT_FULL_CLOSE_CODE) this.terminal = true;
|
|
1485
|
+
if (!settled2) {
|
|
1486
|
+
settled2 = true;
|
|
1487
|
+
this.pendingConnect = null;
|
|
1095
1488
|
clearTimeout(welcomeTimer);
|
|
1096
1489
|
reject(sdkError3("error"));
|
|
1097
1490
|
return;
|
|
@@ -1120,7 +1513,7 @@ var RoomConnection = class {
|
|
|
1120
1513
|
await new Promise((r) => setTimeout(r, 400 * this.reconnectAttempts));
|
|
1121
1514
|
if (this.intentionalClose || this.ended || this.terminal) return;
|
|
1122
1515
|
try {
|
|
1123
|
-
const grant = (_a = this.devGrant) != null ? _a : await requestTicket(this.code);
|
|
1516
|
+
const grant = (_a = this.devGrant) != null ? _a : await requestTicket({ roomId: this.code });
|
|
1124
1517
|
if (this.intentionalClose || this.ended || this.terminal) return;
|
|
1125
1518
|
await this.connect(grant);
|
|
1126
1519
|
if (this.intentionalClose) this.leave();
|
|
@@ -1155,6 +1548,12 @@ var RoomConnection = class {
|
|
|
1155
1548
|
var _a;
|
|
1156
1549
|
this.intentionalClose = true;
|
|
1157
1550
|
this.connected = false;
|
|
1551
|
+
if (this.pendingConnect) {
|
|
1552
|
+
const pending = this.pendingConnect;
|
|
1553
|
+
this.pendingConnect = null;
|
|
1554
|
+
clearTimeout(pending.timer);
|
|
1555
|
+
pending.reject(sdkError3("error"));
|
|
1556
|
+
}
|
|
1158
1557
|
try {
|
|
1159
1558
|
(_a = this.ws) == null ? void 0 : _a.close(1e3);
|
|
1160
1559
|
} catch (err) {
|
|
@@ -1174,23 +1573,57 @@ var RoomConnection = class {
|
|
|
1174
1573
|
}
|
|
1175
1574
|
}
|
|
1176
1575
|
};
|
|
1576
|
+
var MATCH_ATTEMPTS = 3;
|
|
1577
|
+
function normalizeWelcomeTimeoutMs(value) {
|
|
1578
|
+
if (typeof value !== "number" || !isFinite(value)) return WELCOME_TIMEOUT_MS;
|
|
1579
|
+
return Math.min(MAX_WELCOME_TIMEOUT_MS, Math.max(MIN_WELCOME_TIMEOUT_MS, Math.floor(value)));
|
|
1580
|
+
}
|
|
1581
|
+
async function matchJoin(joinData, timeoutMs) {
|
|
1582
|
+
let lastError = sdkError3("error");
|
|
1583
|
+
for (let attempt = 0; attempt < MATCH_ATTEMPTS; attempt++) {
|
|
1584
|
+
const grant = await requestTicket({ matchmake: true });
|
|
1585
|
+
if (!grant.roomId) throw sdkError3("error");
|
|
1586
|
+
try {
|
|
1587
|
+
return await new RoomConnection(grant.roomId, null, joinData, timeoutMs).connect(grant);
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
lastError = error;
|
|
1590
|
+
const detail = error == null ? void 0 : error.detail;
|
|
1591
|
+
if (detail !== "room_full" && detail !== "room_over") throw error;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
throw lastError;
|
|
1595
|
+
}
|
|
1177
1596
|
function joinRoom(options) {
|
|
1178
1597
|
const opts = options != null ? options : {};
|
|
1179
1598
|
if (typeof window === "undefined" || typeof WebSocket === "undefined") {
|
|
1180
1599
|
return Promise.reject(sdkError3("unsupported"));
|
|
1181
1600
|
}
|
|
1182
|
-
const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
|
|
1183
1601
|
let joinData;
|
|
1184
1602
|
try {
|
|
1185
1603
|
joinData = serializeJoinData(opts.joinData);
|
|
1186
1604
|
} catch (err) {
|
|
1187
1605
|
return Promise.reject(err);
|
|
1188
1606
|
}
|
|
1607
|
+
const timeoutMs = normalizeWelcomeTimeoutMs(opts.timeoutMs);
|
|
1608
|
+
if (opts.match) {
|
|
1609
|
+
if (opts.code || opts.create || opts.roomUrl || opts.ticket) {
|
|
1610
|
+
return Promise.reject(sdkError3("rejected"));
|
|
1611
|
+
}
|
|
1612
|
+
return matchJoin(joinData, timeoutMs);
|
|
1613
|
+
}
|
|
1614
|
+
const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
|
|
1189
1615
|
if (opts.roomUrl && opts.ticket) {
|
|
1190
|
-
const devGrant = {
|
|
1191
|
-
|
|
1616
|
+
const devGrant = {
|
|
1617
|
+
ticket: opts.ticket,
|
|
1618
|
+
roomUrl: opts.roomUrl,
|
|
1619
|
+
expiresAt: 0,
|
|
1620
|
+
roomId: code
|
|
1621
|
+
};
|
|
1622
|
+
return new RoomConnection(code, devGrant, joinData, timeoutMs).connect(devGrant);
|
|
1192
1623
|
}
|
|
1193
|
-
return requestTicket(
|
|
1624
|
+
return requestTicket({ roomId: code }).then(
|
|
1625
|
+
(grant) => new RoomConnection(code, null, joinData, timeoutMs).connect(grant)
|
|
1626
|
+
);
|
|
1194
1627
|
}
|
|
1195
1628
|
var multiplayer = { joinRoom };
|
|
1196
1629
|
// Annotate the CommonJS export names for ESM import in node:
|