@indiegems/gem-web-sdk 20.0.0 → 21.0.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/brand/ticket-hero-128.png +0 -0
  3. package/brand/ticket-hero-256.png +0 -0
  4. package/brand/ticket-hero-384.png +0 -0
  5. package/brand/ticket-hero-512.png +0 -0
  6. package/brand/ticket-hero-768.png +0 -0
  7. package/brand/ticket-icon-20-1.5x.png +0 -0
  8. package/brand/ticket-icon-20-1x.png +0 -0
  9. package/brand/ticket-icon-20-2x.png +0 -0
  10. package/brand/ticket-icon-20-3x.png +0 -0
  11. package/brand/ticket-icon-32-1.5x.png +0 -0
  12. package/brand/ticket-icon-32-1x.png +0 -0
  13. package/brand/ticket-icon-32-2x.png +0 -0
  14. package/brand/ticket-icon-32-3x.png +0 -0
  15. package/brand/ticket-icon-ingame-20-1.5x.png +0 -0
  16. package/brand/ticket-icon-ingame-20-1x.png +0 -0
  17. package/brand/ticket-icon-ingame-20-2x.png +0 -0
  18. package/brand/ticket-icon-ingame-20-3x.png +0 -0
  19. package/brand/ticket-icon-ingame-32-1.5x.png +0 -0
  20. package/brand/ticket-icon-ingame-32-1x.png +0 -0
  21. package/brand/ticket-icon-ingame-32-2x.png +0 -0
  22. package/brand/ticket-icon-ingame-32-3x.png +0 -0
  23. package/brand/ticket-pile-1-128.png +0 -0
  24. package/brand/ticket-pile-1-256.png +0 -0
  25. package/brand/ticket-pile-1-384.png +0 -0
  26. package/brand/ticket-pile-1-512.png +0 -0
  27. package/brand/ticket-pile-1-768.png +0 -0
  28. package/brand/ticket-pile-2-128.png +0 -0
  29. package/brand/ticket-pile-2-256.png +0 -0
  30. package/brand/ticket-pile-2-384.png +0 -0
  31. package/brand/ticket-pile-2-512.png +0 -0
  32. package/brand/ticket-pile-2-768.png +0 -0
  33. package/dist/function.d.ts +1 -1
  34. package/dist/game.d.ts +52 -4
  35. package/dist/gem-web-sdk.bridge.js +2 -2
  36. package/dist/gem-web-sdk.conformance.js +3 -3
  37. package/dist/gem-web-sdk.esm.js +3 -3
  38. package/dist/gem-web-sdk.function.js +3 -3
  39. package/dist/gem-web-sdk.game.js +3 -3
  40. package/dist/gem-web-sdk.min.js +3 -3
  41. package/dist/gem-web-sdk.sdk.js +3 -3
  42. package/dist/gem-web-sdk.server.js +2 -2
  43. package/dist/index.d.ts +57 -6
  44. package/dist/sdk.d.ts +52 -4
  45. package/docs/analytics.md +11 -0
  46. package/docs/api/api-reference-function.md +7 -2
  47. package/docs/api/api-reference-game.md +15 -9
  48. package/docs/api/api-reference-server.md +1 -1
  49. package/docs/api/reference.json +8 -8
  50. package/docs/brand-art.md +65 -16
  51. package/docs/changelog.md +31 -0
  52. package/docs/dedicated-server-flow.md +30 -2
  53. package/docs/getting-started.md +3 -3
  54. package/docs/manifest.json +18 -18
  55. package/docs/match-session.md +9 -0
  56. package/docs/p2p-and-parties.md +79 -6
  57. package/docs/server-bundles.md +2 -2
  58. package/docs/what-a-game-must-do.md +8 -5
  59. package/package.json +1 -1
  60. package/schema/bridge.v1.json +67 -2
  61. package/schema/capabilities.public.json +6 -0
package/dist/sdk.d.ts CHANGED
@@ -1511,7 +1511,7 @@ declare class FunctionsApi {
1511
1511
  }
1512
1512
  type SessionEndReason = "grant_expiring" | "session_ended" | "navigating" | "kill_switch" | "pagehide";
1513
1513
  type MatchTransport = "dedicated" | "p2p";
1514
- type PlatformEventName = "gem_session_start" | "gem_session_end" | "gem_pause" | "gem_resume" | "gem_boot_ready" | "gem_match_started" | "gem_match_ended" | "gem_auth_lost";
1514
+ type PlatformEventName = "gem_session_start" | "gem_session_end" | "gem_pause" | "gem_resume" | "gem_boot_ready" | "gem_match_started" | "gem_match_ended" | "gem_auth_lost" | "gem_graphics_context_lost";
1515
1515
  declare const PLATFORM_EVENTS: readonly PlatformEventName[];
1516
1516
  type AnalyticsValue = string | number;
1517
1517
  interface AnalyticsRejection$1 {
@@ -2254,6 +2254,15 @@ interface GemSignalEvent {
2254
2254
  readonly matchId: string;
2255
2255
  readonly data: unknown;
2256
2256
  }
2257
+ interface GemRelayDraining {
2258
+ readonly gem: typeof PROTOCOL;
2259
+ readonly type: "gem.relayDraining";
2260
+ readonly matchId: string;
2261
+ readonly clusterTargetId: string;
2262
+ readonly relayIps: string[];
2263
+ readonly drainingSince: string;
2264
+ readonly deadline: string;
2265
+ }
2257
2266
  interface GemDedicatedEvent {
2258
2267
  readonly gem: typeof PROTOCOL;
2259
2268
  readonly type: "gem.dedicatedEvent";
@@ -2570,6 +2579,7 @@ interface TelemetrySample {
2570
2579
  readonly type: string;
2571
2580
  readonly value?: number;
2572
2581
  readonly transport?: TransportLabel;
2582
+ readonly labels?: Readonly<Record<string, string>>;
2573
2583
  }
2574
2584
  interface P2pApiOptions {
2575
2585
  readonly client: GemClient;
@@ -2686,9 +2696,12 @@ interface IceCandidateLike {
2686
2696
  readonly sdpMid?: string | null;
2687
2697
  readonly sdpMLineIndex?: number | null;
2688
2698
  }
2699
+ type EventListenerLike = {
2700
+ bivarianceHack(event: unknown): void;
2701
+ }["bivarianceHack"];
2689
2702
  interface EventTargetLike {
2690
- addEventListener(type: string, listener: (event: never) => void): void;
2691
- removeEventListener(type: string, listener: (event: never) => void): void;
2703
+ addEventListener(type: string, listener: EventListenerLike): void;
2704
+ removeEventListener(type: string, listener: EventListenerLike): void;
2692
2705
  }
2693
2706
  interface DataChannelLike extends EventTargetLike {
2694
2707
  readonly readyState: string;
@@ -2703,6 +2716,16 @@ interface DataChannelInitLike {
2703
2716
  readonly negotiated?: boolean;
2704
2717
  readonly id?: number;
2705
2718
  }
2719
+ interface IceConfigurationLike {
2720
+ iceServers?: {
2721
+ urls: string | string[];
2722
+ username?: string;
2723
+ credential?: string;
2724
+ }[];
2725
+ }
2726
+ interface StatsReportLike {
2727
+ forEach(callback: (value: unknown, key: string) => void): void;
2728
+ }
2706
2729
  interface PeerConnectionLike extends EventTargetLike {
2707
2730
  readonly localDescription: SessionDescriptionLike | null;
2708
2731
  readonly remoteDescription: SessionDescriptionLike | null;
@@ -2715,6 +2738,10 @@ interface PeerConnectionLike extends EventTargetLike {
2715
2738
  setRemoteDescription(description: SessionDescriptionLike): Promise<void>;
2716
2739
  addIceCandidate(candidate: IceCandidateLike): Promise<void>;
2717
2740
  close(): void;
2741
+ setConfiguration?(configuration: IceConfigurationLike): void;
2742
+ getConfiguration?(): IceConfigurationLike;
2743
+ restartIce?(): void;
2744
+ getStats?(): Promise<StatsReportLike>;
2718
2745
  }
2719
2746
  interface PeerConnectionConfig {
2720
2747
  readonly iceServers: readonly {
@@ -2749,6 +2776,7 @@ interface P2pSessionOptions {
2749
2776
  readonly baseUrl: string;
2750
2777
  readonly signaling: SignalingClient;
2751
2778
  readonly clusterTargetId?: ClusterTargetLike;
2779
+ readonly clusterTargetIds?: readonly string[];
2752
2780
  readonly peerConnectionFactory?: PeerConnectionFactory;
2753
2781
  readonly certificate?: unknown;
2754
2782
  readonly connectTimeoutMs?: number;
@@ -2765,7 +2793,11 @@ declare class P2pSession {
2765
2793
  private readonly keys;
2766
2794
  private localPlayerId;
2767
2795
  private migration;
2768
- private iceServers;
2796
+ private lease;
2797
+ private clusterTargetIds;
2798
+ private leaseRenewal;
2799
+ private leaseAskedAt;
2800
+ private leaseRenewalWarned;
2769
2801
  private credential;
2770
2802
  private nextPeerId;
2771
2803
  private ended;
@@ -2779,6 +2811,7 @@ declare class P2pSession {
2779
2811
  private readonly answeredSessions;
2780
2812
  private credentialGeneration;
2781
2813
  private credentialDeadline;
2814
+ private readonly drainTimers;
2782
2815
  constructor(options: P2pSessionOptions);
2783
2816
  get matchId(): string;
2784
2817
  get playerId(): string;
@@ -2804,6 +2837,7 @@ declare class P2pSession {
2804
2837
  private fetchCredential;
2805
2838
  private waitForCredential;
2806
2839
  private onOffer;
2840
+ private answer;
2807
2841
  private onAnswer;
2808
2842
  private hostVerified;
2809
2843
  private maybeAuthenticate;
@@ -2814,6 +2848,17 @@ declare class P2pSession {
2814
2848
  private watchMigration;
2815
2849
  private onMatchEnded;
2816
2850
  private onSignalingError;
2851
+ private onRelayDraining;
2852
+ private onDrainDeadline;
2853
+ private withoutDrainingRelays;
2854
+ private restartIceToHost;
2855
+ private offerIceRestart;
2856
+ private progressRestart;
2857
+ private reportRestart;
2858
+ private takeLease;
2859
+ private refreshLease;
2860
+ private reconfigureLinks;
2861
+ private iceServers;
2817
2862
  private openLink;
2818
2863
  private flushCandidates;
2819
2864
  private onReliableOpen;
@@ -2822,6 +2867,8 @@ declare class P2pSession {
2822
2867
  private refuseLink;
2823
2868
  private finishAdmission;
2824
2869
  private onIceState;
2870
+ private recoverHostLink;
2871
+ private voteHostUnreachable;
2825
2872
  private onConnectionState;
2826
2873
  private expose;
2827
2874
  private dropLink;
@@ -4726,6 +4773,7 @@ interface FrameCallbacks {
4726
4773
  onTicketEvent?(msg: GemTicketEvent): void;
4727
4774
  onSocketState?(msg: GemSocketState): void;
4728
4775
  onSignalEvent?(msg: GemSignalEvent): void;
4776
+ onRelayDraining?(msg: GemRelayDraining): void;
4729
4777
  onDedicatedEvent?(msg: GemDedicatedEvent): void;
4730
4778
  onClusterTarget?(msg: GemClusterTarget): void;
4731
4779
  onPartyResult?(msg: GemPartyResult): void;
package/docs/analytics.md CHANGED
@@ -131,10 +131,21 @@ and a game that declares no events at all still gets them:
131
131
  | `gem_boot_ready` | — | `boot_ms` |
132
132
  | `gem_match_started` / `gem_match_ended` | `transport` (`dedicated`, `p2p`) | — |
133
133
  | `gem_auth_lost` | — | — |
134
+ | `gem_graphics_context_lost` | `gpu`, `host`, `route` | `context_ms`, `hidden`, `nth_loss` |
134
135
 
135
136
  `reason` on a session end is one of `grant_expiring`, `session_ended`, `navigating`,
136
137
  `kill_switch` or `pagehide`.
137
138
 
139
+ `gem_graphics_context_lost` is the browser taking one of your WebGL contexts away — on a phone
140
+ usually a GPU driver fault rather than your code. `gpu` is the GPU family (`powervr_d_series`,
141
+ `powervr_other`, `adreno`, `mali`, `xclipse`, `apple`, `nvidia`, `amd`, `intel`, `software`,
142
+ `other`, `unknown`), `route` is how the browser drives it (`angle_gles`, `angle_vulkan`,
143
+ `angle_d3d11`, `angle_d3d9`, `angle_metal`, `angle_opengl`, `angle_other`, `native_gl`, `webgpu`,
144
+ `unknown`) and `host` is `android_webview` (the Arcade app) or `other`. `hidden` is 1 when the page
145
+ was hidden at the time, which is a phone reclaiming a backgrounded game's memory rather than a
146
+ crash in front of the player. It is sent whether or not your game recovers from the loss, at most
147
+ five times a page.
148
+
138
149
  **The `gem_` prefix is reserved.** Your config cannot declare an id beginning with it and
139
150
  `gem.analytics.track` refuses one rather than sending it, so pick another name.
140
151
 
@@ -1,6 +1,6 @@
1
1
  # The function surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/function` v20.0.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
3
+ Generated from `@indiegems/gem-web-sdk/function` v21.0.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
4
4
 
5
5
  `@indiegems/gem-web-sdk/function` exports exactly the symbols on this page and nothing else.
6
6
 
@@ -470,7 +470,7 @@ the reading a funnel should treat as approximate.
470
470
  ## PlatformEventName
471
471
 
472
472
  ```ts
473
- type PlatformEventName = "gem_session_start" | "gem_session_end" | "gem_pause" | "gem_resume" | "gem_boot_ready" | "gem_match_started" | "gem_match_ended" | "gem_auth_lost"
473
+ type PlatformEventName = "gem_session_start" | "gem_session_end" | "gem_pause" | "gem_resume" | "gem_boot_ready" | "gem_match_started" | "gem_match_ended" | "gem_auth_lost" | "gem_graphics_context_lost"
474
474
  ```
475
475
 
476
476
  Every event the SDK emits by itself.
@@ -488,6 +488,11 @@ config names them as steps.
488
488
  | `gem_boot_ready` | — | `boot_ms` |
489
489
  | `gem_match_started` / `gem_match_ended` | `transport` (`MatchTransport`) | — |
490
490
  | `gem_auth_lost` | — | — |
491
+ | `gem_graphics_context_lost` | `gpu`, `host`, `route` | `context_ms`, `hidden`, `nth_loss` |
492
+
493
+ `gem_graphics_context_lost` is the browser taking a WebGL context away —
494
+ usually a GPU driver fault, sized per device across every game
495
+ (`analytics/graphics.ts` lists the dimension values).
491
496
 
492
497
  <a id="gemerrorreason"></a>
493
498
 
@@ -1,6 +1,6 @@
1
1
  # The browser surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v20.0.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v21.0.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
4
4
 
5
5
  `@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/game` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
6
 
@@ -4012,7 +4012,7 @@ How a match was hosted — the closed set `gem_match_started` and
4012
4012
  ## PlatformEventName
4013
4013
 
4014
4014
  ```ts
4015
- type PlatformEventName = "gem_session_start" | "gem_session_end" | "gem_pause" | "gem_resume" | "gem_boot_ready" | "gem_match_started" | "gem_match_ended" | "gem_auth_lost"
4015
+ type PlatformEventName = "gem_session_start" | "gem_session_end" | "gem_pause" | "gem_resume" | "gem_boot_ready" | "gem_match_started" | "gem_match_ended" | "gem_auth_lost" | "gem_graphics_context_lost"
4016
4016
  ```
4017
4017
 
4018
4018
  Every event the SDK emits by itself.
@@ -4030,6 +4030,11 @@ config names them as steps.
4030
4030
  | `gem_boot_ready` | — | `boot_ms` |
4031
4031
  | `gem_match_started` / `gem_match_ended` | `transport` ([MatchTransport](#matchtransport)) | — |
4032
4032
  | `gem_auth_lost` | — | — |
4033
+ | `gem_graphics_context_lost` | `gpu`, `host`, `route` | `context_ms`, `hidden`, `nth_loss` |
4034
+
4035
+ `gem_graphics_context_lost` is the browser taking a WebGL context away —
4036
+ usually a GPU driver fault, sized per device across every game
4037
+ (`analytics/graphics.ts` lists the dimension values).
4033
4038
 
4034
4039
  <a id="platform_events"></a>
4035
4040
 
@@ -9504,8 +9509,9 @@ ARCADE_EMBEDDERS: readonly string[]
9504
9509
  ```
9505
9510
 
9506
9511
  The origins the Gem Arcade embeds a game from, which every game accepts
9507
- without naming them: the website, the developer site, and the Gem Arcade
9508
- app.
9512
+ without naming them: the website and the Gem Arcade app. The list also
9513
+ holds `https://developer.gemarcade.com`, which is reserved and not live —
9514
+ no page is served there, so nothing frames a game from it.
9509
9515
 
9510
9516
  You do not pass these. They are always allowed, and
9511
9517
  [GemOptions.allowedEmbedders](#gemoptions-allowedembedders) adds to them rather than replacing them.
@@ -9534,8 +9540,8 @@ readonly allowedEmbedders?: readonly string[]
9534
9540
  Extra origins allowed to embed your game, **added to** the ones the Gem
9535
9541
  Arcade uses. Most games leave this out.
9536
9542
 
9537
- The website, the developer site and the Gem Arcade app are always
9538
- allowed — see [ARCADE_EMBEDDERS](#arcade_embedders) — so do not list them. Set this only when a site
9543
+ The website and the Gem Arcade app are always allowed — see
9544
+ [ARCADE_EMBEDDERS](#arcade_embedders) — so do not list them. Set this only when a site
9539
9545
  of your own embeds your game.
9540
9546
 
9541
9547
  ```js
@@ -12593,9 +12599,9 @@ await gem.awaitBridge(); // before loading your main bundle
12593
12599
  const me = await gem.awaitIdentity(); // who is playing, once sign-in completes
12594
12600
  ```
12595
12601
 
12596
- No options are needed. The Gem Arcade's website, developer site and app are
12597
- always allowed to embed your game ([ARCADE_EMBEDDERS](#arcade_embedders)), and the arcade
12598
- tells your game which build it is.
12602
+ No options are needed. The Gem Arcade's website and app are always allowed to
12603
+ embed your game ([ARCADE_EMBEDDERS](#arcade_embedders)), and the arcade tells your game
12604
+ which build it is.
12599
12605
 
12600
12606
  A dev host on your own machine (`gemctl serve`, the Gem Editor) needs no
12601
12607
  entry either: it frames a locally-served game from a loopback origin whose
@@ -1,6 +1,6 @@
1
1
  # The server surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v20.0.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v21.0.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
4
4
 
5
5
  `@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/server` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
6
 
@@ -2,7 +2,7 @@
2
2
  "schema_version": 2,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "20.0.0"
5
+ "version": "21.0.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -4733,9 +4733,9 @@
4733
4733
  "name": "PlatformEventName",
4734
4734
  "anchor": "platformeventname",
4735
4735
  "group": "analytics",
4736
- "signature": "type PlatformEventName = \"gem_session_start\" | \"gem_session_end\" | \"gem_pause\" | \"gem_resume\" | \"gem_boot_ready\" | \"gem_match_started\" | \"gem_match_ended\" | \"gem_auth_lost\"",
4736
+ "signature": "type PlatformEventName = \"gem_session_start\" | \"gem_session_end\" | \"gem_pause\" | \"gem_resume\" | \"gem_boot_ready\" | \"gem_match_started\" | \"gem_match_ended\" | \"gem_auth_lost\" | \"gem_graphics_context_lost\"",
4737
4737
  "documented": true,
4738
- "summary": "Every event the SDK emits by itself.\n\nA game never sends these — `gem.analytics.track` refuses the whole\n`gem_` namespace — but the names are published because a game reading its\nown funnel needs the vocabulary, and because a funnel declared in the game\nconfig names them as steps.\n\n| Event | Dimensions | Measures |\n|---|---|---|\n| `gem_session_start` | — | — |\n| `gem_session_end` | `reason` ([SessionEndReason](api-reference-game.md#sessionendreason)) | `duration_ms` |\n| `gem_pause` / `gem_resume` | — | — |\n| `gem_boot_ready` | — | `boot_ms` |\n| `gem_match_started` / `gem_match_ended` | `transport` ([MatchTransport](api-reference-game.md#matchtransport)) | — |\n| `gem_auth_lost` | — | — |",
4738
+ "summary": "Every event the SDK emits by itself.\n\nA game never sends these — `gem.analytics.track` refuses the whole\n`gem_` namespace — but the names are published because a game reading its\nown funnel needs the vocabulary, and because a funnel declared in the game\nconfig names them as steps.\n\n| Event | Dimensions | Measures |\n|---|---|---|\n| `gem_session_start` | — | — |\n| `gem_session_end` | `reason` ([SessionEndReason](api-reference-game.md#sessionendreason)) | `duration_ms` |\n| `gem_pause` / `gem_resume` | — | — |\n| `gem_boot_ready` | — | `boot_ms` |\n| `gem_match_started` / `gem_match_ended` | `transport` ([MatchTransport](api-reference-game.md#matchtransport)) | — |\n| `gem_auth_lost` | — | — |\n| `gem_graphics_context_lost` | `gpu`, `host`, `route` | `context_ms`, `hidden`, `nth_loss` |\n\n`gem_graphics_context_lost` is the browser taking a WebGL context away —\nusually a GPU driver fault, sized per device across every game\n(`analytics/graphics.ts` lists the dimension values).",
4739
4739
  "deprecated": null,
4740
4740
  "params": [],
4741
4741
  "returns": null,
@@ -11139,7 +11139,7 @@
11139
11139
  "group": "start",
11140
11140
  "signature": "ARCADE_EMBEDDERS: readonly string[]",
11141
11141
  "documented": true,
11142
- "summary": "The origins the Gem Arcade embeds a game from, which every game accepts\nwithout naming them: the website, the developer site, and the Gem Arcade\napp.\n\nYou do not pass these. They are always allowed, and\n[GemOptions.allowedEmbedders](api-reference-game.md#gemoptions-allowedembedders) adds to them rather than replacing them.",
11142
+ "summary": "The origins the Gem Arcade embeds a game from, which every game accepts\nwithout naming them: the website and the Gem Arcade app. The list also\nholds `https://developer.gemarcade.com`, which is reserved and not live —\nno page is served there, so nothing frames a game from it.\n\nYou do not pass these. They are always allowed, and\n[GemOptions.allowedEmbedders](api-reference-game.md#gemoptions-allowedembedders) adds to them rather than replacing them.",
11143
11143
  "deprecated": null,
11144
11144
  "params": [],
11145
11145
  "returns": null,
@@ -11171,7 +11171,7 @@
11171
11171
  "group": "start",
11172
11172
  "signature": "readonly allowedEmbedders?: readonly string[]",
11173
11173
  "documented": true,
11174
- "summary": "Extra origins allowed to embed your game, **added to** the ones the Gem\nArcade uses. Most games leave this out.\n\nThe website, the developer site and the Gem Arcade app are always\nallowed — see [ARCADE_EMBEDDERS](api-reference-game.md#arcade_embedders) — so do not list them. Set this only when a site\nof your own embeds your game.\n\n```js\ncreateGem({ allowedEmbedders: ['https://my-own-site.example'] })\n```\n\nA page whose origin is not allowed gets no connection, and — deliberately —\nis told nothing about why. That means a typo here looks exactly like a\nhandshake that has not arrived: pass [GemOptions.onRefused](api-reference-game.md#gemoptions-onrefused) while you\nare integrating, because it is the only place the difference shows.\n\n⚠️ **Bake these in. Never build them from anything the page can\ninfluence** — not `location`, not a query parameter, not\n`document.referrer`. A value the embedder supplies is a check that can never\nfail. Do not add a `localhost:<port>` entry for your dev host either; the\nSDK admits one on your own machine by rule.",
11174
+ "summary": "Extra origins allowed to embed your game, **added to** the ones the Gem\nArcade uses. Most games leave this out.\n\nThe website and the Gem Arcade app are always allowed — see\n[ARCADE_EMBEDDERS](api-reference-game.md#arcade_embedders) — so do not list them. Set this only when a site\nof your own embeds your game.\n\n```js\ncreateGem({ allowedEmbedders: ['https://my-own-site.example'] })\n```\n\nA page whose origin is not allowed gets no connection, and — deliberately —\nis told nothing about why. That means a typo here looks exactly like a\nhandshake that has not arrived: pass [GemOptions.onRefused](api-reference-game.md#gemoptions-onrefused) while you\nare integrating, because it is the only place the difference shows.\n\n⚠️ **Bake these in. Never build them from anything the page can\ninfluence** — not `location`, not a query parameter, not\n`document.referrer`. A value the embedder supplies is a check that can never\nfail. Do not add a `localhost:<port>` entry for your dev host either; the\nSDK admits one on your own machine by rule.",
11175
11175
  "deprecated": null,
11176
11176
  "params": [],
11177
11177
  "returns": null,
@@ -13974,7 +13974,7 @@
13974
13974
  "group": "start",
13975
13975
  "signature": "function createGem(options?: GemOptions): Gem",
13976
13976
  "documented": true,
13977
- "summary": "Connect your game to the Gem Arcade. This is the one call every game makes.\n\nIt assembles everything and connects to the arcade that framed your game. The\nsame code runs on your machine and in production without a flag — but it needs\na frame either way. Opened at the top level it throws\n[GemUnframedError](api-reference-game.md#gemunframederror), which says how to run it: in the Arcade, the Editor,\nor the Gem dev host on your machine (`npm run dev`, signed in to `gemctl`).\n\nOne dependency, imported like anything else. The install is in\n[getting-started](../getting-started.md#3-add-the-sdk).\n\n```js\nimport { createGem } from '@indiegems/gem-web-sdk/sdk';\n\nconst gem = createGem();\n\nawait gem.awaitBridge(); // before loading your main bundle\nconst me = await gem.awaitIdentity(); // who is playing, once sign-in completes\n```\n\nNo options are needed. The Gem Arcade's website, developer site and app are\nalways allowed to embed your game ([ARCADE_EMBEDDERS](api-reference-game.md#arcade_embedders)), and the arcade\ntells your game which build it is.\n\nA dev host on your own machine (`gemctl serve`, the Gem Editor) needs no\nentry either: it frames a locally-served game from a loopback origin whose\nport changes every run, and the SDK admits it by rule — only while the game\nitself is served from loopback, so nothing changes once you publish.\n\nPass `allowedEmbedders` only when a site of your own frames your game. It\nadds to the arcade's origins; see [GemOptions.allowedEmbedders](api-reference-game.md#gemoptions-allowedembedders).",
13977
+ "summary": "Connect your game to the Gem Arcade. This is the one call every game makes.\n\nIt assembles everything and connects to the arcade that framed your game. The\nsame code runs on your machine and in production without a flag — but it needs\na frame either way. Opened at the top level it throws\n[GemUnframedError](api-reference-game.md#gemunframederror), which says how to run it: in the Arcade, the Editor,\nor the Gem dev host on your machine (`npm run dev`, signed in to `gemctl`).\n\nOne dependency, imported like anything else. The install is in\n[getting-started](../getting-started.md#3-add-the-sdk).\n\n```js\nimport { createGem } from '@indiegems/gem-web-sdk/sdk';\n\nconst gem = createGem();\n\nawait gem.awaitBridge(); // before loading your main bundle\nconst me = await gem.awaitIdentity(); // who is playing, once sign-in completes\n```\n\nNo options are needed. The Gem Arcade's website and app are always allowed to\nembed your game ([ARCADE_EMBEDDERS](api-reference-game.md#arcade_embedders)), and the arcade tells your game\nwhich build it is.\n\nA dev host on your own machine (`gemctl serve`, the Gem Editor) needs no\nentry either: it frames a locally-served game from a loopback origin whose\nport changes every run, and the SDK admits it by rule — only while the game\nitself is served from loopback, so nothing changes once you publish.\n\nPass `allowedEmbedders` only when a site of your own frames your game. It\nadds to the arcade's origins; see [GemOptions.allowedEmbedders](api-reference-game.md#gemoptions-allowedembedders).",
13978
13978
  "deprecated": null,
13979
13979
  "params": [
13980
13980
  {
@@ -30371,9 +30371,9 @@
30371
30371
  "name": "PlatformEventName",
30372
30372
  "anchor": "platformeventname",
30373
30373
  "group": "analytics",
30374
- "signature": "type PlatformEventName = \"gem_session_start\" | \"gem_session_end\" | \"gem_pause\" | \"gem_resume\" | \"gem_boot_ready\" | \"gem_match_started\" | \"gem_match_ended\" | \"gem_auth_lost\"",
30374
+ "signature": "type PlatformEventName = \"gem_session_start\" | \"gem_session_end\" | \"gem_pause\" | \"gem_resume\" | \"gem_boot_ready\" | \"gem_match_started\" | \"gem_match_ended\" | \"gem_auth_lost\" | \"gem_graphics_context_lost\"",
30375
30375
  "documented": true,
30376
- "summary": "Every event the SDK emits by itself.\n\nA game never sends these — `gem.analytics.track` refuses the whole\n`gem_` namespace — but the names are published because a game reading its\nown funnel needs the vocabulary, and because a funnel declared in the game\nconfig names them as steps.\n\n| Event | Dimensions | Measures |\n|---|---|---|\n| `gem_session_start` | — | — |\n| `gem_session_end` | `reason` ([SessionEndReason](api-reference-function.md#sessionendreason)) | `duration_ms` |\n| `gem_pause` / `gem_resume` | — | — |\n| `gem_boot_ready` | — | `boot_ms` |\n| `gem_match_started` / `gem_match_ended` | `transport` (`MatchTransport`) | — |\n| `gem_auth_lost` | — | — |",
30376
+ "summary": "Every event the SDK emits by itself.\n\nA game never sends these — `gem.analytics.track` refuses the whole\n`gem_` namespace — but the names are published because a game reading its\nown funnel needs the vocabulary, and because a funnel declared in the game\nconfig names them as steps.\n\n| Event | Dimensions | Measures |\n|---|---|---|\n| `gem_session_start` | — | — |\n| `gem_session_end` | `reason` ([SessionEndReason](api-reference-function.md#sessionendreason)) | `duration_ms` |\n| `gem_pause` / `gem_resume` | — | — |\n| `gem_boot_ready` | — | `boot_ms` |\n| `gem_match_started` / `gem_match_ended` | `transport` (`MatchTransport`) | — |\n| `gem_auth_lost` | — | — |\n| `gem_graphics_context_lost` | `gpu`, `host`, `route` | `context_ms`, `hidden`, `nth_loss` |\n\n`gem_graphics_context_lost` is the browser taking a WebGL context away —\nusually a GPU driver fault, sized per device across every game\n(`analytics/graphics.ts` lists the dimension values).",
30377
30377
  "deprecated": null,
30378
30378
  "params": [],
30379
30379
  "returns": null,
package/docs/brand-art.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  The SDK ships the platform's own art for its two currencies, **Gems** and **Coins**, and for
4
4
  **trophies**. You need it only if your game draws Gems, Coins or a trophy on its own screens. Read
5
- §1 before you pick a file: each currency has two kinds of art, and which one you use depends on what
6
- the screen is for.
5
+ §1 before you pick a file: each currency has more than one kind of art, and which one you use
6
+ depends on what the screen is for.
7
7
 
8
8
  > **Draw the platform's current marks with `gem.branding`, and keep these files as the fallback.**
9
9
  > `gem.branding.currencyMark('GEMS', { size: 20 })` and `gem.branding.trophy()` hand you the same
@@ -19,25 +19,35 @@ screen, and how much more your game can still pay (the earn allowance) — and n
19
19
  own. §5 is the creator's ruling and is not optional.
20
20
 
21
21
  > **The Coins art is a 3D render, open to a designer pass.** It is a gold coin with the platform's
22
- > gem in relief at its centre, a raised rim and a ridged edge, so a player reads it as the Arcade's
23
- > currency in every game rather than one game's gold. It replaced the tangerine coin of 2026-10-21,
24
- > which replaced the tangerine ticket of 2026-09-28. The files keep their `ticket-*` names, as the
25
- > currency keeps its slug `ARCADE_TICKETS`. If the art is redrawn again, the new art replaces these
26
- > files **under the same names** in a later SDK release, so a game that imports them picks it up on
27
- > its next SDK update. Lay your screens out from the sizes on this page, not from the drawing.
28
-
29
- ## 1. Two kinds of currency art: pick by the ROLE of the screen
22
+ > gem at its centre, so a player reads it as the Arcade's currency in every game rather than one
23
+ > game's gold. The hero and the two piles (§3) are one coin — painted gold, a thick rounded edge, a
24
+ > faceted gem standing on its face; the hero's also has a thin groove inside its rim and its gem
25
+ > raised on a sloped base — and the icons keep the earlier gold coin, whose rings and gem channel
26
+ > read better in a few pixels. Every coin is turned so its edge shows on the right. This
27
+ > art replaced the gold coin of 2026-10-25, which replaced the tangerine coin of 2026-10-21. The
28
+ > files keep their `ticket-*` names, as the currency keeps its slug `ARCADE_TICKETS`. If the art is
29
+ > redrawn again, the new art replaces these files **under the same names** in a later SDK release,
30
+ > so a game that imports them picks it up on its next SDK update. Lay your screens out from the
31
+ > sizes on this page, not from the drawing.
32
+
33
+ ## 1. Pick the currency art by the role of the screen
30
34
 
31
35
  | You are drawing | Use | Drawn at |
32
36
  | --------------- | --- | -------- |
33
37
  | Gems **inline**, beside a number: a price, a "+15" reward label, a small chip | the **icon**, `gem-icon-20-*` or `gem-icon-32-*` | 16–40 CSS px |
34
38
  | Gems as **the subject of a reward presentation**: a victory or reward screen where the gem is what the player looks at, such as a victory overlay with a sunburst behind the reward | the **hero**, `gem-hero-*` | 64–256 CSS px |
35
- | Coins **inline**, beside a number | the **icon**, `ticket-icon-20-*` or `ticket-icon-32-*` | 16–40 CSS px |
39
+ | Coins **inline** in your game's screens, beside a number | the **in-game icon**, `ticket-icon-ingame-20-*` or `ticket-icon-ingame-32-*` | 16–40 CSS px |
40
+ | Coins as **a reward sized by how much it is**: a reward card, a results screen's payout | a **pile**, `ticket-pile-1-*` or `ticket-pile-2-*` (§3) | 64–256 CSS px |
36
41
  | Coins as **the subject of a reward presentation** | the **hero**, `ticket-hero-*` | 64–256 CSS px |
37
42
 
38
43
  The role decides first and the size follows from it. A price tag is inline even if you make it
39
44
  large; a reward screen is a presentation even on a small phone.
40
45
 
46
+ `ticket-icon-20-*` and `ticket-icon-32-*` are the **arcade's wallet icon**: the same coin inside the
47
+ violet rim the Gems icon wears, which the arcade draws in its own wallet. Inside your game that rim
48
+ reads as the arcade's chrome, so draw the in-game icon there; a game that already draws the wallet
49
+ icon keeps working, and moves to the in-game one when it next touches the screen.
50
+
41
51
  - **Never use the hero at icon sizes.** It is one render drawn to be seen large; shrunk to 20 px its
42
52
  outline goes under a pixel, its facet lines vanish and the gem smudges into the background.
43
53
  - **Never use the icon at hero sizes.** Each icon file is a separate render drawn for one small box,
@@ -114,12 +124,30 @@ outline weight — 1.5 px at 20, 2 px at 32.
114
124
  | | | `ticket-icon-32-2x.png` | 64 × 64 |
115
125
  | | | `ticket-icon-32-3x.png` | 96 × 96 |
116
126
 
117
- Like the Gems icon, each size is **its own render** of the hero's coin, not the hero scaled down.
118
- The coin is turned a little so its edge shows, and the lines that part its rim, field and gem — a
119
- groove inside the rim, a crease above the edge, a channel round the gem — are drawn wider and
127
+ Like the Gems icon, each size is **its own render**, not the hero scaled down: the earlier gold
128
+ coin, turned a little so its edge shows on the right, with the lines that part its rim, field and
129
+ gem — a groove inside the rim, a crease above the edge, a channel round the gem — drawn wider and
120
130
  darker the smaller the size, so they still read in a few pixels. The 20 px coin has a slightly
121
131
  smaller gem and no ridges on its edge. Each is drawn on its own pixel grid inside the gem icon's
122
- violet outline. The arcade's wallet draws the same files.
132
+ violet outline. These are the **wallet icon**: the arcade's wallet draws the same files.
133
+
134
+ ### The in-game Coins icon
135
+
136
+ The same coin for **your game's own screens**, without the violet rim. A burnt-orange edge runs
137
+ round its light right-hand side instead, so it stays distinct on a pale background, and its edge
138
+ band is smooth. It comes in the same renditions and ratios, framed exactly as the wallet icon, so
139
+ one swaps for the other in the same box.
140
+
141
+ | Rendition | For a CSS box of | File | Pixels |
142
+ | --------- | ---------------- | ---- | ------ |
143
+ | 20 | up to about 24 px | `ticket-icon-ingame-20-1x.png` | 20 × 20 |
144
+ | | | `ticket-icon-ingame-20-1.5x.png` | 30 × 30 |
145
+ | | | `ticket-icon-ingame-20-2x.png` | 40 × 40 |
146
+ | | | `ticket-icon-ingame-20-3x.png` | 60 × 60 |
147
+ | 32 | above 24, up to about 40 px | `ticket-icon-ingame-32-1x.png` | 32 × 32 |
148
+ | | | `ticket-icon-ingame-32-1.5x.png` | 48 × 48 |
149
+ | | | `ticket-icon-ingame-32-2x.png` | 64 × 64 |
150
+ | | | `ticket-icon-ingame-32-3x.png` | 96 × 96 |
123
151
 
124
152
  ## 3. The hero art
125
153
 
@@ -147,10 +175,31 @@ size you draw it multiplied by the screen's `devicePixelRatio`:
147
175
  **Do not draw it larger than 256 CSS pixels.** On a 3× screen that needs more than the largest file,
148
176
  and the art softens as it is stretched. Below 64 px, use the icon (§2).
149
177
 
178
+ ### The coin piles
179
+
180
+ Two heaps of the hero's coin, for a Coins reward your game **offers or has just paid** where the
181
+ size of the heap says how much: a reward card, or a results screen's payout. Pile 1 is a few short
182
+ stacks; pile 2 holds about twice the coins.
183
+
184
+ | Pile | Use it for | Files |
185
+ | ---- | ---------- | ----- |
186
+ | 1 | an everyday reward | `ticket-pile-1-128.png`, `ticket-pile-1-256.png`, `ticket-pile-1-384.png`, `ticket-pile-1-512.png`, `ticket-pile-1-768.png` |
187
+ | 2 | a bigger reward | `ticket-pile-2-128.png`, `ticket-pile-2-256.png`, `ticket-pile-2-384.png`, `ticket-pile-2-512.png`, `ticket-pile-2-768.png` |
188
+
189
+ **Pick the pile by where the reward sits among your game's own rewards, not by a number of Coins.**
190
+ Where a reward's size doesn't matter, the hero does as well.
191
+
192
+ Both piles share one frame: a square, transparent PNG with a margin all round, each heap centred
193
+ across and standing on the same baseline. Pile 2 fills the frame and pile 1 is drawn smaller, so the
194
+ two side by side climb from left to right with no scaling in your layout. Draw them at the hero's
195
+ sizes, **64 to 256 CSS pixels**, and pick the file as for the hero above.
196
+
150
197
  ## 4. Loading and drawing the art
151
198
 
152
- The platform serves every one of these files through `gem.branding` ([branding.md](branding.md)),
199
+ The platform serves the icons and the heroes through `gem.branding` ([branding.md](branding.md)),
153
200
  which is what to draw first. The copies below are the fallback for when it answers `unavailable`.
201
+ The in-game Coins icon and the coin piles are not in the platform's manifest yet: draw them from
202
+ here.
154
203
 
155
204
  All of it is in the package you already install, under `brand/`, and published beside it on the CDN —
156
205
  the same bytes either way, for the version you are on.
package/docs/changelog.md CHANGED
@@ -16,6 +16,37 @@ Its entries are grouped under up to five headings:
16
16
  - **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
17
17
  - **New capability**: something a game can now do. Nothing changes unless you use it.
18
18
 
19
+ ## 21.0.0
20
+
21
+ Upgrading from 20.1.0. Impact: required.
22
+
23
+ ### May stop
24
+
25
+ - You do not need to write your own code to count lost WebGL contexts for analytics. Every game now reports each loss automatically, with no code of its own. The SDK only observes the loss, so whether your game restores its context stays your decision, and your own handling keeps working unchanged.
26
+
27
+ ### Rebuild
28
+
29
+ - The set of platform event names now includes a new member, gem_graphics_context_lost, which the SDK emits when the browser takes away one of the game's WebGL contexts. Recompiling is enough for most games. If your game switches over every platform event name exhaustively, or maps each name to a value, add a case for the new name so it still compiles.
30
+
31
+ ### New capability
32
+
33
+ - A new in-game Coins icon is available in two sizes, 20 and 32, each at four pixel ratios: 1x, 1.5x, 2x and 3x. It is the Coins coin without the violet rim, so it does not look like the Arcade's own chrome inside a game. Use it wherever your game shows Coins. The wallet icon keeps its violet rim.
34
+ - Two coin piles are now available for showing a Coins reward sized by how much it is: pile 1 for an everyday reward and pile 2 for a bigger one. Each comes in five widths: 128, 256, 384, 512 and 768. They share one frame and baseline, so a row of them climbs from the smaller to the larger. Larger piles are reserved for the Arcade's own shop. The Coins hero art remains the choice when the size of the reward does not matter.
35
+
36
+ ## 20.1.0
37
+
38
+ Upgrading from 20.0.0. Impact: required.
39
+
40
+ ### Rebuild
41
+
42
+ - The list of capability names a game can advertise gained one value, p2pRelayDrain. Recompile. If the game switches exhaustively over capability names, add a case for it. Otherwise nothing else changes.
43
+ - The list of messages the Arcade page sends to the game frame gained one entry, gem.relayDraining, and the parent message type gained the matching member. Recompile. If the game handles every parent message in an exhaustive switch, add a case for the new one. Otherwise nothing else changes.
44
+
45
+ ### New capability
46
+
47
+ - A game can now be told when a relay carrying its peer-to-peer match is draining and by when. Pass onRelayDraining in the frame options, or implement it on the frame callbacks, to receive the notice. The SDK already moves the connection off the draining relay by itself, so use this only to inform the player or log it. The notice is sent only by an Arcade page that supports it.
48
+ - Telemetry samples can now carry labels, so a game that reads or forwards telemetry samples can see per-sample label values such as outcome and reason.
49
+
19
50
  ## 20.0.0
20
51
 
21
52
  Upgrading from 19.0.2. Impact: required.
@@ -715,11 +715,39 @@ host does, and the SDK does it for you:
715
715
 
716
716
  | What gem-api answers | Where | What the SDK does |
717
717
  | --- | --- | --- |
718
- | `invalid_cluster_target` | `create()`, `quickJoin()` | Waits up to five seconds for the page to send a different list, and tries once more with it |
718
+ | `invalid_cluster_target` | `create()` | Creates in the next cluster the page sent, at once, until every one has been refused; then waits up to five seconds for the page to send a different list, and tries once more with a cluster it has not yet tried |
719
+ | `invalid_cluster_target` | `quickJoin()` | Waits up to five seconds for the page to send a different list, and tries once more with it |
719
720
  | `invalid_cluster_target` | `startMatch()` — the platform does not run the room's cluster | Starts once in this player's best other cluster, naming it, which moves the room |
720
721
  | `capacity_unavailable` twice in a row | `startMatch()` | Moves to the next acceptable cluster, once, and keeps waiting there |
721
722
 
722
- **Each of them happens once.** A second refusal is the answer, and it reaches you as the error.
723
+ **Each wait and each move happens once.** `create()` trying each cluster the page sent counts as one
724
+ step, and it tries three at most, because the page sends three at most. A refusal after that is the
725
+ answer, and it reaches you as the error.
726
+
727
+ ### When the platform hides a cluster
728
+
729
+ An operator can **hide** a cluster to take it out of service without stopping it: it leaves the list
730
+ the page measures, it takes no new rooms, no new matches and no new TURN relay leases, and the
731
+ matches already running on it carry on. A page that measured before the hide still lists it until
732
+ it measures again, so the SDK is what moves that player on:
733
+
734
+ - **`create()`** is refused with `invalid_cluster_target` for the hidden cluster and creates in the
735
+ next one the page sent, with no wait.
736
+ - **`quickJoin()` and matchmaking** need nothing: the platform drops the hidden cluster from the list
737
+ they send and serves the rest.
738
+ - **`startMatch()`** in a room on the hidden cluster is refused the same way and starts once in the
739
+ host's best other cluster, which moves the room — as for any cluster the platform stopped running.
740
+ - **A P2P relay lease** is answered from another cluster in the same region when there is one, and
741
+ otherwise refused; the SDK then leases from the next cluster the page sent. See
742
+ `p2p-and-parties.md` §4.
743
+
744
+ An earlier SDK version reaches the same place more slowly or not at all. Its `create()` names only the
745
+ best cluster, waits up to five seconds for a list the page will not send, and then gives you the
746
+ refusal, so a game that creates rooms shows an error until the page measures again. Its P2P lease asks
747
+ the hidden cluster once and, with no other cluster in that region to answer for it, connects without
748
+ a relay. Its `startMatch()` and matchmaking behave as above, and so does its `quickJoin()` from
749
+ 5.11.0 on, when the SDK began sending the page's whole list; before that a quick-join names one
750
+ cluster, and one naming only the hidden cluster is refused.
723
751
 
724
752
  **An automatic start has nobody to answer, so the host's SDK answers it.** When a room's template
725
753
  starts the match as it fills to `min_players` and the platform refuses that start, no call is waiting
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "20.0.0"
53
+ "@indiegems/gem-web-sdk": "21.0.0"
54
54
  }
55
55
  ```
56
56
 
@@ -137,8 +137,8 @@ const me = await gem.awaitIdentity();
137
137
 
138
138
  Two things about that call are worth getting right the first time.
139
139
 
140
- **You do not list who may embed your game.** The Gem Arcade's website, developer site and app
141
- are always allowed, and so is a dev host on your own machine (`gemctl serve`, the Gem Editor) while your
140
+ **You do not list who may embed your game.** The Gem Arcade's website and app are always
141
+ allowed, and so is a dev host on your own machine (`gemctl serve`, the Gem Editor) while your
142
142
  game is served from loopback. `allowedEmbedders` exists only for a page beyond those — see
143
143
  [the embedder allowlist](what-a-game-must-do.md#1-the-embedder-allowlist) — and it adds to the
144
144
  arcade's origins rather than replacing them.