@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,552 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
From nothing to a game you can play in the Gem Arcade. Everything else in this section
|
|
4
|
+
assumes you have finished this page.
|
|
5
|
+
|
|
6
|
+
The platform is browser games: HTML, JavaScript and WebAssembly, served by the arcade and
|
|
7
|
+
played in a sandboxed frame. There is no engine to install and no editor to learn. If you
|
|
8
|
+
already have a web game, you are most of the way here.
|
|
9
|
+
|
|
10
|
+
## 1. Install gemctl and sign in
|
|
11
|
+
|
|
12
|
+
`gemctl` is the platform from the command line — it registers your game, serves it while you
|
|
13
|
+
work, and publishes it when you are ready. Ask the platform team where to get it; it is not
|
|
14
|
+
on npm.
|
|
15
|
+
|
|
16
|
+
Once it is installed, sign in:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
gemctl auth login
|
|
20
|
+
gemctl auth status # who you are, and which games you can reach
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The session lives in your OS keyring rather than in a file. If `auth status` shows no games,
|
|
24
|
+
that is an access problem rather than an empty account — ask whoever administers your
|
|
25
|
+
organisation before assuming you need to create one.
|
|
26
|
+
|
|
27
|
+
## 2. Point the directory at your game
|
|
28
|
+
|
|
29
|
+
Every command below is scoped to one game, identified by a UUID.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gemctl game list # the games this account can reach
|
|
33
|
+
gemctl settings set game_id <uuid> # bind this directory to one
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`gemctl settings` reports the id it resolved **and where it came from**, which is the first
|
|
37
|
+
thing to check if a command acts on the wrong game.
|
|
38
|
+
|
|
39
|
+
## 3. Add the SDK
|
|
40
|
+
|
|
41
|
+
The SDK is what connects your game to the platform — the signed-in player, their saved
|
|
42
|
+
progress, rooms and matches.
|
|
43
|
+
|
|
44
|
+
**It ships inside your game.** One dependency, and your bundler compiles it in like anything
|
|
45
|
+
else. The same install covers every part of the SDK — the browser client, and `defineServer`
|
|
46
|
+
if your game's matches run server logic of their own. You do not choose between them at
|
|
47
|
+
install time; a room template in your game config decides, and the code you write is the same
|
|
48
|
+
either way.
|
|
49
|
+
|
|
50
|
+
```jsonc
|
|
51
|
+
// package.json
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@indiegems/gem-web-sdk": "7.4.1"
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { createGem, text } from '@indiegems/gem-web-sdk/sdk';
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That is the whole of it. **Nothing goes into your HTML** — there is no `<script>` tag for
|
|
62
|
+
the SDK, and if you are editing `index.html` while wiring this up, you are on the wrong
|
|
63
|
+
path. It is a public package on npm, so no account, no credential and no repository access
|
|
64
|
+
is involved, and nothing is built when you install.
|
|
65
|
+
|
|
66
|
+
**`/sdk` is the import, from every seat.** It carries the whole surface, so client code, a
|
|
67
|
+
dedicated server bundle and a listen host all import the same path and the same names.
|
|
68
|
+
`.`, `/game` and `/server` stay published as narrow doors over the same modules — a game
|
|
69
|
+
wants none of them unless it knows why.
|
|
70
|
+
|
|
71
|
+
**`dependencies`, not `devDependencies`.** The SDK is part of what your game ships — your
|
|
72
|
+
bundler compiles it into the browser build and into the server bundle — so a `--production`
|
|
73
|
+
install still has to resolve it. In `devDependencies` it works on your machine and fails in
|
|
74
|
+
whatever builds your release.
|
|
75
|
+
|
|
76
|
+
### Upgrading, and the lockfile
|
|
77
|
+
|
|
78
|
+
**Pin an exact version — `X.Y.Z`, not `^X.Y.Z`.** A range lets an ordinary install move you
|
|
79
|
+
to a release you did not choose, and the client build and the server bundle can then end up
|
|
80
|
+
on different ones. With an exact pin, `npm update` leaves you where you are. To upgrade, change
|
|
81
|
+
the version, install again, and commit the lockfile. `npm outdated` tells you when a newer
|
|
82
|
+
release exists.
|
|
83
|
+
|
|
84
|
+
**Commit the lockfile.** npm and pnpm both record the package's hash on the first install;
|
|
85
|
+
**install from that lockfile** — `npm ci`, or `pnpm install --frozen-lockfile` — and every
|
|
86
|
+
install afterwards either gets the same bytes or fails. An install that re-resolves (a changed
|
|
87
|
+
`package.json`, `pnpm update`, a missing lockfile) records the hash again from whatever it
|
|
88
|
+
fetches. Uncommitted, the record is thrown away on every clone.
|
|
89
|
+
|
|
90
|
+
**Projects set up before 5.9.0 name a download URL instead** —
|
|
91
|
+
`https://downloads.gemarcade.com/sdk/vX.Y.Z/gem-web-sdk.tgz`. That URL keeps working, and it
|
|
92
|
+
is the same package byte for byte, but new releases are announced as npm versions. To move
|
|
93
|
+
over, replace the URL with the version number, install again, and commit the lockfile.
|
|
94
|
+
|
|
95
|
+
Each release's package is also published beside its hash on that download host, if you want to
|
|
96
|
+
check it by hand.
|
|
97
|
+
[`server-bundles.md`](server-bundles.md#where-the-server-half-of-the-sdk-comes-from) has the
|
|
98
|
+
recipe, and what that check does and does not prove.
|
|
99
|
+
|
|
100
|
+
### The first connect is not instant, and that is normal
|
|
101
|
+
|
|
102
|
+
The Arcade offers your game a connection on a repeating schedule rather than once, because it
|
|
103
|
+
cannot know when your code is ready to hear one. A game that boots through a module graph
|
|
104
|
+
connects on the first offer that lands after its listener attaches — **usually well under a
|
|
105
|
+
second, and up to about two in the worst case.** Nothing is wrong, nothing is retrying, and
|
|
106
|
+
the Arcade never gives up.
|
|
107
|
+
|
|
108
|
+
**That time is not a stall unless you make it one.** Call `gem.frame.ready()` and put your
|
|
109
|
+
own boot screen up first, then spend the wait loading assets.
|
|
110
|
+
[The boot phase](what-a-game-must-do.md#the-boot-phase-settle-the-bridge-before-the-heavy-bundle)
|
|
111
|
+
is where that time is either spent on work you owed anyway or wasted staring at a blank
|
|
112
|
+
screen, and it is the one part of this you own.
|
|
113
|
+
|
|
114
|
+
## 4. Connect
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { createGem } from '@indiegems/gem-web-sdk/sdk';
|
|
118
|
+
|
|
119
|
+
const gem = createGem();
|
|
120
|
+
|
|
121
|
+
// Before loading your main bundle. The arcade re-offers the connection until
|
|
122
|
+
// your game answers, but a busy main thread delays delivery of one that has
|
|
123
|
+
// already arrived.
|
|
124
|
+
await gem.awaitBridge();
|
|
125
|
+
|
|
126
|
+
// Who is playing, once sign-in completes. A room call made before this lands
|
|
127
|
+
// waits for it on its own (SDK 3.8.0 and later; before that it throws), so
|
|
128
|
+
// await it when you want the ids rather than as a gate you must pass first.
|
|
129
|
+
const me = await gem.awaitIdentity();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Two things about that call are worth getting right the first time.
|
|
133
|
+
|
|
134
|
+
**You do not list who may embed your game.** The Gem Arcade's website and app are always
|
|
135
|
+
allowed, and so is a dev host on your own machine (`gemctl serve`, the Gem Editor) while your
|
|
136
|
+
game is served from loopback. `allowedEmbedders` exists only for a page beyond those — see
|
|
137
|
+
[the embedder allowlist](what-a-game-must-do.md#1-the-embedder-allowlist) — and it adds to the
|
|
138
|
+
arcade's origins rather than replacing them.
|
|
139
|
+
|
|
140
|
+
**You do not declare your version.** The arcade routed this player to one of your builds, so it
|
|
141
|
+
knows which build that is and tells your game — and that is what every room and match call
|
|
142
|
+
reports. A `clientVersion` option still exists and is deprecated; see
|
|
143
|
+
[what a game must do](what-a-game-must-do.md#2-your-version-number-which-you-no-longer-declare)
|
|
144
|
+
for when an older arcade still needs it.
|
|
145
|
+
|
|
146
|
+
**One more line if you render with WebGL**, and it is not part of `createGem`:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Players can attach a screenshot to a bug report, and without that flag the browser may clear
|
|
153
|
+
your drawing buffer before the platform can read it. The SDK detects that and refuses rather
|
|
154
|
+
than sending your team **a fully black image with no error attached** — but the cost is that
|
|
155
|
+
your reports arrive with no picture. Two-dimensional canvas games need nothing here.
|
|
156
|
+
[The full section](what-a-game-must-do.md#bug-reports-players-send-you-and-the-one-webgl-flag-that-decides-them)
|
|
157
|
+
covers overriding the capture and attaching your own debug state, which is the part actually
|
|
158
|
+
worth spending effort on.
|
|
159
|
+
|
|
160
|
+
**There is no offline mode.** A game must be framed by an arcade page to reach the platform:
|
|
161
|
+
unframed it still constructs and runs, but the bridge never connects and anything needing a
|
|
162
|
+
token waits forever. To run framed without publishing, open your served build in the Gem
|
|
163
|
+
Arcade app (step 6 below), or use the SDK repository's `examples/dev-harness/` — a local
|
|
164
|
+
parent page that frames your dev server and completes a real handshake.
|
|
165
|
+
|
|
166
|
+
## 5. Tell the arcade when you are ready
|
|
167
|
+
|
|
168
|
+
The arcade shows a loading state until your game says it is playable. If you never say so, a
|
|
169
|
+
player watches a spinner over a game that is already running.
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
gem.frame.progress(loaded, total); // while assets load
|
|
173
|
+
gem.frame.ready(); // once the player can actually play
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
And give players a way out — a visible control that calls `gem.exit()`. A game a player
|
|
177
|
+
cannot leave is one they leave by closing the tab.
|
|
178
|
+
|
|
179
|
+
## 6. Play it
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
gemctl serve start
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Add `--tls` to open it in the Gem Arcade app. This serves your build and prints a link. Open it on this machine, or on a phone from the
|
|
186
|
+
arcade website, with no link to copy and no token to type.
|
|
187
|
+
|
|
188
|
+
### The developer panel
|
|
189
|
+
|
|
190
|
+
The arcade carries a panel that renders over your game, **inside your game's frame**, and your
|
|
191
|
+
game does not have to do anything for it. It shows identity, preferences, the bridge and its
|
|
192
|
+
boot timeline, the token, rooms, characters, your game's declared config — room templates and
|
|
193
|
+
save slots — and this player's saves, settings, inventory and currencies. It can **act**, not only
|
|
194
|
+
read: it writes against those values, and its frame controls can end the session. Treat it as a
|
|
195
|
+
console, not an inspector.
|
|
196
|
+
|
|
197
|
+
Add `devtools=1` to the query string of the play URL — `&devtools=1` if the link already has
|
|
198
|
+
one:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
https://gemarcade.com/play/<uuid>?devtools=1
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
That is the arcade **website's** play page, and the flag is read there. A build you are serving
|
|
205
|
+
locally is framed by the Gem Arcade app — a different parent — so if you need the panel before
|
|
206
|
+
you publish, `gem.devtools.open()` from your own code is the route that does not depend on
|
|
207
|
+
which parent framed you.
|
|
208
|
+
|
|
209
|
+
It opens a round trip after your game first asks for a token: the permission rides that grant,
|
|
210
|
+
not the handshake.
|
|
211
|
+
|
|
212
|
+
**The value must be exactly `1`.**
|
|
213
|
+
|
|
214
|
+
**It opens only for Contributor or above on that game** — the account signed into the arcade
|
|
215
|
+
_in that tab_, which is not necessarily the one `gemctl` holds. That is why the flag is safe to
|
|
216
|
+
leave in a link.
|
|
217
|
+
|
|
218
|
+
**If it does not appear**, open the console on the **arcade page — the top frame, not your
|
|
219
|
+
game's** — and read the lines prefixed `[play]`, plus any `[GameBridge]` ones. **You will see
|
|
220
|
+
exactly one of these**, so stop once you have matched it:
|
|
221
|
+
|
|
222
|
+
- **A `[play]` line about the value**, immediately, when it is not exactly `1`.
|
|
223
|
+
- **A `[play]` line saying the frame declined**, on the first grant, when the published build's
|
|
224
|
+
SDK predates the panel — v2.4.1 or newer carries it. Following this page you are already on a
|
|
225
|
+
newer one; this binds a build you published earlier.
|
|
226
|
+
- **A `[play]` line after 15 seconds** naming three causes it cannot tell apart: the account may
|
|
227
|
+
not have developer access on this game, the game may never have connected to the arcade or
|
|
228
|
+
never asked it for a token, or that build's SDK may predate the panel. A `[GameBridge]` line
|
|
229
|
+
about a refused mint, or a `handshake_timeout`, tells the second from the first — and
|
|
230
|
+
[the handshake is caught, not awaited](what-a-game-must-do.md#3-the-handshake-is-caught-not-awaited)
|
|
231
|
+
is where a game that never connects is usually losing.
|
|
232
|
+
- **A `[play]` line saying the panel was asked for, and then nothing on screen.** The request
|
|
233
|
+
arrived and the panel itself failed to load. That is reported in **your game's** frame console
|
|
234
|
+
and network tab, under `gem-web-sdk.devtools.min.js` — the SDK pins that file by hash, so a
|
|
235
|
+
blocked or altered fetch fails closed.
|
|
236
|
+
|
|
237
|
+
Once it is open:
|
|
238
|
+
|
|
239
|
+
- **Escape closes it** while it has focus; your game's own Escape handler still gets the key
|
|
240
|
+
otherwise. There is a close control too.
|
|
241
|
+
- **Closing it is final for that page load** — for the panel the arcade opened. Reload to get
|
|
242
|
+
it back, or call `gem.devtools.open()` from your own code, which can reopen it.
|
|
243
|
+
- **Fullscreen covers it.** Leave fullscreen to see it again.
|
|
244
|
+
|
|
245
|
+
The panel is also reachable from inside your game — see
|
|
246
|
+
[GemDevTools.open](https://gemarcade.com/developer/api/game#gemdevtools-open), which answers
|
|
247
|
+
`false` for a session that was not granted it rather than throwing — though it does reject if a
|
|
248
|
+
panel it _is_ allowed to open fails to load, so catch it either way.
|
|
249
|
+
|
|
250
|
+
If you put your own button on it, gate the button on
|
|
251
|
+
[`onDevToolsPermissionChange`](https://gemarcade.com/developer/api/game#gemoptions-ondevtoolspermissionchange)
|
|
252
|
+
rather than checking anything at boot:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
// Hidden until told otherwise. This never fires for a player, so nothing will
|
|
256
|
+
// hide it for you — a button that starts visible is one every player has and
|
|
257
|
+
// nobody can use.
|
|
258
|
+
let debugButtonVisible = false;
|
|
259
|
+
|
|
260
|
+
createGem({
|
|
261
|
+
onDevToolsPermissionChange: (canOpen) => setDebugButtonVisible(canOpen),
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The permission rides your session's first token, which arrives a round trip _after_ the game
|
|
266
|
+
connects — so anything you read while starting up is read before the answer exists, and a
|
|
267
|
+
developer would get no button. It is called again if the permission is withdrawn, so the button
|
|
268
|
+
disappears rather than staying on screen doing nothing.
|
|
269
|
+
|
|
270
|
+
`gem.devtools.open()` is what the button calls. It answers `false` rather than throwing for a
|
|
271
|
+
session that was not granted the panel, but it does **reject** if a permitted panel fails to
|
|
272
|
+
load — so catch it.
|
|
273
|
+
|
|
274
|
+
### Linking to your game
|
|
275
|
+
|
|
276
|
+
The play URL takes your own query parameters, and the arcade passes them into your game's
|
|
277
|
+
frame. So a game can hand a friend a link to itself — to a puzzle, a board or a level — and
|
|
278
|
+
read the values back when the link is opened.
|
|
279
|
+
|
|
280
|
+
**Ask the arcade to share the link.** Give your Share button to `gem.share.request`, with the
|
|
281
|
+
parameters the link should open your game with:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
// Show the button only where the arcade shares, and ask again on the click:
|
|
285
|
+
// `request` throws once `available` has gone false.
|
|
286
|
+
shareButton.hidden = !gem.share.available;
|
|
287
|
+
shareButton.addEventListener('click', () => {
|
|
288
|
+
if (!gem.share.available) return;
|
|
289
|
+
gem.share.request({ params: { jar: 'JAR-K7P3QXM', n: '2', beat: '812' } });
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The arcade builds the link, which unfurls with your game's own card, and shares it through the
|
|
294
|
+
player's share sheet, a copy, or its own Share panel. Your game is told nothing back — not
|
|
295
|
+
whether the player shared, cancelled, or closed the panel — and `request` returns nothing.
|
|
296
|
+
Where the arcade handles it, `gem.share.available` is true; outside the arcade, before the
|
|
297
|
+
bridge is up, and anywhere the arcade does not share your game, it is false.
|
|
298
|
+
|
|
299
|
+
`request` checks every parameter against the rules below **before** anything is sent, and
|
|
300
|
+
throws on the first one the arcade would refuse, naming it: more than 16, a key that breaks the
|
|
301
|
+
key rule or is one of the arcade's own, or a value that is not a string, is too long, or holds
|
|
302
|
+
a character a link cannot carry. A mistake shows up at your desk, not as a Share button that
|
|
303
|
+
does nothing for players — try it in the SDK's `examples/dev-harness/` with **share** ticked.
|
|
304
|
+
|
|
305
|
+
**Where `available` is false, build the link yourself.** Use the `/games/<id>` page with
|
|
306
|
+
`play=1`, which opens straight into your game and unfurls with its card, and hand it to the
|
|
307
|
+
browser's own share sheet where there is one:
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
https://gemarcade.com/games/<id>?play=1&jar=JAR-K7P3QXM&n=2&beat=812
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```js
|
|
314
|
+
// GAME_ID: your game's id, as in its page's address on the arcade.
|
|
315
|
+
const url = new URL(`https://gemarcade.com/games/${GAME_ID}`);
|
|
316
|
+
url.search = new URLSearchParams({ play: '1', jar: 'JAR-K7P3QXM', n: '2', beat: '812' }).toString();
|
|
317
|
+
// Cancelling the share sheet rejects with AbortError; that is a normal outcome.
|
|
318
|
+
if (navigator.share) await navigator.share({ url: url.href }).catch(() => {});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Either way, read the values back from your own `location.search` — asking for the keys you
|
|
322
|
+
defined, and checking each value before it does anything:
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
const launch = new URLSearchParams(location.search);
|
|
326
|
+
|
|
327
|
+
// Read only the keys you defined. Parse each into the type you expect and check
|
|
328
|
+
// it against the range you expect; anything else counts as "the link had none".
|
|
329
|
+
const jarRaw = launch.get('jar');
|
|
330
|
+
const jar = jarRaw && /^JAR-[A-Z0-9]{7}$/.test(jarRaw) ? jarRaw : null;
|
|
331
|
+
|
|
332
|
+
const n = Number(launch.get('n'));
|
|
333
|
+
const jarIndex = Number.isInteger(n) && n >= 1 && n <= JAR_COUNT ? n : null;
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
That is the whole contract for your own keys. The bridge carries no launch message, and the SDK
|
|
337
|
+
has no API for them: the arcade does the forwarding, so a build you published on any SDK version
|
|
338
|
+
gets it without a code change. The one key that is not yours is the platform's `gemChallenge`
|
|
339
|
+
(below), which belongs to [friend challenges](friends.md#friend-challenges): never define it, and
|
|
340
|
+
never parse it yourself. The SDK will read it for you, and that is the only launch key it will
|
|
341
|
+
ever read: the method ships in a later release, and until then a game has nothing to do with it.
|
|
342
|
+
|
|
343
|
+
What reaches the frame:
|
|
344
|
+
|
|
345
|
+
- **The query only.** A fragment (`#…`) is never forwarded.
|
|
346
|
+
- **Not the arcade's own keys.** These are stripped and never reach the game, and they are the
|
|
347
|
+
only keys the arcade keeps for itself; it never interprets yours:
|
|
348
|
+
- Store and play-page switches: `play`, `channel`, `purchase`.
|
|
349
|
+
- Platform switches: `devtools`, `devLocale`, `playOverlay`.
|
|
350
|
+
- The arcade's own sign-in return marker: `social`.
|
|
351
|
+
- Attribution and campaign keys, which the arcade keeps out of every game: `_s`, `utm_*`
|
|
352
|
+
(every key with that prefix, matched case-sensitively, so UTM_X and utmost are yours),
|
|
353
|
+
`fbclid`, `ttclid`, `gclid`, `msclkid`.
|
|
354
|
+
- `embedder`, which stays what
|
|
355
|
+
[the embedder allowlist](what-a-game-must-do.md#1-the-embedder-allowlist) says: never read
|
|
356
|
+
from the URL — a link cannot even try.
|
|
357
|
+
|
|
358
|
+
A key starting with `_` can never be one of yours: the key rule below requires a leading
|
|
359
|
+
letter, so the arcade keeps that space for itself.
|
|
360
|
+
|
|
361
|
+
- **One platform key.** `gemChallenge` does reach the frame — the arcade adds it when a player
|
|
362
|
+
opens a friend challenge — but it is the platform's, not yours. Do not define a key of that
|
|
363
|
+
name, and treat every key beginning with `gem` as reserved: a friend-challenge template refuses
|
|
364
|
+
a param named that way.
|
|
365
|
+
- **Bounded.** At most 16 parameters. A key is a letter followed by up to 63 letters, digits,
|
|
366
|
+
`_` or `-`. A value is at most 512 characters with no control or format characters. An entry
|
|
367
|
+
that breaks a rule is dropped on its own; the rest of the link still arrives.
|
|
368
|
+
|
|
369
|
+
Four things to hold onto when you share:
|
|
370
|
+
|
|
371
|
+
**Call `request` synchronously from your own Share button's click handler.** Never from a
|
|
372
|
+
timer, after an `await`, or on a game event such as a win: the arcade only shares on the
|
|
373
|
+
player's own click, and anywhere else it opens its Share panel instead, if it does anything.
|
|
374
|
+
|
|
375
|
+
**Never gate progress or a reward on sharing.** You cannot tell whether a player shared — the
|
|
376
|
+
arcade does not say, by design — so a reward for sharing is a reward for pressing a button.
|
|
377
|
+
|
|
378
|
+
**Put no personal identifiers in the parameters.** No player id, name, email or anything that
|
|
379
|
+
leads back to a person: the link is public, and it is handed to whoever the player sends it to.
|
|
380
|
+
Name the puzzle, not the player.
|
|
381
|
+
|
|
382
|
+
**The share text is the arcade's own.** It is the arcade's "Play {game} on Gem Arcade", in the
|
|
383
|
+
player's language, and your game supplies none.
|
|
384
|
+
|
|
385
|
+
And four things to hold onto when a link opens your game:
|
|
386
|
+
|
|
387
|
+
**Read only the keys you defined, and validate every value before it does anything.** Ignore
|
|
388
|
+
the rest of `location.search`, unknown keys included. For each key you do read, parse the
|
|
389
|
+
value into the type you expect and check it against the range you expect — an index is an
|
|
390
|
+
integer within the count you have, a code matches the shape of the codes you issue — and
|
|
391
|
+
treat anything else exactly as if the link had carried nothing. The arcade's bounds above are
|
|
392
|
+
containment, not validation: they keep a value from being enormous or unprintable and say
|
|
393
|
+
nothing about whether it makes sense to your game.
|
|
394
|
+
|
|
395
|
+
**Every value is text written by whoever made the link.** Render it as text, never as markup —
|
|
396
|
+
`textContent`, not `innerHTML`, and never inside a template that becomes HTML. And do not
|
|
397
|
+
trust it as a fact: `beat=812` means the link _says_ there is a score of 812 to beat, not that
|
|
398
|
+
anyone reached it. "Beat 812" is a claim, and the score is a number someone typed.
|
|
399
|
+
|
|
400
|
+
**Any dev or debug switch your game reads from its own URL is now one shared link away.** A
|
|
401
|
+
`?cheats=1` that used to need a developer typing it can be sent to anyone. Gate such switches
|
|
402
|
+
on a build flag, not the URL.
|
|
403
|
+
|
|
404
|
+
**The link replays.** The parameters stay in the store page's URL after the game exits, and the
|
|
405
|
+
frame is mounted with them again on the next press of Play. A game that must not act on a link
|
|
406
|
+
twice — one that grants something, or opens a challenge meant to be accepted once — should
|
|
407
|
+
remember, per tab, that it consumed it. `sessionStorage` is that scope.
|
|
408
|
+
|
|
409
|
+
## 7. Publish
|
|
410
|
+
|
|
411
|
+
### First, say which SDK the build was made with
|
|
412
|
+
|
|
413
|
+
**A published web build has to say which gem-web-sdk it was made with.** That is how a game
|
|
414
|
+
left on a version the platform has stopped supporting gets found and its author told, instead
|
|
415
|
+
of a player finding out.
|
|
416
|
+
|
|
417
|
+
The SDK ships a command that records it. Run it after your bundler, on the directory you
|
|
418
|
+
upload:
|
|
419
|
+
|
|
420
|
+
```jsonc
|
|
421
|
+
// package.json
|
|
422
|
+
"scripts": {
|
|
423
|
+
"build": "vite build && gem-sdk-stamp dist"
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
It writes one file, `gem-web-sdk.json`, at the root of that directory, and that is where the
|
|
428
|
+
version is read from:
|
|
429
|
+
|
|
430
|
+
```json
|
|
431
|
+
{
|
|
432
|
+
"schema_version": 1,
|
|
433
|
+
"sdk_version": "X.Y.Z"
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**You do not pass the version.** It is read from the copy of the package your bundler compiled
|
|
438
|
+
in, so upgrading the dependency is the only thing that changes it and the two can never
|
|
439
|
+
disagree. Running it twice is safe — it replaces what it finds.
|
|
440
|
+
|
|
441
|
+
**Your bundler does not matter.** It is a command rather than a plugin, so anything that ends
|
|
442
|
+
with a directory you upload can run it, including a build you assembled by hand.
|
|
443
|
+
|
|
444
|
+
### Then upload and promote
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
gemctl build upload --dir <your-build-output> --platform web --version 1.0.0
|
|
448
|
+
gemctl build promote 1.0.0 --platform web --channel dev
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**`--platform` is required and has no default**, and `web` is the one a browser game
|
|
452
|
+
publishes to — `universal` and `mobile` no longer accept new builds at all. **`--version`** is
|
|
453
|
+
a semver string of your choosing that has never been used for this game and platform, not even
|
|
454
|
+
by a build since deleted: to change what players get, upload a new version and promote it rather
|
|
455
|
+
than replacing one in place. Promote names the version too, because that is also how you roll
|
|
456
|
+
back — point the channel at the previous one.
|
|
457
|
+
|
|
458
|
+
This `--version` is the number the arcade reports to your game when it routes a player to this
|
|
459
|
+
build, and the number every room and match call is then checked against. Nothing in your source
|
|
460
|
+
has to agree with it.
|
|
461
|
+
|
|
462
|
+
A game appears in a channel's list only when **both** a build and a store page are assigned
|
|
463
|
+
to that channel — see the store page commands for the second half.
|
|
464
|
+
|
|
465
|
+
Start on `dev`, move to `test` for a wider group, and `release` when it is ready for players.
|
|
466
|
+
|
|
467
|
+
**A game may hold 25 builds per platform by default, and you do not have to manage that.** The
|
|
468
|
+
ceiling is per game and per platform rather than across either, and support can raise or lower
|
|
469
|
+
it. An upload at the ceiling still succeeds: the platform removes the least recently used build
|
|
470
|
+
that nothing is using — one per upload, never more — and `gemctl build upload` and the Gem Editor
|
|
471
|
+
say which one. A build was last used when it was uploaded or when it last left a channel, so one
|
|
472
|
+
that left `release` last week counts as used last week however long ago it was uploaded.
|
|
473
|
+
|
|
474
|
+
**Some builds are never removed:** a build on any channel; the two most recent builds to leave
|
|
475
|
+
`release`, so there is always one to roll back to; anything uploaded in the last 30 minutes; a
|
|
476
|
+
server build a live match may still need; and a blacklisted build. An upload made with a CI or
|
|
477
|
+
service-client credential removes only builds that same credential uploaded.
|
|
478
|
+
|
|
479
|
+
**A removed build is a deleted build.** Support can recover it for 30 days, as with one you delete
|
|
480
|
+
yourself, and its version number cannot be used again — upload a new version instead, which
|
|
481
|
+
`--version next` picks for you. An upload is refused (`build_count_exceeded`) only when nothing
|
|
482
|
+
can be removed, and the refusal says when the next slot frees up — or that only deleting a build
|
|
483
|
+
yourself will. A ceiling lowered below what you already hold never removes builds for you:
|
|
484
|
+
uploads are refused until you are back under it.
|
|
485
|
+
|
|
486
|
+
### Deleting a version you no longer want
|
|
487
|
+
|
|
488
|
+
A game config and a store page are versioned the way a build is — every upload is a new version,
|
|
489
|
+
and a channel points at one — and an old version of either can be deleted the same way:
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
gemctl config list --all # every config version, and the channel serving each
|
|
493
|
+
gemctl config delete <version> --yes
|
|
494
|
+
|
|
495
|
+
gemctl store list --all # every store-page version, and the channel showing each
|
|
496
|
+
gemctl store delete <version> --yes
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**You never have to.** A version no channel points at reaches nobody. Delete one to clear out
|
|
500
|
+
versions you will not promote again.
|
|
501
|
+
|
|
502
|
+
**A delete is recoverable for 30 days, but not by you.** The version leaves the list at once and
|
|
503
|
+
can no longer be promoted. Support can restore it for 30 days; after that it is removed for good,
|
|
504
|
+
and a store-page version takes its images and trailers with it. That is why the command asks for
|
|
505
|
+
`--yes`. To see deleted versions and when each one is removed, add `--include-deleted`:
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
gemctl config list --all --include-deleted
|
|
509
|
+
gemctl store list --all --include-deleted
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Take it off its channels first.** A version a channel still points at cannot be deleted, and
|
|
513
|
+
the refusal names the channels. Promote another version to each of them, or
|
|
514
|
+
`gemctl config unassign --channel <channel>` / `gemctl store unassign --channel <channel>` to
|
|
515
|
+
leave the channel with nothing, then delete.
|
|
516
|
+
|
|
517
|
+
**Deleting is not how you roll back.** Rolling back is promoting the earlier version, exactly as
|
|
518
|
+
it is for a build. Deleting a version changes nothing a player sees, because a version players
|
|
519
|
+
can see is one the platform will not let you delete.
|
|
520
|
+
|
|
521
|
+
When a delete is refused, nothing has changed, and the refusal's code says why:
|
|
522
|
+
|
|
523
|
+
| Code | Why | What to do |
|
|
524
|
+
| ----------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
525
|
+
| `version_assigned_to_channel` | A channel still points at this version | Promote another version to that channel, or unassign it, then delete |
|
|
526
|
+
| `version_pending` | A config version that is still being validated | Wait until `gemctl config view <version>` shows it settled |
|
|
527
|
+
| `revision_processing` | A store-page version that is still being processed | Wait until `gemctl store view <version>` shows `ready` or `failed` |
|
|
528
|
+
| `revision_in_experiment` | A store-page experiment that has not concluded uses this version | Conclude the experiment, or unassign the channel it runs on, then delete |
|
|
529
|
+
| `not_found` | There is no such version, or it is already deleted | `--include-deleted` shows which |
|
|
530
|
+
|
|
531
|
+
Promoting a deleted version is refused too (`cannot_assign_deleted_version`), and so is using a
|
|
532
|
+
deleted store-page version in an experiment. Upload it again to get a new version, or ask support
|
|
533
|
+
to restore the old one while its 30 days last.
|
|
534
|
+
|
|
535
|
+
Deleting takes your own sign-in, with an Owner, Admin or Contributor role on the game. A
|
|
536
|
+
credential made for a build pipeline can upload and promote, and cannot delete.
|
|
537
|
+
|
|
538
|
+
## What to read next
|
|
539
|
+
|
|
540
|
+
- **[What a game still has to do for itself](what-a-game-must-do.md)** — the parts of the
|
|
541
|
+
integration the SDK does not do for you, and which gaps you will hit first.
|
|
542
|
+
- **[The platform rules a game finds out for itself](platform-rules.md)** — the rules the
|
|
543
|
+
backend enforces that no client-side signature states. Read it before you debug
|
|
544
|
+
matchmaking.
|
|
545
|
+
- **[Saving a player's progress](saving-player-progress.md)** — if your game has anything
|
|
546
|
+
worth keeping between sessions.
|
|
547
|
+
- **[Inventory and catalogs](inventory-and-catalogs.md)** — if it has an economy: currencies,
|
|
548
|
+
items, shops or loot. Start with **[the shop tutorial](game-economy-tutorial.md)** to build one.
|
|
549
|
+
- **[Rooms and the match lifecycle](rooms-and-matches.md)** — if it is multiplayer.
|
|
550
|
+
|
|
551
|
+
The [API reference](https://gemarcade.com/developer/api) has every symbol the SDK publishes,
|
|
552
|
+
with signatures and descriptions.
|