@indiegems/gem-web-sdk 7.4.0 → 7.4.2
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/README.md +54 -0
- package/dist/gem-web-sdk.esm.js +1 -1
- package/dist/gem-web-sdk.game.js +1 -1
- package/dist/gem-web-sdk.min.js +1 -1
- package/dist/gem-web-sdk.sdk.js +1 -1
- package/dist/gem-web-sdk.server.js +1 -1
- package/docs/achievements.md +232 -0
- package/docs/analytics.md +271 -0
- package/docs/api/api-reference-game.md +15308 -0
- package/docs/api/api-reference-server.md +4397 -0
- package/docs/api/reference.json +24130 -0
- package/docs/arcade-economy.md +263 -0
- package/docs/async-opponents.md +396 -0
- package/docs/brand-art.md +334 -0
- package/docs/cross-promotion.md +137 -0
- package/docs/daily-challenges.md +583 -0
- package/docs/dedicated-server-flow.md +879 -0
- package/docs/deprecation-policy.md +124 -0
- package/docs/friends.md +233 -0
- package/docs/game-economy-tutorial.md +326 -0
- package/docs/getting-started.md +552 -0
- package/docs/inventory-and-catalogs.md +1035 -0
- package/docs/leaderboards.md +277 -0
- package/docs/manifest.json +135 -0
- package/docs/match-session.md +288 -0
- package/docs/p2p-and-parties.md +783 -0
- package/docs/platform-rules.md +372 -0
- package/docs/player-sign-in.md +89 -0
- package/docs/room-shapes.md +195 -0
- package/docs/rooms-and-matches.md +779 -0
- package/docs/saving-player-progress.md +457 -0
- package/docs/server-bundles.md +1041 -0
- package/docs/serverless-functions.md +748 -0
- package/docs/trading-and-gifting.md +98 -0
- package/docs/what-a-game-must-do.md +1231 -0
- package/package.json +7 -2
- package/schema/bridge.v1.json +5 -5
- 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.
|
package/docs/friends.md
ADDED
|
@@ -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.
|