@bountyboard/arcade-sdk 1.0.1 → 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 +3 -1
- package/CHANGELOG.md +30 -0
- package/README.md +13 -3
- package/dist/global-sdk.d.ts +13 -0
- package/dist/global.js +58 -10
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/multiplayer.cjs +58 -10
- 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 +58 -10
- package/dist/multiplayer.js.map +1 -1
- package/dist/{types-BKChtV3H.d.cts → types-Cq8p7ZkC.d.cts} +16 -0
- package/dist/{types-BKChtV3H.d.ts → types-Cq8p7ZkC.d.ts} +16 -0
- package/package.json +5 -3
package/AGENTS.md
CHANGED
|
@@ -72,7 +72,9 @@ the safe/default experience the alphabetically-first name. Don't re-randomize cl
|
|
|
72
72
|
assert that `show()` is called directly by your UI handler and only final status `'viewed'`
|
|
73
73
|
grants the reward.
|
|
74
74
|
- Multiplayer: run the room server locally (`wrangler dev` with `DEV_ALLOW_UNSIGNED=1`) and pass
|
|
75
|
-
`{ roomUrl, ticket }` overrides to `joinRoom` for development.
|
|
75
|
+
`{ roomUrl, ticket }` overrides to `joinRoom` for development. Exercise a disconnect/reconnect,
|
|
76
|
+
assert `trySend()` returns false while offline, and verify the authoritative module validates the
|
|
77
|
+
optional `joinData` rather than treating avatar/loadout/mode selections as trusted state.
|
|
76
78
|
|
|
77
79
|
## Design intent (read docs/game-design-playbook.md before building a NEW game)
|
|
78
80
|
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @bountyboard/arcade-sdk
|
|
2
|
+
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 10a4997: Support production real-time game modules with bounded join data, observable reconnect state, and explicit `trySend` backpressure.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- f701c2c: Require Bounty Board-approved, host-session-bound tickets for multiplayer rooms and harden the authoritative room server's edge authorization.
|
|
12
|
+
- 732b929: Keep the Bounty Board host fullscreen overlay from covering provider iframe ad
|
|
13
|
+
controls on touch devices.
|
|
14
|
+
- 72daa42: Ship the package changelog and add reproducible release validation for npm,
|
|
15
|
+
browser-script, ESM, CommonJS, TypeScript, global, and multiplayer consumers.
|
|
16
|
+
|
|
17
|
+
## 1.0.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Added a two-stage rewarded-ad preparation flow that preserves the browser's
|
|
22
|
+
direct user activation requirement, with expanded integration guidance and
|
|
23
|
+
regression coverage.
|
|
24
|
+
|
|
25
|
+
## 1.0.0
|
|
26
|
+
|
|
27
|
+
### Major Changes
|
|
28
|
+
|
|
29
|
+
- Initial public release with leaderboard, cloud-save, rewarded-ad,
|
|
30
|
+
experimentation, player, sitelock, and multiplayer APIs.
|
package/README.md
CHANGED
|
@@ -95,21 +95,31 @@ not break, but new integrations should use the prepared flow because an asynchro
|
|
|
95
95
|
```ts
|
|
96
96
|
import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';
|
|
97
97
|
|
|
98
|
-
const room = await joinRoom({
|
|
98
|
+
const room = await joinRoom({
|
|
99
|
+
create: true, // or code: 'ABCD'
|
|
100
|
+
joinData: { avatar: 'golem', mode: 'ffa' }, // untrusted, validated by your module
|
|
101
|
+
});
|
|
99
102
|
room.on('snapshot', ({ state }) => render(state)); // server-authoritative state
|
|
100
103
|
room.on('end', ({ results }) => showResults(results));
|
|
101
|
-
room.
|
|
104
|
+
room.on('connection', status => showReconnecting(!status.connected));
|
|
105
|
+
if (!room.trySend({ move: dir })) showReconnecting(true); // inputs only
|
|
102
106
|
```
|
|
103
107
|
|
|
104
108
|
Rooms run on Bounty Board's authoritative room servers: clients send inputs, the server
|
|
105
109
|
validates and simulates, and each player receives only the state they're allowed to see
|
|
106
110
|
(your hiders stay hidden). Auth is a short-lived signed ticket the SDK obtains from the
|
|
107
111
|
host automatically; off Bounty Board `joinRoom` rejects with `code: 'unsupported'`.
|
|
112
|
+
`joinData` is limited to a 1 KiB JSON object, is never trusted or broadcast by the
|
|
113
|
+
platform, and must not contain secrets. The room runtime supports separate simulation
|
|
114
|
+
and snapshot rates (for example 60 Hz simulation / 30 Hz snapshots), a 15-second seat
|
|
115
|
+
grace for automatic reconnects, and bounded server frames.
|
|
108
116
|
|
|
109
117
|
Multiplayer is currently a curated integration: Bounty Board must add and deploy a
|
|
110
118
|
server-side game module for your slug before `joinRoom()` can accept players. The
|
|
111
119
|
initial reference module is `hide-and-seek`; arbitrary games do not become
|
|
112
|
-
authoritative multiplayer games merely by importing the client SDK.
|
|
120
|
+
authoritative multiplayer games merely by importing the client SDK. A production
|
|
121
|
+
module must validate join data and every input, clear held controls on disconnect,
|
|
122
|
+
serialize only per-viewer-safe state, and return final authoritative standings.
|
|
113
123
|
|
|
114
124
|
## Docs
|
|
115
125
|
|
package/dist/global-sdk.d.ts
CHANGED
|
@@ -220,6 +220,13 @@ interface BBArcadeMpJoinOptions {
|
|
|
220
220
|
code?: string;
|
|
221
221
|
/** Create a new room (a share code is generated for you). */
|
|
222
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>>;
|
|
223
230
|
/**
|
|
224
231
|
* Local development only: connect straight to a room server (e.g. wrangler
|
|
225
232
|
* dev with DEV_ALLOW_UNSIGNED=1) instead of asking the Bounty Board host for
|
|
@@ -240,6 +247,8 @@ interface BBArcadeMpRoomEvents {
|
|
|
240
247
|
/** The match finished; results were reported by the server, not by clients. */
|
|
241
248
|
end: { results: BBArcadeMpResult[] };
|
|
242
249
|
error: { code: string };
|
|
250
|
+
/** Connection state, including successful automatic reconnection. */
|
|
251
|
+
connection: { connected: boolean; reconnecting: boolean };
|
|
243
252
|
/** Connection dropped. reconnecting: true means the SDK is retrying. */
|
|
244
253
|
close: { reconnecting: boolean };
|
|
245
254
|
}
|
|
@@ -254,8 +263,12 @@ interface BBArcadeMpRoom {
|
|
|
254
263
|
players: BBArcadeMpPlayer[];
|
|
255
264
|
/** Latest authoritative state snapshot (as filtered for you). */
|
|
256
265
|
state: unknown;
|
|
266
|
+
/** Whether the room currently has an accepted WebSocket connection. */
|
|
267
|
+
connected: boolean;
|
|
257
268
|
/** Send an INPUT to the server (never authoritative state — the server simulates). */
|
|
258
269
|
send(input: unknown): void;
|
|
270
|
+
/** Like send(), but reports false when disconnected or the socket raced closed. */
|
|
271
|
+
trySend(input: unknown): boolean;
|
|
259
272
|
/** Subscribe to a room event; returns an unsubscribe function. */
|
|
260
273
|
on<K extends keyof BBArcadeMpRoomEvents>(
|
|
261
274
|
event: K,
|
package/dist/global.js
CHANGED
|
@@ -884,6 +884,7 @@
|
|
|
884
884
|
// src/multiplayer.ts
|
|
885
885
|
var WELCOME_TIMEOUT_MS = 1e4;
|
|
886
886
|
var MAX_RECONNECT_ATTEMPTS = 3;
|
|
887
|
+
var MAX_JOIN_DATA_BYTES = 1024;
|
|
887
888
|
var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
888
889
|
var CODE_LENGTH = 4;
|
|
889
890
|
function sdkError3(code) {
|
|
@@ -905,15 +906,42 @@
|
|
|
905
906
|
function generateCode() {
|
|
906
907
|
return randomChars(CODE_LENGTH);
|
|
907
908
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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;
|
|
912
940
|
}
|
|
913
941
|
function requestTicket(roomId) {
|
|
914
942
|
const request = BBArcade._request;
|
|
915
943
|
if (typeof request !== "function") return Promise.reject(sdkError3("unsupported"));
|
|
916
|
-
return request("mp_ticket", { roomId
|
|
944
|
+
return request("mp_ticket", { roomId }).then((grant) => {
|
|
917
945
|
const g = grant;
|
|
918
946
|
if (!g || typeof g.ticket !== "string" || typeof g.roomUrl !== "string") {
|
|
919
947
|
throw sdkError3("error");
|
|
@@ -922,10 +950,11 @@
|
|
|
922
950
|
});
|
|
923
951
|
}
|
|
924
952
|
var RoomConnection = class {
|
|
925
|
-
constructor(code, devGrant) {
|
|
953
|
+
constructor(code, devGrant, joinData) {
|
|
926
954
|
this.playerId = "";
|
|
927
955
|
this.players = [];
|
|
928
956
|
this.state = null;
|
|
957
|
+
this.connected = false;
|
|
929
958
|
this.ws = null;
|
|
930
959
|
// Keyed by event name; values are erased to (unknown) => void internally and
|
|
931
960
|
// retyped at the on()/emit() boundary, which keeps the public API strictly
|
|
@@ -940,12 +969,13 @@
|
|
|
940
969
|
this.reconnectAttempts = 0;
|
|
941
970
|
this.code = code;
|
|
942
971
|
this.devGrant = devGrant;
|
|
972
|
+
this.joinData = joinData;
|
|
943
973
|
}
|
|
944
974
|
/** Connect and resolve on the server's welcome (post-ticket-verification). */
|
|
945
975
|
connect(grant) {
|
|
946
976
|
return new Promise((resolve, reject) => {
|
|
947
977
|
let settled = false;
|
|
948
|
-
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));
|
|
949
979
|
let ws;
|
|
950
980
|
try {
|
|
951
981
|
ws = new WebSocket(url);
|
|
@@ -973,15 +1003,18 @@
|
|
|
973
1003
|
}
|
|
974
1004
|
if (!msg || typeof msg.t !== "string") return;
|
|
975
1005
|
if (msg.t === "welcome") {
|
|
1006
|
+
const reconnected = this.reconnectAttempts > 0;
|
|
976
1007
|
this.playerId = String((_a = msg.playerId) != null ? _a : "");
|
|
977
1008
|
this.players = Array.isArray(msg.players) ? msg.players : [];
|
|
978
1009
|
this.state = (_b = msg.state) != null ? _b : null;
|
|
1010
|
+
this.connected = true;
|
|
979
1011
|
this.reconnectAttempts = 0;
|
|
980
1012
|
if (!settled) {
|
|
981
1013
|
settled = true;
|
|
982
1014
|
clearTimeout(welcomeTimer);
|
|
983
1015
|
resolve(this);
|
|
984
1016
|
}
|
|
1017
|
+
if (reconnected) this.emit("connection", { connected: true, reconnecting: false });
|
|
985
1018
|
return;
|
|
986
1019
|
}
|
|
987
1020
|
if (msg.t === "snapshot") {
|
|
@@ -1031,6 +1064,7 @@
|
|
|
1031
1064
|
}
|
|
1032
1065
|
};
|
|
1033
1066
|
ws.onclose = () => {
|
|
1067
|
+
this.connected = false;
|
|
1034
1068
|
if (!settled) {
|
|
1035
1069
|
settled = true;
|
|
1036
1070
|
clearTimeout(welcomeTimer);
|
|
@@ -1038,6 +1072,7 @@
|
|
|
1038
1072
|
return;
|
|
1039
1073
|
}
|
|
1040
1074
|
if (this.intentionalClose || this.ended || this.terminal) {
|
|
1075
|
+
this.emit("connection", { connected: false, reconnecting: false });
|
|
1041
1076
|
this.emit("close", { reconnecting: false });
|
|
1042
1077
|
return;
|
|
1043
1078
|
}
|
|
@@ -1055,6 +1090,7 @@
|
|
|
1055
1090
|
return;
|
|
1056
1091
|
}
|
|
1057
1092
|
this.reconnectAttempts += 1;
|
|
1093
|
+
this.emit("connection", { connected: false, reconnecting: true });
|
|
1058
1094
|
this.emit("close", { reconnecting: true });
|
|
1059
1095
|
await new Promise((r) => setTimeout(r, 400 * this.reconnectAttempts));
|
|
1060
1096
|
if (this.intentionalClose || this.ended || this.terminal) return;
|
|
@@ -1068,11 +1104,16 @@
|
|
|
1068
1104
|
}
|
|
1069
1105
|
}
|
|
1070
1106
|
send(input) {
|
|
1071
|
-
|
|
1107
|
+
this.trySend(input);
|
|
1108
|
+
}
|
|
1109
|
+
trySend(input) {
|
|
1110
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
|
|
1072
1111
|
this.seq += 1;
|
|
1073
1112
|
try {
|
|
1074
1113
|
this.ws.send(JSON.stringify({ t: "input", seq: this.seq, data: input }));
|
|
1114
|
+
return true;
|
|
1075
1115
|
} catch (err) {
|
|
1116
|
+
return false;
|
|
1076
1117
|
}
|
|
1077
1118
|
}
|
|
1078
1119
|
on(event, handler) {
|
|
@@ -1088,6 +1129,7 @@
|
|
|
1088
1129
|
leave() {
|
|
1089
1130
|
var _a;
|
|
1090
1131
|
this.intentionalClose = true;
|
|
1132
|
+
this.connected = false;
|
|
1091
1133
|
try {
|
|
1092
1134
|
(_a = this.ws) == null ? void 0 : _a.close(1e3);
|
|
1093
1135
|
} catch (err) {
|
|
@@ -1113,11 +1155,17 @@
|
|
|
1113
1155
|
return Promise.reject(sdkError3("unsupported"));
|
|
1114
1156
|
}
|
|
1115
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
|
+
}
|
|
1116
1164
|
if (opts.roomUrl && opts.ticket) {
|
|
1117
1165
|
const devGrant = { ticket: opts.ticket, roomUrl: opts.roomUrl, expiresAt: 0 };
|
|
1118
|
-
return new RoomConnection(code, devGrant).connect(devGrant);
|
|
1166
|
+
return new RoomConnection(code, devGrant, joinData).connect(devGrant);
|
|
1119
1167
|
}
|
|
1120
|
-
return requestTicket(code).then((grant) => new RoomConnection(code, null).connect(grant));
|
|
1168
|
+
return requestTicket(code).then((grant) => new RoomConnection(code, null, joinData).connect(grant));
|
|
1121
1169
|
}
|
|
1122
1170
|
var multiplayer = { joinRoom };
|
|
1123
1171
|
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as BBArcadeSDK } from './types-
|
|
2
|
-
export { a as BBArcadeConfig, b as BBArcadeError, c as BBArcadeErrorCode, d as BBArcadeLockToHostOptions, e as BBArcadePlayer, f as BBArcadePrepareRewardedAdOptions, g as BBArcadePreparedRewardedAd, h as BBArcadeRewardedAdOptions, i as BBArcadeRewardedAdPreparation, j as BBArcadeRewardedAdPrepareFailure, k as BBArcadeRewardedAdResult, l as BBArcadeRewardedAdStatus, m as BBArcadeRewardedAdsConfig, n as BBArcadeRewardedBreakOptions } from './types-
|
|
1
|
+
import { B as BBArcadeSDK } from './types-Cq8p7ZkC.cjs';
|
|
2
|
+
export { a as BBArcadeConfig, b as BBArcadeError, c as BBArcadeErrorCode, d as BBArcadeLockToHostOptions, e as BBArcadePlayer, f as BBArcadePrepareRewardedAdOptions, g as BBArcadePreparedRewardedAd, h as BBArcadeRewardedAdOptions, i as BBArcadeRewardedAdPreparation, j as BBArcadeRewardedAdPrepareFailure, k as BBArcadeRewardedAdResult, l as BBArcadeRewardedAdStatus, m as BBArcadeRewardedAdsConfig, n as BBArcadeRewardedBreakOptions } from './types-Cq8p7ZkC.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Wire-protocol constants for the bb-arcade postMessage protocol. The protocol
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as BBArcadeSDK } from './types-
|
|
2
|
-
export { a as BBArcadeConfig, b as BBArcadeError, c as BBArcadeErrorCode, d as BBArcadeLockToHostOptions, e as BBArcadePlayer, f as BBArcadePrepareRewardedAdOptions, g as BBArcadePreparedRewardedAd, h as BBArcadeRewardedAdOptions, i as BBArcadeRewardedAdPreparation, j as BBArcadeRewardedAdPrepareFailure, k as BBArcadeRewardedAdResult, l as BBArcadeRewardedAdStatus, m as BBArcadeRewardedAdsConfig, n as BBArcadeRewardedBreakOptions } from './types-
|
|
1
|
+
import { B as BBArcadeSDK } from './types-Cq8p7ZkC.js';
|
|
2
|
+
export { a as BBArcadeConfig, b as BBArcadeError, c as BBArcadeErrorCode, d as BBArcadeLockToHostOptions, e as BBArcadePlayer, f as BBArcadePrepareRewardedAdOptions, g as BBArcadePreparedRewardedAd, h as BBArcadeRewardedAdOptions, i as BBArcadeRewardedAdPreparation, j as BBArcadeRewardedAdPrepareFailure, k as BBArcadeRewardedAdResult, l as BBArcadeRewardedAdStatus, m as BBArcadeRewardedAdsConfig, n as BBArcadeRewardedBreakOptions } from './types-Cq8p7ZkC.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Wire-protocol constants for the bb-arcade postMessage protocol. The protocol
|
package/dist/multiplayer.cjs
CHANGED
|
@@ -909,6 +909,7 @@ var BBArcade = typeof window === "undefined" ? createInertBBArcade() : createBBA
|
|
|
909
909
|
// src/multiplayer.ts
|
|
910
910
|
var WELCOME_TIMEOUT_MS = 1e4;
|
|
911
911
|
var MAX_RECONNECT_ATTEMPTS = 3;
|
|
912
|
+
var MAX_JOIN_DATA_BYTES = 1024;
|
|
912
913
|
var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
913
914
|
var CODE_LENGTH = 4;
|
|
914
915
|
function sdkError3(code) {
|
|
@@ -930,15 +931,42 @@ function randomChars(length) {
|
|
|
930
931
|
function generateCode() {
|
|
931
932
|
return randomChars(CODE_LENGTH);
|
|
932
933
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
934
|
+
function utf8ByteLength(value) {
|
|
935
|
+
let bytes = 0;
|
|
936
|
+
for (let i = 0; i < value.length; i++) {
|
|
937
|
+
const code = value.charCodeAt(i);
|
|
938
|
+
if (code < 128) bytes += 1;
|
|
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;
|
|
949
|
+
}
|
|
950
|
+
return bytes;
|
|
951
|
+
}
|
|
952
|
+
function serializeJoinData(data) {
|
|
953
|
+
if (data === void 0) return null;
|
|
954
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) throw sdkError3("rejected");
|
|
955
|
+
let serialized;
|
|
956
|
+
try {
|
|
957
|
+
serialized = JSON.stringify(data);
|
|
958
|
+
} catch (e) {
|
|
959
|
+
throw sdkError3("rejected");
|
|
960
|
+
}
|
|
961
|
+
if (!serialized || utf8ByteLength(serialized) > MAX_JOIN_DATA_BYTES) {
|
|
962
|
+
throw sdkError3("rejected");
|
|
963
|
+
}
|
|
964
|
+
return serialized;
|
|
937
965
|
}
|
|
938
966
|
function requestTicket(roomId) {
|
|
939
967
|
const request = BBArcade._request;
|
|
940
968
|
if (typeof request !== "function") return Promise.reject(sdkError3("unsupported"));
|
|
941
|
-
return request("mp_ticket", { roomId
|
|
969
|
+
return request("mp_ticket", { roomId }).then((grant) => {
|
|
942
970
|
const g = grant;
|
|
943
971
|
if (!g || typeof g.ticket !== "string" || typeof g.roomUrl !== "string") {
|
|
944
972
|
throw sdkError3("error");
|
|
@@ -947,10 +975,11 @@ function requestTicket(roomId) {
|
|
|
947
975
|
});
|
|
948
976
|
}
|
|
949
977
|
var RoomConnection = class {
|
|
950
|
-
constructor(code, devGrant) {
|
|
978
|
+
constructor(code, devGrant, joinData) {
|
|
951
979
|
this.playerId = "";
|
|
952
980
|
this.players = [];
|
|
953
981
|
this.state = null;
|
|
982
|
+
this.connected = false;
|
|
954
983
|
this.ws = null;
|
|
955
984
|
// Keyed by event name; values are erased to (unknown) => void internally and
|
|
956
985
|
// retyped at the on()/emit() boundary, which keeps the public API strictly
|
|
@@ -965,12 +994,13 @@ var RoomConnection = class {
|
|
|
965
994
|
this.reconnectAttempts = 0;
|
|
966
995
|
this.code = code;
|
|
967
996
|
this.devGrant = devGrant;
|
|
997
|
+
this.joinData = joinData;
|
|
968
998
|
}
|
|
969
999
|
/** Connect and resolve on the server's welcome (post-ticket-verification). */
|
|
970
1000
|
connect(grant) {
|
|
971
1001
|
return new Promise((resolve, reject) => {
|
|
972
1002
|
let settled = false;
|
|
973
|
-
const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket);
|
|
1003
|
+
const url = grant.roomUrl + (grant.roomUrl.indexOf("?") === -1 ? "?" : "&") + "ticket=" + encodeURIComponent(grant.ticket) + (this.joinData === null ? "" : "&join=" + encodeURIComponent(this.joinData));
|
|
974
1004
|
let ws;
|
|
975
1005
|
try {
|
|
976
1006
|
ws = new WebSocket(url);
|
|
@@ -998,15 +1028,18 @@ var RoomConnection = class {
|
|
|
998
1028
|
}
|
|
999
1029
|
if (!msg || typeof msg.t !== "string") return;
|
|
1000
1030
|
if (msg.t === "welcome") {
|
|
1031
|
+
const reconnected = this.reconnectAttempts > 0;
|
|
1001
1032
|
this.playerId = String((_a = msg.playerId) != null ? _a : "");
|
|
1002
1033
|
this.players = Array.isArray(msg.players) ? msg.players : [];
|
|
1003
1034
|
this.state = (_b = msg.state) != null ? _b : null;
|
|
1035
|
+
this.connected = true;
|
|
1004
1036
|
this.reconnectAttempts = 0;
|
|
1005
1037
|
if (!settled) {
|
|
1006
1038
|
settled = true;
|
|
1007
1039
|
clearTimeout(welcomeTimer);
|
|
1008
1040
|
resolve(this);
|
|
1009
1041
|
}
|
|
1042
|
+
if (reconnected) this.emit("connection", { connected: true, reconnecting: false });
|
|
1010
1043
|
return;
|
|
1011
1044
|
}
|
|
1012
1045
|
if (msg.t === "snapshot") {
|
|
@@ -1056,6 +1089,7 @@ var RoomConnection = class {
|
|
|
1056
1089
|
}
|
|
1057
1090
|
};
|
|
1058
1091
|
ws.onclose = () => {
|
|
1092
|
+
this.connected = false;
|
|
1059
1093
|
if (!settled) {
|
|
1060
1094
|
settled = true;
|
|
1061
1095
|
clearTimeout(welcomeTimer);
|
|
@@ -1063,6 +1097,7 @@ var RoomConnection = class {
|
|
|
1063
1097
|
return;
|
|
1064
1098
|
}
|
|
1065
1099
|
if (this.intentionalClose || this.ended || this.terminal) {
|
|
1100
|
+
this.emit("connection", { connected: false, reconnecting: false });
|
|
1066
1101
|
this.emit("close", { reconnecting: false });
|
|
1067
1102
|
return;
|
|
1068
1103
|
}
|
|
@@ -1080,6 +1115,7 @@ var RoomConnection = class {
|
|
|
1080
1115
|
return;
|
|
1081
1116
|
}
|
|
1082
1117
|
this.reconnectAttempts += 1;
|
|
1118
|
+
this.emit("connection", { connected: false, reconnecting: true });
|
|
1083
1119
|
this.emit("close", { reconnecting: true });
|
|
1084
1120
|
await new Promise((r) => setTimeout(r, 400 * this.reconnectAttempts));
|
|
1085
1121
|
if (this.intentionalClose || this.ended || this.terminal) return;
|
|
@@ -1093,11 +1129,16 @@ var RoomConnection = class {
|
|
|
1093
1129
|
}
|
|
1094
1130
|
}
|
|
1095
1131
|
send(input) {
|
|
1096
|
-
|
|
1132
|
+
this.trySend(input);
|
|
1133
|
+
}
|
|
1134
|
+
trySend(input) {
|
|
1135
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
|
|
1097
1136
|
this.seq += 1;
|
|
1098
1137
|
try {
|
|
1099
1138
|
this.ws.send(JSON.stringify({ t: "input", seq: this.seq, data: input }));
|
|
1139
|
+
return true;
|
|
1100
1140
|
} catch (err) {
|
|
1141
|
+
return false;
|
|
1101
1142
|
}
|
|
1102
1143
|
}
|
|
1103
1144
|
on(event, handler) {
|
|
@@ -1113,6 +1154,7 @@ var RoomConnection = class {
|
|
|
1113
1154
|
leave() {
|
|
1114
1155
|
var _a;
|
|
1115
1156
|
this.intentionalClose = true;
|
|
1157
|
+
this.connected = false;
|
|
1116
1158
|
try {
|
|
1117
1159
|
(_a = this.ws) == null ? void 0 : _a.close(1e3);
|
|
1118
1160
|
} catch (err) {
|
|
@@ -1138,11 +1180,17 @@ function joinRoom(options) {
|
|
|
1138
1180
|
return Promise.reject(sdkError3("unsupported"));
|
|
1139
1181
|
}
|
|
1140
1182
|
const code = opts.create || !opts.code ? generateCode() : opts.code.toUpperCase();
|
|
1183
|
+
let joinData;
|
|
1184
|
+
try {
|
|
1185
|
+
joinData = serializeJoinData(opts.joinData);
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
return Promise.reject(err);
|
|
1188
|
+
}
|
|
1141
1189
|
if (opts.roomUrl && opts.ticket) {
|
|
1142
1190
|
const devGrant = { ticket: opts.ticket, roomUrl: opts.roomUrl, expiresAt: 0 };
|
|
1143
|
-
return new RoomConnection(code, devGrant).connect(devGrant);
|
|
1191
|
+
return new RoomConnection(code, devGrant, joinData).connect(devGrant);
|
|
1144
1192
|
}
|
|
1145
|
-
return requestTicket(code).then((grant) => new RoomConnection(code, null).connect(grant));
|
|
1193
|
+
return requestTicket(code).then((grant) => new RoomConnection(code, null, joinData).connect(grant));
|
|
1146
1194
|
}
|
|
1147
1195
|
var multiplayer = { joinRoom };
|
|
1148
1196
|
// Annotate the CommonJS export names for ESM import in node:
|