@indiegems/gem-web-sdk 5.10.0 → 5.12.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/dist/sdk.d.ts CHANGED
@@ -539,6 +539,16 @@ interface Progression {
539
539
  readonly currentValue: number;
540
540
  readonly completionCount: number;
541
541
  readonly isCompleted: boolean;
542
+ readonly achievements?: readonly ProgressionAchievement[];
543
+ readonly achievementsUnavailable?: boolean;
544
+ }
545
+ interface ProgressionAchievement {
546
+ readonly id: string;
547
+ readonly threshold: number;
548
+ readonly points: number;
549
+ readonly hidden: boolean;
550
+ readonly unlockedAt?: number;
551
+ readonly revoked?: boolean;
542
552
  }
543
553
  interface CurrencyBucket {
544
554
  readonly bucket: string;
@@ -1036,14 +1046,26 @@ declare class ChallengesApi {
1036
1046
  constructor(api: GameApi$1);
1037
1047
  list(options?: ListChallengesOptions): Promise<Challenge[]>;
1038
1048
  }
1039
- interface LeaderboardEntry extends RankedRow {
1049
+ interface PlayerBadge {
1050
+ readonly itemId: string;
1051
+ readonly iconUrl: string;
1052
+ }
1053
+ interface PlayerIdentity {
1054
+ readonly playerId?: string;
1055
+ readonly displayName?: PlayerText;
1056
+ readonly discriminator?: string;
1057
+ readonly displayTag?: PlayerText;
1058
+ readonly avatarIconId?: string;
1059
+ readonly avatarIconUrl?: string;
1060
+ readonly badges: readonly PlayerBadge[];
1061
+ }
1062
+ declare function mapIdentity(row: unknown): PlayerIdentity;
1063
+ declare function platformIconUrl(url: string | undefined): string | undefined;
1064
+ interface LeaderboardEntry extends RankedRow, PlayerIdentity {
1040
1065
  readonly rank: number;
1041
1066
  readonly occurredAt: number;
1042
1067
  readonly entityId?: string;
1043
1068
  readonly entityType?: EntityType$1;
1044
- readonly displayName?: string;
1045
- readonly discriminator?: string;
1046
- readonly displayTag?: string;
1047
1069
  readonly stats?: Readonly<Record<string, number>>;
1048
1070
  readonly properties?: Readonly<Record<string, unknown>>;
1049
1071
  readonly isViewer?: boolean;
@@ -1105,11 +1127,8 @@ interface ShowcaseValue {
1105
1127
  readonly value: unknown;
1106
1128
  readonly updatedAt: number;
1107
1129
  }
1108
- interface ShowcaseFriendEntry {
1130
+ interface ShowcaseFriendEntry extends PlayerIdentity {
1109
1131
  readonly playerId: string;
1110
- readonly displayName?: string;
1111
- readonly discriminator?: string;
1112
- readonly displayTag?: string;
1113
1132
  readonly value: unknown;
1114
1133
  readonly isViewer: boolean;
1115
1134
  readonly updatedAt: number;
@@ -1131,13 +1150,8 @@ declare class ShowcaseApi {
1131
1150
  set(key: string, value: unknown, options?: ShowcaseCallOptions): Promise<ShowcaseValue>;
1132
1151
  clear(key: string, options?: ShowcaseCallOptions): Promise<void>;
1133
1152
  }
1134
- interface Friend {
1153
+ interface Friend extends PlayerIdentity {
1135
1154
  readonly playerId: string;
1136
- readonly displayName?: string;
1137
- readonly discriminator?: string;
1138
- readonly displayTag?: string;
1139
- readonly avatarIconId?: string;
1140
- readonly avatarIconUrl?: string;
1141
1155
  }
1142
1156
  interface FriendsPage {
1143
1157
  readonly friends: readonly Friend[];
@@ -1178,13 +1192,8 @@ interface ProfileShowcaseEntry {
1178
1192
  readonly value: unknown;
1179
1193
  readonly updatedAt: number;
1180
1194
  }
1181
- interface PlayerProfile {
1195
+ interface PlayerProfile extends PlayerIdentity {
1182
1196
  readonly playerId: string;
1183
- readonly displayName?: string;
1184
- readonly discriminator?: string;
1185
- readonly displayTag?: string;
1186
- readonly avatarIconId?: string;
1187
- readonly avatarIconUrl?: string;
1188
1197
  readonly showcase: readonly ProfileShowcaseEntry[];
1189
1198
  readonly generatedAt: number;
1190
1199
  }
@@ -1465,16 +1474,13 @@ interface AutoReconnectOptions {
1465
1474
  readonly attempts?: number;
1466
1475
  readonly maxDelayMs?: number;
1467
1476
  }
1468
- interface RoomMember {
1477
+ interface RoomMember extends PlayerIdentity {
1469
1478
  readonly playerId: string;
1470
1479
  readonly isReady: boolean;
1471
1480
  readonly role: string;
1472
1481
  readonly joinedAt: number;
1473
1482
  readonly groupId?: string;
1474
1483
  readonly platform?: string;
1475
- readonly displayName?: PlayerText;
1476
- readonly displayTag?: PlayerText;
1477
- readonly discriminator?: string;
1478
1484
  }
1479
1485
  interface Room {
1480
1486
  readonly id: string;
@@ -1492,6 +1498,7 @@ interface Room {
1492
1498
  readonly roomArgsTemplate?: readonly RoomArgEntry[];
1493
1499
  readonly gameChannel: string;
1494
1500
  readonly region?: string;
1501
+ readonly clusterTargetId?: string;
1495
1502
  readonly matchId?: string;
1496
1503
  readonly version: number;
1497
1504
  readonly templateId?: string;
@@ -1501,6 +1508,10 @@ interface Room {
1501
1508
  readonly updatedAt: number;
1502
1509
  readonly startedAt?: number;
1503
1510
  }
1511
+ interface MatchAutoStartFailed {
1512
+ readonly roomId: string;
1513
+ readonly code: string;
1514
+ }
1504
1515
  interface RoomSummary {
1505
1516
  readonly id: string;
1506
1517
  readonly gameId: string;
@@ -1603,6 +1614,9 @@ interface RoomEventMap {
1603
1614
  matchStartWaiting: [
1604
1615
  StartMatchWaiting
1605
1616
  ];
1617
+ matchAutoStartFailed: [
1618
+ MatchAutoStartFailed
1619
+ ];
1606
1620
  matchAllocationStarted: [
1607
1621
  string
1608
1622
  ];
@@ -1700,6 +1714,7 @@ interface RoomManager {
1700
1714
  once<K extends RoomEventName>(event: K, listener: (...args: RoomEventMap[K]) => void): () => void;
1701
1715
  handleRoomEvent(event: RelayedRoomEvent): void;
1702
1716
  handleSeated(roomId: string): void;
1717
+ handleAutoStartFailed(roomId: string, code: string): void;
1703
1718
  reconcileSeats(): Promise<void>;
1704
1719
  reconcile(hint?: MatchEndHint): Promise<void>;
1705
1720
  startPolling(intervalMs?: number): void;
@@ -1796,6 +1811,12 @@ interface GemSeatedEvent {
1796
1811
  readonly type: "gem.seatedEvent";
1797
1812
  readonly roomId: string;
1798
1813
  }
1814
+ interface GemAutoStartFailedEvent {
1815
+ readonly gem: typeof PROTOCOL;
1816
+ readonly type: "gem.autoStartFailedEvent";
1817
+ readonly roomId: string;
1818
+ readonly code: string;
1819
+ }
1799
1820
  interface GemSignalEvent {
1800
1821
  readonly gem: typeof PROTOCOL;
1801
1822
  readonly type: "gem.signalEvent";
@@ -1824,6 +1845,7 @@ interface GemClusterTarget {
1824
1845
  readonly gem: typeof PROTOCOL;
1825
1846
  readonly type: "gem.clusterTarget";
1826
1847
  readonly clusterTargetId: string;
1848
+ readonly clusterTargetIds?: string[];
1827
1849
  }
1828
1850
  interface GemScreenshotDenied {
1829
1851
  readonly gem: typeof PROTOCOL;
@@ -3470,7 +3492,7 @@ interface GemServer {
3470
3492
  declare function createGemServer(host: ServerBundleHost, options?: GemServerOptions): GemServer;
3471
3493
  interface GemServerProfile {
3472
3494
  readonly displayName: PlayerText;
3473
- readonly displayTag: string;
3495
+ readonly displayTag?: PlayerText;
3474
3496
  readonly discriminator?: string;
3475
3497
  }
3476
3498
  interface GemMatchServer extends GemServer {
@@ -3619,6 +3641,11 @@ interface RewardCue {
3619
3641
  };
3620
3642
  }
3621
3643
  type FrameState = "waiting" | "connected" | "refused" | "closed";
3644
+ interface FramePlayer extends PlayerIdentity {
3645
+ readonly displayName: PlayerText;
3646
+ readonly displayHandle: PlayerText;
3647
+ readonly avatarUrl?: string;
3648
+ }
3622
3649
  interface FrameCallbacks {
3623
3650
  onToken?(msg: GemToken): void;
3624
3651
  onPause?(msg: GemPause): void;
@@ -3635,14 +3662,10 @@ interface FrameCallbacks {
3635
3662
  onTokenDenied?(msg: GemTokenDenied): void;
3636
3663
  onLocalServer?(msg: GemLocalServer): void;
3637
3664
  onLocalServerDenied?(msg: GemLocalServerDenied): void;
3638
- onPlayer?(player: {
3639
- displayName: PlayerText;
3640
- displayHandle: PlayerText;
3641
- displayTag: string;
3642
- avatarUrl?: string;
3643
- }): void;
3665
+ onPlayer?(player: FramePlayer): void;
3644
3666
  onRoomEvent?(msg: GemRoomEvent): void;
3645
3667
  onSeatedEvent?(msg: GemSeatedEvent): void;
3668
+ onAutoStartFailedEvent?(msg: GemAutoStartFailedEvent): void;
3646
3669
  onSocketState?(msg: GemSocketState): void;
3647
3670
  onSignalEvent?(msg: GemSignalEvent): void;
3648
3671
  onDedicatedEvent?(msg: GemDedicatedEvent): void;
@@ -3718,6 +3741,7 @@ declare class GemFrame {
3718
3741
  private screenshotDenied;
3719
3742
  }
3720
3743
  declare const DEVICE_SETTINGS_MAX_BYTES = 16384;
3744
+ declare const ARCADE_EMBEDDERS: readonly string[];
3721
3745
  interface GemOptions {
3722
3746
  readonly allowedEmbedders?: readonly string[];
3723
3747
  readonly baseUrl?: string;
@@ -3750,10 +3774,9 @@ interface GemOptions {
3750
3774
  }) => Promise<ImageBitmap | null>;
3751
3775
  readonly handshakeWarningMs?: number | null;
3752
3776
  }
3753
- interface GemPlayer {
3777
+ interface GemPlayer extends PlayerIdentity {
3754
3778
  readonly displayName: PlayerText;
3755
3779
  readonly displayHandle: PlayerText;
3756
- readonly displayTag: string;
3757
3780
  readonly avatarUrl?: string;
3758
3781
  }
3759
3782
  interface GemPrefs {
@@ -3838,4 +3861,4 @@ interface ExitAnalytics {
3838
3861
  }
3839
3862
  declare function leaveAndExit(rooms: ExitRooms, bridge: ExitBridge | null, reason?: string, analytics?: ExitAnalytics): Promise<void>;
3840
3863
  declare function createGem(options?: GemOptions): Gem;
3841
- export { type AccountApi, type AdFormat, type AdOutcome, type AdOutcomeKind, type AdRequest, type AdsApi, AnalyticsApi, type AnalyticsFlushOptions, type AnalyticsFlushResult, type AnalyticsOptions, type AnalyticsRejection, type AnalyticsValue, AuthError, type AuthFailure, type AuthState, type Base64String, type BootMark, type BootTimeline, type BroadcastOptions, CONDITIONAL_HEADERS, type Challenge, type ChallengeWindow, ChallengesApi, type ClientOptions, type Clock, type ConditionalRequestHeaders, type ConfiguredPurchasePayment, type ConfiguredPurchaseRequest, type ConfiguredPurchaseResult, DEVICE_SETTINGS_MAX_BYTES, DedicatedServerError, type DedicatedServerFailure, type DedicatedServerSession, type DevToolsOpener, type DeviceSettings, type DeviceSettingsApi, type DeviceSettingsPutOutcome, type DirectPurchaseRequest, type DirectPurchaseResult, type EntityLeaderboard, type EntityLeaderboardEntry, type EntityLeaderboardOptions, type EntityType$1 as EntityType, type ExitAnalytics, type ExitBridge, type ExitRooms, type FrameState, type Friend, FriendsApi, type FriendsPage, type GameApi, type GameConfig, type GameServerLink, type Gem, GemApiError, GemClient, type GemCohort, type GemCohorts, GemConfigError, type GemDevTools, type GemErrorReason, GemFrame, type GemIdentity, type GemInsets, type GemMatchApi, type GemMatchServer, type GemMatchServerModule, type GemOptions, type GemPlayer, type GemPrefs, GemSaveError, type GemServer, type GemServerBundle, type GemServerFactory, type GemServerHost, type GemServerHostProfile, type GemServerIdentity, type GemServerLifecycle, type GemServerOptions, type GemServerProfile, type GemTerminationNotice, type GemViewport, type GetLeaderboardOptions, type HostTokenSource, type HostTokenSourceOptions, type JoinWelcome, type Leaderboard, type LeaderboardEntry, LeaderboardsApi, type ListChallengesOptions, type ListFriendsOptions, MAX_PERIODS_BACK, type MatchConnectOptions, type MatchConnection, type MatchHosting, type MatchHostingMode, type MatchServeOptions, type MatchTransport, type MessageMeta, type MultiTransportSession, PLATFORM_EVENTS, PlatformApi, PlatformCurrenciesApi, type PlatformCurrency, type PlatformCurrencyAccess, type PlatformCurrencyPurchasePayment, type PlatformCurrencyPurchaseRequest, type PlatformCurrencyPurchaseResult, type PlatformCurrencyReadOptions, type PlatformEventName, type PlayerProfile, PlayerText, type ProfileCallOptions, type ProfileShowcaseEntry, ProfilesApi, type PurchaseApi, type PurchaseOptions, type PurchaseOutcome, type PurchaseRequest, type PutBytesOptions, type PutSaveOptions, type RankedRow, type RequestOptions, type RewardCue, type RoomArgValue, type SavePage, type SaveRecord, type SendOptions$1 as SendOptions, type ServerApi, type ServerBundleHost, type ServerTokenGrant, type SessionEndReason, ShowcaseApi, type ShowcaseCallOptions, type ShowcaseFriendEntry, type ShowcaseFriends, type ShowcaseValue, type SignInOutcome, type SignInOutcomeKind, type SnapshotCurrency, type SnapshotItem, type TokenGrant, type Transport, type TransportAdvert, type TransportKind, TransportRefusal, type TransportRequest, type TransportResponse, UNRELIABLE_CHANNEL_LABEL, type VersionedGetOptions, type VersionedResponse, asPlayerText, createBootTimeline, createGem, createGemServer, createHostTokenSource, createMatchServer, defineServer, leaveAndExit, markOnResolve, presentJoinCredential, readTemplateTransports, readTransportAdvert, reportOnResolve, systemClock, text, toBase64, webTransportAvailable };
3864
+ export { ARCADE_EMBEDDERS, type AccountApi, type AdFormat, type AdOutcome, type AdOutcomeKind, type AdRequest, type AdsApi, AnalyticsApi, type AnalyticsFlushOptions, type AnalyticsFlushResult, type AnalyticsOptions, type AnalyticsRejection, type AnalyticsValue, AuthError, type AuthFailure, type AuthState, type Base64String, type BootMark, type BootTimeline, type BroadcastOptions, CONDITIONAL_HEADERS, type Challenge, type ChallengeWindow, ChallengesApi, type ClientOptions, type Clock, type ConditionalRequestHeaders, type ConfiguredPurchasePayment, type ConfiguredPurchaseRequest, type ConfiguredPurchaseResult, DEVICE_SETTINGS_MAX_BYTES, DedicatedServerError, type DedicatedServerFailure, type DedicatedServerSession, type DevToolsOpener, type DeviceSettings, type DeviceSettingsApi, type DeviceSettingsPutOutcome, type DirectPurchaseRequest, type DirectPurchaseResult, type EntityLeaderboard, type EntityLeaderboardEntry, type EntityLeaderboardOptions, type EntityType$1 as EntityType, type ExitAnalytics, type ExitBridge, type ExitRooms, type FrameState, type Friend, FriendsApi, type FriendsPage, type GameApi, type GameConfig, type GameServerLink, type Gem, GemApiError, GemClient, type GemCohort, type GemCohorts, GemConfigError, type GemDevTools, type GemErrorReason, GemFrame, type GemIdentity, type GemInsets, type GemMatchApi, type GemMatchServer, type GemMatchServerModule, type GemOptions, type GemPlayer, type GemPrefs, GemSaveError, type GemServer, type GemServerBundle, type GemServerFactory, type GemServerHost, type GemServerHostProfile, type GemServerIdentity, type GemServerLifecycle, type GemServerOptions, type GemServerProfile, type GemTerminationNotice, type GemViewport, type GetLeaderboardOptions, type HostTokenSource, type HostTokenSourceOptions, type JoinWelcome, type Leaderboard, type LeaderboardEntry, LeaderboardsApi, type ListChallengesOptions, type ListFriendsOptions, MAX_PERIODS_BACK, type MatchConnectOptions, type MatchConnection, type MatchHosting, type MatchHostingMode, type MatchServeOptions, type MatchTransport, type MessageMeta, type MultiTransportSession, PLATFORM_EVENTS, PlatformApi, PlatformCurrenciesApi, type PlatformCurrency, type PlatformCurrencyAccess, type PlatformCurrencyPurchasePayment, type PlatformCurrencyPurchaseRequest, type PlatformCurrencyPurchaseResult, type PlatformCurrencyReadOptions, type PlatformEventName, type PlayerBadge, type PlayerIdentity, type PlayerProfile, PlayerText, type ProfileCallOptions, type ProfileShowcaseEntry, ProfilesApi, type PurchaseApi, type PurchaseOptions, type PurchaseOutcome, type PurchaseRequest, type PutBytesOptions, type PutSaveOptions, type RankedRow, type RequestOptions, type RewardCue, type RoomArgValue, type SavePage, type SaveRecord, type SendOptions$1 as SendOptions, type ServerApi, type ServerBundleHost, type ServerTokenGrant, type SessionEndReason, ShowcaseApi, type ShowcaseCallOptions, type ShowcaseFriendEntry, type ShowcaseFriends, type ShowcaseValue, type SignInOutcome, type SignInOutcomeKind, type SnapshotCurrency, type SnapshotItem, type TokenGrant, type Transport, type TransportAdvert, type TransportKind, TransportRefusal, type TransportRequest, type TransportResponse, UNRELIABLE_CHANNEL_LABEL, type VersionedGetOptions, type VersionedResponse, asPlayerText, createBootTimeline, createGem, createGemServer, createHostTokenSource, createMatchServer, defineServer, leaveAndExit, mapIdentity, markOnResolve, platformIconUrl, presentJoinCredential, readTemplateTransports, readTransportAdvert, reportOnResolve, systemClock, text, toBase64, webTransportAvailable };
package/dist/server.d.ts CHANGED
@@ -1369,7 +1369,7 @@ declare const text: {
1369
1369
  };
1370
1370
  interface GemServerProfile {
1371
1371
  readonly displayName: PlayerText;
1372
- readonly displayTag: string;
1372
+ readonly displayTag?: PlayerText;
1373
1373
  readonly discriminator?: string;
1374
1374
  }
1375
1375
  interface GemMatchServer extends GemServer {
@@ -1435,4 +1435,12 @@ interface RuntimeShutdownPayload {
1435
1435
  }
1436
1436
  type GemServerFactory = () => GemServerLifecycle;
1437
1437
  declare function defineServer(setup: (gem: GemMatchServer) => GemServerBundle, options?: GemServerOptions): GemServerFactory;
1438
- export { type AnalyticsFlushResult, type AnalyticsRejection, type AnalyticsValue, AuthError, type AuthFailure, type AuthState, type Base64String, type BroadcastOptions, CONDITIONAL_HEADERS, type ClientOptions, type Clock, type ConditionalRequestHeaders, type GameApi, GemApiError, GemClient, type GemErrorReason, type GemMatchServer, type GemServer, type GemServerBundle, type GemServerFactory, type GemServerHost, type GemServerHostProfile, type GemServerIdentity, type GemServerLifecycle, type GemServerOptions, type GemServerProfile, type HostTokenSource, type HostTokenSourceOptions, PlayerText, type RequestOptions, type RoomArgValue, type RuntimeShutdownPayload, type SendOptions, type ServerAnalytics, type ServerApi, type ServerBundleHost, type ServerShutdownInfo, type ServerTokenGrant, type TokenGrant, type Transport, TransportRefusal, type TransportRequest, type TransportResponse, type VersionedGetOptions, type VersionedResponse, asPlayerText, createGemServer, createHostTokenSource, createMatchServer, defineServer, systemClock, text, toBase64 };
1438
+ interface AchievementReceipt {
1439
+ readonly achievementId: string;
1440
+ readonly progressionDefinitionId: string;
1441
+ readonly points: number;
1442
+ readonly outcome: "unlocked" | "already_unlocked" | "suppressed";
1443
+ readonly reason?: string;
1444
+ }
1445
+ declare function achievementReceipts(order: unknown): AchievementReceipt[];
1446
+ export { type AchievementReceipt, type AnalyticsFlushResult, type AnalyticsRejection, type AnalyticsValue, AuthError, type AuthFailure, type AuthState, type Base64String, type BroadcastOptions, CONDITIONAL_HEADERS, type ClientOptions, type Clock, type ConditionalRequestHeaders, type GameApi, GemApiError, GemClient, type GemErrorReason, type GemMatchServer, type GemServer, type GemServerBundle, type GemServerFactory, type GemServerHost, type GemServerHostProfile, type GemServerIdentity, type GemServerLifecycle, type GemServerOptions, type GemServerProfile, type HostTokenSource, type HostTokenSourceOptions, PlayerText, type RequestOptions, type RoomArgValue, type RuntimeShutdownPayload, type SendOptions, type ServerAnalytics, type ServerApi, type ServerBundleHost, type ServerShutdownInfo, type ServerTokenGrant, type TokenGrant, type Transport, TransportRefusal, type TransportRequest, type TransportResponse, type VersionedGetOptions, type VersionedResponse, achievementReceipts, asPlayerText, createGemServer, createHostTokenSource, createMatchServer, defineServer, systemClock, text, toBase64 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "5.10.0",
3
+ "version": "5.12.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -489,30 +489,100 @@
489
489
  "stripping it turns decomposed José into Jose and makes Thai unreadable —",
490
490
  "and inconsistently, since NFC é survives while NFD e+U+0301 does not.",
491
491
  "",
492
- "`displayTag` and `avatarUrl` sit OUTSIDE `untrusted` because they are",
493
- "platform-assigned, not player-authored. `displayHandle` is inside because",
494
- "it embeds displayName — otherwise the precomposed 'safe' field is a",
495
- "laundering path for the unsafe one."
492
+ "THE TRUST SPLIT. Outside",
493
+ "`untrusted` there are ONLY platform-assigned values: ids, the",
494
+ "discriminator digits, and platform image URLs. Every field that carries",
495
+ "the player's name sits inside it — `displayTag` because `Name#0001`",
496
+ "embeds displayName, which otherwise makes the precomposed 'safe' field a",
497
+ "laundering path for the unsafe one. This SDK's own schema test fails the",
498
+ "build if a name-bearing field appears anywhere else in this schema.",
499
+ "",
500
+ "The names follow gem-api's flat identity fields (`player_id`,",
501
+ "`display_name`, `discriminator`, `display_tag`, `avatar_icon_url`,",
502
+ "`badges`), which every REST surface carries and the SDK maps with one",
503
+ "`mapIdentity`. This message used to call the DISCRIMINATOR",
504
+ "`displayTag` — the same name gem-api gives the whole `Name#0001` — so",
505
+ "one name meant two things in two safety classes.",
506
+ "",
507
+ "LEGACY FIELDS, AND WHY THEY ARE STILL REQUIRED. `displayTag` (top level),",
508
+ "`untrusted.displayHandle` and `avatarUrl` are the earlier spellings of",
509
+ "`discriminator`, `untrusted.displayTag` and `avatarIconUrl`. A game",
510
+ "bundles the SDK it was built with, and every frame built before the new",
511
+ "names existed REQUIRES the first two — a parent that dropped them would",
512
+ "have its gem.player refused by every one of those games. Keeping them REQUIRED",
513
+ "makes that obligation mechanical on the sending side rather than a note:",
514
+ "a parent's own generated validator refuses a message without them. A",
515
+ "parent fills each from its successor and never from anything else —",
516
+ "`GemHost.sendPlayer` takes no input for them at all. A frame built from",
517
+ "this schema reads them only as the fallback for a parent older than the",
518
+ "new names, which sends nothing else. They are removed by the next `v`,",
519
+ "whose N-1 window is what lets them go.",
520
+ "",
521
+ "ADDITIVE, so no `v` bump: every new field is optional (an older parent",
522
+ "sends none of them and must still be accepted), and an older frame",
523
+ "ignores fields it does not know (rule 4)."
496
524
  ],
497
525
  "fields": {
498
526
  "untrusted": {
499
527
  "kind": "object",
500
528
  "fields": {
501
529
  "displayName": { "kind": "string", "maxLength": 64 },
530
+ "displayTag": {
531
+ "kind": "string",
532
+ "maxLength": 80,
533
+ "optional": true,
534
+ "$comment": "Precomposed `Name#0001` — gem-api's `display_tag` — so no game has to know the convention. Player-authored because it embeds the name. gem-web's AGENTS.md states the platform rule flatly: never display a username; always display_name + discriminator."
535
+ },
502
536
  "displayHandle": {
503
537
  "kind": "string",
504
538
  "maxLength": 80,
505
- "$comment": "Precomposed `DisplayName#1234`, so no game has to know the convention. gem-web's AGENTS.md states the platform rule flatly: never display a username; always display_name + display_tag."
539
+ "$comment": "LEGACY — the earlier spelling of `untrusted.displayTag`, and the same value (`displayName` alone when the player has no tag). Required because older frames require it; see the message comment. A current frame reads it only when `untrusted.displayTag` is absent."
506
540
  }
507
541
  }
508
542
  },
509
- "displayTag": { "kind": "string", "maxLength": 8 },
543
+ "playerId": {
544
+ "kind": "string",
545
+ "maxLength": 64,
546
+ "optional": true,
547
+ "$comment": "The same id the token carries, so a game can key its own row in a roster or a board without decoding anything. Optional only because an older parent does not send it."
548
+ },
549
+ "discriminator": {
550
+ "kind": "string",
551
+ "maxLength": 8,
552
+ "optional": true,
553
+ "$comment": "The platform-assigned digits after the `#` (`0001`), which tell two players with the same name apart. Not player-authored."
554
+ },
555
+ "avatarIconUrl": {
556
+ "kind": "string",
557
+ "maxLength": 2048,
558
+ "format": "url",
559
+ "optional": true,
560
+ "$comment": "The portrait's THUMBNAIL (gem-api `avatar_icon_url`). Absent means no portrait; a parent never sends an empty string. Structurally validated only — https (or loopback), no userinfo, parsed rather than regex-matched. It is NOT origin-pinned: the validator is a pure function with no configuration, so it cannot know the CDN origin. An earlier version of this comment claimed a pin that the generated code did not emit — a documented control that did not exist. THE CONSUMER MUST PIN THE ORIGIN before putting this in an <img src> — the SDK's `platformIconUrl` does — or a hostile parent supplies a request-time beacon carrying the Referer."
561
+ },
562
+ "badges": {
563
+ "kind": "array",
564
+ "maxItems": 2,
565
+ "optional": true,
566
+ "$comment": "Equipped badges in display order — empty until platform achievements equip any, and never more than two. Each is an item id and a thumbnail URL, the same pin caveat as `avatarIconUrl`.",
567
+ "items": {
568
+ "kind": "object",
569
+ "fields": {
570
+ "itemId": { "kind": "string", "maxLength": 64 },
571
+ "iconUrl": { "kind": "string", "maxLength": 2048, "format": "url" }
572
+ }
573
+ }
574
+ },
575
+ "displayTag": {
576
+ "kind": "string",
577
+ "maxLength": 8,
578
+ "$comment": "LEGACY — the earlier spelling of `discriminator`, and the same value (empty when the player has none). NOT the name-bearing `untrusted.displayTag`: this one has only ever carried the digits. Required because older frames require it; see the message comment. A current frame reads it only when `discriminator` is absent."
579
+ },
510
580
  "avatarUrl": {
511
581
  "kind": "string",
512
582
  "maxLength": 2048,
513
583
  "format": "url",
514
584
  "optional": true,
515
- "$comment": "Structurally validated only — https (or loopback), no userinfo, parsed rather than regex-matched. It is NOT origin-pinned: the validator is a pure function with no configuration, so it cannot know the CDN origin. An earlier version of this comment claimed a pin that the generated code did not emit — a documented control that did not exist. THE CONSUMER MUST PIN THE ORIGIN before putting this in an <img src>, or a hostile parent supplies a request-time beacon carrying the Referer."
585
+ "$comment": "LEGACY — the earlier spelling of `avatarIconUrl`, and the same value. Same pin caveat. A current frame reads it only when `avatarIconUrl` is absent."
516
586
  }
517
587
  },
518
588
  "rateLimit": { "perMinute": 12 }
@@ -995,6 +1065,45 @@
995
1065
  },
996
1066
  "rateLimit": { "perMinute": 60 }
997
1067
  },
1068
+ {
1069
+ "type": "gem.autoStartFailedEvent",
1070
+ "direction": "parentToFrame",
1071
+ "transport": "port",
1072
+ "$comment": [
1073
+ "The platform tried to start a match in this player's room on its own —",
1074
+ "the room filled to its template's `min_players` — and was refused.",
1075
+ "Gated by the `autoStartFailure` capability; the mapping lives in",
1076
+ "schema/capabilities.json and nowhere else.",
1077
+ "",
1078
+ "gem-api's `game_room_auto_start_failed`, sent to the room's HOST only,",
1079
+ "because the host is the one member who can act: start the match itself.",
1080
+ "No match exists and the room stays open, so there is no",
1081
+ "`game_room_match_failed` or `match_ended` to follow — which is why this",
1082
+ "is not either of those.",
1083
+ "",
1084
+ "ITS OWN TYPE, NOT A VALUE IN gem.roomEvent'S ENUM, for the reason",
1085
+ "gem.seatedEvent gives: the generated validator rejects an unknown enum",
1086
+ "value, and the frame reports that rejection to the game and to the",
1087
+ "embedder as a protocol error. Every already-published game would see",
1088
+ "one each time an automatic start failed. An unknown TYPE is ignored,",
1089
+ "and the capability keeps the parent from sending it at all.",
1090
+ "",
1091
+ "THE CODE IS WHAT CROSSES, and it is an open vocabulary. It is the error",
1092
+ "code a manual start would have been answered with for the same refusal:",
1093
+ "`invalid_cluster_target` (the room's cluster is no longer configured),",
1094
+ "`capacity_unavailable`, or one of the start route's version, engine and",
1095
+ "server-build codes. A string rather than an enum so a code gem-api adds",
1096
+ "later reaches the game rather than breaking the message.",
1097
+ "",
1098
+ "PARENT TO FRAME ONLY. A frame-authored failure would let a game tell the",
1099
+ "parent a room it is in cannot start."
1100
+ ],
1101
+ "fields": {
1102
+ "roomId": { "kind": "string", "maxLength": 64 },
1103
+ "code": { "kind": "string", "maxLength": 64 }
1104
+ },
1105
+ "rateLimit": { "perMinute": 30 }
1106
+ },
998
1107
 
999
1108
  {
1000
1109
  "type": "gem.signalEvent",
@@ -1219,40 +1328,60 @@
1219
1328
  "direction": "parentToFrame",
1220
1329
  "transport": "port",
1221
1330
  "$comment": [
1222
- "Where this player should be allocated. Gated by the `clusterTarget`",
1223
- "capability.",
1224
- "",
1225
- "ONE RESOLVED ID, NEVER THE MEASUREMENT THAT PRODUCED IT. The parent",
1226
- "ranks regions by timing STUN Binding requests against the probe",
1227
- "endpoints `GET /v1/regions` publishes, once per player session. What",
1228
- "crosses is the winner. What must never cross is the vector: a",
1229
- "round-trip time to every region is coarse geolocation plus a network",
1230
- "fingerprint, and it would be readable by third-party game code that has",
1231
- "no use for it. There is no ranking, no runner-up, no per-region timing",
1232
- "and no `probe_targets_fresh` here, and adding any of them is not an",
1233
- "additive change however optional the field is.",
1331
+ "Where this player's rooms and matches should be placed. Gated by the",
1332
+ "`clusterTarget` capability.",
1333
+ "",
1334
+ "THE ACCEPTABLE CLUSTERS, BEST FIRST, NEVER THE MEASUREMENT THAT PRODUCED",
1335
+ "THEM. The parent ranks regions by timing STUN Binding requests against",
1336
+ "the probe endpoints `GET /v1/regions` publishes, and keeps the ones close",
1337
+ "enough to the fastest that a player would not notice the difference —",
1338
+ "at most three. What crosses is those ids, in order. What must never",
1339
+ "cross is the vector: a round-trip time to every region is coarse",
1340
+ "geolocation plus a network fingerprint, and it would be readable by",
1341
+ "third-party game code that has no use for it. There is no per-region",
1342
+ "timing, no list of the regions that were NOT acceptable, and no",
1343
+ "`probe_targets_fresh` here, and adding any of them is not an additive",
1344
+ "change however optional the field is. Three ids with no timings is",
1345
+ "continent-level information, about what one id already said.",
1346
+ "",
1347
+ "TWO FIELDS FOR ONE ANSWER, because frames that predate the list are in",
1348
+ "the wild. `clusterTargetId` is the best cluster, always the head of the",
1349
+ "list, and it is all an older frame reads. `clusterTargetIds` is the",
1350
+ "list; a frame that reads it prefers it, and a message without one is a",
1351
+ "list of one.",
1234
1352
  "",
1235
1353
  "PUSHED, NOT ASKED, because the fact exists before the frame does — the",
1236
1354
  "Arcade page measures at startup and the same measurement survives a game",
1237
1355
  "swap. Sent once after the handshake; sent again only if the parent's",
1238
- "answer actually changes.",
1356
+ "answer actually changes. A frame refused a cluster waits a few seconds",
1357
+ "for exactly that, so a parent that re-measures should send the result.",
1239
1358
  "",
1240
1359
  "ABSENCE IS EXPRESSED BY NOT SENDING. There is no empty or sentinel",
1241
- "value, and the field is required for that reason: a parent with no",
1242
- "answer stays quiet, the frame omits the cluster target from its",
1243
- "allocation request, and the server is left to decide. That is what",
1244
- "`/v1/regions` asks a client with no `default_cluster_target_id` to do.",
1245
- "",
1246
- "NOT the room's `region`. That one is a free-form matchmaking label",
1247
- "gem-api stores on the room and filters quick-join by; it plays no part",
1248
- "in allocation. This is the cluster the match is placed in. Two nearby",
1249
- "words for two unrelated facts, so they are named apart — DESIGN.md §1."
1360
+ "value, and the id is required for that reason: a parent with no answer",
1361
+ "stays quiet, the frame names no cluster on its room and allocation",
1362
+ "requests, and the server is left to decide.",
1363
+ "",
1364
+ "NOT the room's retired `region`, a free-form label gem-api no longer",
1365
+ "stores or matches on. A room's cluster is where its matches start and",
1366
+ "which quick-join requests can find it."
1250
1367
  ],
1251
1368
  "fields": {
1252
1369
  "clusterTargetId": {
1253
1370
  "kind": "string",
1254
1371
  "maxLength": 64,
1255
- "$comment": "`cluster_target_id` as `/v1/regions` reports it, e.g. `ct_azureeastus01`."
1372
+ "$comment": "`cluster_target_id` as `/v1/regions` reports it, e.g. `ct_azureeastus01`. The best cluster: the head of `clusterTargetIds` when that is sent."
1373
+ },
1374
+ "clusterTargetIds": {
1375
+ "kind": "array",
1376
+ "maxItems": 3,
1377
+ "items": { "kind": "string", "maxLength": 64 },
1378
+ "optional": true,
1379
+ "$comment": [
1380
+ "Every acceptable cluster, best first, `clusterTargetId` included.",
1381
+ "Bounded at three, which is also gem-api's own limit on a quick-join",
1382
+ "list: a fourth entry would be refused there, and it would be one",
1383
+ "more fact about where the player is than any game needs."
1384
+ ]
1256
1385
  }
1257
1386
  },
1258
1387
  "rateLimit": { "perMinute": 12 }
@@ -1975,9 +2104,11 @@
1975
2104
  "player's one real wallet (gem-api's rule), so a cue confirms the same",
1976
2105
  "way everywhere; a grant the receipt reports suppressed confirms nothing.",
1977
2106
  "",
1978
- "Gated by `rewardCue`: sent only to a parent that advertised it, which is",
1979
- "how a game learns (`gem.cueReward` returns false) that it should play its",
1980
- "own celebration instead."
2107
+ "Gated by `rewardCue`: sent only to a parent that advertised it; to any",
2108
+ "other, `gem.cueReward` returns false and the game does NOTHING. There is",
2109
+ "no fallback celebration: Gems display belongs to the arcade (the",
2110
+ "creator's ruling, 2026-09-24), so a game draws no Gems flight, counter or",
2111
+ "celebration of its own. `origin` is the centre of the game's Gems icon."
1981
2112
  ],
1982
2113
  "fields": {
1983
2114
  "currencySlug": { "kind": "string", "maxLength": 32 },