@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.
- 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 +530 -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,277 @@
|
|
|
1
|
+
# Leaderboards
|
|
2
|
+
|
|
3
|
+
Games do not have to rank anything. This is a capability, not an obligation — but if your game
|
|
4
|
+
has a score, a fastest time, or a longest streak worth comparing between players, the platform
|
|
5
|
+
can hold the standings and your game can read them with the token your frame already holds.
|
|
6
|
+
|
|
7
|
+
A leaderboard here is **read-only from the browser, by design**. Your game reads standings and
|
|
8
|
+
renders them; it cannot write a score, because a browser credential is public and a
|
|
9
|
+
client-asserted score would be forgeable. Where the rows actually come from is covered
|
|
10
|
+
[below](#where-the-rows-come-from) — read that section before you design around this feature,
|
|
11
|
+
because it decides whether your game can use it at all today.
|
|
12
|
+
|
|
13
|
+
## Which read do you want
|
|
14
|
+
|
|
15
|
+
One declaration, four reads. Picking the wrong one is the commonest mistake here, and the
|
|
16
|
+
tempting shortcut — read the global board and filter it client-side for the player — is the wrong
|
|
17
|
+
answer twice over: `maxSize` caps the board, so a player outside the cut simply is not in the
|
|
18
|
+
response, and their rank among everyone is not the rank most screens want anyway.
|
|
19
|
+
|
|
20
|
+
| Read | Ranks | `rank` means | Freshness |
|
|
21
|
+
| --------------------------------------------- | ------------------------------ | ---------------------------------- | ---------------------------------------- |
|
|
22
|
+
| `gem.leaderboards.get(id)` | everyone, capped at `maxSize` | position on the whole board | cached; poll at `updateFrequencySeconds` |
|
|
23
|
+
| `gem.leaderboards.friends(id)` | the viewer and their friends | position within that circle | cached, per viewer, same cadence |
|
|
24
|
+
| `gem.leaderboards.mine(id)` | the player, against themselves | **your own** best-to-worst | exact when read — do not poll |
|
|
25
|
+
| `gem.leaderboards.character(id, characterId)` | one character, against itself | **that character's** best-to-worst | exact when read — do not poll |
|
|
26
|
+
|
|
27
|
+
`mine()` is the one to reach for when the question is _"is this a personal best?"_ or _"show my
|
|
28
|
+
last runs"_. `get()` is for a standings screen. `friends()` is for a standings screen among people
|
|
29
|
+
the player knows.
|
|
30
|
+
|
|
31
|
+
`character()` is `mine()` for a character. **Read the leg your server WRITES.** Runs carry the
|
|
32
|
+
entity that owns them, so if your server reports character-owned runs, `mine()` returns nothing and
|
|
33
|
+
`character()` is the read you want — and vice versa. That mismatch is the commonest reason one of
|
|
34
|
+
these boards is unexpectedly empty, and it is silent: both legs answer `200` with `entries: []`.
|
|
35
|
+
|
|
36
|
+
## Two roles, and they are not the same person
|
|
37
|
+
|
|
38
|
+
The same split as save slots: one half is something you publish, the other is code.
|
|
39
|
+
|
|
40
|
+
| | Who | When |
|
|
41
|
+
| --------------------- | ------------------------ | ----------------------------------- |
|
|
42
|
+
| **Declaring a board** | you, the developer | once, in your game config |
|
|
43
|
+
| **Reading a board** | your game's running code | whenever a standings screen renders |
|
|
44
|
+
|
|
45
|
+
A board your config does not declare does not exist: reading it is refused with `not_found`,
|
|
46
|
+
at the first call and at every retry after it.
|
|
47
|
+
|
|
48
|
+
## 1. Declare the board, in your game config
|
|
49
|
+
|
|
50
|
+
Two sections of the game config work together. A **run type** names the values a single play
|
|
51
|
+
produces — up to two integers and two floats, each with a friendly name — and a **leaderboard**
|
|
52
|
+
says how to rank runs of that type:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
run_types:
|
|
56
|
+
- id: race
|
|
57
|
+
int_value_1_name: score
|
|
58
|
+
|
|
59
|
+
leaderboards:
|
|
60
|
+
- id: top-score
|
|
61
|
+
run_type: race
|
|
62
|
+
order_by:
|
|
63
|
+
- column: int_value_1
|
|
64
|
+
direction: desc
|
|
65
|
+
max_size: 100
|
|
66
|
+
update_frequency_seconds: 600
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The friendly name is what your game reads back: a row on this board carries `stats.score`,
|
|
70
|
+
not `stats.int_value_1`. `max_size` caps the board at up to 1000 rows, and
|
|
71
|
+
`update_frequency_seconds` is how often the served board refreshes — more on that below.
|
|
72
|
+
|
|
73
|
+
Three flags decide what a row shows, all declared on the board rather than chosen by the
|
|
74
|
+
reader: `expose_player` (identity — on by default), `expose_stats` (the run's values — on by
|
|
75
|
+
default), and `expose_properties` (the run's freeform properties — off by default).
|
|
76
|
+
|
|
77
|
+
## 2. Read it, whole
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const board = await gem.leaderboards.get('top-score');
|
|
81
|
+
console.log(`generated ${board.generatedAt}, refreshes every ${board.updateFrequencySeconds}s`);
|
|
82
|
+
for (const row of board.entries) {
|
|
83
|
+
const name = row.displayName ? text.toPlain(row.displayName) : 'Anonymous player';
|
|
84
|
+
render(row.rank, name, row.discriminator, row.stats?.score);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A row carries the identity fields every surface that shows a player carries — the name and tag as
|
|
89
|
+
`PlayerText`, the discriminator, the portrait and badges — and a row with no `playerId` is an
|
|
90
|
+
anonymous player who keeps their rank. [Showing a player](what-a-game-must-do.md#showing-a-player-one-identity-on-every-surface) is how to render them.
|
|
91
|
+
|
|
92
|
+
The whole board arrives in one response, already sorted, at most `max_size` rows. There is no
|
|
93
|
+
paging and nothing further to assemble.
|
|
94
|
+
|
|
95
|
+
**On the CACHED reads — `get` and `friends` — poll at the cadence the response states, not
|
|
96
|
+
faster.** The platform serves a cached rendering and regenerates it at most once per
|
|
97
|
+
`updateFrequencySeconds`; polling faster re-downloads the same rendering. `generatedAt` says when
|
|
98
|
+
the rendering you hold was made.
|
|
99
|
+
|
|
100
|
+
That rule is about these two reads only. The entity reads —
|
|
101
|
+
[`mine()`](#one-players-own-runs-mine) and `character()` — carry no cadence, because they are not
|
|
102
|
+
refreshed on one: they are exact when read, and are called on an event rather than on a timer.
|
|
103
|
+
|
|
104
|
+
Two facts about rows worth designing for:
|
|
105
|
+
|
|
106
|
+
- **A player can hold several rows.** Ranks are per run, arcade-style — the same player's
|
|
107
|
+
second-best run is still on the board if it ranks.
|
|
108
|
+
- **An anonymous row is still a row.** A deleted player's entry keeps its rank — removing it
|
|
109
|
+
would shift everyone below it — and arrives with an `entityId` but no display fields. Render
|
|
110
|
+
it as "anonymous" rather than skipping it, or every rank under it will look wrong.
|
|
111
|
+
|
|
112
|
+
## Buckets — one declaration, many boards
|
|
113
|
+
|
|
114
|
+
A board that declares `bucket_by` is partitioned: each distinct bucket value is its own board
|
|
115
|
+
with its own ranks. Declare the bucket column on the run type, list its allowed values, and
|
|
116
|
+
name the bucket when you read:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
run_types:
|
|
120
|
+
- id: race
|
|
121
|
+
int_value_1_name: score
|
|
122
|
+
bucket_1_name: region
|
|
123
|
+
bucket_1_allowed_values: [na, eu]
|
|
124
|
+
|
|
125
|
+
leaderboards:
|
|
126
|
+
- id: regional
|
|
127
|
+
run_type: race
|
|
128
|
+
order_by:
|
|
129
|
+
- column: int_value_1
|
|
130
|
+
direction: desc
|
|
131
|
+
bucket_by: [bucket_1]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
const board = await gem.leaderboards.get('regional', { bucket1: 'na' });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A bucketed board owes exactly its declared buckets — omitting one, or supplying one the board
|
|
139
|
+
does not declare, is refused with `invalid`.
|
|
140
|
+
|
|
141
|
+
## Friends — the same board, restricted to people the player knows
|
|
142
|
+
|
|
143
|
+
Every declared board also answers a **friend view**: `GET /v1/leaderboards/{id}/friends` is the
|
|
144
|
+
same board — same run type, ordering, buckets, expose flags, `max_size` cap and freshness window —
|
|
145
|
+
ranking only the runs owned by the viewer and their accepted friends. The friend set is resolved
|
|
146
|
+
on the backend at generation time, and the re-ranking is the platform's work: your game is handed
|
|
147
|
+
finished rows and joins nothing. Your game _can_ also read the friend list itself now —
|
|
148
|
+
`gem.friends`, covered in [`friends.md`](friends.md) — but that does not replace this view. A
|
|
149
|
+
friend standings screen wants the ranked subset, not a list to filter the global board by, and
|
|
150
|
+
the global board's `max_size` cap means filtering it client-side would silently drop any friend
|
|
151
|
+
ranked below the cutoff.
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
const board = await gem.leaderboards.friends('top-score');
|
|
155
|
+
for (const row of board.entries) {
|
|
156
|
+
const name = row.displayName ? text.toPlain(row.displayName) : 'Anonymous player';
|
|
157
|
+
render(row.rank, row.isViewer ? 'you' : name, row.stats?.score);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Four ways a friend view differs from the board it restricts:
|
|
162
|
+
|
|
163
|
+
- **Ranks are positions within the view,** not the run's global rank — a friend circle has its
|
|
164
|
+
own first place.
|
|
165
|
+
- **The viewer's own rows say so.** They carry `isViewer: true` — present even on a board that
|
|
166
|
+
hides identity fields, because the viewer is being told about their own row, nobody else's. On
|
|
167
|
+
`gem.leaderboards.get` the field is **absent**, not `false`: a global board does not answer that
|
|
168
|
+
question at all, and the difference matters if you write `row.isViewer ? 'you' : name`.
|
|
169
|
+
- **It is a player surface only.** A game server token is refused — a server has no friend
|
|
170
|
+
list — and character-owned runs are not ranked here, because friendship is a player-to-player
|
|
171
|
+
relation.
|
|
172
|
+
- **The cache is per viewer,** on the same `update_frequency_seconds` cadence — a new friendship,
|
|
173
|
+
or a friend's new run, appears within one window.
|
|
174
|
+
|
|
175
|
+
A bucketed board buckets its friend view the same way: name the buckets in the query string
|
|
176
|
+
(`/v1/leaderboards/regional/friends?bucket_1=na`), owing exactly the buckets the board declares.
|
|
177
|
+
|
|
178
|
+
## One player's own runs — `mine()`
|
|
179
|
+
|
|
180
|
+
The two reads above rank a _field_. This one ranks **one entity against itself**: your own runs on
|
|
181
|
+
a board, best first, so `rank: 1` is your personal best. It is the read behind "New personal
|
|
182
|
+
best!", and behind a run-history panel.
|
|
183
|
+
|
|
184
|
+
⚠️ **Before you write it, read [Where the rows come from](#where-the-rows-come-from) below.** A
|
|
185
|
+
browser cannot report a run, so a game where neither a dedicated server nor a serverless function
|
|
186
|
+
reports runs will get an empty board here forever — and empty is also the correct answer for every
|
|
187
|
+
player's first session, so the two are indistinguishable from the response alone. That is the
|
|
188
|
+
single most likely reason this returns nothing.
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
const mine = await gem.leaderboards.mine('time-trial');
|
|
192
|
+
if (mine.personalBest) showBest(mine.personalBest.stats?.finish_ms);
|
|
193
|
+
for (const row of mine.entries) render(row.rank, row.stats?.finish_ms);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**When to call it.** Not on a timer — there is deliberately no `updateFrequencySeconds` on the
|
|
197
|
+
result, because the read is exact when made rather than refreshed on a cadence. Call it once a run
|
|
198
|
+
has been reported. ⚠️ Reporting is your _server's_ job, or your function's, and your client has no
|
|
199
|
+
happens-after edge with a server: a read fired the instant a match ends can legitimately miss the
|
|
200
|
+
run just played. Read it when your server has told your client the run landed, or once the function
|
|
201
|
+
that recorded it has answered.
|
|
202
|
+
|
|
203
|
+
Five things that differ from the reads above:
|
|
204
|
+
|
|
205
|
+
- **`rank` is a position among this entity's own runs**, not a global rank. It is the number most
|
|
206
|
+
games actually want, and the one the global board cannot give you unless the player happens to be
|
|
207
|
+
on it.
|
|
208
|
+
- **`personalBest` is returned outright**, and is the same row as `entries[0]`. Render one or the
|
|
209
|
+
other, not both. It is `null` when the entity has no eligible run.
|
|
210
|
+
- **`totalRuns` counts your eligible runs before `limit`** and after any bucket filter, and is
|
|
211
|
+
capped — past the cap `totalRunsExact` goes `false` and the honest rendering is `` `${totalRuns}+` ``.
|
|
212
|
+
A run missing a value the board sorts on is not on the board and is not counted.
|
|
213
|
+
- **`limit` (1–100, default 25) slices; it does not select.** A read at `limit=5` and a read at the
|
|
214
|
+
default return the same list, cut differently. There is no cursor: history deeper than the cap is
|
|
215
|
+
not reachable here.
|
|
216
|
+
- **`expose_stats: false` means something different here.** On the reads above it hides stats
|
|
217
|
+
entirely; here it narrows them to the columns the board _sorts on_ — the numbers being ranked.
|
|
218
|
+
A run type may name a column no board orders by, and that is where server-authored values live,
|
|
219
|
+
so those stay hidden.
|
|
220
|
+
|
|
221
|
+
**Characters: read the leg your server writes.** Runs carry the entity that owns them, so a game
|
|
222
|
+
whose server reports character-owned runs gets nothing from `mine()`, and vice versa. That mismatch
|
|
223
|
+
is the second most likely reason a board looks empty.
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
const theirs = await gem.leaderboards.character('time-trial', characterId);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Where the rows come from
|
|
230
|
+
|
|
231
|
+
Runs are reported **from the server side** — by a dedicated server during its match, or by your
|
|
232
|
+
game's serverless function — never from the browser. That is the integrity model: the standings
|
|
233
|
+
are only worth rendering because nothing a player controls can write to them.
|
|
234
|
+
|
|
235
|
+
Two consequences to plan around:
|
|
236
|
+
|
|
237
|
+
- A game whose matches run entirely in the browser (single-player, or peer-to-peer multiplayer)
|
|
238
|
+
reaches a board through a serverless function, which checks a finished run and records it with
|
|
239
|
+
no match and no server of yours:
|
|
240
|
+
[What `call-api` can reach](serverless-functions.md#what-call-api-can-reach). ⚠️ A function
|
|
241
|
+
that records the score its client sent, unchecked, hands players the write this model keeps
|
|
242
|
+
from them. Otherwise its scoring matches need to be dedicated-server matches —
|
|
243
|
+
[`dedicated-server-flow.md`](dedicated-server-flow.md). If what you
|
|
244
|
+
wanted was competition rather than a ranking, [`async-opponents.md`](async-opponents.md) is the
|
|
245
|
+
mode that needs no server of yours: the platform holds a snapshot of one player's run for another
|
|
246
|
+
player's game to play against later.
|
|
247
|
+
- The server-bundle call that reports runs is **not available yet**. The read surface this page
|
|
248
|
+
documents works today, and a board with no runs yet is an ordinary empty board — `entries` is
|
|
249
|
+
`[]`, which your standings screen must render as a designed state, because every new game's
|
|
250
|
+
board starts that way.
|
|
251
|
+
|
|
252
|
+
## When a read is refused
|
|
253
|
+
|
|
254
|
+
**Branch on `err.code` when the server sent one, and on `err.reason` otherwise.** They are
|
|
255
|
+
different fields with different vocabularies, and mixing them is the easy mistake: `bad_request` is
|
|
256
|
+
a `code`, and testing `err.reason === 'bad_request'` never matches, because a 400 arrives as
|
|
257
|
+
`reason: 'invalid'`.
|
|
258
|
+
|
|
259
|
+
| What you get | What happened | What to do |
|
|
260
|
+
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
261
|
+
| `reason: 'not_found'`, `code: 'leaderboard_not_found'` | no board with this id in the channel's active config | check the id against your config, and that the config is assigned to the channel you are on |
|
|
262
|
+
| `reason: 'invalid'`, `code: 'invalid_bucket'` | bucket values do not match the board's `bucket_by` | supply exactly the buckets the board declares |
|
|
263
|
+
| `reason: 'not_found'`, `code: 'leaderboard_not_found'`, on `mine()` / `character()` | **also** the entity is not one you may read — an unknown character, one you do not own, one in another game, or a malformed id | **the same code as the row above**, which is the point: the entity read answers ONE refusal for every reason that depends on the target, deliberately, because a distinguishable answer would tell a caller whether a given player plays this game. Check the entity id as well as the board id — nothing in the response separates them |
|
|
264
|
+
| `reason: 'forbidden'`, on `mine()` / `character()` / `friends()` | the caller may not act for this entity at all, or (on `friends()`) carries no player identity at all — a game server token, say. Decided from the caller's own claims, not from what was asked for | this one is worth telling apart, and it is not about the target: it will not start working because the player played more. Treat it as a bug in how the call was made, not as an empty board. `get()` never answers this — the global board needs no viewer |
|
|
265
|
+
| `reason: 'invalid'`, `code: 'bad_request'` | `limit` is not an integer in 1–100 | the SDK refuses this locally with a `TypeError` before any request, so seeing it from the server means something else built the URL |
|
|
266
|
+
| `TypeError` (thrown, no request sent) | `limit` out of range, passed to `mine()` / `character()` | pass an integer 1–100, or omit it for the default 25 |
|
|
267
|
+
|
|
268
|
+
The _target-dependent_ refusals on the entity read are deliberately uninformative and that is the
|
|
269
|
+
feature: "no such board", "no such entity" and "not yours" are one answer. Do not build retry or
|
|
270
|
+
fallback logic that tries to tell those apart — there is nothing there to read. `forbidden` is not
|
|
271
|
+
an exception to that set so much as outside it: it is about the caller rather than the target, so
|
|
272
|
+
it stays distinguishable and is worth its own branch.
|
|
273
|
+
|
|
274
|
+
**A session that cannot be refreshed is not in this table.** The SDK replays a `401` against a
|
|
275
|
+
fresh token and, if that fails too, raises `AuthError` rather than `GemApiError` — a different
|
|
276
|
+
class, and one that carries no `reason` at all. A `catch` written from this table alone falls
|
|
277
|
+
through it.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": [
|
|
3
|
+
"When each published guide in this folder last changed. Written by",
|
|
4
|
+
"`node scripts/docs-manifest.mjs`; `--check` fails when a guide changed and its",
|
|
5
|
+
"entry did not. `updated` is when the change was recorded, not when it shipped."
|
|
6
|
+
],
|
|
7
|
+
"schema_version": 1,
|
|
8
|
+
"documents": [
|
|
9
|
+
{
|
|
10
|
+
"file": "achievements.md",
|
|
11
|
+
"sha256": "2bb4790db96a48171826a9cb3ea8e517d9128f07e55b1fd71b072a491021a15b",
|
|
12
|
+
"updated": "2026-09-28T01:10:05Z"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"file": "analytics.md",
|
|
16
|
+
"sha256": "3ca38659d3c7fd9087d0f17d6c0a31659246724c227cf63701e1bd9a058f0df2",
|
|
17
|
+
"updated": "2026-09-16T22:32:35Z"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"file": "arcade-economy.md",
|
|
21
|
+
"sha256": "7840941e761c1a460b6dc83ed4cec1ae8838161e0b37ec16cb184c66ccee1385",
|
|
22
|
+
"updated": "2026-10-01T19:42:50Z"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"file": "async-opponents.md",
|
|
26
|
+
"sha256": "9bf14e66a03c4cbdc21208456eabae13fa419ebdfa09db6cac708f21610014d7",
|
|
27
|
+
"updated": "2026-09-28T19:56:41Z"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"file": "brand-art.md",
|
|
31
|
+
"sha256": "cf54939a0fcee1f5a6e11b9de52b5ed498ec8c373683e92aa5bc6654b12eb0ab",
|
|
32
|
+
"updated": "2026-10-01T03:49:20Z"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"file": "cross-promotion.md",
|
|
36
|
+
"sha256": "3a8121a9c973e4a1bfed076a3226d139f8b4e38a173a06ea9f08000c72bca4b4",
|
|
37
|
+
"updated": "2026-09-29T17:32:40Z"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"file": "daily-challenges.md",
|
|
41
|
+
"sha256": "6b3fac0f91442561e45af35ee1213685b007f27782fd40434eb1bf2423de797e",
|
|
42
|
+
"updated": "2026-10-01T16:38:53Z"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"file": "dedicated-server-flow.md",
|
|
46
|
+
"sha256": "06dfa9341e924628f67d33e8751f523ea27a3bc91eb07f1f52aadc5e2aa62de7",
|
|
47
|
+
"updated": "2026-09-24T19:20:12Z"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"file": "deprecation-policy.md",
|
|
51
|
+
"sha256": "2923c4af22f266bb545e0218efb3ffb3c7cc7184ea538e431b6b87352c6b9e51",
|
|
52
|
+
"updated": "2026-09-24T16:41:50Z"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"file": "friends.md",
|
|
56
|
+
"sha256": "1c0a427a4e86d4c0e48faad9a80bdef9d528a60485b5aadee18d7759d9051e63",
|
|
57
|
+
"updated": "2026-10-01T16:38:53Z"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"file": "game-economy-tutorial.md",
|
|
61
|
+
"sha256": "7c94ec70c9e2490bdef23b92743b48409a3c6e22d9f26aff8c09fd86af84eae1",
|
|
62
|
+
"updated": "2026-10-01T19:42:50Z"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"file": "getting-started.md",
|
|
66
|
+
"sha256": "00ca91b36945d37bade4954a204b3bcb9af39c142011647220b3c7a4c587d44f",
|
|
67
|
+
"updated": "2026-10-01T19:58:52Z"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"file": "inventory-and-catalogs.md",
|
|
71
|
+
"sha256": "23de4799808ec3745664eb8c8fd948891921d5077535e27155c7e4ed4b584fd8",
|
|
72
|
+
"updated": "2026-10-01T19:42:50Z"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"file": "leaderboards.md",
|
|
76
|
+
"sha256": "940be101a9a06a6ddad85a1afe18a23e9acca43772d158ff7be70ed0f2f6b28d",
|
|
77
|
+
"updated": "2026-09-28T19:46:26Z"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"file": "match-session.md",
|
|
81
|
+
"sha256": "738c54766f5489ba7509f5f31ef1387a2a80ad89f03eaccd52113a18f2765b0d",
|
|
82
|
+
"updated": "2026-09-25T21:03:24Z"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"file": "p2p-and-parties.md",
|
|
86
|
+
"sha256": "3887bc706d098014cefab84e9b8bd5b964841888c242a7ccb1f799a9b9723fc0",
|
|
87
|
+
"updated": "2026-09-24T01:00:22Z"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"file": "platform-rules.md",
|
|
91
|
+
"sha256": "269f2d75a717de583af5ca7e0b91655a2d1edcedb5745c6f2d6f8f0be4dcfbdc",
|
|
92
|
+
"updated": "2026-09-27T19:51:18Z"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"file": "player-sign-in.md",
|
|
96
|
+
"sha256": "df1e39628ccba04b6f7bcd0b90381dfa34a4480330a6af041d9b71bcaefddb5f",
|
|
97
|
+
"updated": "2026-09-17T20:51:57Z"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"file": "room-shapes.md",
|
|
101
|
+
"sha256": "5aab02d65a7573ac690649b70e5db4fed4f779c3102c35e64a8ced02218410a9",
|
|
102
|
+
"updated": "2026-09-29T23:14:43Z"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"file": "rooms-and-matches.md",
|
|
106
|
+
"sha256": "d8c397bc3657d7f46dba7ca0af81dcf181148df798dbaced27dfd294889a1da6",
|
|
107
|
+
"updated": "2026-09-25T02:14:45Z"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"file": "saving-player-progress.md",
|
|
111
|
+
"sha256": "e556b9ef033b0ae611a7d136da3422c8145367939848eda193ab2674087c884f",
|
|
112
|
+
"updated": "2026-10-01T18:37:21Z"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"file": "server-bundles.md",
|
|
116
|
+
"sha256": "d9a5cac21415179fceb8b5d584e7563820634558f01ebeb4c2aa9ce56dad0139",
|
|
117
|
+
"updated": "2026-10-01T19:58:52Z"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"file": "serverless-functions.md",
|
|
121
|
+
"sha256": "3b61ee955d3bc439c39a7cad9339f8ffe8c0fb584f0a25621b877e185160a45a",
|
|
122
|
+
"updated": "2026-10-01T18:13:51Z"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"file": "trading-and-gifting.md",
|
|
126
|
+
"sha256": "d7ee3c85540ab8aae29988c16a76bee3ba03cc66b10d2faaf5cd7210a80a0544",
|
|
127
|
+
"updated": "2026-08-26T03:27:49Z"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"file": "what-a-game-must-do.md",
|
|
131
|
+
"sha256": "f34156c8e369197a1b7ce010ccd837e5ee38cd20150c3836e9da8f38811b06e6",
|
|
132
|
+
"updated": "2026-10-01T19:03:17Z"
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|