@indiegems/gem-web-sdk 11.1.2 → 13.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.
- package/CHANGELOG.md +37 -0
- package/README.md +3 -1
- package/dist/function.d.ts +9 -1
- package/dist/game.d.ts +88 -13
- package/dist/gem-web-sdk.bridge.js +1 -1
- package/dist/gem-web-sdk.esm.js +2 -2
- package/dist/gem-web-sdk.function.js +2 -2
- package/dist/gem-web-sdk.game.js +2 -2
- package/dist/gem-web-sdk.min.js +2 -2
- package/dist/gem-web-sdk.sdk.js +2 -2
- package/dist/gem-web-sdk.server-api.js +1 -1
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +100 -16
- package/dist/sdk.d.ts +98 -14
- package/dist/server-api.d.ts +9 -1
- package/dist/server.d.ts +10 -1
- package/docs/api/api-reference-function.md +8076 -0
- package/docs/api/api-reference-game.md +719 -165
- package/docs/api/api-reference-server.md +5 -3
- package/docs/api/reference.json +10310 -225
- package/docs/arcade-avatars.md +303 -0
- package/docs/changelog.md +37 -0
- package/docs/getting-started.md +5 -2
- package/docs/manifest.json +21 -16
- package/docs/rooms-and-matches.md +64 -1
- package/docs/saving-player-progress.md +16 -10
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions-js.md +118 -44
- package/docs/serverless-functions.md +26 -11
- package/docs/what-a-game-must-do.md +53 -46
- package/package.json +1 -1
- package/schema/bridge.v1.json +282 -18
- package/schema/capabilities.public.json +12 -0
package/schema/bridge.v1.json
CHANGED
|
@@ -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 —",
|
|
688
|
+
"`small_human` is the first — and a player wears at most one character",
|
|
689
|
+
"per 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`. 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.
|
|
878
|
-
"
|
|
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",
|
|
@@ -1514,6 +1584,90 @@
|
|
|
1514
1584
|
},
|
|
1515
1585
|
"rateLimit": { "perMinute": 30 }
|
|
1516
1586
|
},
|
|
1587
|
+
{
|
|
1588
|
+
"type": "gem.ticketEvent",
|
|
1589
|
+
"direction": "parentToFrame",
|
|
1590
|
+
"transport": "port",
|
|
1591
|
+
"$comment": [
|
|
1592
|
+
"A matchmaking ticket this player is on changed state. Gated by the",
|
|
1593
|
+
"`ticketEvents` capability; the mapping lives in schema/capabilities.json",
|
|
1594
|
+
"and nowhere else.",
|
|
1595
|
+
"",
|
|
1596
|
+
"gem-api's `matchmaking_ticket_queued`, `_matched`, `_expired`,",
|
|
1597
|
+
"`_cancelled` and `_failed`, which it sends to EVERY player a ticket",
|
|
1598
|
+
"seats: its owner and, for a party's ticket, every member. A party",
|
|
1599
|
+
"queues as one ticket and it is the leader's, so a member's frame is on a",
|
|
1600
|
+
"ticket it never created and cannot read — only the owner may. This is",
|
|
1601
|
+
"the only thing that can tell that frame its party is queued, and how",
|
|
1602
|
+
"the wait ended.",
|
|
1603
|
+
"",
|
|
1604
|
+
"A HINT, LIKE EVERY RELAY, and for a member a hint with nothing behind",
|
|
1605
|
+
"it to re-read. So the frame gives it exactly the authority of a",
|
|
1606
|
+
"display: a member's view of the ticket can be shown and cannot be",
|
|
1607
|
+
"cancelled or kept alive, and nothing is entered on the strength of it.",
|
|
1608
|
+
"A `matched` makes the frame look for the seat over its OWN token, which",
|
|
1609
|
+
"is where a room is believed. For the ticket's OWNER the event is a cue",
|
|
1610
|
+
"to ask the platform at once, exactly as a relayed match start is.",
|
|
1611
|
+
"",
|
|
1612
|
+
"ITS OWN TYPE, for the reason gem.seatedEvent gives: an unknown TYPE is",
|
|
1613
|
+
"ignored by any frame, and the capability keeps the parent from sending",
|
|
1614
|
+
"it to a frame that would not understand it.",
|
|
1615
|
+
"",
|
|
1616
|
+
"NOTHING ABOUT ANYBODY CROSSES. The push names no player and no other",
|
|
1617
|
+
"ticket, and neither does this. The templates gem-api lists on `queued`",
|
|
1618
|
+
"stop at the parent: the frame's game chose them, or its leader's did.",
|
|
1619
|
+
"",
|
|
1620
|
+
"`event` IS CLOSED, and it is the ticket's status. A status gem-api adds",
|
|
1621
|
+
"later is not relayed until this enum names it — the parent drops what",
|
|
1622
|
+
"it cannot name rather than send a value the frame would refuse.",
|
|
1623
|
+
"",
|
|
1624
|
+
"PARENT TO FRAME ONLY. A frame-authored ticket event would let a game",
|
|
1625
|
+
"tell the parent, and through it nobody, that a queue it does not own",
|
|
1626
|
+
"moved."
|
|
1627
|
+
],
|
|
1628
|
+
"fields": {
|
|
1629
|
+
"event": {
|
|
1630
|
+
"kind": "enum",
|
|
1631
|
+
"values": ["queued", "matched", "expired", "cancelled", "failed"],
|
|
1632
|
+
"$comment": "The ticket's status after the change. `queued` is its creation; every other value is final."
|
|
1633
|
+
},
|
|
1634
|
+
"ticketId": { "kind": "string", "maxLength": 64 },
|
|
1635
|
+
"partySize": {
|
|
1636
|
+
"kind": "int",
|
|
1637
|
+
"min": 1,
|
|
1638
|
+
"max": 64,
|
|
1639
|
+
"optional": true,
|
|
1640
|
+
"$comment": "How many players the ticket seats. Sent with `queued`."
|
|
1641
|
+
},
|
|
1642
|
+
"queuedAt": {
|
|
1643
|
+
"kind": "int",
|
|
1644
|
+
"min": 0,
|
|
1645
|
+
"max": 4102444800,
|
|
1646
|
+
"optional": true,
|
|
1647
|
+
"$comment": "When the ticket was queued, in Unix seconds. Sent with `queued`."
|
|
1648
|
+
},
|
|
1649
|
+
"expiresAt": {
|
|
1650
|
+
"kind": "int",
|
|
1651
|
+
"min": 0,
|
|
1652
|
+
"max": 4102444800,
|
|
1653
|
+
"optional": true,
|
|
1654
|
+
"$comment": "When the ticket gives up if it is still waiting, in Unix seconds. Sent with `queued`. A frame that has seen no ending once this long has passed stops showing the ticket as waiting: the pushes are not retried, and a member has no other way to learn the ticket is over."
|
|
1655
|
+
},
|
|
1656
|
+
"reason": {
|
|
1657
|
+
"kind": "string",
|
|
1658
|
+
"maxLength": 64,
|
|
1659
|
+
"optional": true,
|
|
1660
|
+
"$comment": "Why a ticket ended without a match: `timed_out`, `heartbeat_lapsed`, `requested`, `party_changed`, `handoff_failed`. Open, so a reason added later reaches the game."
|
|
1661
|
+
},
|
|
1662
|
+
"roomId": {
|
|
1663
|
+
"kind": "string",
|
|
1664
|
+
"maxLength": 64,
|
|
1665
|
+
"optional": true,
|
|
1666
|
+
"$comment": "The room a `matched` ticket's players were seated in. A place to look, not a seat: the frame reads the room over its own token and checks this player is a member."
|
|
1667
|
+
}
|
|
1668
|
+
},
|
|
1669
|
+
"rateLimit": { "perMinute": 60 }
|
|
1670
|
+
},
|
|
1517
1671
|
|
|
1518
1672
|
{
|
|
1519
1673
|
"type": "gem.signalEvent",
|
|
@@ -2655,7 +2809,16 @@
|
|
|
2655
2809
|
"direction": "frameToParent",
|
|
2656
2810
|
"transport": "port",
|
|
2657
2811
|
"$comment": [
|
|
2658
|
-
"
|
|
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",
|
|
2659
2822
|
"the player and which it decides itself — Roblox's UserChoice versus",
|
|
2660
2823
|
"`StarterPlayer.Dev*`, declared at runtime (owner decision 2026-10-11).",
|
|
2661
2824
|
"The settings are the four camera and controls fields of",
|
|
@@ -2736,9 +2899,7 @@
|
|
|
2736
2899
|
"keeps over a game, in a corner `gem.viewport.reserved` names, which is how",
|
|
2737
2900
|
"a player leaves, mutes and reports a problem.",
|
|
2738
2901
|
"",
|
|
2739
|
-
" settings
|
|
2740
|
-
" told to; a parent MAY show a Settings entry for it",
|
|
2741
|
-
" (the Gem Arcade no longer does: see below)",
|
|
2902
|
+
" settings DEPRECATED, always false from this SDK: see below",
|
|
2742
2903
|
" items up to four entries of the game's own, in this order,",
|
|
2743
2904
|
" below the arcade's entries and visibly apart from them",
|
|
2744
2905
|
"",
|
|
@@ -2768,16 +2929,22 @@
|
|
|
2768
2929
|
"Gated by `gameMenu`: sent only to a parent that advertised it. To any",
|
|
2769
2930
|
"other, `gem.setMenu` returns false.",
|
|
2770
2931
|
"",
|
|
2771
|
-
"
|
|
2772
|
-
"
|
|
2773
|
-
"
|
|
2774
|
-
"
|
|
2775
|
-
"
|
|
2776
|
-
"
|
|
2777
|
-
"`gem.menuSelect`
|
|
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."
|
|
2778
2942
|
],
|
|
2779
2943
|
"fields": {
|
|
2780
|
-
"settings": {
|
|
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
|
+
},
|
|
2781
2948
|
"items": {
|
|
2782
2949
|
"kind": "array",
|
|
2783
2950
|
"maxItems": 4,
|
|
@@ -2807,11 +2974,15 @@
|
|
|
2807
2974
|
"direction": "parentToFrame",
|
|
2808
2975
|
"transport": "port",
|
|
2809
2976
|
"$comment": [
|
|
2810
|
-
"The player chose one of the game's entries in the arcade's menu:
|
|
2811
|
-
"
|
|
2977
|
+
"The player chose one of the game's entries in the arcade's menu: one of",
|
|
2978
|
+
"the items `gem.menu` declared.",
|
|
2812
2979
|
"",
|
|
2813
|
-
" entry: settings open your settings screen",
|
|
2814
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.",
|
|
2815
2986
|
"",
|
|
2816
2987
|
"Sent only for what the frame's LATEST `gem.menu` declared, and only to a",
|
|
2817
2988
|
"frame that advertised `gameMenu`. The parent closes its menu first, so the",
|
|
@@ -3346,6 +3517,94 @@
|
|
|
3346
3517
|
"$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."
|
|
3347
3518
|
}
|
|
3348
3519
|
},
|
|
3520
|
+
{
|
|
3521
|
+
"type": "gem.requestAvatar3dPurchase",
|
|
3522
|
+
"direction": "frameToParent",
|
|
3523
|
+
"transport": "port",
|
|
3524
|
+
"$comment": [
|
|
3525
|
+
"Ask the parent to buy one of THIS GAME's 3D avatars (an Arcade item of",
|
|
3526
|
+
"type `avatar_3d` the game owns and has put on sale) with platform",
|
|
3527
|
+
"currency, on the player's behalf, and WEAR it. Gated by the",
|
|
3528
|
+
"`avatar3dPurchases` capability. Answered by the existing",
|
|
3529
|
+
"`gem.purchaseResult`, whose `purchases` gate every SDK that can send",
|
|
3530
|
+
"this also advertises.",
|
|
3531
|
+
"",
|
|
3532
|
+
"A SEPARATE MESSAGE, NOT A THIRD `purchaseKind`. `gem.requestPurchase`'s",
|
|
3533
|
+
"`purchaseKind` is a closed enum, and `check-schema-freeze` refuses a",
|
|
3534
|
+
"new value there because an N-1 parent would refuse the whole message.",
|
|
3535
|
+
"And the two buy different things: that one grants a game-scope item",
|
|
3536
|
+
"through the game's purchase catalog; this one grants a PLATFORM item",
|
|
3537
|
+
"the player owns across the Arcade, priced on the item's own offer",
|
|
3538
|
+
"(gem-api `GET /v1/games/{game_id}/avatars-3d`), and puts it on.",
|
|
3539
|
+
"",
|
|
3540
|
+
"EVERY OBLIGATION ON `gem.requestPurchase` APPLIES HERE, UNCHANGED:",
|
|
3541
|
+
"payable currencies only, resolved exactly; price it yourself from",
|
|
3542
|
+
"platform records, under the player's session (the offer's price in",
|
|
3543
|
+
"`currencySlug`); the confirmation is the parent's own sheet, on user",
|
|
3544
|
+
"activation only; back off after a cancel; one prompt at a time, quiet",
|
|
3545
|
+
"after paint; one purchase per ask; meter it in money, server-side;",
|
|
3546
|
+
"treat every field as untrusted text. A shortfall is never a sale.",
|
|
3547
|
+
"",
|
|
3548
|
+
"AND THREE OF ITS OWN:",
|
|
3549
|
+
"",
|
|
3550
|
+
" WEAR IT, THEN SAY SO, THEN ANSWER. On success the purchase wears the",
|
|
3551
|
+
" avatar in its family's slot (gem-api's purchase does both in one",
|
|
3552
|
+
" transaction). Re-send `gem.player`, with the new character in",
|
|
3553
|
+
" `avatars3d` and in the flat `avatar3d*` fields where its family is the",
|
|
3554
|
+
" game's first, BEFORE `gem.purchaseResult` `ok`, so a game that",
|
|
3555
|
+
" redraws from the player never shows the old look after an `ok`.",
|
|
3556
|
+
"",
|
|
3557
|
+
" ALREADY OWNED IS A DECLINE, NOT A SALE. A player who owns the item is",
|
|
3558
|
+
" answered `declined` / `already_owned` before any sheet; a parent may",
|
|
3559
|
+
" instead offer to wear it in its own chrome, which is a wear, not a",
|
|
3560
|
+
" purchase, and answers `declined` / `already_owned` all the same.",
|
|
3561
|
+
"",
|
|
3562
|
+
" `purchaseId` IS OPTIONAL HERE. The SDK confirms an `ok` against the",
|
|
3563
|
+
" platform by asking whether the player now OWNS the item (gem-api",
|
|
3564
|
+
" `GET /v1/games/{game_id}/avatars-3d/{item_id}/owned`, with the game's",
|
|
3565
|
+
" own token), not by reading a receipt, so a parent may omit it. When",
|
|
3566
|
+
" sent it follows `gem.purchaseResult.purchaseId`'s rules: an opaque",
|
|
3567
|
+
" token minted for the disclosure, never an order id.",
|
|
3568
|
+
"",
|
|
3569
|
+
"REASONS. `gem.purchaseResult.reason`'s allow-list holds, with these",
|
|
3570
|
+
"rows for this ask's own refusals:",
|
|
3571
|
+
"",
|
|
3572
|
+
" CONDITION OUTCOME REASON",
|
|
3573
|
+
" the player already owns the item declined already_owned",
|
|
3574
|
+
" gem-api 404 not_found, item_not_purchasable, declined not_purchasable",
|
|
3575
|
+
" or not_yet_writable (sales are off); the",
|
|
3576
|
+
" item is retired, not this game's, not an",
|
|
3577
|
+
" avatar, or not on sale",
|
|
3578
|
+
" the offer has no price in that currency declined no_price_in_currency",
|
|
3579
|
+
" gem-api 402 insufficient_balance or declined insufficient_funds",
|
|
3580
|
+
" wallet_negative",
|
|
3581
|
+
" gem-api 409 price_mismatch declined price_mismatch",
|
|
3582
|
+
"",
|
|
3583
|
+
"and everything else as `gem.purchaseResult.reason` says — `error` for",
|
|
3584
|
+
"anything not listed there or here."
|
|
3585
|
+
],
|
|
3586
|
+
"fields": {
|
|
3587
|
+
"requestId": {
|
|
3588
|
+
"kind": "string",
|
|
3589
|
+
"maxLength": 64,
|
|
3590
|
+
"$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."
|
|
3591
|
+
},
|
|
3592
|
+
"itemId": {
|
|
3593
|
+
"kind": "string",
|
|
3594
|
+
"maxLength": 64,
|
|
3595
|
+
"$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."
|
|
3596
|
+
},
|
|
3597
|
+
"currencySlug": {
|
|
3598
|
+
"kind": "string",
|
|
3599
|
+
"maxLength": 32,
|
|
3600
|
+
"$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."
|
|
3601
|
+
}
|
|
3602
|
+
},
|
|
3603
|
+
"rateLimit": {
|
|
3604
|
+
"perMinute": 12,
|
|
3605
|
+
"$comment": "gem.requestPurchase's limit and its argument: it bounds a BUGGY game, not a hostile one."
|
|
3606
|
+
}
|
|
3607
|
+
},
|
|
3349
3608
|
{
|
|
3350
3609
|
"type": "gem.purchaseResult",
|
|
3351
3610
|
"direction": "parentToFrame",
|
|
@@ -3353,6 +3612,11 @@
|
|
|
3353
3612
|
"$comment": [
|
|
3354
3613
|
"The answer to one gem.requestPurchase. Gated by the `purchases` capability.",
|
|
3355
3614
|
"",
|
|
3615
|
+
"ALSO the answer to one gem.requestAvatar3dPurchase,",
|
|
3616
|
+
"with the same fields and the same three outcomes; that message lists the",
|
|
3617
|
+
"reasons of its own and says why `purchaseId` may be absent on its `ok`.",
|
|
3618
|
+
"The `already_owned` reason is sent only there.",
|
|
3619
|
+
"",
|
|
3356
3620
|
"THREE OUTCOMES, AND A REASON ON THE TWO THAT ARE NOT `ok`. `outcome` says",
|
|
3357
3621
|
"what the game should DO;",
|
|
3358
3622
|
"`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": [
|
|
@@ -93,6 +99,12 @@
|
|
|
93
99
|
"gem.autoStartFailedEvent"
|
|
94
100
|
]
|
|
95
101
|
},
|
|
102
|
+
"ticketEvents": {
|
|
103
|
+
"implemented": true,
|
|
104
|
+
"gates": [
|
|
105
|
+
"gem.ticketEvent"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
96
108
|
"partySeating": {
|
|
97
109
|
"implemented": true,
|
|
98
110
|
"gates": [
|