@indiegems/gem-web-sdk 9.1.3 → 10.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 +19 -0
- package/README.md +1 -2
- package/dist/function.d.ts +1 -1
- package/dist/game.d.ts +72 -21
- 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.js +1 -1
- package/dist/index.d.ts +71 -23
- package/dist/sdk.d.ts +72 -21
- package/docs/api/api-reference-game.md +2844 -2564
- package/docs/api/api-reference-server.md +706 -706
- package/docs/api/reference.json +791 -1927
- package/docs/changelog.md +19 -0
- package/docs/daily-challenges.md +123 -1
- package/docs/getting-started.md +5 -5
- package/docs/manifest.json +14 -14
- package/docs/platform-rules.md +2 -2
- package/docs/rooms-and-matches.md +128 -4
- package/docs/server-bundles.md +4 -4
- package/docs/serverless-functions-js.md +3 -1
- package/package.json +1 -1
package/docs/changelog.md
CHANGED
|
@@ -16,6 +16,25 @@ Its entries are grouped under up to five headings:
|
|
|
16
16
|
- **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
|
|
17
17
|
- **New capability**: something a game can now do. Nothing changes unless you use it.
|
|
18
18
|
|
|
19
|
+
## 10.0.0
|
|
20
|
+
|
|
21
|
+
Upgrading from 9.1.4. Impact: required.
|
|
22
|
+
|
|
23
|
+
### Rebuild
|
|
24
|
+
|
|
25
|
+
- The OrderSource type now accepts two more values, test_account_provision and developer_grant. Recompile your game. If your code handles order sources in an exhaustive switch or a lookup table typed by OrderSource, add handling for both new values, or the build may fail to compile.
|
|
26
|
+
|
|
27
|
+
### New capability
|
|
28
|
+
|
|
29
|
+
- A game can now put a player in a matchmaking queue for a mode that needs a formed lobby. Call RoomManager.queueForMatch with the template slugs you want. It returns a MatchTicket whose outcome settles once and whose cancel method leaves the queue. When the platform finds a match it creates the room, seats everyone and starts the match, and you follow it with the same code you use for any joined room. Options are described by QueueForMatchOptions. Ticket endings are described by MatchTicketOutcome, and the waiting state by MatchTicketStatus. RoomManager.currentTicket exposes the ticket the player is waiting on. The SDK keeps the ticket alive by itself, cancels it when the tab closes, and lets a player queue from inside a room. After a match the player leaves the room they queued from when the tab closes.
|
|
30
|
+
- The lower-level rooms API now has createTicket, heartbeatTicket and cancelTicket calls for working with matchmaking tickets directly. Most games should use RoomManager.queueForMatch, which sends the heartbeats and cancellation for you.
|
|
31
|
+
|
|
32
|
+
## 9.1.4
|
|
33
|
+
|
|
34
|
+
Upgrading from 9.1.3. Impact: none.
|
|
35
|
+
|
|
36
|
+
Nothing in this release asks anything of a game.
|
|
37
|
+
|
|
19
38
|
## 9.1.3
|
|
20
39
|
|
|
21
40
|
Upgrading from 9.1.2. Impact: none.
|
package/docs/daily-challenges.md
CHANGED
|
@@ -78,9 +78,118 @@ Changing only these two is not a change of schedule, so the upload does not refu
|
|
|
78
78
|
`completion_threshold` is the target. `client_writable` decides who may report it, and
|
|
79
79
|
[section 4](#4-who-may-report-it) is where that choice actually matters.
|
|
80
80
|
|
|
81
|
+
`display_name` and `description` are what a player reads. To translate them, or to have the
|
|
82
|
+
platform write the target into them, see
|
|
83
|
+
[section 2a](#2a-translate-the-copy-and-write-the-target-into-it).
|
|
84
|
+
|
|
81
85
|
**Upload the config the way you upload any other.** A challenge is not promoted differently and
|
|
82
86
|
does not need a new build to reach players.
|
|
83
87
|
|
|
88
|
+
## 2a. Translate the copy, and write the target into it
|
|
89
|
+
|
|
90
|
+
Both of these are optional, and a config that uses neither keeps working as it is. A progression
|
|
91
|
+
may carry your own translations of its name and description, and a displayed challenge may have the
|
|
92
|
+
platform write its target into the text:
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
progressions:
|
|
96
|
+
- id: 9b1d6e40-52c7-4f0a-8e3d-1a7c4b9f2d65
|
|
97
|
+
slug: clear-lines
|
|
98
|
+
display_name: 'Clear {target, plural, one {# line} other {# lines}}'
|
|
99
|
+
description: 'Clear lines in a single run.'
|
|
100
|
+
localized:
|
|
101
|
+
de:
|
|
102
|
+
display_name: 'Räume {target, plural, one {# Reihe} other {# Reihen}} ab'
|
|
103
|
+
description: 'Räume Reihen in einem Durchgang ab.'
|
|
104
|
+
pl:
|
|
105
|
+
description: 'Usuń linie w jednej rozgrywce.'
|
|
106
|
+
config:
|
|
107
|
+
completion_threshold: 150
|
|
108
|
+
reset_schedule: { type: daily }
|
|
109
|
+
platform_display: challenge
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Quote the text. YAML reads an unquoted value that starts with `{` as a mapping, so
|
|
113
|
+
`display_name: {target} points` is not a string.
|
|
114
|
+
|
|
115
|
+
### Translations
|
|
116
|
+
|
|
117
|
+
Any progression may carry `localized`, whether or not it is a displayed challenge.
|
|
118
|
+
|
|
119
|
+
- **The keys are the eleven locales:** `de`, `es`, `fr`, `it`, `ja`, `ko`, `pl`, `pt-BR`, `ru`,
|
|
120
|
+
`tr`, `zh-Hans`. `en` is refused as a key, and so is any other key.
|
|
121
|
+
- **Each entry has two optional fields**, `display_name` and `description`. You may translate one
|
|
122
|
+
and not the other.
|
|
123
|
+
- **The `display_name` and `description` you write outside `localized` are the default text**, and
|
|
124
|
+
the platform treats them as English. A missing locale, or a missing field in a locale, falls back
|
|
125
|
+
to the default text, one field at a time. In the example, a Polish reader gets the default name
|
|
126
|
+
and the Polish description, and a French reader gets the default text for both.
|
|
127
|
+
- **An empty translation is refused.** To show the default text, remove the key.
|
|
128
|
+
- **Translations are optional**, and promoting a config does not depend on them.
|
|
129
|
+
|
|
130
|
+
### Write the target with {target}
|
|
131
|
+
|
|
132
|
+
On a displayed challenge (`platform_display: challenge`), `{target}` in `display_name` or
|
|
133
|
+
`description` is replaced by the challenge's `completion_threshold`. It works in the default text
|
|
134
|
+
and in every translation. Copy that already contains the literal text `{target}` is now shown with
|
|
135
|
+
the number in its place.
|
|
136
|
+
|
|
137
|
+
| You write | Target | A player reads |
|
|
138
|
+
| ------------------------------------------------------ | ------ | --------------------- |
|
|
139
|
+
| `Score {target} points` | 12000 | Score 12,000 points |
|
|
140
|
+
| `Erziele {target} Punkte`, under `de` | 12000 | Erziele 12.000 Punkte |
|
|
141
|
+
| `Clear {target, plural, one {# line} other {# lines}}` | 1 | Clear 1 line |
|
|
142
|
+
| the same | 150 | Clear 150 lines |
|
|
143
|
+
|
|
144
|
+
- **`target` is the only name.** Any other `{name}` is refused, and so is `{target}` on a
|
|
145
|
+
progression with no `completion_threshold`.
|
|
146
|
+
- **The number is grouped the way the reader's language groups it:** `12,000` in English, `12.000`
|
|
147
|
+
in German. Each text uses one language for its words, its plural rule and its number.
|
|
148
|
+
- **A plural needs an `other` branch.** `Clear {target, plural, one {# line} other {# lines}}` is
|
|
149
|
+
"Clear 1 line" at 1 and "Clear 150 lines" at 150.
|
|
150
|
+
- **Write category branches (`one`, `few`, `many`, `other`), not `=N`.** An exact branch is
|
|
151
|
+
accepted only when `N` equals the current threshold, so if you later change the threshold, copy
|
|
152
|
+
that used `=N` can fail its next upload. Each text uses the categories its own language has: a
|
|
153
|
+
category that no number takes in that language (`few` in English) is refused, and so is a branch
|
|
154
|
+
written twice.
|
|
155
|
+
- **Each player reads the target they are working toward.** A player who started a period before
|
|
156
|
+
you changed the threshold keeps reading their own number, in the copy and on the meter alike.
|
|
157
|
+
Section 8 has what a change of threshold does to a period in progress.
|
|
158
|
+
|
|
159
|
+
### The rules for a displayed challenge's copy
|
|
160
|
+
|
|
161
|
+
- **On a displayed challenge the name and description are templates, and `{` and `}` are syntax.**
|
|
162
|
+
On any other progression they are plain text, and braces mean nothing.
|
|
163
|
+
- **The name is at most 64 characters and the description at most 280, counted
|
|
164
|
+
on the line a player is shown**, in every language, never on the template. Nothing is truncated;
|
|
165
|
+
text that renders too long is refused. A long plural is therefore fine when each line it renders
|
|
166
|
+
is short. The text as written may be up to 256 characters for a name and 4096 for a description.
|
|
167
|
+
- **A displayed challenge's name and description are each one line.**
|
|
168
|
+
- **The copy is screened at upload, in every language.** It is refused for words that claim to
|
|
169
|
+
speak for the platform or name a platform role, for the name of a platform currency, for reward
|
|
170
|
+
or credential words, and for links.
|
|
171
|
+
- **Some problems are refused today, and some only draw a warning.** A problem in a `localized`
|
|
172
|
+
entry, or in text that uses `{target}`, is refused. A problem in a displayed challenge's default
|
|
173
|
+
text that does not use `{target}` (a stray brace, a line break, a screened word) draws the
|
|
174
|
+
warning `progressions_copy_refusal_pending` with the same message, and the upload succeeds. That
|
|
175
|
+
warning will become a refusal, so fix the text when you see it. It follows the text, not its age:
|
|
176
|
+
default text without `{target}` on a challenge you add today draws the same warning.
|
|
177
|
+
- **A translation on a progression that is not a displayed challenge is plain text**, held to 64 /
|
|
178
|
+
280 characters as written. It is still refused for a link or for words that claim to speak for
|
|
179
|
+
the platform, and its description may have line breaks.
|
|
180
|
+
|
|
181
|
+
### What a player reads, and what your game reads
|
|
182
|
+
|
|
183
|
+
- **Where the platform shows a progression's name and description, a reader gets your translation
|
|
184
|
+
when there is one for their language, and the default text otherwise.**
|
|
185
|
+
- **Your game's own reads do not return this text.** `gem.challenges.list()` and the progression
|
|
186
|
+
read (`currentValue`, `isCompleted`) carry no name and no description.
|
|
187
|
+
- **`gem.gameConfig.load()` returns the config as you wrote it:** `display_name` and `description`
|
|
188
|
+
with `{target}` still in them, not replaced, and not chosen for the player's language. It also
|
|
189
|
+
returns your `localized` entries as written.
|
|
190
|
+
- **A game that shows its own challenge UI keeps rendering its own strings**, in
|
|
191
|
+
`gem.prefs.locale`.
|
|
192
|
+
|
|
84
193
|
## 3. Report progress from your game
|
|
85
194
|
|
|
86
195
|
The write is the ordinary progression write. Your game already knows how to make it:
|
|
@@ -506,7 +615,9 @@ shows, just quietly meaningless.
|
|
|
506
615
|
|
|
507
616
|
- **A new challenge gets a new id.** Always.
|
|
508
617
|
- **To rename what the player sees, change `display_name`.** That is free and is not part of the
|
|
509
|
-
challenge's identity.
|
|
618
|
+
challenge's identity. Update that name's translations in `localized` too
|
|
619
|
+
([section 2a](#2a-translate-the-copy-and-write-the-target-into-it)): a translation you leave
|
|
620
|
+
behind keeps showing the old name to readers of that language.
|
|
510
621
|
- **Changing a progression's `reset_schedule` is refused at upload**, for the same reason: a daily
|
|
511
622
|
that becomes a weekly would carry its old id into a history that then describes two different
|
|
512
623
|
things. A different period means a new progression. (Rewriting `0 0 * * *` as `daily` is not a
|
|
@@ -590,6 +701,17 @@ own streak on your store page. Your game does not have to render any of it.
|
|
|
590
701
|
| `cron expression never fires` | an expression naming a date that does not exist (`0 0 30 2 *`) | fix the expression |
|
|
591
702
|
| `reset_schedule changed from … to …` | you changed a live progression's period | use a NEW progression; `display_name` is what renames one |
|
|
592
703
|
| warning `platform_grant_max_per_window_ignored` (or `platform_grant_cap_uses_retired_max_per_entity`) | a mapping declares `max_per_window` (or `max_per_entity`) | nothing is refused and the upload succeeds; remove the key, because the platform sets the caps |
|
|
704
|
+
| a refusal at `localized.<key>` | the key is not one of the eleven locales, or it is `en` | use a listed locale; the text outside `localized` is the English |
|
|
705
|
+
| a refusal at a `localized.<locale>` field that is empty | an empty translation | remove the key to show the default text |
|
|
706
|
+
| a refusal naming a `{name}` other than `target` | a displayed challenge's copy referred to something else | `target` is the only name; remove the braces from the copy |
|
|
707
|
+
| a refusal for `{target}` with no threshold | the progression has no `completion_threshold` | set one, or remove `{target}` |
|
|
708
|
+
| a refusal on a plural branch | no `other` branch, an `=N` that is not the threshold, or a category the language does not have | write the category branches that language has, and always `other` |
|
|
709
|
+
| a refusal for text that is too long | the line a player is shown passes 64 (name) or 280 (description) | shorten the text; nothing is truncated for you |
|
|
710
|
+
| warning `progressions_copy_refusal_pending` | default text without `{target}` on a displayed challenge breaks a copy rule | the upload succeeds; fix the text now, because this will become a refusal |
|
|
711
|
+
|
|
712
|
+
A refusal about a progression's copy reads `progressions[<i>] (<slug>): <path>: <reason>`, where
|
|
713
|
+
`<path>` is `display_name`, `description`, `localized.<locale>.display_name`,
|
|
714
|
+
`localized.<locale>.description`, or `localized.<key>` for a refused key.
|
|
593
715
|
|
|
594
716
|
Every refusal names the field and the reason. **Read the error rather than a list written down
|
|
595
717
|
here** — the platform is what decides these, and this table is only as current as the day it was
|
package/docs/getting-started.md
CHANGED
|
@@ -50,7 +50,7 @@ either way.
|
|
|
50
50
|
```jsonc
|
|
51
51
|
// package.json
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@indiegems/gem-web-sdk": "
|
|
53
|
+
"@indiegems/gem-web-sdk": "10.0.0"
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -248,12 +248,12 @@ Once it is open:
|
|
|
248
248
|
- **Fullscreen covers it.** Leave fullscreen to see it again.
|
|
249
249
|
|
|
250
250
|
The panel is also reachable from inside your game — see
|
|
251
|
-
[GemDevTools.open](
|
|
251
|
+
[GemDevTools.open](api/api-reference-game.md#gemdevtools-open), which answers
|
|
252
252
|
`false` for a session that was not granted it rather than throwing — though it does reject if a
|
|
253
253
|
panel it _is_ allowed to open fails to load, so catch it either way.
|
|
254
254
|
|
|
255
255
|
If you put your own button on it, gate the button on
|
|
256
|
-
[`onDevToolsPermissionChange`](
|
|
256
|
+
[`onDevToolsPermissionChange`](api/api-reference-game.md#gemoptions-ondevtoolspermissionchange)
|
|
257
257
|
rather than checking anything at boot:
|
|
258
258
|
|
|
259
259
|
```js
|
|
@@ -559,5 +559,5 @@ credential made for a build pipeline can upload and promote, and cannot delete.
|
|
|
559
559
|
items, shops or loot. Start with **[the shop tutorial](game-economy-tutorial.md)** to build one.
|
|
560
560
|
- **[Rooms and the match lifecycle](rooms-and-matches.md)** — if it is multiplayer.
|
|
561
561
|
|
|
562
|
-
The [API reference](
|
|
563
|
-
|
|
562
|
+
The [API reference](api/api-reference-game.md) has every symbol the SDK publishes, with
|
|
563
|
+
signatures and descriptions; the [server half](api/api-reference-server.md) is beside it.
|
package/docs/manifest.json
CHANGED
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"file": "changelog.md",
|
|
36
|
-
"sha256": "
|
|
37
|
-
"updated": "2026-10-
|
|
36
|
+
"sha256": "cdaa2661a18afd096def083e5a44b2e125275d2aaa210b5d1e496631bf18f457",
|
|
37
|
+
"updated": "2026-10-10T18:47:07Z"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"file": "cross-promotion.md",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"file": "daily-challenges.md",
|
|
46
|
-
"sha256": "
|
|
47
|
-
"updated": "2026-10-
|
|
46
|
+
"sha256": "f4842b2fe01c8780020326f4722ab69dc01c1f40e1e6455cf2fb27752094383d",
|
|
47
|
+
"updated": "2026-10-10T14:37:50Z"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"file": "dedicated-server-flow.md",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"file": "getting-started.md",
|
|
71
|
-
"sha256": "
|
|
72
|
-
"updated": "2026-10-
|
|
71
|
+
"sha256": "39812f4b84097fd29f4038324a9070eeddf526ca294f7cb5b7b3913335ac34bf",
|
|
72
|
+
"updated": "2026-10-10T18:47:07Z"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"file": "inventory-and-catalogs.md",
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"file": "platform-rules.md",
|
|
96
|
-
"sha256": "
|
|
97
|
-
"updated": "2026-10-
|
|
96
|
+
"sha256": "b259bcebc6ae12b1db9c7279a6e2adb174af9d60090bf31001e23616f6423fbf",
|
|
97
|
+
"updated": "2026-10-10T00:12:33Z"
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
"file": "player-sign-in.md",
|
|
@@ -113,8 +113,8 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"file": "rooms-and-matches.md",
|
|
116
|
-
"sha256": "
|
|
117
|
-
"updated": "2026-10-
|
|
116
|
+
"sha256": "23540a20bdd8c8d058eb06d8ef11085ffd8eaaeeaadc93a807608b7c279e240c",
|
|
117
|
+
"updated": "2026-10-10T18:05:31Z"
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"file": "saving-player-progress.md",
|
|
@@ -123,13 +123,13 @@
|
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"file": "server-bundles.md",
|
|
126
|
-
"sha256": "
|
|
127
|
-
"updated": "2026-10-
|
|
126
|
+
"sha256": "43a7674a6da40e2091538d6a24664a66c4cf607cc16b6027f2ddf6fd7421861c",
|
|
127
|
+
"updated": "2026-10-10T18:47:07Z"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
"file": "serverless-functions-js.md",
|
|
131
|
-
"sha256": "
|
|
132
|
-
"updated": "2026-10-
|
|
131
|
+
"sha256": "56588d50d5a59f066164565470663f8f9da03fe85d98a96d3754e6ec14eb9fac",
|
|
132
|
+
"updated": "2026-10-10T00:12:33Z"
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"file": "serverless-functions.md",
|
package/docs/platform-rules.md
CHANGED
|
@@ -295,12 +295,12 @@ them to do something is.
|
|
|
295
295
|
Both of these are working code in this repository. Neither was linked from anywhere until this page
|
|
296
296
|
existed, which is how integrators end up rebuilding them.
|
|
297
297
|
|
|
298
|
-
-
|
|
298
|
+
- **`examples/e2e/` — the bridge, end to end, in a real browser.** A real
|
|
299
299
|
parent page and a real game frame, served from two different origins, exchanging a real
|
|
300
300
|
`MessagePort`. `pnpm e2e` builds the artifacts and serves it. This is the thing to run when a
|
|
301
301
|
handshake is not landing and you need to know whether the problem is the SDK or your page:
|
|
302
302
|
`?sandbox=strict` reproduces the sandbox failure on demand.
|
|
303
|
-
-
|
|
303
|
+
- **`examples/server-bundle/echo.mjs` — a complete dedicated
|
|
304
304
|
server for a match, in one file.** The lifecycle a bundle implements and the first message on the
|
|
305
305
|
channel, running. Its contract is [`server-bundles.md`](server-bundles.md).
|
|
306
306
|
|
|
@@ -746,16 +746,140 @@ for it would find it, and the room would still be briefly the wrong size — a v
|
|
|
746
746
|
by a hidden one. The platform refuses the field outright, for this reason: a
|
|
747
747
|
developer meets the rule locally rather than as a 400 in production.
|
|
748
748
|
|
|
749
|
+
## 5c. Queueing for a match: tickets
|
|
750
|
+
|
|
751
|
+
`quickJoin()` puts a player in a room at once and lets the room fill around them. That is right for a
|
|
752
|
+
drop-in mode and wrong for one that must not start short — a duel, a team mode — because the first
|
|
753
|
+
player in sits alone in a lobby, and a match is sized by who is there when it starts.
|
|
754
|
+
|
|
755
|
+
`queueForMatch()` is the other way in. The player waits in a queue; when the platform has enough
|
|
756
|
+
players for one of the templates the ticket named, it makes the room, seats every one of them and
|
|
757
|
+
starts the match. Nobody hosts, and nobody calls `startMatch()`.
|
|
758
|
+
|
|
759
|
+
```js
|
|
760
|
+
const ticket = await gem.rooms.queueForMatch({ templateSlugs: ['duel', 'brawl'] });
|
|
761
|
+
showSearching({ expiresAt: ticket.expiresAt, onCancel: () => ticket.cancel() });
|
|
762
|
+
|
|
763
|
+
const outcome = await ticket.outcome;
|
|
764
|
+
if (outcome.status === 'matched') enterMatch(outcome.room);
|
|
765
|
+
else backToMenu(outcome.status);
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
**A template has to opt in.** A room template is queueable when its `matchmaking` block says
|
|
769
|
+
`enabled: true`; a ticket naming one that does not is refused whole, **400
|
|
770
|
+
`matchmaking_not_enabled`**. `fill_window_seconds` in that block is how long a queue holds out for
|
|
771
|
+
`max_players` before it accepts `min_players`. The block cannot be combined with `auto_start_host`.
|
|
772
|
+
|
|
773
|
+
**A ticket names every mode the player accepts**, one to eight templates, and is matched under
|
|
774
|
+
whichever fills first. That is one ticket, not several: a player can be on one ticket at a time, across
|
|
775
|
+
every game.
|
|
776
|
+
|
|
777
|
+
### What the SDK does while the ticket waits
|
|
778
|
+
|
|
779
|
+
- **It keeps the ticket alive.** A ticket nobody heartbeats is expired by the platform, so that a
|
|
780
|
+
player who closed the tab does not hold a place in a lobby. The interval is the platform's setting
|
|
781
|
+
and arrives on the ticket; the SDK reads it from there and a game never sends a heartbeat.
|
|
782
|
+
- **The heartbeat is also how it learns the answer.** Each one returns the ticket as it stands, so a
|
|
783
|
+
match is noticed within one interval even when nothing crosses the bridge.
|
|
784
|
+
- **A relayed push makes it ask at once.** The match's own start is relayed like any room event (§3),
|
|
785
|
+
and one that arrives about a room the manager is not in, while a ticket waits, is treated as a cue:
|
|
786
|
+
the ticket is read immediately, and once more a second later if it still says `queued` — the
|
|
787
|
+
platform sends the push a moment before it marks the ticket. So on a page that relays, a match is
|
|
788
|
+
noticed in about the time the push takes, not at the next heartbeat. A tab returning to the
|
|
789
|
+
foreground is the same cue. Like every relay it is a hint: nothing is entered on its strength.
|
|
790
|
+
- **It refuses a second thing in flight.** While a ticket waits, `create`, `join`, `joinByCode`,
|
|
791
|
+
`quickJoin` and another `queueForMatch` are refused locally.
|
|
792
|
+
- **It cancels the ticket when the tab closes.** See below.
|
|
793
|
+
- **It raises no `seated` for the ticket's own room.** The owner is told by the outcome. Party
|
|
794
|
+
members, who hold no ticket, are told by `seated` as they are for any room their leader enters.
|
|
795
|
+
|
|
796
|
+
### Queueing from inside a room
|
|
797
|
+
|
|
798
|
+
A player sitting in a hub or a social lobby can queue without leaving it. The ticket takes no seat
|
|
799
|
+
while it waits, and `gem.rooms.currentRoom` stays the room they are in.
|
|
800
|
+
|
|
801
|
+
When the ticket is matched, **the manager moves and the seat stays**. `currentRoom` becomes the
|
|
802
|
+
match's room and `joined` fires for it. The room the player queued from is not left: no leave is
|
|
803
|
+
sent, no `left` is raised, and the platform still counts them as a member there. When the match is
|
|
804
|
+
over, `gem.rooms.leave()` then `gem.rooms.join(hub.id)` puts them back — `join()` on a room a player
|
|
805
|
+
already holds a seat in answers with the room.
|
|
806
|
+
|
|
807
|
+
Keep the hub's id if you want that return trip: `currentRoom` is the match's room now, and the
|
|
808
|
+
manager does not hand the old one back.
|
|
809
|
+
|
|
810
|
+
**A tab closed before the player goes back leaves both rooms.** The SDK remembers the room a matched
|
|
811
|
+
ticket moved it out of, and on `pagehide` sends a keepalive leave for it as well as for the room it
|
|
812
|
+
is in. Otherwise the hub seat would outlive the page with nothing tracking it, and the player's next
|
|
813
|
+
session would start already inside a room. Once the player is back in the hub, it is the tracked
|
|
814
|
+
room again and the ordinary leave covers it.
|
|
815
|
+
|
|
816
|
+
### When the tab closes
|
|
817
|
+
|
|
818
|
+
**The ticket is cancelled as the page goes away.** On `pagehide` the SDK sends the cancel as a
|
|
819
|
+
keepalive request, which the browser delivers after the document is gone, and settles the ticket
|
|
820
|
+
`cancelled`. Without it a closed tab's ticket would stay matchable until the platform noticed nobody
|
|
821
|
+
was keeping it alive, and a match formed in that window seats a player who is not there.
|
|
822
|
+
|
|
823
|
+
This also happens when the page is put in the back/forward cache. A frozen page keeps nothing alive,
|
|
824
|
+
so the player cannot come back to the same ticket; they come back to one that reads `cancelled` and
|
|
825
|
+
queue again.
|
|
826
|
+
|
|
827
|
+
The platform's own timeout is still there for a tab that crashed or lost its network, where no
|
|
828
|
+
request can be sent. It is the fallback, not the mechanism.
|
|
829
|
+
|
|
830
|
+
### How a ticket ends
|
|
831
|
+
|
|
832
|
+
`ticket.outcome` settles once, and **resolves** for every ending the platform reports — these are
|
|
833
|
+
outcomes to branch on, not errors to catch:
|
|
834
|
+
|
|
835
|
+
| `status` | `reason` | What happened | Queue again? |
|
|
836
|
+
| --- | --- | --- | --- |
|
|
837
|
+
| `matched` | — | Seated, and the match is starting. `gem.rooms.currentRoom` is `outcome.room`, `joined` has fired, and the match events follow as for any room. | — |
|
|
838
|
+
| `expired` | `timed_out` | Nobody to play with before the ticket's expiry. | on a fresh press |
|
|
839
|
+
| `expired` | `heartbeat_lapsed` | The tab was throttled or offline too long to keep it alive. | on a fresh press |
|
|
840
|
+
| `cancelled` | `requested` | `ticket.cancel()`. | — |
|
|
841
|
+
| `cancelled` | `party_changed` | The party gained or lost a member while it waited. | once the party is settled |
|
|
842
|
+
| `failed` | `handoff_failed` | A match formed and could not be started, several times over. | later; this is the platform's fault |
|
|
843
|
+
| `lost` | — | The platform no longer has the ticket. Queues are held in memory and a restart loses them. | yes |
|
|
844
|
+
| `unavailable` | — | Matchmaking is turned off, or stopped answering for longer than a ticket survives. | not in a loop |
|
|
845
|
+
|
|
846
|
+
`lost` and `unavailable` are the SDK's own conclusions. A heartbeat answered "no such ticket" ends
|
|
847
|
+
the wait as `lost` at once. Three heartbeats in a row that got no usable answer end it too — as
|
|
848
|
+
`unavailable` when the platform said matchmaking is off, `lost` otherwise — and the SDK sends one
|
|
849
|
+
cancel on its way out, in case the requests arrived and only the answers were lost.
|
|
850
|
+
|
|
851
|
+
It **rejects** in one case: the ticket was matched and the room it was seated in could not be read. The
|
|
852
|
+
seat stands, so `myRooms()` shows the room and `join(id)` enters it.
|
|
853
|
+
|
|
854
|
+
**`cancel()` can lose the race.** A ticket the platform has already seated is never undone, so
|
|
855
|
+
`cancel()` resolves the ticket's real outcome, and that may be `matched`. Backing out from there is
|
|
856
|
+
`gem.rooms.leave()`.
|
|
857
|
+
|
|
858
|
+
### The refusals when queueing
|
|
859
|
+
|
|
860
|
+
`queueForMatch()` itself rejects with `GemApiError` when no ticket was made. Branch on `code`:
|
|
861
|
+
|
|
862
|
+
| Code | Meaning |
|
|
863
|
+
| --- | --- |
|
|
864
|
+
| `already_queued` (409) | This player, or someone in their party, is already on a ticket — another tab, or another game. |
|
|
865
|
+
| `matchmaking_not_enabled` (400) | A named template has no enabled `matchmaking` block. A config fault, not a player one. |
|
|
866
|
+
| `matchmaking_unavailable` (503) | Matchmaking is turned off for the whole platform, or its store is not answering. |
|
|
867
|
+
|
|
868
|
+
⚠️ **Nothing falls back to quick-join, and a game should not either.** Not on
|
|
869
|
+
`matchmaking_unavailable`, not on `expired`. Quick-join puts the player in a different kind of room —
|
|
870
|
+
alone, in a mode that was queued for precisely because it needs a lobby. Tell the player matchmaking is
|
|
871
|
+
unavailable and leave the button where it is.
|
|
872
|
+
|
|
749
873
|
## 6. Scope
|
|
750
874
|
|
|
751
875
|
**In:** rooms, membership, host, ready state, the match lifecycle, and the dedicated-server connect
|
|
752
876
|
flow reshaped around them. P2P _allocation_ falls out of the same shape — `startMatch` is one route
|
|
753
877
|
and a P2P match simply reports `ready` at once.
|
|
754
878
|
|
|
755
|
-
**Out:**
|
|
756
|
-
|
|
757
|
-
There is no ranked
|
|
758
|
-
|
|
879
|
+
**Out:** matching by anything but arrival order. `quickJoin()` is exactly `quick_join`: join an open
|
|
880
|
+
public room matching the template, in a cluster this player can play well in, or create one.
|
|
881
|
+
`queueForMatch()` (§5c) waits for a full lobby, oldest ticket first. There is no ranked system and no
|
|
882
|
+
skill rating, and inventing one is not a small addition to this.
|
|
759
883
|
|
|
760
884
|
**Elsewhere:** P2P _connection_ and parties. Both are
|
|
761
885
|
[`p2p-and-parties.md`](p2p-and-parties.md). Three things surface here:
|
package/docs/server-bundles.md
CHANGED
|
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
|
|
|
80
80
|
also publishes on its download host, with the hash beside it:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
curl -fsSL -O https://downloads.gemarcade.com/sdk/
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v10.0.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v10.0.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -261,7 +261,7 @@ other version-skew member on `host`:
|
|
|
261
261
|
| `leave` | `cause` — why the connection ended, as the runtime saw it; `superseded` means the same player is already back on a newer connection and the `join` that says so follows at once |
|
|
262
262
|
| `message` | `reliable` / `ordered` — [the delivery class the sender asked for](#delivery-classes-the-flags-are-the-senders-request); `sessionId`; `playerNumber` |
|
|
263
263
|
|
|
264
|
-
A complete working example is
|
|
264
|
+
A complete working example is `examples/server-bundle/echo.mjs`
|
|
265
265
|
— a chat relay in one file, exercising every lifecycle method, the startup reads and most of the
|
|
266
266
|
runtime surface. This repo's suite drives it against the contract below, so it is a starting point
|
|
267
267
|
rather than a sketch.
|
|
@@ -493,7 +493,7 @@ and inherits none of that, which is why the example above loops around it.
|
|
|
493
493
|
|
|
494
494
|
A complete, runnable version of this — with the slot's access rules, the room template that makes a
|
|
495
495
|
dedicated server exist at all, and a real match's logs — is
|
|
496
|
-
|
|
496
|
+
`examples/server-saves/` in the SDK repository. It refuses the
|
|
497
497
|
upload outright where the snippet above merely skips an unexpected header, because a grant naming
|
|
498
498
|
something else is a platform bug worth surfacing rather than working around.
|
|
499
499
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Serverless functions in JavaScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**The platform does not run JavaScript functions yet.** This page describes how one is written
|
|
4
|
+
and how it behaves; today a function you publish is a WebAssembly component, as
|
|
5
|
+
[serverless-functions.md](serverless-functions.md) describes.
|
|
4
6
|
|
|
5
7
|
A serverless function can be written in JavaScript or TypeScript instead of Rust: the language your
|
|
6
8
|
game is already written in, so the verification code in your browser game moves into a function
|