@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.
@@ -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
- * 'before_reward_error' | 'ad_break_error'.
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
- /** Requested ad size. */
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. Pause your game, then call showAd() to run it.
133
- * When omitted, the ad shows immediately.
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
- * Show a rewarded ad via Google's H5 placement API. Resolves with the full
287
- * result object; only status 'viewed' should grant the reward. Prefer
288
- * rewardedBreak() unless you need the low-level callbacks.
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
- * Developer-standard rewarded break. Show your own "Watch ad for reward" UI
295
- * first; resolves true only when the ad was actually viewed. Accepts an
296
- * options bag or a bare onStart callback.
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 = false;
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
- rewardedBreakActive = true;
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 (startCalled) return;
676
- startCalled = true;
677
- safeCall(rawOptions.onStart);
678
- showAd();
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 = false;
755
+ if (rewardedBreakActive === activeBreak) rewardedBreakActive = null;
684
756
  return Boolean(result && result.status === "viewed");
685
757
  },
686
758
  function() {
687
- rewardedBreakActive = false;
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
- var guestKey = null;
826
- function getGuestKey() {
827
- if (!guestKey) guestKey = randomChars(16);
828
- return guestKey;
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, guestKey: getGuestKey() }).then((grant) => {
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
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
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