@hyve-sdk/js 2.12.0 → 2.13.0-canary.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/README.md +16 -2
- package/dist/index.d.mts +140 -5
- package/dist/index.d.ts +140 -5
- package/dist/index.js +205 -10
- package/dist/index.mjs +205 -10
- package/dist/react.d.mts +20 -1
- package/dist/react.d.ts +20 -1
- package/dist/react.js +202 -10
- package/dist/react.mjs +202 -10
- package/package.json +16 -13
package/dist/react.d.mts
CHANGED
|
@@ -138,6 +138,10 @@ interface GetGameDataLeaderboardParams {
|
|
|
138
138
|
*
|
|
139
139
|
* @packageDocumentation
|
|
140
140
|
*/
|
|
141
|
+
/**
|
|
142
|
+
* Ad types the SDK's `show()` accepts. Limited to formats with a working
|
|
143
|
+
* native path; `'preroll'` maps to a native interstitial (Decision 2).
|
|
144
|
+
*/
|
|
141
145
|
type AdType = 'rewarded' | 'interstitial' | 'preroll';
|
|
142
146
|
interface AdResult {
|
|
143
147
|
success: boolean;
|
|
@@ -149,6 +153,19 @@ interface AdResult {
|
|
|
149
153
|
interface AdConfig {
|
|
150
154
|
sound?: 'on' | 'off';
|
|
151
155
|
debug?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Route ad requests through the native AdMob bridge when running inside the
|
|
158
|
+
* Hyve mobile shell (`window.ReactNativeWebView` present).
|
|
159
|
+
*
|
|
160
|
+
* Phase 0 rollout flag: defaults to `false`, where ads always use the
|
|
161
|
+
* existing H5 / Playgama web path. The native path is wired up behind this
|
|
162
|
+
* flag in a later phase — until then this is an inert toggle that exists so
|
|
163
|
+
* later releases can flip behavior without an API change. See
|
|
164
|
+
* `docs/admob-migration.md`.
|
|
165
|
+
*
|
|
166
|
+
* @default false
|
|
167
|
+
*/
|
|
168
|
+
useNativeAds?: boolean;
|
|
152
169
|
onBeforeAd?: (type: AdType) => void;
|
|
153
170
|
onAfterAd?: (type: AdType) => void;
|
|
154
171
|
onRewardEarned?: () => void;
|
|
@@ -480,9 +497,11 @@ declare class HyveClient {
|
|
|
480
497
|
/**
|
|
481
498
|
* Show an ad
|
|
482
499
|
* @param type Type of ad to show ('rewarded', 'interstitial', or 'preroll')
|
|
500
|
+
* @param placement Optional placement key, forwarded to the native AdMob path
|
|
501
|
+
* to resolve a per-placement unit ID. Ignored on the web (H5/Playgama) path.
|
|
483
502
|
* @returns Promise resolving to ad result
|
|
484
503
|
*/
|
|
485
|
-
showAd(type: AdType): Promise<AdResult>;
|
|
504
|
+
showAd(type: AdType, placement?: string): Promise<AdResult>;
|
|
486
505
|
/**
|
|
487
506
|
* Required lifecycle telemetry — Gameplay start.
|
|
488
507
|
* Also notifies CrazyGames that gameplay has started.
|
package/dist/react.d.ts
CHANGED
|
@@ -138,6 +138,10 @@ interface GetGameDataLeaderboardParams {
|
|
|
138
138
|
*
|
|
139
139
|
* @packageDocumentation
|
|
140
140
|
*/
|
|
141
|
+
/**
|
|
142
|
+
* Ad types the SDK's `show()` accepts. Limited to formats with a working
|
|
143
|
+
* native path; `'preroll'` maps to a native interstitial (Decision 2).
|
|
144
|
+
*/
|
|
141
145
|
type AdType = 'rewarded' | 'interstitial' | 'preroll';
|
|
142
146
|
interface AdResult {
|
|
143
147
|
success: boolean;
|
|
@@ -149,6 +153,19 @@ interface AdResult {
|
|
|
149
153
|
interface AdConfig {
|
|
150
154
|
sound?: 'on' | 'off';
|
|
151
155
|
debug?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Route ad requests through the native AdMob bridge when running inside the
|
|
158
|
+
* Hyve mobile shell (`window.ReactNativeWebView` present).
|
|
159
|
+
*
|
|
160
|
+
* Phase 0 rollout flag: defaults to `false`, where ads always use the
|
|
161
|
+
* existing H5 / Playgama web path. The native path is wired up behind this
|
|
162
|
+
* flag in a later phase — until then this is an inert toggle that exists so
|
|
163
|
+
* later releases can flip behavior without an API change. See
|
|
164
|
+
* `docs/admob-migration.md`.
|
|
165
|
+
*
|
|
166
|
+
* @default false
|
|
167
|
+
*/
|
|
168
|
+
useNativeAds?: boolean;
|
|
152
169
|
onBeforeAd?: (type: AdType) => void;
|
|
153
170
|
onAfterAd?: (type: AdType) => void;
|
|
154
171
|
onRewardEarned?: () => void;
|
|
@@ -480,9 +497,11 @@ declare class HyveClient {
|
|
|
480
497
|
/**
|
|
481
498
|
* Show an ad
|
|
482
499
|
* @param type Type of ad to show ('rewarded', 'interstitial', or 'preroll')
|
|
500
|
+
* @param placement Optional placement key, forwarded to the native AdMob path
|
|
501
|
+
* to resolve a per-placement unit ID. Ignored on the web (H5/Playgama) path.
|
|
483
502
|
* @returns Promise resolving to ad result
|
|
484
503
|
*/
|
|
485
|
-
showAd(type: AdType): Promise<AdResult>;
|
|
504
|
+
showAd(type: AdType, placement?: string): Promise<AdResult>;
|
|
486
505
|
/**
|
|
487
506
|
* Required lifecycle telemetry — Gameplay start.
|
|
488
507
|
* Also notifies CrazyGames that gameplay has started.
|
package/dist/react.js
CHANGED
|
@@ -170,6 +170,12 @@ function parseUrlParams(searchParams) {
|
|
|
170
170
|
var NativeBridge = class {
|
|
171
171
|
static handlers = /* @__PURE__ */ new Map();
|
|
172
172
|
static isInitialized = false;
|
|
173
|
+
/**
|
|
174
|
+
* Pending `sendAdRequest()` promises, keyed by the ad type native echoes
|
|
175
|
+
* back in `AD_RESULT.type` ("rewarded" | "interstitial"). One in-flight
|
|
176
|
+
* request per type — ads are shown sequentially.
|
|
177
|
+
*/
|
|
178
|
+
static adResultResolvers = /* @__PURE__ */ new Map();
|
|
173
179
|
/**
|
|
174
180
|
* Checks if the app is running inside a React Native WebView
|
|
175
181
|
*/
|
|
@@ -222,16 +228,20 @@ var NativeBridge = class {
|
|
|
222
228
|
"PUSH_PERMISSION_DENIED" /* PUSH_PERMISSION_DENIED */,
|
|
223
229
|
"PRODUCTS_RESULT" /* PRODUCTS_RESULT */,
|
|
224
230
|
"PURCHASE_COMPLETE" /* PURCHASE_COMPLETE */,
|
|
225
|
-
"PURCHASE_ERROR" /* PURCHASE_ERROR
|
|
231
|
+
"PURCHASE_ERROR" /* PURCHASE_ERROR */,
|
|
232
|
+
"AD_RESULT" /* AD_RESULT */
|
|
226
233
|
];
|
|
227
234
|
if (!nativeResponseTypes.includes(data.type)) {
|
|
228
235
|
return;
|
|
229
236
|
}
|
|
237
|
+
if (data.type === "AD_RESULT" /* AD_RESULT */) {
|
|
238
|
+
this.resolveAdResult(data.payload);
|
|
239
|
+
}
|
|
230
240
|
const handler = this.handlers.get(data.type);
|
|
231
241
|
if (handler) {
|
|
232
242
|
logger.debug(`[NativeBridge] Handling message: ${data.type}`);
|
|
233
243
|
handler(data.payload);
|
|
234
|
-
} else {
|
|
244
|
+
} else if (data.type !== "AD_RESULT" /* AD_RESULT */) {
|
|
235
245
|
logger.warn(`[NativeBridge] No handler registered for: ${data.type}`);
|
|
236
246
|
}
|
|
237
247
|
} catch (error) {
|
|
@@ -357,6 +367,81 @@ var NativeBridge = class {
|
|
|
357
367
|
static purchase(productId, userId) {
|
|
358
368
|
this.send("PURCHASE" /* PURCHASE */, { productId, userId });
|
|
359
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Sends a native ad request and resolves with the `AD_RESULT` native sends
|
|
372
|
+
* back. Never rejects — a missing native context, timeout, or superseded
|
|
373
|
+
* request resolves to `{ success: false }` with an error code so callers can
|
|
374
|
+
* decide whether to fall back to the web ad path.
|
|
375
|
+
*
|
|
376
|
+
* @param messageType - `SHOW_REWARDED_AD` or `SHOW_INTERSTITIAL_AD`
|
|
377
|
+
* @param payload - `{ gameId, format, placement? }`
|
|
378
|
+
* @param timeoutMs - How long to wait for `AD_RESULT` before giving up
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* const result = await NativeBridge.sendAdRequest(
|
|
382
|
+
* NativeMessageType.SHOW_REWARDED_AD,
|
|
383
|
+
* { gameId: "123", format: "rewarded", placement: "level_end" },
|
|
384
|
+
* );
|
|
385
|
+
* if (result.success) grantReward();
|
|
386
|
+
*/
|
|
387
|
+
static sendAdRequest(messageType, payload, timeoutMs = 12e4) {
|
|
388
|
+
const expectedType = messageType === "SHOW_REWARDED_AD" /* SHOW_REWARDED_AD */ ? "rewarded" : "interstitial";
|
|
389
|
+
return new Promise((resolve) => {
|
|
390
|
+
if (!this.isNativeContext()) {
|
|
391
|
+
resolve({ type: expectedType, success: false, error: "no_native_context" });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
this.initialize();
|
|
395
|
+
const existing = this.adResultResolvers.get(expectedType);
|
|
396
|
+
if (existing) {
|
|
397
|
+
logger.warn(`[NativeBridge] Superseding in-flight ${expectedType} ad request`);
|
|
398
|
+
existing({ type: expectedType, success: false, error: "superseded" });
|
|
399
|
+
}
|
|
400
|
+
let settled = false;
|
|
401
|
+
const timer = setTimeout(() => {
|
|
402
|
+
if (settled) return;
|
|
403
|
+
settled = true;
|
|
404
|
+
this.adResultResolvers.delete(expectedType);
|
|
405
|
+
logger.warn(`[NativeBridge] Ad request timed out: ${expectedType}`);
|
|
406
|
+
resolve({ type: expectedType, success: false, error: "timeout" });
|
|
407
|
+
}, timeoutMs);
|
|
408
|
+
this.adResultResolvers.set(expectedType, (result) => {
|
|
409
|
+
if (settled) return;
|
|
410
|
+
settled = true;
|
|
411
|
+
clearTimeout(timer);
|
|
412
|
+
this.adResultResolvers.delete(expectedType);
|
|
413
|
+
resolve(result);
|
|
414
|
+
});
|
|
415
|
+
this.send(messageType, payload);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Registers a handler invoked for every `AD_RESULT` message from native,
|
|
420
|
+
* in addition to resolving any pending {@link sendAdRequest} promise.
|
|
421
|
+
* Useful for diagnostics or availability tracking.
|
|
422
|
+
*
|
|
423
|
+
* @param handler - Called with the raw `AD_RESULT` payload
|
|
424
|
+
*/
|
|
425
|
+
static onAdResult(handler) {
|
|
426
|
+
this.on("AD_RESULT" /* AD_RESULT */, handler);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Resolves the pending {@link sendAdRequest} promise that matches an
|
|
430
|
+
* incoming `AD_RESULT` payload. No-op when nothing is waiting (e.g. a late
|
|
431
|
+
* result after a timeout).
|
|
432
|
+
*/
|
|
433
|
+
static resolveAdResult(payload) {
|
|
434
|
+
if (!payload?.type) {
|
|
435
|
+
logger.warn("[NativeBridge] AD_RESULT received without a type");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const resolver = this.adResultResolvers.get(payload.type);
|
|
439
|
+
if (resolver) {
|
|
440
|
+
resolver(payload);
|
|
441
|
+
} else {
|
|
442
|
+
logger.debug(`[NativeBridge] No pending ad request for: ${payload.type}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
360
445
|
};
|
|
361
446
|
|
|
362
447
|
// src/utils/jwt.ts
|
|
@@ -713,10 +798,12 @@ function getAttributionData() {
|
|
|
713
798
|
}
|
|
714
799
|
|
|
715
800
|
// src/services/ads.ts
|
|
801
|
+
var FALLBACK_ERROR_CODES = ["not_configured", "config_fetch_failed"];
|
|
716
802
|
var AdsService = class {
|
|
717
803
|
config = {
|
|
718
804
|
sound: "on",
|
|
719
805
|
debug: false,
|
|
806
|
+
useNativeAds: false,
|
|
720
807
|
onBeforeAd: () => {
|
|
721
808
|
},
|
|
722
809
|
onAfterAd: () => {
|
|
@@ -727,6 +814,16 @@ var AdsService = class {
|
|
|
727
814
|
// Cached init promise — ensures adConfig() is only called once
|
|
728
815
|
initPromise = null;
|
|
729
816
|
ready = false;
|
|
817
|
+
// Game this SDK instance serves — sent to native so it can resolve unit IDs.
|
|
818
|
+
// The SDK never stores or resolves unit IDs itself (Decision 7).
|
|
819
|
+
gameId = null;
|
|
820
|
+
/**
|
|
821
|
+
* Set the game ID used in native ad requests. Called by the client once the
|
|
822
|
+
* game ID is known (from URL params or JWT). No-op for the web ad path.
|
|
823
|
+
*/
|
|
824
|
+
setGameId(gameId) {
|
|
825
|
+
this.gameId = gameId;
|
|
826
|
+
}
|
|
730
827
|
/**
|
|
731
828
|
* Optionally configure the ads service.
|
|
732
829
|
* Not required — ads work without calling this.
|
|
@@ -740,7 +837,10 @@ var AdsService = class {
|
|
|
740
837
|
onRewardEarned: config.onRewardEarned ?? this.config.onRewardEarned
|
|
741
838
|
};
|
|
742
839
|
if (this.config.debug) {
|
|
743
|
-
logger.debug("[AdsService] Configuration updated:", {
|
|
840
|
+
logger.debug("[AdsService] Configuration updated:", {
|
|
841
|
+
sound: this.config.sound,
|
|
842
|
+
useNativeAds: this.config.useNativeAds
|
|
843
|
+
});
|
|
744
844
|
}
|
|
745
845
|
}
|
|
746
846
|
/**
|
|
@@ -777,10 +877,93 @@ var AdsService = class {
|
|
|
777
877
|
}
|
|
778
878
|
/**
|
|
779
879
|
* Show an ad. Auto-initializes on the first call.
|
|
780
|
-
*
|
|
880
|
+
*
|
|
881
|
+
* Inside the Hyve mobile shell (`window.ReactNativeWebView` present) with
|
|
882
|
+
* `useNativeAds` enabled, the request is routed to native AdMob via the
|
|
883
|
+
* bridge. Otherwise — or when native reports the slot is `not_configured` /
|
|
884
|
+
* `config_fetch_failed` — it falls back to the Google H5 web path.
|
|
885
|
+
*
|
|
886
|
+
* Returns `success: false` if ads are disabled or unavailable.
|
|
887
|
+
*
|
|
888
|
+
* @param type - Ad type to show
|
|
889
|
+
* @param placement - Optional placement key, forwarded to native; ignored on
|
|
890
|
+
* the web path (H5 has no placement concept).
|
|
781
891
|
*/
|
|
782
|
-
async show(type) {
|
|
892
|
+
async show(type, placement) {
|
|
783
893
|
const requestedAt = Date.now();
|
|
894
|
+
if (this.isNativeAdsContext()) {
|
|
895
|
+
const nativeResult = await this.showNative(type, placement, requestedAt);
|
|
896
|
+
if (nativeResult) return nativeResult;
|
|
897
|
+
}
|
|
898
|
+
return this.showWeb(type, requestedAt, true);
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* True when running inside the Hyve mobile shell with native ads enabled.
|
|
902
|
+
*/
|
|
903
|
+
isNativeAdsContext() {
|
|
904
|
+
return NativeBridge.isNativeContext() && this.config.useNativeAds === true;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Route an ad request to native AdMob via the bridge (Decision 7 — native
|
|
908
|
+
* owns unit-ID resolution; the SDK only sends `{ gameId, format, placement }`).
|
|
909
|
+
*
|
|
910
|
+
* Returns the resolved `AdResult`, or `null` to indicate the caller should
|
|
911
|
+
* fall back to the web ad path for this single call.
|
|
912
|
+
*/
|
|
913
|
+
async showNative(type, placement, requestedAt) {
|
|
914
|
+
if (!this.gameId) {
|
|
915
|
+
if (this.config.debug) {
|
|
916
|
+
logger.debug("[AdsService] Native ads enabled but no gameId set \u2014 using web path");
|
|
917
|
+
}
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
const format = type === "rewarded" ? "rewarded" : "interstitial";
|
|
921
|
+
const messageType = type === "rewarded" ? "SHOW_REWARDED_AD" /* SHOW_REWARDED_AD */ : "SHOW_INTERSTITIAL_AD" /* SHOW_INTERSTITIAL_AD */;
|
|
922
|
+
if (this.config.debug) {
|
|
923
|
+
logger.debug(`[AdsService] Requesting native ${type} ad (format: ${format})`, {
|
|
924
|
+
gameId: this.gameId,
|
|
925
|
+
placement
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
this.config.onBeforeAd(type);
|
|
929
|
+
const result = await NativeBridge.sendAdRequest(messageType, {
|
|
930
|
+
gameId: this.gameId,
|
|
931
|
+
format,
|
|
932
|
+
placement
|
|
933
|
+
});
|
|
934
|
+
const completedAt = Date.now();
|
|
935
|
+
if (result.error && FALLBACK_ERROR_CODES.includes(result.error)) {
|
|
936
|
+
if (this.config.debug) {
|
|
937
|
+
logger.debug(`[AdsService] Native ad ${result.error} \u2014 falling back to web path`);
|
|
938
|
+
}
|
|
939
|
+
return this.showWeb(type, requestedAt, false);
|
|
940
|
+
}
|
|
941
|
+
this.config.onAfterAd(type);
|
|
942
|
+
if (type === "rewarded" && result.success) {
|
|
943
|
+
this.config.onRewardEarned();
|
|
944
|
+
}
|
|
945
|
+
if (this.config.debug) {
|
|
946
|
+
logger.debug("[AdsService] Native ad result:", {
|
|
947
|
+
type,
|
|
948
|
+
success: result.success,
|
|
949
|
+
error: result.error
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
return {
|
|
953
|
+
success: result.success,
|
|
954
|
+
type,
|
|
955
|
+
error: result.error ? new Error(result.error) : void 0,
|
|
956
|
+
requestedAt,
|
|
957
|
+
completedAt
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Show an ad via the Google H5 web path. Auto-initializes on first call.
|
|
962
|
+
*
|
|
963
|
+
* @param fireBeforeAd - When false, `onBeforeAd` is not fired (the native
|
|
964
|
+
* fallback path already fired it before delegating here).
|
|
965
|
+
*/
|
|
966
|
+
async showWeb(type, requestedAt, fireBeforeAd) {
|
|
784
967
|
const ready = await this.initialize();
|
|
785
968
|
if (!ready || !window.adBreak) {
|
|
786
969
|
return {
|
|
@@ -791,12 +974,15 @@ var AdsService = class {
|
|
|
791
974
|
completedAt: Date.now()
|
|
792
975
|
};
|
|
793
976
|
}
|
|
794
|
-
return this.showAdBreak(type);
|
|
977
|
+
return this.showAdBreak(type, fireBeforeAd);
|
|
795
978
|
}
|
|
796
979
|
/**
|
|
797
980
|
* Show an ad break via the Google H5 API.
|
|
981
|
+
*
|
|
982
|
+
* @param fireBeforeAd - When false, skips `onBeforeAd` (already fired by the
|
|
983
|
+
* native fallback path that delegated here).
|
|
798
984
|
*/
|
|
799
|
-
async showAdBreak(type) {
|
|
985
|
+
async showAdBreak(type, fireBeforeAd = true) {
|
|
800
986
|
const requestedAt = Date.now();
|
|
801
987
|
return new Promise((resolve) => {
|
|
802
988
|
const googleType = type === "rewarded" ? "reward" : type === "preroll" ? "start" : "next";
|
|
@@ -804,7 +990,9 @@ var AdsService = class {
|
|
|
804
990
|
if (this.config.debug) {
|
|
805
991
|
logger.debug(`[AdsService] Showing ${type} ad`);
|
|
806
992
|
}
|
|
807
|
-
|
|
993
|
+
if (fireBeforeAd) {
|
|
994
|
+
this.config.onBeforeAd(type);
|
|
995
|
+
}
|
|
808
996
|
const adBreakConfig = {
|
|
809
997
|
type: googleType,
|
|
810
998
|
name: adName,
|
|
@@ -1972,6 +2160,7 @@ var HyveClient = class {
|
|
|
1972
2160
|
this.gameId = params.gameId;
|
|
1973
2161
|
logger.info("Game ID extracted from game-id parameter:", this.gameId);
|
|
1974
2162
|
}
|
|
2163
|
+
this.adsService.setGameId(this.gameId);
|
|
1975
2164
|
if (this.jwtToken) {
|
|
1976
2165
|
logger.info("Authentication successful via JWT");
|
|
1977
2166
|
} else {
|
|
@@ -2285,6 +2474,7 @@ var HyveClient = class {
|
|
|
2285
2474
|
this.userId = null;
|
|
2286
2475
|
this.jwtToken = null;
|
|
2287
2476
|
this.gameId = null;
|
|
2477
|
+
this.adsService.setGameId(null);
|
|
2288
2478
|
logger.info("User logged out");
|
|
2289
2479
|
}
|
|
2290
2480
|
/**
|
|
@@ -2459,9 +2649,11 @@ var HyveClient = class {
|
|
|
2459
2649
|
/**
|
|
2460
2650
|
* Show an ad
|
|
2461
2651
|
* @param type Type of ad to show ('rewarded', 'interstitial', or 'preroll')
|
|
2652
|
+
* @param placement Optional placement key, forwarded to the native AdMob path
|
|
2653
|
+
* to resolve a per-placement unit ID. Ignored on the web (H5/Playgama) path.
|
|
2462
2654
|
* @returns Promise resolving to ad result
|
|
2463
2655
|
*/
|
|
2464
|
-
async showAd(type) {
|
|
2656
|
+
async showAd(type, placement) {
|
|
2465
2657
|
if (this.crazyGamesService) {
|
|
2466
2658
|
if (this.crazyGamesInitPromise) {
|
|
2467
2659
|
await this.crazyGamesInitPromise;
|
|
@@ -2500,7 +2692,7 @@ var HyveClient = class {
|
|
|
2500
2692
|
});
|
|
2501
2693
|
}
|
|
2502
2694
|
}
|
|
2503
|
-
return this.adsService.show(type);
|
|
2695
|
+
return this.adsService.show(type, placement);
|
|
2504
2696
|
}
|
|
2505
2697
|
/**
|
|
2506
2698
|
* Required lifecycle telemetry — Gameplay start.
|