@bountyboard/arcade-sdk 1.0.0 → 1.1.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 +13 -6
- package/CHANGELOG.md +30 -0
- package/README.md +49 -4
- package/dist/{chunk-WCGBR3MI.js → chunk-OWOESH4X.js} +94 -11
- package/dist/chunk-OWOESH4X.js.map +1 -0
- package/dist/global-sdk.d.ts +78 -10
- package/dist/global.js +151 -20
- package/dist/index.cjs +93 -10
- 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 +151 -20
- 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 +59 -11
- package/dist/multiplayer.js.map +1 -1
- package/dist/{types-B6xH-lDi.d.cts → types-Cq8p7ZkC.d.cts} +72 -11
- package/dist/{types-B6xH-lDi.d.ts → types-Cq8p7ZkC.d.ts} +72 -11
- package/docs/game-design-playbook.md +5 -3
- package/package.json +5 -3
- package/dist/chunk-WCGBR3MI.js.map +0 -1
package/dist/global-sdk.d.ts
CHANGED
|
@@ -103,13 +103,19 @@ interface BBArcadeRewardedAdResult {
|
|
|
103
103
|
* Failure detail when status is 'unavailable' or 'error', e.g.
|
|
104
104
|
* 'host_disabled' | 'ad_break_unavailable' | 'ad_break_timeout' |
|
|
105
105
|
* 'no_rewarded_ad' | 'ad_in_progress' | 'show_ad_error' |
|
|
106
|
-
* '
|
|
106
|
+
* 'direct_user_action_required' | 'before_reward_error' |
|
|
107
|
+
* 'ad_break_error'.
|
|
107
108
|
*/
|
|
108
109
|
error?: string;
|
|
109
110
|
/** Google's H5 breakStatus string, when the placement reported one. */
|
|
110
111
|
breakStatus?: string;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/** A terminal failure returned while preparing a rewarded placement. */
|
|
115
|
+
type BBArcadeRewardedAdPrepareFailure = BBArcadeRewardedAdResult & {
|
|
116
|
+
status: 'unavailable' | 'error';
|
|
117
|
+
};
|
|
118
|
+
|
|
113
119
|
/** Options for rewardedAd()/showRewardedAd() (all optional). */
|
|
114
120
|
interface BBArcadeRewardedAdOptions {
|
|
115
121
|
/** Placement name for analytics, e.g. 'revive' (alias: name). */
|
|
@@ -120,7 +126,7 @@ interface BBArcadeRewardedAdOptions {
|
|
|
120
126
|
reward?: string;
|
|
121
127
|
/** Supply your own ad-break id instead of the generated one. */
|
|
122
128
|
adBreakId?: string;
|
|
123
|
-
/**
|
|
129
|
+
/** Legacy SDK analytics metadata; never forwarded as a Google adBreak field. */
|
|
124
130
|
size?: 'small' | 'medium' | 'large';
|
|
125
131
|
/** Legacy AdSense channel label. Ignored on Bounty Board. */
|
|
126
132
|
adChannel?: string;
|
|
@@ -129,8 +135,10 @@ interface BBArcadeRewardedAdOptions {
|
|
|
129
135
|
/** Per-call override of how long to wait for an ad, in ms (max 15000). */
|
|
130
136
|
loadTimeoutMs?: number;
|
|
131
137
|
/**
|
|
132
|
-
* Called when an ad is ready.
|
|
133
|
-
*
|
|
138
|
+
* Called when an ad is ready. Retain showAd and invoke it directly from the
|
|
139
|
+
* player's click/tap handler. An asynchronous call without active browser
|
|
140
|
+
* user activation fails with 'direct_user_action_required'. Prefer
|
|
141
|
+
* prepareRewardedAd() for the typed two-stage flow.
|
|
134
142
|
*/
|
|
135
143
|
beforeReward?: (showAd: () => void) => void;
|
|
136
144
|
/** The player watched the ad to completion — grant the reward. */
|
|
@@ -151,6 +159,38 @@ interface BBArcadeRewardedAdOptions {
|
|
|
151
159
|
onUnavailable?: (result: BBArcadeRewardedAdResult) => void;
|
|
152
160
|
}
|
|
153
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Options for prepareRewardedAd(). The preparation owns Google's beforeReward
|
|
164
|
+
* callback so it can retain showAdFn safely; onStart runs synchronously inside
|
|
165
|
+
* the returned handle's show() method.
|
|
166
|
+
*/
|
|
167
|
+
type BBArcadePrepareRewardedAdOptions = Omit<BBArcadeRewardedAdOptions, 'beforeReward'> & {
|
|
168
|
+
/** Called synchronously immediately before Google's retained showAdFn. */
|
|
169
|
+
onStart?: () => void;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A rewarded placement Google has made available. Call show() directly from
|
|
174
|
+
* the player's click/tap handler; do not await or schedule other work first.
|
|
175
|
+
*/
|
|
176
|
+
interface BBArcadePreparedRewardedAd {
|
|
177
|
+
status: 'ready';
|
|
178
|
+
placement: string;
|
|
179
|
+
reward: string;
|
|
180
|
+
adBreakId: string;
|
|
181
|
+
size?: 'small' | 'medium' | 'large';
|
|
182
|
+
/**
|
|
183
|
+
* One-shot display call. Invokes Google's retained showAdFn synchronously,
|
|
184
|
+
* then resolves with the final structured outcome. Only 'viewed' grants.
|
|
185
|
+
*/
|
|
186
|
+
show(): Promise<BBArcadeRewardedAdResult>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Result of prepareRewardedAd(): a ready handle or a terminal failure. */
|
|
190
|
+
type BBArcadeRewardedAdPreparation =
|
|
191
|
+
| BBArcadePreparedRewardedAd
|
|
192
|
+
| BBArcadeRewardedAdPrepareFailure;
|
|
193
|
+
|
|
154
194
|
/** Options for rewardedBreak(): everything rewardedAd() takes, plus onStart. */
|
|
155
195
|
interface BBArcadeRewardedBreakOptions extends BBArcadeRewardedAdOptions {
|
|
156
196
|
/** Called once right before the ad shows — pause gameplay/audio here. */
|
|
@@ -180,6 +220,13 @@ interface BBArcadeMpJoinOptions {
|
|
|
180
220
|
code?: string;
|
|
181
221
|
/** Create a new room (a share code is generated for you). */
|
|
182
222
|
create?: boolean;
|
|
223
|
+
/**
|
|
224
|
+
* Game-defined, JSON-serializable data supplied to the authoritative module
|
|
225
|
+
* before it admits this player (avatar/loadout/mode selections, for example).
|
|
226
|
+
* The room server treats this as untrusted input and limits it to 1 KiB.
|
|
227
|
+
* Never put credentials or other secrets here.
|
|
228
|
+
*/
|
|
229
|
+
joinData?: Readonly<Record<string, unknown>>;
|
|
183
230
|
/**
|
|
184
231
|
* Local development only: connect straight to a room server (e.g. wrangler
|
|
185
232
|
* dev with DEV_ALLOW_UNSIGNED=1) instead of asking the Bounty Board host for
|
|
@@ -200,6 +247,8 @@ interface BBArcadeMpRoomEvents {
|
|
|
200
247
|
/** The match finished; results were reported by the server, not by clients. */
|
|
201
248
|
end: { results: BBArcadeMpResult[] };
|
|
202
249
|
error: { code: string };
|
|
250
|
+
/** Connection state, including successful automatic reconnection. */
|
|
251
|
+
connection: { connected: boolean; reconnecting: boolean };
|
|
203
252
|
/** Connection dropped. reconnecting: true means the SDK is retrying. */
|
|
204
253
|
close: { reconnecting: boolean };
|
|
205
254
|
}
|
|
@@ -214,8 +263,12 @@ interface BBArcadeMpRoom {
|
|
|
214
263
|
players: BBArcadeMpPlayer[];
|
|
215
264
|
/** Latest authoritative state snapshot (as filtered for you). */
|
|
216
265
|
state: unknown;
|
|
266
|
+
/** Whether the room currently has an accepted WebSocket connection. */
|
|
267
|
+
connected: boolean;
|
|
217
268
|
/** Send an INPUT to the server (never authoritative state — the server simulates). */
|
|
218
269
|
send(input: unknown): void;
|
|
270
|
+
/** Like send(), but reports false when disconnected or the socket raced closed. */
|
|
271
|
+
trySend(input: unknown): boolean;
|
|
219
272
|
/** Subscribe to a room event; returns an unsubscribe function. */
|
|
220
273
|
on<K extends keyof BBArcadeMpRoomEvents>(
|
|
221
274
|
event: K,
|
|
@@ -283,17 +336,32 @@ interface BBArcadeSDK {
|
|
|
283
336
|
/** Signal that the immersive WebXR session ended. */
|
|
284
337
|
xrSessionEnd(): void;
|
|
285
338
|
/**
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
339
|
+
* Low-level rewarded placement API. Resolves with the full result object;
|
|
340
|
+
* only status 'viewed' should grant. Prefer prepareRewardedAd() for new
|
|
341
|
+
* integrations so Google's show function stays in the player's click stack.
|
|
289
342
|
*/
|
|
290
343
|
rewardedAd(options?: BBArcadeRewardedAdOptions): Promise<BBArcadeRewardedAdResult>;
|
|
291
344
|
/** Alias of rewardedAd. */
|
|
292
345
|
showRewardedAd(options?: BBArcadeRewardedAdOptions): Promise<BBArcadeRewardedAdResult>;
|
|
293
346
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
347
|
+
* Safely prepare a rewarded placement without showing it. When status is
|
|
348
|
+
* 'ready', call result.show() directly from the player's click/tap handler;
|
|
349
|
+
* it resolves with the full final result and only 'viewed' should grant.
|
|
350
|
+
*/
|
|
351
|
+
prepareRewardedAd(
|
|
352
|
+
options?: BBArcadePrepareRewardedAdOptions
|
|
353
|
+
): Promise<BBArcadeRewardedAdPreparation>;
|
|
354
|
+
/** Alias of prepareRewardedAd. */
|
|
355
|
+
prepareRewardedBreak(
|
|
356
|
+
options?: BBArcadePrepareRewardedAdOptions
|
|
357
|
+
): Promise<BBArcadeRewardedAdPreparation>;
|
|
358
|
+
/**
|
|
359
|
+
* Legacy one-call rewarded break. Resolves true only when the ad was viewed.
|
|
360
|
+
*
|
|
361
|
+
* @deprecated Use prepareRewardedAd(), then call the ready handle's show()
|
|
362
|
+
* directly from the player's click/tap handler. Google's beforeReward may be
|
|
363
|
+
* asynchronous, so this convenience call cannot preserve user activation in
|
|
364
|
+
* every browser.
|
|
297
365
|
*/
|
|
298
366
|
rewardedBreak(optionsOrOnStart?: BBArcadeRewardedBreakOptions | (() => void)): Promise<boolean>;
|
|
299
367
|
/**
|
package/dist/global.js
CHANGED
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
function cleanRewardedSize(value) {
|
|
43
43
|
return value === "small" || value === "medium" || value === "large" ? value : "";
|
|
44
44
|
}
|
|
45
|
+
function hasTransientUserActivation() {
|
|
46
|
+
if (typeof navigator === "undefined") return false;
|
|
47
|
+
const activation = navigator.userActivation;
|
|
48
|
+
return (activation == null ? void 0 : activation.isActive) === true;
|
|
49
|
+
}
|
|
45
50
|
function safeCall(fn, arg) {
|
|
46
51
|
if (typeof fn !== "function") return;
|
|
47
52
|
try {
|
|
@@ -118,7 +123,7 @@
|
|
|
118
123
|
function createBBArcade() {
|
|
119
124
|
let adPlacementLoad = null;
|
|
120
125
|
let adPreloadConfigured = false;
|
|
121
|
-
let rewardedBreakActive =
|
|
126
|
+
let rewardedBreakActive = null;
|
|
122
127
|
let rewardedBreakSequence = 0;
|
|
123
128
|
function isLocalAdHost() {
|
|
124
129
|
try {
|
|
@@ -529,6 +534,16 @@
|
|
|
529
534
|
function show() {
|
|
530
535
|
if (shown) return;
|
|
531
536
|
shown = true;
|
|
537
|
+
if (!hasTransientUserActivation()) {
|
|
538
|
+
const activationError = Object.assign(
|
|
539
|
+
{ status: "error", error: "direct_user_action_required" },
|
|
540
|
+
base
|
|
541
|
+
);
|
|
542
|
+
post("rewarded_ad", void 0, Object.assign({ event: "error" }, activationError));
|
|
543
|
+
safeCall(options.onError, activationError);
|
|
544
|
+
done(activationError);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
532
547
|
post("rewarded_ad", void 0, Object.assign({ event: "started" }, base));
|
|
533
548
|
try {
|
|
534
549
|
showAdFn();
|
|
@@ -552,6 +567,7 @@
|
|
|
552
567
|
base
|
|
553
568
|
);
|
|
554
569
|
post("rewarded_ad", void 0, Object.assign({ event: "error" }, beforeError));
|
|
570
|
+
if (shown) return;
|
|
555
571
|
safeCall(options.onError, beforeError);
|
|
556
572
|
done(beforeError);
|
|
557
573
|
}
|
|
@@ -560,9 +576,9 @@
|
|
|
560
576
|
adBreak({
|
|
561
577
|
type: "reward",
|
|
562
578
|
name: options.placement,
|
|
563
|
-
size: options.size || void 0,
|
|
564
579
|
beforeReward: startAd,
|
|
565
580
|
adViewed: function() {
|
|
581
|
+
if (resolved) return;
|
|
566
582
|
status = "viewed";
|
|
567
583
|
const viewed = Object.assign({ status }, base);
|
|
568
584
|
post("rewarded_ad", void 0, Object.assign({ event: "viewed" }, viewed));
|
|
@@ -570,6 +586,7 @@
|
|
|
570
586
|
safeCall(options.onReward, viewed);
|
|
571
587
|
},
|
|
572
588
|
adDismissed: function() {
|
|
589
|
+
if (resolved) return;
|
|
573
590
|
if (status !== "viewed") status = "dismissed";
|
|
574
591
|
const dismissed = Object.assign({ status }, base);
|
|
575
592
|
post("rewarded_ad", void 0, Object.assign({ event: "dismissed" }, dismissed));
|
|
@@ -577,6 +594,7 @@
|
|
|
577
594
|
safeCall(options.onDismissed, dismissed);
|
|
578
595
|
},
|
|
579
596
|
adBreakDone: function(placementInfo) {
|
|
597
|
+
if (resolved) return;
|
|
580
598
|
const result = rewardOffered || status !== "unavailable" ? { status } : { status: "unavailable", error: "no_rewarded_ad" };
|
|
581
599
|
if (placementInfo && typeof placementInfo === "object") {
|
|
582
600
|
result.breakStatus = cleanText(
|
|
@@ -610,6 +628,52 @@
|
|
|
610
628
|
});
|
|
611
629
|
});
|
|
612
630
|
}
|
|
631
|
+
function prepareRewardedAd(rawOptions) {
|
|
632
|
+
const normalized = normalizeRewardedOptions(rawOptions);
|
|
633
|
+
const base = {
|
|
634
|
+
placement: normalized.placement,
|
|
635
|
+
reward: normalized.reward,
|
|
636
|
+
adBreakId: normalized.adBreakId
|
|
637
|
+
};
|
|
638
|
+
if (normalized.size) base.size = normalized.size;
|
|
639
|
+
let ready2 = false;
|
|
640
|
+
let finalPromise;
|
|
641
|
+
return new Promise(function(resolvePreparation) {
|
|
642
|
+
finalPromise = rewardedAd(
|
|
643
|
+
Object.assign({}, rawOptions, {
|
|
644
|
+
// Preserve the id used by the ready handle and every lifecycle event;
|
|
645
|
+
// rewardedAd normalizes again, but this explicit id keeps both stages
|
|
646
|
+
// tied to the same placement.
|
|
647
|
+
adBreakId: normalized.adBreakId,
|
|
648
|
+
beforeReward: function(showAd) {
|
|
649
|
+
if (ready2) return;
|
|
650
|
+
ready2 = true;
|
|
651
|
+
let shown = false;
|
|
652
|
+
const prepared = Object.assign(
|
|
653
|
+
{ status: "ready" },
|
|
654
|
+
base,
|
|
655
|
+
{
|
|
656
|
+
show: function() {
|
|
657
|
+
if (!shown) {
|
|
658
|
+
shown = true;
|
|
659
|
+
if (hasTransientUserActivation()) {
|
|
660
|
+
safeCall(rawOptions && rawOptions.onStart);
|
|
661
|
+
}
|
|
662
|
+
showAd();
|
|
663
|
+
}
|
|
664
|
+
return finalPromise;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
);
|
|
668
|
+
resolvePreparation(prepared);
|
|
669
|
+
}
|
|
670
|
+
})
|
|
671
|
+
);
|
|
672
|
+
finalPromise.then(function(result) {
|
|
673
|
+
if (!ready2) resolvePreparation(result);
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
}
|
|
613
677
|
function preloadRewardedAds(rawOptions) {
|
|
614
678
|
const options = normalizeRewardedOptions(rawOptions);
|
|
615
679
|
if (!areRewardedAdsEnabled()) return Promise.resolve(false);
|
|
@@ -656,7 +720,7 @@
|
|
|
656
720
|
reward: normalized.reward
|
|
657
721
|
};
|
|
658
722
|
if (normalized.size) base.size = normalized.size;
|
|
659
|
-
if (rewardedBreakActive) {
|
|
723
|
+
if (rewardedBreakActive && rewardedBreakActive.state !== "ready") {
|
|
660
724
|
post(
|
|
661
725
|
"rewarded_ad",
|
|
662
726
|
void 0,
|
|
@@ -667,24 +731,32 @@
|
|
|
667
731
|
);
|
|
668
732
|
return Promise.resolve(false);
|
|
669
733
|
}
|
|
670
|
-
|
|
734
|
+
const activeBreak = { state: "loading" };
|
|
735
|
+
rewardedBreakActive = activeBreak;
|
|
671
736
|
let startCalled = false;
|
|
737
|
+
const callerBeforeReward = rawOptions.beforeReward;
|
|
672
738
|
return rewardedAd(
|
|
673
739
|
Object.assign({}, rawOptions, {
|
|
674
740
|
beforeReward: function(showAd) {
|
|
675
|
-
if (
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
741
|
+
if (rewardedBreakActive === activeBreak) activeBreak.state = "ready";
|
|
742
|
+
const showWithStart = function() {
|
|
743
|
+
if (startCalled || rewardedBreakActive !== activeBreak) return;
|
|
744
|
+
startCalled = true;
|
|
745
|
+
activeBreak.state = "showing";
|
|
746
|
+
if (hasTransientUserActivation()) safeCall(rawOptions.onStart);
|
|
747
|
+
showAd();
|
|
748
|
+
};
|
|
749
|
+
if (typeof callerBeforeReward === "function") callerBeforeReward(showWithStart);
|
|
750
|
+
else showWithStart();
|
|
679
751
|
}
|
|
680
752
|
})
|
|
681
753
|
).then(
|
|
682
754
|
function(result) {
|
|
683
|
-
rewardedBreakActive =
|
|
755
|
+
if (rewardedBreakActive === activeBreak) rewardedBreakActive = null;
|
|
684
756
|
return Boolean(result && result.status === "viewed");
|
|
685
757
|
},
|
|
686
758
|
function() {
|
|
687
|
-
rewardedBreakActive =
|
|
759
|
+
if (rewardedBreakActive === activeBreak) rewardedBreakActive = null;
|
|
688
760
|
return false;
|
|
689
761
|
}
|
|
690
762
|
);
|
|
@@ -722,6 +794,8 @@
|
|
|
722
794
|
},
|
|
723
795
|
rewardedAd,
|
|
724
796
|
showRewardedAd: rewardedAd,
|
|
797
|
+
prepareRewardedAd,
|
|
798
|
+
prepareRewardedBreak: prepareRewardedAd,
|
|
725
799
|
rewardedBreak,
|
|
726
800
|
save: function(blob) {
|
|
727
801
|
return request("save", String(blob));
|
|
@@ -767,6 +841,13 @@
|
|
|
767
841
|
reward: "reward",
|
|
768
842
|
adBreakId: "bbad_inert"
|
|
769
843
|
});
|
|
844
|
+
const prepareUnavailable = () => Promise.resolve({
|
|
845
|
+
status: "unavailable",
|
|
846
|
+
error: "ad_break_unavailable",
|
|
847
|
+
placement: "rewarded",
|
|
848
|
+
reward: "reward",
|
|
849
|
+
adBreakId: "bbad_inert"
|
|
850
|
+
});
|
|
770
851
|
return {
|
|
771
852
|
lockToHost: noop,
|
|
772
853
|
init: () => Promise.resolve(),
|
|
@@ -783,6 +864,8 @@
|
|
|
783
864
|
xrSessionEnd: noop,
|
|
784
865
|
rewardedAd: unavailable,
|
|
785
866
|
showRewardedAd: unavailable,
|
|
867
|
+
prepareRewardedAd: prepareUnavailable,
|
|
868
|
+
prepareRewardedBreak: prepareUnavailable,
|
|
786
869
|
rewardedBreak: () => Promise.resolve(false),
|
|
787
870
|
save: () => Promise.reject(sdkError2("unsupported")),
|
|
788
871
|
load: () => Promise.reject(sdkError2("unsupported")),
|
|
@@ -801,6 +884,7 @@
|
|
|
801
884
|
// src/multiplayer.ts
|
|
802
885
|
var WELCOME_TIMEOUT_MS = 1e4;
|
|
803
886
|
var MAX_RECONNECT_ATTEMPTS = 3;
|
|
887
|
+
var MAX_JOIN_DATA_BYTES = 1024;
|
|
804
888
|
var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
805
889
|
var CODE_LENGTH = 4;
|
|
806
890
|
function sdkError3(code) {
|
|
@@ -822,15 +906,42 @@
|
|
|
822
906
|
function generateCode() {
|
|
823
907
|
return randomChars(CODE_LENGTH);
|
|
824
908
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
909
|
+
function utf8ByteLength(value) {
|
|
910
|
+
let bytes = 0;
|
|
911
|
+
for (let i = 0; i < value.length; i++) {
|
|
912
|
+
const code = value.charCodeAt(i);
|
|
913
|
+
if (code < 128) bytes += 1;
|
|
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;
|
|
924
|
+
}
|
|
925
|
+
return bytes;
|
|
926
|
+
}
|
|
927
|
+
function serializeJoinData(data) {
|
|
928
|
+
if (data === void 0) return null;
|
|
929
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) throw sdkError3("rejected");
|
|
930
|
+
let serialized;
|
|
931
|
+
try {
|
|
932
|
+
serialized = JSON.stringify(data);
|
|
933
|
+
} catch (e) {
|
|
934
|
+
throw sdkError3("rejected");
|
|
935
|
+
}
|
|
936
|
+
if (!serialized || utf8ByteLength(serialized) > MAX_JOIN_DATA_BYTES) {
|
|
937
|
+
throw sdkError3("rejected");
|
|
938
|
+
}
|
|
939
|
+
return serialized;
|
|
829
940
|
}
|
|
830
941
|
function requestTicket(roomId) {
|
|
831
942
|
const request = BBArcade._request;
|
|
832
943
|
if (typeof request !== "function") return Promise.reject(sdkError3("unsupported"));
|
|
833
|
-
return request("mp_ticket", { roomId
|
|
944
|
+
return request("mp_ticket", { roomId }).then((grant) => {
|
|
834
945
|
const g = grant;
|
|
835
946
|
if (!g || typeof g.ticket !== "string" || typeof g.roomUrl !== "string") {
|
|
836
947
|
throw sdkError3("error");
|
|
@@ -839,10 +950,11 @@
|
|
|
839
950
|
});
|
|
840
951
|
}
|
|
841
952
|
var RoomConnection = class {
|
|
842
|
-
constructor(code, devGrant) {
|
|
953
|
+
constructor(code, devGrant, joinData) {
|
|
843
954
|
this.playerId = "";
|
|
844
955
|
this.players = [];
|
|
845
956
|
this.state = null;
|
|
957
|
+
this.connected = false;
|
|
846
958
|
this.ws = null;
|
|
847
959
|
// Keyed by event name; values are erased to (unknown) => void internally and
|
|
848
960
|
// retyped at the on()/emit() boundary, which keeps the public API strictly
|
|
@@ -857,12 +969,13 @@
|
|
|
857
969
|
this.reconnectAttempts = 0;
|
|
858
970
|
this.code = code;
|
|
859
971
|
this.devGrant = devGrant;
|
|
972
|
+
this.joinData = joinData;
|
|
860
973
|
}
|
|
861
974
|
/** Connect and resolve on the server's welcome (post-ticket-verification). */
|
|
862
975
|
connect(grant) {
|
|
863
976
|
return new Promise((resolve, reject) => {
|
|
864
977
|
let settled = false;
|
|
865
|
-
const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket);
|
|
978
|
+
const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket) + (this.joinData === null ? "" : "&join=" + encodeURIComponent(this.joinData));
|
|
866
979
|
let ws;
|
|
867
980
|
try {
|
|
868
981
|
ws = new WebSocket(url);
|
|
@@ -890,15 +1003,18 @@
|
|
|
890
1003
|
}
|
|
891
1004
|
if (!msg || typeof msg.t !== "string") return;
|
|
892
1005
|
if (msg.t === "welcome") {
|
|
1006
|
+
const reconnected = this.reconnectAttempts > 0;
|
|
893
1007
|
this.playerId = String((_a = msg.playerId) != null ? _a : "");
|
|
894
1008
|
this.players = Array.isArray(msg.players) ? msg.players : [];
|
|
895
1009
|
this.state = (_b = msg.state) != null ? _b : null;
|
|
1010
|
+
this.connected = true;
|
|
896
1011
|
this.reconnectAttempts = 0;
|
|
897
1012
|
if (!settled) {
|
|
898
1013
|
settled = true;
|
|
899
1014
|
clearTimeout(welcomeTimer);
|
|
900
1015
|
resolve(this);
|
|
901
1016
|
}
|
|
1017
|
+
if (reconnected) this.emit("connection", { connected: true, reconnecting: false });
|
|
902
1018
|
return;
|
|
903
1019
|
}
|
|
904
1020
|
if (msg.t === "snapshot") {
|
|
@@ -948,6 +1064,7 @@
|
|
|
948
1064
|
}
|
|
949
1065
|
};
|
|
950
1066
|
ws.onclose = () => {
|
|
1067
|
+
this.connected = false;
|
|
951
1068
|
if (!settled) {
|
|
952
1069
|
settled = true;
|
|
953
1070
|
clearTimeout(welcomeTimer);
|
|
@@ -955,6 +1072,7 @@
|
|
|
955
1072
|
return;
|
|
956
1073
|
}
|
|
957
1074
|
if (this.intentionalClose || this.ended || this.terminal) {
|
|
1075
|
+
this.emit("connection", { connected: false, reconnecting: false });
|
|
958
1076
|
this.emit("close", { reconnecting: false });
|
|
959
1077
|
return;
|
|
960
1078
|
}
|
|
@@ -972,6 +1090,7 @@
|
|
|
972
1090
|
return;
|
|
973
1091
|
}
|
|
974
1092
|
this.reconnectAttempts += 1;
|
|
1093
|
+
this.emit("connection", { connected: false, reconnecting: true });
|
|
975
1094
|
this.emit("close", { reconnecting: true });
|
|
976
1095
|
await new Promise((r) => setTimeout(r, 400 * this.reconnectAttempts));
|
|
977
1096
|
if (this.intentionalClose || this.ended || this.terminal) return;
|
|
@@ -985,11 +1104,16 @@
|
|
|
985
1104
|
}
|
|
986
1105
|
}
|
|
987
1106
|
send(input) {
|
|
988
|
-
|
|
1107
|
+
this.trySend(input);
|
|
1108
|
+
}
|
|
1109
|
+
trySend(input) {
|
|
1110
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
|
|
989
1111
|
this.seq += 1;
|
|
990
1112
|
try {
|
|
991
1113
|
this.ws.send(JSON.stringify({ t: "input", seq: this.seq, data: input }));
|
|
1114
|
+
return true;
|
|
992
1115
|
} catch (err) {
|
|
1116
|
+
return false;
|
|
993
1117
|
}
|
|
994
1118
|
}
|
|
995
1119
|
on(event, handler) {
|
|
@@ -1005,6 +1129,7 @@
|
|
|
1005
1129
|
leave() {
|
|
1006
1130
|
var _a;
|
|
1007
1131
|
this.intentionalClose = true;
|
|
1132
|
+
this.connected = false;
|
|
1008
1133
|
try {
|
|
1009
1134
|
(_a = this.ws) == null ? void 0 : _a.close(1e3);
|
|
1010
1135
|
} catch (err) {
|
|
@@ -1030,11 +1155,17 @@
|
|
|
1030
1155
|
return Promise.reject(sdkError3("unsupported"));
|
|
1031
1156
|
}
|
|
1032
1157
|
const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
|
|
1158
|
+
let joinData;
|
|
1159
|
+
try {
|
|
1160
|
+
joinData = serializeJoinData(opts.joinData);
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
return Promise.reject(err);
|
|
1163
|
+
}
|
|
1033
1164
|
if (opts.roomUrl && opts.ticket) {
|
|
1034
1165
|
const devGrant = { ticket: opts.ticket, roomUrl: opts.roomUrl, expiresAt: 0 };
|
|
1035
|
-
return new RoomConnection(code, devGrant).connect(devGrant);
|
|
1166
|
+
return new RoomConnection(code, devGrant, joinData).connect(devGrant);
|
|
1036
1167
|
}
|
|
1037
|
-
return requestTicket(code).then((grant) => new RoomConnection(code, null).connect(grant));
|
|
1168
|
+
return requestTicket(code).then((grant) => new RoomConnection(code, null, joinData).connect(grant));
|
|
1038
1169
|
}
|
|
1039
1170
|
var multiplayer = { joinRoom };
|
|
1040
1171
|
|