@indiegems/gem-web-sdk 12.0.0 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -563,7 +563,13 @@
563
563
  "`avatar3dIconUrl`) is re-sent the same way: whenever it changes, from",
564
564
  "either tab of the parent's picker, the whole message goes again. It is",
565
565
  "a separate worn choice from the portrait — wearing one never changes",
566
- "the other — so a message may carry either, both or neither."
566
+ "the other — so a message may carry either, both or neither.",
567
+ "",
568
+ "`avatars3d` (one worn character per rig family) is re-sent the same",
569
+ "way, and also after a 3D character bought through",
570
+ "`gem.requestAvatar3dPurchase` is worn: the parent sends this message",
571
+ "BEFORE it answers `gem.purchaseResult`, so a game that redraws from",
572
+ "the player already shows the purchase when the answer lands."
567
573
  ],
568
574
  "fields": {
569
575
  "untrusted": {
@@ -672,6 +678,70 @@
672
678
  "optional": true,
673
679
  "$comment": "The worn 3D character's PORTRAIT RENDER (gem-api `avatar_3d_icon_url`) — a picture of the model, for a roster or a menu where loading the model is too much. NOT the 2D portrait (`avatarIconUrl`), which is a separate worn choice. Absent when no 3D character is worn. Same pin caveat as `avatarIconUrl`."
674
680
  },
681
+ "avatars3d": {
682
+ "kind": "array",
683
+ "maxItems": 8,
684
+ "optional": true,
685
+ "$comment": [
686
+ "Every 3D character the player WEARS, one per RIG FAMILY (gem-api",
687
+ "`avatars_3d`). A family is a rig contract — `small_human` and",
688
+ "`adult_human` today — and a player wears at most one character per",
689
+ "family, so a game that accepts several families reads the one it",
690
+ "draws by `family`.",
691
+ "",
692
+ "THE FLAT FIELDS STAY, AND THEY ARE ONE OF THESE. `avatar3dId`,",
693
+ "`avatar3dModelUrl` and `avatar3dIconUrl` carry the worn character of",
694
+ "the game's FIRST declared family that has one (`small_human` when the",
695
+ "game declares none), so an older frame keeps working unchanged. A",
696
+ "parent that sends this list sends the flat fields too, describing the",
697
+ "same item as the matching entry.",
698
+ "",
699
+ "ABSENT OR EMPTY means no 3D character is worn in any family — the",
700
+ "game's default look — or an older parent. Bounded at eight, the",
701
+ "most families a game may declare; a parent sends the families the",
702
+ "game declared, in the game's order, and drops an entry it cannot fit",
703
+ "in these bounds rather than losing the whole message to it.",
704
+ "",
705
+ "THE URLS ARE BOUNDED AT 256, NOT 2048 like the flat fields, because",
706
+ "of `maxMessageBytes`: eight entries of two 2048-character URLs would",
707
+ "declare about 98 KB on their own, past the 64 KB cap the whole",
708
+ "message is held to. An item CDN URL is about a hundred characters.",
709
+ "",
710
+ "Platform-assigned, so it sits on the trusted side of the `untrusted`",
711
+ "split. `id` and `family` are BOUNDED HERE AND SHAPE-CHECKED BY THE",
712
+ "FRAME, as `avatar3dId` is: the frame keeps an entry only when `id` is",
713
+ "a UUID and `family` is a lowercase code, and drops the entry — never",
714
+ "the message — otherwise. The URLs carry `avatarIconUrl`'s pin caveat:",
715
+ "THE CONSUMER MUST PIN THE ORIGIN before fetching either."
716
+ ],
717
+ "items": {
718
+ "kind": "object",
719
+ "fields": {
720
+ "family": {
721
+ "kind": "string",
722
+ "maxLength": 32,
723
+ "$comment": "The rig family (gem-api `family`), e.g. `small_human` or `adult_human`. A lowercase code, checked by the frame rather than by a format here, so one family name this frame does not recognise costs the entry and never the message."
724
+ },
725
+ "id": {
726
+ "kind": "string",
727
+ "maxLength": 64,
728
+ "$comment": "The worn character's item-definition id (gem-api `avatar_3d_id`), a UUID. Shape-checked by the frame like `avatar3dId`."
729
+ },
730
+ "modelUrl": {
731
+ "kind": "string",
732
+ "maxLength": 256,
733
+ "format": "url",
734
+ "$comment": "The character's binary glTF (gem-api `model_url`), immutable at its URL and built on the family's rig. Same pin caveat as `avatarIconUrl`."
735
+ },
736
+ "iconUrl": {
737
+ "kind": "string",
738
+ "maxLength": 256,
739
+ "format": "url",
740
+ "$comment": "A rendered portrait of the character (gem-api `icon_url`). Same pin caveat as `avatarIconUrl`."
741
+ }
742
+ }
743
+ }
744
+ },
675
745
  "badges": {
676
746
  "kind": "array",
677
747
  "maxItems": 2,
@@ -874,8 +944,8 @@
874
944
  "and `touchMovement`, Roblox's per-device camera settings. A parent built",
875
945
  "against an older SDK sends the object without them, and the SDK fills",
876
946
  "each missing one with its default (100, 100, false, `floating`), so on",
877
- "the game side all four are present whenever the object is. A game says",
878
- "which of them it leaves to the player with `gem.controlsDeclare`.",
947
+ "the game side all four are present whenever the object is. They are the",
948
+ "player's Arcade-wide defaults; a game says nothing back about them.",
879
949
  "",
880
950
  "THE GRAPHICS TWO ARE OPTIONAL INSIDE IT TOO (owner decision",
881
951
  "2026-10-11): `graphicsQuality` and `graphicsQualityAuto`, the quality",
@@ -2611,8 +2681,8 @@
2611
2681
  "moved (or is about to move) a progression mapped onto a platform currency,",
2612
2682
  "and the game would like the arcade to show it landing: the currency flying",
2613
2683
  "from `origin` into the arcade's chrome — in the Gem Arcade, into its Gem",
2614
- "button at the frame's top-left — and the total moving when the platform",
2615
- "confirms the grant.",
2684
+ "button, in whichever corner of the frame it sits — and the total moving",
2685
+ "when the platform confirms the grant.",
2616
2686
  "",
2617
2687
  "A PRESENTATION CUE, NOT A CLAIM. Nothing here is trusted as a number. The",
2618
2688
  "amount that lands and the player's new total reach the parent from gem-api",
@@ -2739,7 +2809,16 @@
2739
2809
  "direction": "frameToParent",
2740
2810
  "transport": "port",
2741
2811
  "$comment": [
2742
- "Which of the player's camera and controls settings this game leaves to",
2812
+ "DEPRECATED, NEVER SENT BY THIS SDK (owner decision 2026-10-12). The",
2813
+ "arcade's Settings show the player's own Arcade-wide defaults and nothing",
2814
+ "a game says, so `gem.controls.declare` is gone and the SDK no longer",
2815
+ "advertises `controls`. A parent ACCEPTS AND DROPS it from an older SDK",
2816
+ "for the N-1 window (DESIGN §9): no `unsupported_capability`, no",
2817
+ "`rate_limited`, no answer. A sender stopping is not a breaking change,",
2818
+ "so `v` stays; the type is removed from this schema in a later, separate",
2819
+ "change.",
2820
+ "",
2821
+ "What it was: which of the player's camera and controls settings this game leaves to",
2743
2822
  "the player and which it decides itself — Roblox's UserChoice versus",
2744
2823
  "`StarterPlayer.Dev*`, declared at runtime (owner decision 2026-10-11).",
2745
2824
  "The settings are the four camera and controls fields of",
@@ -2820,9 +2899,7 @@
2820
2899
  "keeps over a game, in a corner `gem.viewport.reserved` names, which is how",
2821
2900
  "a player leaves, mutes and reports a problem.",
2822
2901
  "",
2823
- " settings: true the game has a settings screen and will open it when",
2824
- " told to; a parent MAY show a Settings entry for it",
2825
- " (the Gem Arcade no longer does: see below)",
2902
+ " settings DEPRECATED, always false from this SDK: see below",
2826
2903
  " items up to four entries of the game's own, in this order,",
2827
2904
  " below the arcade's entries and visibly apart from them",
2828
2905
  "",
@@ -2852,16 +2929,22 @@
2852
2929
  "Gated by `gameMenu`: sent only to a parent that advertised it. To any",
2853
2930
  "other, `gem.setMenu` returns false.",
2854
2931
  "",
2855
- "THE GEM ARCADE NO LONGER SHOWS A SETTINGS ENTRY FOR `settings: true`.",
2856
- "A game draws its own settings button (top-right is the recommendation)",
2857
- "and the Arcade's menu holds the player's Arcade-wide settings instead,",
2858
- "which reach the game as `gem.prefs.recommended`. The field STAYS: removing",
2859
- "it is a `v` bump, every game that sends it keeps working, and another",
2860
- "embedder may still show the entry. A parent that ignores it sends no",
2861
- "`gem.menuSelect` for it."
2932
+ "`settings` IS DEPRECATED (owner decision 2026-10-12). It said the game",
2933
+ "had a settings screen the parent's menu could open. A game's settings",
2934
+ "open from its own button, never from the parent's menu, which holds the",
2935
+ "player's Arcade-wide settings instead (they reach the game as",
2936
+ "`gem.prefs.recommended`). This SDK ALWAYS SENDS `false` and never claims",
2937
+ "a settings screen; an older one may still send `true`. A parent IGNORES",
2938
+ "it: it draws no Settings entry for a game and sends no `gem.menuSelect`",
2939
+ "for one. The field stays REQUIRED on the wire for the N-1 window (a",
2940
+ "parent validating against an earlier copy refuses a `gem.menu` without",
2941
+ "it); removing it is a later, separate change."
2862
2942
  ],
2863
2943
  "fields": {
2864
- "settings": { "kind": "boolean" },
2944
+ "settings": {
2945
+ "kind": "boolean",
2946
+ "$comment": "DEPRECATED (owner decision 2026-10-12): always false from this SDK, and ignored by a parent. Required on the wire for the N-1 window."
2947
+ },
2865
2948
  "items": {
2866
2949
  "kind": "array",
2867
2950
  "maxItems": 4,
@@ -2891,11 +2974,15 @@
2891
2974
  "direction": "parentToFrame",
2892
2975
  "transport": "port",
2893
2976
  "$comment": [
2894
- "The player chose one of the game's entries in the arcade's menu: its",
2895
- "Settings entry, or one of the items `gem.menu` declared.",
2977
+ "The player chose one of the game's entries in the arcade's menu: one of",
2978
+ "the items `gem.menu` declared.",
2896
2979
  "",
2897
- " entry: settings open your settings screen",
2898
2980
  " entry: item, id the item with this id",
2981
+ " entry: settings DEPRECATED (owner decision 2026-10-12): it",
2982
+ " opened the settings screen of a game that sent",
2983
+ " `settings: true`. This SDK never does, and ignores",
2984
+ " it; an older one still opens its settings on it.",
2985
+ " A parent sends it to no game.",
2899
2986
  "",
2900
2987
  "Sent only for what the frame's LATEST `gem.menu` declared, and only to a",
2901
2988
  "frame that advertised `gameMenu`. The parent closes its menu first, so the",
@@ -2921,6 +3008,53 @@
2921
3008
  "rateLimit": { "perMinute": 60 }
2922
3009
  },
2923
3010
 
3011
+ {
3012
+ "type": "gem.gemCorner",
3013
+ "direction": "frameToParent",
3014
+ "transport": "port",
3015
+ "$comment": [
3016
+ "The corner of the frame the game would like the arcade's own control",
3017
+ "over it to sit in - in the Gem Arcade, the Gem button that opens its",
3018
+ "menu. With no message at all it sits top-left, which is also the",
3019
+ "recommendation: a game asks for another corner only when its own HUD",
3020
+ "needs that one.",
3021
+ "",
3022
+ "SCREEN STATE, NOT A TOGGLE, like `gem.menu`. Each message is the whole",
3023
+ "answer and the latest one wins. The SDK holds it from before the bridge",
3024
+ "is up and re-sends it after every handshake, so a parent that reconnects",
3025
+ "puts the control where the game had asked.",
3026
+ "",
3027
+ "A REQUEST, APPLIED ON THE PARENT'S TERMS. The parent holds a move while",
3028
+ "the player is using the control or one of its surfaces is open, keeps a",
3029
+ "minimum dwell between moves and applies only the latest request after",
3030
+ "one, and puts a player's own choice of corner, should it ever offer one,",
3031
+ "above the game's. So a game never lays out around the corner it asked",
3032
+ "for: it lays out around the rectangles `gem.viewport` sends in",
3033
+ "`reserved`, which say where the control actually is.",
3034
+ "",
3035
+ "ONE ENUM, AND IT ONLY GROWS. A later anchor is a new value behind a",
3036
+ "capability of its own, sent only to a parent that advertised that",
3037
+ "capability, so `v` does not move; a parent that advertised only",
3038
+ "`gemCorner` is only ever sent these four.",
3039
+ "",
3040
+ "A PRESENTATION REQUEST ONLY, and not an authorization control. The game",
3041
+ "picks one of four places the parent defines, never a coordinate, a size,",
3042
+ "a visibility or a look; the parent's dwell and holds, not this rate",
3043
+ "limit, are what keep a game from making the control run away.",
3044
+ "",
3045
+ "Gated by `gemCorner`: sent only to a parent that advertised it. To any",
3046
+ "other, `gem.setGemCorner` returns false."
3047
+ ],
3048
+ "fields": {
3049
+ "corner": {
3050
+ "kind": "enum",
3051
+ "values": ["top-left", "top-right", "bottom-left", "bottom-right"],
3052
+ "$comment": "Physical, not logical: `left` is the frame's left edge in every language, so a right-to-left locale never mirrors a game's choice."
3053
+ }
3054
+ },
3055
+ "rateLimit": { "perMinute": 12 }
3056
+ },
3057
+
2924
3058
  {
2925
3059
  "type": "gem.requestShare",
2926
3060
  "direction": "frameToParent",
@@ -3430,6 +3564,94 @@
3430
3564
  "$comment": "Bounds a BUGGY game. NOT a security control: a hostile game posts on the port it holds rather than calling the SDK, so a frame-side limit is one the attacker enforces against itself. See METER IT IN MONEY, SERVER-SIDE above."
3431
3565
  }
3432
3566
  },
3567
+ {
3568
+ "type": "gem.requestAvatar3dPurchase",
3569
+ "direction": "frameToParent",
3570
+ "transport": "port",
3571
+ "$comment": [
3572
+ "Ask the parent to buy one of THIS GAME's 3D avatars (an Arcade item of",
3573
+ "type `avatar_3d` the game owns and has put on sale) with platform",
3574
+ "currency, on the player's behalf, and WEAR it. Gated by the",
3575
+ "`avatar3dPurchases` capability. Answered by the existing",
3576
+ "`gem.purchaseResult`, whose `purchases` gate every SDK that can send",
3577
+ "this also advertises.",
3578
+ "",
3579
+ "A SEPARATE MESSAGE, NOT A THIRD `purchaseKind`. `gem.requestPurchase`'s",
3580
+ "`purchaseKind` is a closed enum, and `check-schema-freeze` refuses a",
3581
+ "new value there because an N-1 parent would refuse the whole message.",
3582
+ "And the two buy different things: that one grants a game-scope item",
3583
+ "through the game's purchase catalog; this one grants a PLATFORM item",
3584
+ "the player owns across the Arcade, priced on the item's own offer",
3585
+ "(gem-api `GET /v1/games/{game_id}/avatars-3d`), and puts it on.",
3586
+ "",
3587
+ "EVERY OBLIGATION ON `gem.requestPurchase` APPLIES HERE, UNCHANGED:",
3588
+ "payable currencies only, resolved exactly; price it yourself from",
3589
+ "platform records, under the player's session (the offer's price in",
3590
+ "`currencySlug`); the confirmation is the parent's own sheet, on user",
3591
+ "activation only; back off after a cancel; one prompt at a time, quiet",
3592
+ "after paint; one purchase per ask; meter it in money, server-side;",
3593
+ "treat every field as untrusted text. A shortfall is never a sale.",
3594
+ "",
3595
+ "AND THREE OF ITS OWN:",
3596
+ "",
3597
+ " WEAR IT, THEN SAY SO, THEN ANSWER. On success the purchase wears the",
3598
+ " avatar in its family's slot (gem-api's purchase does both in one",
3599
+ " transaction). Re-send `gem.player`, with the new character in",
3600
+ " `avatars3d` and in the flat `avatar3d*` fields where its family is the",
3601
+ " game's first, BEFORE `gem.purchaseResult` `ok`, so a game that",
3602
+ " redraws from the player never shows the old look after an `ok`.",
3603
+ "",
3604
+ " ALREADY OWNED IS A DECLINE, NOT A SALE. A player who owns the item is",
3605
+ " answered `declined` / `already_owned` before any sheet; a parent may",
3606
+ " instead offer to wear it in its own chrome, which is a wear, not a",
3607
+ " purchase, and answers `declined` / `already_owned` all the same.",
3608
+ "",
3609
+ " `purchaseId` IS OPTIONAL HERE. The SDK confirms an `ok` against the",
3610
+ " platform by asking whether the player now OWNS the item (gem-api",
3611
+ " `GET /v1/games/{game_id}/avatars-3d/{item_id}/owned`, with the game's",
3612
+ " own token), not by reading a receipt, so a parent may omit it. When",
3613
+ " sent it follows `gem.purchaseResult.purchaseId`'s rules: an opaque",
3614
+ " token minted for the disclosure, never an order id.",
3615
+ "",
3616
+ "REASONS. `gem.purchaseResult.reason`'s allow-list holds, with these",
3617
+ "rows for this ask's own refusals:",
3618
+ "",
3619
+ " CONDITION OUTCOME REASON",
3620
+ " the player already owns the item declined already_owned",
3621
+ " gem-api 404 not_found, item_not_purchasable, declined not_purchasable",
3622
+ " or not_yet_writable (sales are off); the",
3623
+ " item is retired, not this game's, not an",
3624
+ " avatar, or not on sale",
3625
+ " the offer has no price in that currency declined no_price_in_currency",
3626
+ " gem-api 402 insufficient_balance or declined insufficient_funds",
3627
+ " wallet_negative",
3628
+ " gem-api 409 price_mismatch declined price_mismatch",
3629
+ "",
3630
+ "and everything else as `gem.purchaseResult.reason` says — `error` for",
3631
+ "anything not listed there or here."
3632
+ ],
3633
+ "fields": {
3634
+ "requestId": {
3635
+ "kind": "string",
3636
+ "maxLength": 64,
3637
+ "$comment": "Opaque and frame-chosen. Echo-only, exactly as on gem.requestPurchase: not unique across tabs or reloads, never a key on the money path, and not safe to write into a log line unescaped."
3638
+ },
3639
+ "itemId": {
3640
+ "kind": "string",
3641
+ "maxLength": 64,
3642
+ "$comment": "The avatar's item-definition id (gem-api `item_id` on an offer), a UUID. Matched by the parent against the offers it read itself; never interpolated into a path unchecked."
3643
+ },
3644
+ "currencySlug": {
3645
+ "kind": "string",
3646
+ "maxLength": 32,
3647
+ "$comment": "Which platform currency to pay in, by its exact slug: GEMS or ARCADE_TICKETS. The offer must carry a price in it (`no_price_in_currency` otherwise). Frame-chosen and unconstrained here, exactly as on gem.requestPurchase, so the parent resolves it against platform records."
3648
+ }
3649
+ },
3650
+ "rateLimit": {
3651
+ "perMinute": 12,
3652
+ "$comment": "gem.requestPurchase's limit and its argument: it bounds a BUGGY game, not a hostile one."
3653
+ }
3654
+ },
3433
3655
  {
3434
3656
  "type": "gem.purchaseResult",
3435
3657
  "direction": "parentToFrame",
@@ -3437,6 +3659,11 @@
3437
3659
  "$comment": [
3438
3660
  "The answer to one gem.requestPurchase. Gated by the `purchases` capability.",
3439
3661
  "",
3662
+ "ALSO the answer to one gem.requestAvatar3dPurchase,",
3663
+ "with the same fields and the same three outcomes; that message lists the",
3664
+ "reasons of its own and says why `purchaseId` may be absent on its `ok`.",
3665
+ "The `already_owned` reason is sent only there.",
3666
+ "",
3440
3667
  "THREE OUTCOMES, AND A REASON ON THE TWO THAT ARE NOT `ok`. `outcome` says",
3441
3668
  "what the game should DO;",
3442
3669
  "`reason` says WHY, so a developer can fix a config mistake instead of",
@@ -27,6 +27,12 @@
27
27
  "gem.purchaseResult"
28
28
  ]
29
29
  },
30
+ "avatar3dPurchases": {
31
+ "implemented": true,
32
+ "gates": [
33
+ "gem.requestAvatar3dPurchase"
34
+ ]
35
+ },
30
36
  "friendChallenges": {
31
37
  "implemented": true,
32
38
  "gates": [
@@ -230,6 +236,12 @@
230
236
  "gates": [
231
237
  "gem.controlsDeclare"
232
238
  ]
239
+ },
240
+ "gemCorner": {
241
+ "implemented": true,
242
+ "gates": [
243
+ "gem.gemCorner"
244
+ ]
233
245
  }
234
246
  }
235
247
  }