@indiegems/gem-web-sdk 7.4.0 → 7.4.1

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 (38) hide show
  1. package/README.md +54 -0
  2. package/dist/gem-web-sdk.esm.js +1 -1
  3. package/dist/gem-web-sdk.game.js +1 -1
  4. package/dist/gem-web-sdk.min.js +1 -1
  5. package/dist/gem-web-sdk.sdk.js +1 -1
  6. package/dist/gem-web-sdk.server.js +1 -1
  7. package/docs/achievements.md +232 -0
  8. package/docs/analytics.md +271 -0
  9. package/docs/api/api-reference-game.md +15308 -0
  10. package/docs/api/api-reference-server.md +4397 -0
  11. package/docs/api/reference.json +24130 -0
  12. package/docs/arcade-economy.md +263 -0
  13. package/docs/async-opponents.md +396 -0
  14. package/docs/brand-art.md +334 -0
  15. package/docs/cross-promotion.md +137 -0
  16. package/docs/daily-challenges.md +530 -0
  17. package/docs/dedicated-server-flow.md +879 -0
  18. package/docs/deprecation-policy.md +124 -0
  19. package/docs/friends.md +233 -0
  20. package/docs/game-economy-tutorial.md +326 -0
  21. package/docs/getting-started.md +552 -0
  22. package/docs/inventory-and-catalogs.md +1035 -0
  23. package/docs/leaderboards.md +277 -0
  24. package/docs/manifest.json +135 -0
  25. package/docs/match-session.md +288 -0
  26. package/docs/p2p-and-parties.md +783 -0
  27. package/docs/platform-rules.md +372 -0
  28. package/docs/player-sign-in.md +89 -0
  29. package/docs/room-shapes.md +195 -0
  30. package/docs/rooms-and-matches.md +779 -0
  31. package/docs/saving-player-progress.md +457 -0
  32. package/docs/server-bundles.md +1041 -0
  33. package/docs/serverless-functions.md +748 -0
  34. package/docs/trading-and-gifting.md +98 -0
  35. package/docs/what-a-game-must-do.md +1231 -0
  36. package/package.json +7 -2
  37. package/schema/bridge.v1.json +5 -5
  38. package/schema/capabilities.public.json +189 -0
@@ -0,0 +1,124 @@
1
+ # When a deprecated surface stops working
2
+
3
+ A handful of the surfaces in this SDK are marked **deprecated**. The mark tells you there is
4
+ something better to call. It does not tell you how long the old one has, and that is the
5
+ question you actually need answered before you can decide what to do about it. This page is
6
+ the answer, and it is the only place in this documentation that gives it.
7
+
8
+ Read it when something your game calls is marked deprecated and you are choosing between
9
+ migrating now and folding the migration into your next major upgrade. If nothing you call is
10
+ marked, you do not need this page.
11
+
12
+ - [1. What this covers](#1-what-this-covers)
13
+ - [2. The rule for the package's API surface](#2-the-rule-for-the-packages-api-surface)
14
+ - [3. Runtime-delivered pushes are a separate question](#3-runtime-delivered-pushes-are-a-separate-question)
15
+ - [4. What is deprecated today](#4-what-is-deprecated-today)
16
+ - [5. What this is not](#5-what-this-is-not)
17
+
18
+ ---
19
+
20
+ ## 1. What this covers
21
+
22
+ This policy governs the **API surface of `@indiegems/gem-web-sdk`**: the exports, methods,
23
+ fields and options your game compiles against. A deprecation on one of those is a statement
24
+ about package releases, and section 2 is the statement.
25
+
26
+ **A deprecated surface still works.** The mark removes nothing and changes no behaviour. It is
27
+ a notice period, and the point of a notice period is that you get to spend it rather than
28
+ react to it.
29
+
30
+ Four of the surfaces in section 4 are only half a package question, because the value behind
31
+ them is pushed to your server bundle by the platform rather than returned by a call you make.
32
+ Section 3 is that other half. And three separate things on this platform carry a version;
33
+ section 5 says which of them this is.
34
+
35
+ ## 2. The rule for the package's API surface
36
+
37
+ **A surface deprecated in a `3.x` release keeps working for every remaining `3.x` release, and
38
+ `4.0.0` is the earliest release that may remove it.** The same rule holds at every major: a
39
+ surface first deprecated in a `4.x` release works for the rest of `4.x`, and `5.0.0` is the
40
+ earliest that may remove it. A major that deprecates a surface never removes it in the same
41
+ breath.
42
+
43
+ *Earliest release that may* is the whole of the strength, and it is worth reading twice. It is
44
+ a floor: it names the soonest a removal could reach you, which is the number a migration plan
45
+ needs. It is not a schedule. It does not say the surface is removed in `4.0.0`, and it does
46
+ not put a date on `4.0.0`.
47
+
48
+ What the floor buys you is that the removal lands on an upgrade you already control. No minor
49
+ and no patch release inside `3.x` takes a deprecated surface away, so a dependency range that
50
+ stays inside `3.x` cannot lose one underneath you — not on an install, not on a lockfile
51
+ refresh, not on a transitive bump. The next time you have to think about any of this is the
52
+ next time you deliberately change a major version.
53
+
54
+ This is what package-level semver already costs here rather than a new promise:
55
+ [`server-bundles.md`](server-bundles.md) records the whole package going to `3.0.0` for a
56
+ single removed export, because that is the price of taking a surface away.
57
+
58
+ **So migrate at the major, not at the mark.** A deprecation is worth a note in your backlog
59
+ the day you read it; it is worth your afternoon only when you are already planning the major
60
+ upgrade it lands on.
61
+
62
+ ## 3. Runtime-delivered pushes are a separate question
63
+
64
+ Four of the deprecated surfaces are not values this package produces. `roomArgs` and `roster`
65
+ on the server host — and the same two on the match-server wrapper around it — are **pushed into
66
+ your bundle by the dedicated-server runtime image your bundle runs on**. That splits the
67
+ question in two, and the halves are decided in different places.
68
+
69
+ - **The member is package surface, and section 2 governs it unchanged.** The property keeps
70
+ compiling for the rest of `3.x`, and `4.0.0` is the earliest release that may take it off
71
+ the type.
72
+ - **Whether a value still arrives is the runtime's decision, and no date has been set for it.**
73
+ This package does not build the runtime image and cannot say when a server stops sending one
74
+ of these. That is decided where the image is built. Nothing here names a version for it,
75
+ because a version this package cannot honour would be worse than the silence it replaces.
76
+
77
+ **So treat delivery as something your code checks, not something you can read off a version.**
78
+ That is the advice [`server-bundles.md`](server-bundles.md) already gives and none of it
79
+ changes: read the room when it is there, and keep the deprecated push as the fallback for a
80
+ server running an older image. [Room args](server-bundles.md#room-args) has the shape of both
81
+ and the check that chooses between them.
82
+
83
+ ## 4. What is deprecated today
84
+
85
+ Every deprecated surface this documentation publishes, with what to call instead. A notice you
86
+ meet in the reference or in a guide points here; if a surface is not in this table, it is not
87
+ deprecated.
88
+
89
+ | Surface | Replacement | Earliest release that may remove it | Governing rule |
90
+ | ------- | ----------- | ----------------------------------- | -------------- |
91
+ | `Gem.client` | The named surfaces — `Gem.rooms`, `Gem.me`, `Gem.characters` and the rest | `4.0.0` | Package API surface |
92
+ | `ClientVersion.platform` | None; drop the field, the SDK reports it | `4.0.0` | Package API surface |
93
+ | `ClientVersion.gameVersion` | None; drop the field, the arcade tells your game which build it routed to | `6.0.0` | Package API surface |
94
+ | `CreateRoomOptions.maxPlayers` | The room template's seats | `4.0.0` | Package API surface |
95
+ | `GemServerHost.roomArgs` | `gem.room?.args()` | `4.0.0`, for the member | Package API surface, plus runtime delivery |
96
+ | `GemServerHost.roster` | `gem.room?.get()` | `4.0.0`, for the member | Package API surface, plus runtime delivery |
97
+ | `GemMatchServer.roomArgs` | `gem.room?.args()` | `4.0.0`, for the member | Package API surface, plus runtime delivery |
98
+ | `GemMatchServer.roster` | `gem.room?.get()` | `4.0.0`, for the member | Package API surface, plus runtime delivery |
99
+ | `MatchHosting.stop` | `MatchHosting.end` when the match is over, `MatchHosting.leave` to hand it to another host | `5.0.0` | Package API surface |
100
+ | `GemPlayer.displayHandle` | `PlayerIdentity.displayTag` — the same value | `7.0.0` | Package API surface |
101
+ | `GemPlayer.avatarUrl` | `PlayerIdentity.avatarIconUrl` — the same value | `7.0.0` | Package API surface |
102
+ | `profile(playerId)` | The room read's per-member display identity | Not a package release | Runtime delivery only |
103
+
104
+ **"For the member" is the split from section 3**, and it is the row's real answer: `4.0.0` is
105
+ the earliest release that may take the property off the type, and it says nothing about when a
106
+ server stops putting a value in it.
107
+
108
+ The last row is the one that is only a guide's. `profile(playerId)` is marked deprecated in
109
+ [`server-bundles.md`](server-bundles.md) and carries no mark in the generated reference, so
110
+ there is no package release to name for it and section 3 is its whole rule.
111
+
112
+ ## 5. What this is not
113
+
114
+ Three separate things on this platform carry a version. This policy is about one of them, and
115
+ mistaking it for either of the others produces a plan for a rule that does not exist.
116
+
117
+ - **Not the bridge protocol version.** The handshake between the arcade page and your game
118
+ negotiates its own version, and `README.md` § "Version negotiation" is the rule that governs
119
+ it. Different axis, different trigger, and nothing on this page restates or amends it.
120
+ - **Not your game's compatibility line.** Which builds of *your* game can meet in a room is
121
+ derived from the version the build was published under, and
122
+ [the compatibility line](platform-rules.md#4-the-compatibility-line) is the rule for that.
123
+ - **Not a removal schedule.** This page names the earliest release that may remove a surface,
124
+ which is a floor and not a plan. A surface may outlive the release named beside it.
@@ -0,0 +1,233 @@
1
+ # Friends
2
+
3
+ Your game can read **who the signed-in player's accepted friends are** — display identity only —
4
+ with the token your frame already holds:
5
+
6
+ ```js
7
+ const page = await gem.friends.list();
8
+ for (const friend of page.friends) {
9
+ const name = friend.displayName ? text.toPlain(friend.displayName) : 'a friend';
10
+ render(name, friend.discriminator, platformIconUrl(friend.avatarIconUrl));
11
+ }
12
+ ```
13
+
14
+ That read, and one ask — [sending a friend a challenge](#friend-challenges), which the Arcade
15
+ confirms with the player and sends itself — are the whole surface. The boundary around them is the
16
+ thing to understand before you design against it.
17
+
18
+ ## What the read is, and is not
19
+
20
+ `gem.friends.list()` returns the player's **accepted** friends: for each one, a `playerId` and —
21
+ when the friend has set them — `displayName`, `discriminator`, `displayTag`, a portrait
22
+ (`avatarIconId` and its thumbnail `avatarIconUrl`) and `badges`. These are the identity fields
23
+ every surface that shows a player carries — the name and tag as `PlayerText` — and
24
+ [Showing a player](what-a-game-must-do.md#showing-a-player-one-identity-on-every-surface) is how to render them. Every field beyond `playerId` is optional, and absent means
25
+ absent: render what is there and fall back per field, because a friend with no display name is
26
+ still a friend.
27
+
28
+ Everything else about the friend graph stays on the Arcade, by design:
29
+
30
+ - **No mutations.** There is no add, accept, remove or block here. The graph is managed on
31
+ platform screens with the player's own account credential — a game frame holds a play token,
32
+ and a play token cannot touch the graph. Sending a [friend challenge](#friend-challenges) does
33
+ not touch it either: your game asks, and the Arcade sends only after the player confirms in its
34
+ own sheet.
35
+ - **No states.** Pending requests, blocks and relationship history do not appear. An accepted
36
+ friend appears; everyone else does not exist on this surface.
37
+ - **No presence, no invites.** Nothing here says who is online or delivers an invite — an invite
38
+ is something happening *in* the graph, and it is rendered where the graph lives. See the
39
+ deliberate-absence notes in [`rooms-and-matches.md`](rooms-and-matches.md). A friend challenge
40
+ is not an invite to play together: it is a card in the friend's Arcade inbox, written by the
41
+ platform from your template, that launches your game when they tap it — whenever they next open
42
+ the Arcade, not now.
43
+
44
+ Two properties worth designing around rather than discovering:
45
+
46
+ - **Every accepted friend appears, whether or not they have played your game.** That is the
47
+ point, not an oversight — an invite-a-friend flow needs exactly the friends who have *not*
48
+ played it. If you only want friends with progress in your game, join the list against your own
49
+ data (a friend's `playerId` is the same id leaderboard rows and transfers carry).
50
+ - **It is a player surface only.** On a dedicated server there is no `gem.friends`, and the route
51
+ refuses a game-server token — a server has no friend list. A server that needs a friend fact is
52
+ handed it by a client, and must treat it as a claim, not a fact.
53
+
54
+ ## Pagination
55
+
56
+ The list is paginated behind an opaque cursor: a page carries `nextCursor` exactly when another
57
+ page exists, and you pass it back verbatim.
58
+
59
+ ```js
60
+ let page = await gem.friends.list();
61
+ render(page.friends);
62
+ while (page.nextCursor) {
63
+ page = await gem.friends.list({ cursor: page.nextCursor });
64
+ render(page.friends);
65
+ }
66
+ ```
67
+
68
+ Pages default to 50 friends and cap at 200; a player holds at most 1000 friends, so the whole
69
+ list is never more than five max-size pages. Ordering is newest friendship first. Mint nothing:
70
+ a cursor the server did not hand you is refused with `invalid`, not treated as page one.
71
+
72
+ ## Friends and leaderboards
73
+
74
+ If what you want is a **friend standings screen**, do not build it from this list. Every declared
75
+ board answers a friend view — the same board, re-ranked server-side over the viewer and their
76
+ accepted friends — and that view is the right tool, because ranks are positions within the
77
+ subset and the global board's size cap would make client-side filtering silently lossy.
78
+ [`leaderboards.md`](leaderboards.md) covers it.
79
+
80
+ ## Friend challenges
81
+
82
+ A **friend challenge** is your game asking the Arcade to send one of the player's friends a
83
+ card — from Kate#0001, saying "Can you open jar 2 in fewer than 9 tries?". The line is your
84
+ template, rendered with your params; the Arcade shows who sent it beside the line, from its own
85
+ records, so no template names the sender. The Arcade shows the player a confirmation sheet in
86
+ its own chrome, sends only if they press Send there, and puts the card in the friend's Arcade
87
+ inbox under "Friend challenges". Tapping it launches your game for the friend, with the
88
+ challenge's params in the launch query.
89
+
90
+ ```js
91
+ challengeButton.onclick = async () => {
92
+ try {
93
+ const { outcome } = await gem.friends.challenge({
94
+ playerId: friend.playerId, // from gem.friends.list()
95
+ template: 'beat_code_jar', // a friend_challenge_templates key in your config
96
+ params: { jar: 'K7Q2M9X', n: 2, beat: 9 }, // every param that template declares
97
+ });
98
+ if (outcome === 'accepted') challengeButton.textContent = 'Challenge sent';
99
+ // `cancelled`, `refused` and `unavailable`: carry on, and say nothing.
100
+ } catch (err) {
101
+ if (!(err instanceof FriendChallengeError)) throw err;
102
+ if (err.reason === 'rate_limited') showTryLater(err.retryAfterSeconds);
103
+ else console.error('friend challenge refused:', err.code, err.param, err.detail);
104
+ }
105
+ };
106
+ ```
107
+
108
+ **Your game never writes the words.** It names a friend, a template and the template's params;
109
+ the text is your template from the published config (`friend_challenge_templates`, authored with
110
+ `gemctl` — its `game-config` skill covers it), rendered and translated by the platform, and the
111
+ sender's name comes from the platform's own records. So **never put a player's name in a param**:
112
+ the card already says who sent it, and a name in a param is one nobody checked.
113
+
114
+ ### The four answers
115
+
116
+ `challenge()` resolves for every answer the player or the platform gives, and none of them
117
+ carries a reason:
118
+
119
+ | outcome | what happened | what to show |
120
+ |---|---|---|
121
+ | `accepted` | the player pressed Send and the platform took it | "Challenge sent", or nothing — the Arcade already said so |
122
+ | `cancelled` | the player closed the sheet, or another prompt already held the screen | **nothing** |
123
+ | `refused` | the platform will not send this one — a guest or signed out, not friends with that player, or no access to this channel | **nothing** |
124
+ | `unavailable` | the Arcade could not offer this, or no answer arrived | **nothing**, and never retry it on your own |
125
+
126
+ ⚠️ **`accepted` is not a delivery receipt.** The platform may decide not to deliver a challenge
127
+ — the friend muted your game, has had enough challenges today, or cannot receive them — and
128
+ answers exactly as if it had. That is deliberate: those are facts about players' accounts, and
129
+ your game must not be able to learn them one challenge at a time. There is no "sent" list and no
130
+ read receipt either.
131
+
132
+ `refused` is not a verdict on the player's account either: age and moderation never produce it.
133
+ A player the platform will not let send is answered `accepted` or `cancelled`, by design.
134
+
135
+ `unavailable` does not prove nothing was sent, only that no answer arrived.
136
+
137
+ ### The two that reject
138
+
139
+ Two answers are yours to fix, and they reject with a `FriendChallengeError` — a `GemApiError`, so
140
+ existing `instanceof GemApiError` handling keeps working:
141
+
142
+ - **`invalid`** — the request was malformed or the platform refused it as such. `code` says
143
+ which:
144
+ - `recipient_invalid` — `playerId` is not a player id;
145
+ - `unknown_template` — no template with that key is on this channel;
146
+ - `template_not_on_channel` — the template exists, but the config promoted to this channel
147
+ does not carry it. Only a member of your team is told this, with `channel` and
148
+ `configVersion`; everyone else gets `unknown_template`. Promote the config, then try again by
149
+ hand;
150
+ - `param_invalid` — a param's value is not what the template declares (`param` names the key
151
+ when one is at fault);
152
+ - `param_undeclared` — a param the template does not declare, or more than 8.
153
+
154
+ The SDK checks the shape before asking — a player id, a template key of lowercase letters,
155
+ digits, `_` and `-`, a plain object of strings and integers, at most 8 keys — so most of these
156
+ reach you before anything is shown to the player. `detail` is the platform's sentence, for your
157
+ console, never for a player.
158
+ - **`rate_limited`** — the player has sent too many. `retryAfterSeconds` is the earliest a fresh
159
+ press could succeed, and it can be up to a day.
160
+
161
+ ### Rules that are not optional
162
+
163
+ - **Call it only from a player's press.** Each call puts a sheet in front of the player. Never on
164
+ a timer, at start-up, or once per friend in a loop.
165
+ - **Never retry it automatically** — not on `unavailable`, and not on `rate_limited` even though
166
+ that error is `retryable`. Only a fresh press may ask again.
167
+ - **One ask per press.** Calls are not joined, and an ask arriving while a sheet is open is
168
+ answered `cancelled`.
169
+ - **Never write "today" in a template.** A challenge never expires, so a friend may open it a
170
+ week later. Pass a `date` param and let the line say which day.
171
+ - **Hide the button where it cannot work.** `gem.frame.parentSupports('friendChallenges')` is
172
+ false outside the Arcade — a local dev host included — and `challenge()` answers `unavailable`
173
+ there at once, asking nothing. Try the real flow on your game's dev channel in the Arcade.
174
+
175
+ ### When a challenge launches your game
176
+
177
+ The friend's tap opens your game with the challenge's params in its launch query, read exactly as
178
+ [your own launch keys](getting-started.md#linking-to-your-game) are: the param keys are
179
+ launch-query keys, and every value arrived in a link, so validate it the same way. The Arcade adds
180
+ one key of its own, `gemChallenge`, which is the platform's, not yours: never parse it. The SDK
181
+ method that reads it ships in a later release.
182
+
183
+ **A relaunch replays the same challenge.** When the player presses Play again in the Arcade, it
184
+ reuses the launch query, so the same params arrive again. Make answering a challenge idempotent:
185
+ a second run of the same challenge is a replay, not a new one.
186
+
187
+ ## Inspecting a player: game profiles
188
+
189
+ The "inspect this player" screen — tap a roster row, a leaderboard entry or a friend, see who
190
+ they are and how far they have come — is one read:
191
+
192
+ ```js
193
+ const profile = await gem.profiles.of(playerId);
194
+ if (!profile) {
195
+ renderUnavailable(); // no such player — or they blocked you; you cannot tell
196
+ } else {
197
+ const name = profile.displayName ? text.toPlain(profile.displayName) : 'Anonymous player';
198
+ renderHeader(name, profile.discriminator, platformIconUrl(profile.avatarIconUrl));
199
+ for (const entry of profile.showcase) render(entry.key, entry.value);
200
+ }
201
+ ```
202
+
203
+ One response carries the player's display identity (the same optional fields a friend row has)
204
+ plus **every showcase value your relationship to them admits**: `public` slots if you are anyone
205
+ in the game, `friends` slots too if you are an accepted friend, everything if it is your own
206
+ profile. Entries arrive ordered by key, each naming its slot's declared `visibility`.
207
+
208
+ Three properties to design around:
209
+
210
+ - **A game shapes its profile in the game config, not here.** Profile content *is* the game's
211
+ showcase slots — save schemas declared with a `visibility` of `friends` or `public` — and the
212
+ write path is `gem.showcase.set` (or your dedicated server, for values a modified client must
213
+ not forge). There is no profile write on this surface, and identity is the player's own,
214
+ managed on the Arcade. The showcase section of
215
+ [`saving-player-progress.md`](saving-player-progress.md) covers declaring and writing slots.
216
+ - **`null` is deliberately two answers at once.** An unknown player and a player who blocked the
217
+ viewer are the same `profile_not_found`, so a response can never reveal which. Do not build UI
218
+ that tries to distinguish them.
219
+ - **An empty `showcase` is a real profile, not a failure.** A real, unblocked player with nothing
220
+ visible to you still answers — identity and an empty list. Render "nothing to show", and
221
+ render a profile with no display fields as anonymous rather than broken.
222
+
223
+ Where does a `playerId` come from? Anywhere the platform names a player: `gem.friends` rows,
224
+ leaderboard entries, match rosters, transfer counterparties — they all carry the same id this
225
+ read takes.
226
+
227
+ ## Testing a friends screen
228
+
229
+ The SDK ships no seeded mock ([`what-a-game-must-do.md`](what-a-game-must-do.md) says why), so if
230
+ you stub this surface in your own tests, stub the states the type does not force on you: a friend
231
+ with no display fields at all, the friendless empty state — every friends screen must survive a
232
+ player who knows nobody — and a list long enough to page, because the cursor loop is code that
233
+ otherwise first runs in production against the one player with 300 friends.