@indiegems/gem-web-sdk 7.9.1 → 8.1.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 +45 -0
- package/README.md +1 -0
- package/dist/function.d.ts +26 -1
- package/dist/game.d.ts +44 -1
- package/dist/gem-web-sdk.bridge.js +1 -1
- 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 +2 -2
- package/dist/index.d.ts +21 -3
- package/dist/sdk.d.ts +44 -1
- package/docs/achievements.md +72 -5
- package/docs/api/api-reference-game.md +719 -24
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +671 -15
- package/docs/changelog.md +45 -0
- package/docs/cross-promotion.md +38 -2
- package/docs/friends.md +86 -11
- package/docs/getting-started.md +12 -7
- package/docs/manifest.json +25 -20
- package/docs/saving-player-progress.md +41 -0
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions-js.md +3 -2
- package/docs/serverless-functions.md +53 -7
- package/docs/vibration.md +141 -0
- package/docs/what-a-game-must-do.md +136 -3
- package/package.json +1 -1
- package/schema/bridge.v1.json +102 -1
- package/schema/capabilities.public.json +15 -0
package/docs/changelog.md
CHANGED
|
@@ -16,6 +16,51 @@ 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
|
+
## 8.1.0
|
|
20
|
+
|
|
21
|
+
Upgrading from 8.0.0. Impact: required.
|
|
22
|
+
|
|
23
|
+
### Rebuild
|
|
24
|
+
|
|
25
|
+
- The list of messages a frame sends to its parent, the FrameMessage type and the GemCapability type each gained a portrait borders member. Recompiling is enough for most games. If your code handles every FrameMessage or GemCapability member exhaustively, add a case for the new one so it keeps compiling.
|
|
26
|
+
|
|
27
|
+
### New capability
|
|
28
|
+
|
|
29
|
+
- A game can now ask whether a rewarded placement could pay right now, before it draws its reward button, by calling AdsApi.rewardedStatus with the placement. The answer is a RewardedStatus saying whether the placement is declared, whether it is grantable now, how many grants remain today, when it is next eligible, whether the held limit is reached, and when the allowance resets. Times are in epoch milliseconds. The call never rejects. A failed read, a missing play token or a bad placement resolves as not declared and not grantable, and the read gives up after ten seconds. A placement only counts as grantable when the Arcade you run in supports ads. Failures are reported once per kind through your error handler.
|
|
30
|
+
- A game can now choose which border image from its store page the Arcade paints beside it in portrait by calling Gem.showPortraitBorders with the image's name. The name is a plain string of at most 40 characters, and one that fails the store page's naming rule is refused whole with a console warning. The choice is held until the bridge connects, sent at most once a second with the latest choice winning, and not resent if it matches what was last posted. If the Arcade does not advertise the portraitBorders capability, nothing is sent and nothing is raised.
|
|
31
|
+
- If you embed a game in your own frame, GemFrame.showPortraitBorders lets you send the same border choice, or clear it. It sends only while connected and only to a parent that advertised the portraitBorders capability, and it applies the same name rule as the game side.
|
|
32
|
+
- If you host games, you can now pass onPortraitBorders in your host options to receive the border name a game chose. The new GemPortraitBorders message carries it, and validateGemPortraitBorders checks a message you receive. The host re-checks the name on receipt and drops one that no store page could declare before your callback sees it.
|
|
33
|
+
|
|
34
|
+
## 8.0.0
|
|
35
|
+
|
|
36
|
+
*Written from the API facts alone; no summary was generated for this release.*
|
|
37
|
+
|
|
38
|
+
Upgrading from 7.9.1. Impact: required.
|
|
39
|
+
|
|
40
|
+
### Must change
|
|
41
|
+
|
|
42
|
+
- BucketConfig changed its signature.
|
|
43
|
+
- FRAME_TO_PARENT changed its signature.
|
|
44
|
+
- FrameMessage changed its signature.
|
|
45
|
+
- GemCapability changed its signature.
|
|
46
|
+
|
|
47
|
+
### New capability
|
|
48
|
+
|
|
49
|
+
- FriendChallenge was added.
|
|
50
|
+
- FriendChallengesPage was added.
|
|
51
|
+
- FriendsApi.inbox was added.
|
|
52
|
+
- FriendsApi.launchedChallenge was added.
|
|
53
|
+
- FriendsApi.markAnswered was added.
|
|
54
|
+
- FriendsApiDependencies.launchedChallengeId was added.
|
|
55
|
+
- ListFriendChallengesOptions was added.
|
|
56
|
+
- Gem.vibration was added.
|
|
57
|
+
- GemFrame.requestVibrate was added.
|
|
58
|
+
- VibrationApi was added.
|
|
59
|
+
- GemVibrate was added.
|
|
60
|
+
- HostCallbacks.onVibrate was added.
|
|
61
|
+
- HostOptions.onVibrate was added.
|
|
62
|
+
- validateGemVibrate was added.
|
|
63
|
+
|
|
19
64
|
## 7.9.1
|
|
20
65
|
|
|
21
66
|
Upgrading from 7.9.0. Impact: none.
|
package/docs/cross-promotion.md
CHANGED
|
@@ -13,8 +13,8 @@ const { outcome } = await gem.ads.requestAd({
|
|
|
13
13
|
startNextLevel();
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
That is the whole surface
|
|
17
|
-
tells it.
|
|
16
|
+
That is the whole surface, apart from one read that tells a rewarded button whether it could pay
|
|
17
|
+
(§3). The rest of this page is the four things a game gets wrong when nobody tells it.
|
|
18
18
|
|
|
19
19
|
## 1. The answer is usually no, and no is ordinary
|
|
20
20
|
|
|
@@ -80,6 +80,36 @@ your player is already sitting in, and it is the whole of what changes on the da
|
|
|
80
80
|
rebuild. [`inventory-and-catalogs.md` §6](inventory-and-catalogs.md#6-when-to-re-read) is the
|
|
81
81
|
general form of this.
|
|
82
82
|
|
|
83
|
+
**Whether a rewarded button could pay right now.** A rewarded placement is a button the player
|
|
84
|
+
presses, so you need to know whether to draw it. `gem.ads.rewardedStatus(placement)` reads one
|
|
85
|
+
placement's state from **your own** `rewarded_placements` config — its daily cap, cooldown and
|
|
86
|
+
`max_held` — applied to this player's use of it:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const status = await gem.ads.rewardedStatus('run-banked');
|
|
90
|
+
bankButton.disabled = !status.grantable;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| field | what it says |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `declared` | the config promoted to the channel being played names this placement |
|
|
96
|
+
| `grantable` | pressing the button could pay right now — **not** that an ad will play |
|
|
97
|
+
| `remainingToday` | grants left today under the daily cap; absent when the placement has none |
|
|
98
|
+
| `nextEligibleAt` | when a running cooldown ends, in epoch milliseconds; absent when none runs |
|
|
99
|
+
| `heldLimitReached` | the player already holds the placement's `max_held` — spend one first |
|
|
100
|
+
| `resetsAt` | when today's count resets, the next 00:00 UTC, in epoch milliseconds |
|
|
101
|
+
|
|
102
|
+
**`grantable` is not a promise.** A player can press a `grantable` button and get `no_ad`, for
|
|
103
|
+
reasons your game is not told. Read that `no_ad` like any other (§1) — never guess a reason — and
|
|
104
|
+
re-read your inventory whatever the ask settled as. The status grants nothing and reserves
|
|
105
|
+
nothing.
|
|
106
|
+
|
|
107
|
+
It never rejects, like the rest of `gem.ads`. Without a play token, when the read fails or takes
|
|
108
|
+
longer than ten seconds, or for a placement outside the grammar below, it answers not declared and
|
|
109
|
+
not grantable with no `resetsAt`, and the reason goes to your `onError` handler. It is a read of
|
|
110
|
+
the platform, so call it when you draw the button — on a results or death screen — and not every
|
|
111
|
+
frame, and draw the rest of the screen first rather than holding it on the answer.
|
|
112
|
+
|
|
83
113
|
## 4. The Arcade pauses your game for you
|
|
84
114
|
|
|
85
115
|
While a panel is up, your game is paused the same way it is paused when a player switches tabs —
|
|
@@ -130,6 +160,12 @@ schedule that does not depend on which branch produced it, so there is nothing i
|
|
|
130
160
|
read either. (`shown` is not held back — how long that one takes is how long your player spent
|
|
131
161
|
looking at a panel.)
|
|
132
162
|
|
|
163
|
+
The one thing you can read is your own economy, and never from the answer:
|
|
164
|
+
`gem.ads.rewardedStatus` reports **your own declared** rewarded caps as the platform holds them —
|
|
165
|
+
the platform can hold a placement's daily cap below what you declared, never above — so you can
|
|
166
|
+
tell when a rewarded `no_ad` was your own cap. It never reports anything about the player, the
|
|
167
|
+
platform's spacing between offers, or any limit that spans games.
|
|
168
|
+
|
|
133
169
|
## If you do nothing
|
|
134
170
|
|
|
135
171
|
Nothing happens. A game that never calls `gem.ads.requestAd` is never interrupted, and the call is
|
package/docs/friends.md
CHANGED
|
@@ -11,9 +11,10 @@ for (const friend of page.friends) {
|
|
|
11
11
|
}
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
That read,
|
|
15
|
-
confirms with the player and sends itself —
|
|
16
|
-
|
|
14
|
+
That read, one ask — [sending a friend a challenge](#friend-challenges), which the Arcade
|
|
15
|
+
confirms with the player and sends itself — and [reading the challenges the player
|
|
16
|
+
received](#reading-the-challenges-a-player-received) in your game are the whole surface. The
|
|
17
|
+
boundary around them is the thing to understand before you design against it.
|
|
17
18
|
|
|
18
19
|
## What the read is, and is not
|
|
19
20
|
|
|
@@ -31,7 +32,8 @@ Everything else about the friend graph stays on the Arcade, by design:
|
|
|
31
32
|
platform screens with the player's own account credential — a game frame holds a play token,
|
|
32
33
|
and a play token cannot touch the graph. Sending a [friend challenge](#friend-challenges) does
|
|
33
34
|
not touch it either: your game asks, and the Arcade sends only after the player confirms in its
|
|
34
|
-
own sheet.
|
|
35
|
+
own sheet. The one write a play token has is marking one of the player's own friend challenges
|
|
36
|
+
answered (`gem.friends.markAnswered`), which records that they played it and tells nobody.
|
|
35
37
|
- **No states.** Pending requests, blocks and relationship history do not appear. An accepted
|
|
36
38
|
friend appears; everyone else does not exist on this surface.
|
|
37
39
|
- **No presence, no invites.** Nothing here says who is online or delivers an invite — an invite
|
|
@@ -174,15 +176,88 @@ existing `instanceof GemApiError` handling keeps working:
|
|
|
174
176
|
|
|
175
177
|
### When a challenge launches your game
|
|
176
178
|
|
|
177
|
-
The friend's tap opens your game with the challenge's params in its launch query,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
The friend's tap opens your game with the challenge's params in its launch query, and with one key
|
|
180
|
+
of the platform's own, `gemChallenge`. **Do not read either from the URL to find the challenge.**
|
|
181
|
+
Ask the SDK:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
const challenge = await gem.friends.launchedChallenge();
|
|
185
|
+
if (challenge?.template === 'beat_code_jar') {
|
|
186
|
+
// Check every param before it does anything, as you would a launch key: the template
|
|
187
|
+
// constrained it when it was sent, but your config may have changed since.
|
|
188
|
+
const jar = typeof challenge.params.jar === 'string' && /^[A-Z0-9]{7}$/.test(challenge.params.jar)
|
|
189
|
+
? challenge.params.jar
|
|
190
|
+
: null;
|
|
191
|
+
if (jar) {
|
|
192
|
+
startJar(jar, {
|
|
193
|
+
from: challenge.from, // who sent it, from the platform's records
|
|
194
|
+
replay: challenge.answeredAt !== undefined, // already played once
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`launchedChallenge()` reads `gemChallenge` once, when `createGem` runs — so cleaning your URL
|
|
201
|
+
with `history.replaceState` afterwards changes nothing — and looks the challenge up on the
|
|
202
|
+
platform. The order matters the other way too: call `createGem` before you clean the URL, or
|
|
203
|
+
leave `gemChallenge` in place, because a key removed before `createGem` runs is never seen and
|
|
204
|
+
`launchedChallenge()` resolves `null` with no error. You get the sender as a [friend row](#what-the-read-is-and-is-not), the template key and
|
|
205
|
+
the params exactly as they were sent.
|
|
206
|
+
|
|
207
|
+
It **resolves `null`**, never rejects, for every launch that is not this player's challenge in
|
|
208
|
+
this game: no `gemChallenge` key, a value that is not a challenge id, a link naming someone
|
|
209
|
+
else's challenge, one from another game or channel, and every guest launch. Treat `null` as an
|
|
210
|
+
ordinary launch. Only a real failure — the platform unreachable, a context with no player —
|
|
211
|
+
rejects.
|
|
212
|
+
|
|
213
|
+
**Never put a name in a param, and never read one out.** A param key like `by=`, `from=` or
|
|
214
|
+
`name=` is forbidden: the sender is `challenge.from`, resolved by the platform, and a name in a
|
|
215
|
+
param is one nobody checked — your game would render whatever the sending client put there.
|
|
182
216
|
|
|
183
217
|
**A relaunch replays the same challenge.** When the player presses Play again in the Arcade, it
|
|
184
|
-
reuses the launch query, so the same
|
|
185
|
-
|
|
218
|
+
reuses the launch query, so the same challenge comes back. Tell a replay from a first attempt by
|
|
219
|
+
`answeredAt`, and make answering idempotent.
|
|
220
|
+
|
|
221
|
+
### Marking a challenge answered
|
|
222
|
+
|
|
223
|
+
When the attempt the challenge asked for is over — won or lost — mark it:
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
await gem.friends.markAnswered(challenge.id);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
It is idempotent: a challenge keeps the first time it was answered, and a second call changes
|
|
230
|
+
nothing. It tells the sender nothing — there are no read receipts — so it is your game's own
|
|
231
|
+
record, read back as `answeredAt` on every later read. It rejects `not_found` for an id that is
|
|
232
|
+
not one of this player's challenges in this game and channel (one answer for unknown and
|
|
233
|
+
someone else's), and `invalid`, before anything is sent, for a value that cannot be a challenge
|
|
234
|
+
id at all.
|
|
235
|
+
|
|
236
|
+
### Reading the challenges a player received
|
|
237
|
+
|
|
238
|
+
`gem.friends.inbox()` lists the friend challenges this player holds **in this game, on this
|
|
239
|
+
channel**, in every state:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
let page = await gem.friends.inbox();
|
|
243
|
+
renderChallenges(page.challenges);
|
|
244
|
+
while (page.nextCursor) {
|
|
245
|
+
page = await gem.friends.inbox({ cursor: page.nextCursor });
|
|
246
|
+
renderChallenges(page.challenges);
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
- **Answered challenges are included, and sort last**; within each part, newest first. Filter on
|
|
251
|
+
`answeredAt` if you only want the open ones.
|
|
252
|
+
- **A guest's page is empty**, not a refusal — a guest is never sent a challenge. Build the screen
|
|
253
|
+
for the empty state first.
|
|
254
|
+
- **Paged like the friends list**: default 50, at most 200, an opaque cursor passed back verbatim.
|
|
255
|
+
A player holds at most 200 friend challenges across every game, so one max-size page is all of
|
|
256
|
+
them.
|
|
257
|
+
- `openedAt` is when the player pressed Play on it in the Arcade. There is no "seen" time: that is
|
|
258
|
+
the Arcade's, not your game's.
|
|
259
|
+
|
|
260
|
+
For the challenge this launch carried, use `launchedChallenge()` rather than searching the inbox.
|
|
186
261
|
|
|
187
262
|
## Inspecting a player: game profiles
|
|
188
263
|
|
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": "8.1.0"
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -342,8 +342,11 @@ That is the whole contract for your own keys. The bridge carries no launch messa
|
|
|
342
342
|
has no API for them: the arcade does the forwarding, so a build you published on any SDK version
|
|
343
343
|
gets it without a code change. The one key that is not yours is the platform's `gemChallenge`
|
|
344
344
|
(below), which belongs to [friend challenges](friends.md#friend-challenges): never define it, and
|
|
345
|
-
never parse it yourself. The SDK
|
|
346
|
-
|
|
345
|
+
never parse it yourself. The SDK reads it for you — `gem.friends.launchedChallenge()` answers the
|
|
346
|
+
challenge it names, or `null` — and that is the only launch key it will ever read. It is read once,
|
|
347
|
+
when `createGem` runs, so cleaning your URL afterwards does not lose it. Call `createGem` before
|
|
348
|
+
you clean the URL, or leave `gemChallenge` in place: a key removed before `createGem` runs is never
|
|
349
|
+
seen, and `launchedChallenge()` then resolves `null` with no error.
|
|
347
350
|
|
|
348
351
|
What reaches the frame:
|
|
349
352
|
|
|
@@ -352,7 +355,8 @@ What reaches the frame:
|
|
|
352
355
|
only keys the arcade keeps for itself; it never interprets yours:
|
|
353
356
|
- Store and play-page switches: `play`, `channel`, `purchase`, and `build`, which pins one
|
|
354
357
|
of your own builds when you play it from your Builds page.
|
|
355
|
-
- Platform switches: `devtools`, `devLocale
|
|
358
|
+
- Platform switches: `devtools`, `devLocale`, and `gem_test_ads`, the mode of the arcade's
|
|
359
|
+
rewarded TEST ads.
|
|
356
360
|
- The arcade's own sign-in return marker: `social`.
|
|
357
361
|
- Attribution and campaign keys, which the arcade keeps out of every game: `_s`, `utm_*`
|
|
358
362
|
(every key with that prefix, matched case-sensitively, so UTM_X and utmost are yours),
|
|
@@ -365,9 +369,10 @@ What reaches the frame:
|
|
|
365
369
|
letter, so the arcade keeps that space for itself.
|
|
366
370
|
|
|
367
371
|
- **One platform key.** `gemChallenge` does reach the frame — the arcade adds it when a player
|
|
368
|
-
opens a friend challenge — but it is the platform's, not yours
|
|
369
|
-
|
|
370
|
-
a
|
|
372
|
+
opens a friend challenge — but it is the platform's, not yours: read the challenge with
|
|
373
|
+
`gem.friends.launchedChallenge()`, never from the URL. Do not define a key of that name, and
|
|
374
|
+
treat every key beginning with `gem` as reserved: a friend-challenge template refuses a param
|
|
375
|
+
named that way.
|
|
371
376
|
- **Bounded.** At most 16 parameters. A key is a letter followed by up to 63 letters, digits,
|
|
372
377
|
`_` or `-`. A value is at most 512 characters with no control or format characters. An entry
|
|
373
378
|
that breaks a rule is dropped on its own; the rest of the link still arrives.
|
package/docs/manifest.json
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"documents": [
|
|
9
9
|
{
|
|
10
10
|
"file": "achievements.md",
|
|
11
|
-
"sha256": "
|
|
12
|
-
"updated": "2026-10-
|
|
11
|
+
"sha256": "adc8bfaa769fbb283a53de1190ed85cdf4c1c9753e8cf78335b4595e98f8fad4",
|
|
12
|
+
"updated": "2026-10-06T22:14:30Z"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"file": "analytics.md",
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"file": "changelog.md",
|
|
36
|
-
"sha256": "
|
|
37
|
-
"updated": "2026-10-
|
|
36
|
+
"sha256": "f13808374edf0988e0aef58b857c82298702a67d450962522a532b4827b74c91",
|
|
37
|
+
"updated": "2026-10-07T16:20:42Z"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"file": "cross-promotion.md",
|
|
41
|
-
"sha256": "
|
|
42
|
-
"updated": "2026-
|
|
41
|
+
"sha256": "289cb4826a56b4e7423160e4187af65f4167c80218c2792ca0c01fbd977ecfec",
|
|
42
|
+
"updated": "2026-10-06T18:49:36Z"
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"file": "daily-challenges.md",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"file": "friends.md",
|
|
61
|
-
"sha256": "
|
|
62
|
-
"updated": "2026-10-
|
|
61
|
+
"sha256": "002e6f61050cd188baa0b9b056c9ceaccd96e653361e0c28646baa5632e525df",
|
|
62
|
+
"updated": "2026-10-06T16:37:15Z"
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
"file": "game-economy-tutorial.md",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"file": "getting-started.md",
|
|
71
|
-
"sha256": "
|
|
72
|
-
"updated": "2026-10-
|
|
71
|
+
"sha256": "222dcf81a2ed6d8fc8ffc37d5f7ef146dcf80c4b76f84fd9b8dd53ea1cbce23b",
|
|
72
|
+
"updated": "2026-10-07T16:20:42Z"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"file": "inventory-and-catalogs.md",
|
|
@@ -113,33 +113,38 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"file": "saving-player-progress.md",
|
|
116
|
-
"sha256": "
|
|
117
|
-
"updated": "2026-10-
|
|
116
|
+
"sha256": "08778cccc7d7a9a26a005a070bb2cc98784752ebec9dfce9873ef3d4e674e1e0",
|
|
117
|
+
"updated": "2026-10-05T16:53:25Z"
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"file": "server-bundles.md",
|
|
121
|
-
"sha256": "
|
|
122
|
-
"updated": "2026-10-
|
|
121
|
+
"sha256": "ffc7ec338f3436f73b16f8ba92a54de93e55bd5e9adbe562647e27b4867e4619",
|
|
122
|
+
"updated": "2026-10-07T16:20:42Z"
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"file": "serverless-functions-js.md",
|
|
126
|
-
"sha256": "
|
|
127
|
-
"updated": "2026-10-
|
|
126
|
+
"sha256": "8d5bda66dfebd93e80fe97b2f09c0651c26dc83b16827bdae7aa25f198f89e0b",
|
|
127
|
+
"updated": "2026-10-03T00:26:43Z"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
"file": "serverless-functions.md",
|
|
131
|
-
"sha256": "
|
|
132
|
-
"updated": "2026-10-
|
|
131
|
+
"sha256": "1edaa60a8aaf20499f24d661f169790cacfdb11c26529735a93e5b7b996e5432",
|
|
132
|
+
"updated": "2026-10-03T00:26:43Z"
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"file": "trading-and-gifting.md",
|
|
136
136
|
"sha256": "d7ee3c85540ab8aae29988c16a76bee3ba03cc66b10d2faaf5cd7210a80a0544",
|
|
137
137
|
"updated": "2026-08-26T03:27:49Z"
|
|
138
138
|
},
|
|
139
|
+
{
|
|
140
|
+
"file": "vibration.md",
|
|
141
|
+
"sha256": "3608da64abcef207cf0875f9d32f3aed88d4fb23512bc70d0a9684a570263a44",
|
|
142
|
+
"updated": "2026-10-06T17:36:25Z"
|
|
143
|
+
},
|
|
139
144
|
{
|
|
140
145
|
"file": "what-a-game-must-do.md",
|
|
141
|
-
"sha256": "
|
|
142
|
-
"updated": "2026-10-
|
|
146
|
+
"sha256": "3d3ed15c93368a03b11c0e4e1924797e2638aff5be264a799a259ee065d2dbfc",
|
|
147
|
+
"updated": "2026-10-06T23:08:59Z"
|
|
143
148
|
}
|
|
144
149
|
]
|
|
145
150
|
}
|
|
@@ -436,6 +436,47 @@ game, and a key you experiment with on `dev` cannot break a shipped build.
|
|
|
436
436
|
| a difficulty, a control scheme, a level counter | `gem.me.settings` — a string per key | yes |
|
|
437
437
|
| a volume, a quality preset, a dismissed tip | `gem.deviceSettings` — one object per device | **no** |
|
|
438
438
|
|
|
439
|
+
## Starting over as a new player while you test
|
|
440
|
+
|
|
441
|
+
Testing a first launch, a tutorial or a new player's economy means getting your own account
|
|
442
|
+
back to nothing on your game. Do not write code for it. A throwaway serverless function, or a
|
|
443
|
+
reset button you mean to remove later, is the workaround this replaces. gemctl does it, on
|
|
444
|
+
`dev` or `test`, for the account you are signed in as:
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
gemctl support reset --player me --channel dev --reason "fresh tutorial run"
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`--channel test` works the same way. `--reason` is required and is kept on the audit record, so
|
|
451
|
+
say why in a few words, with no personal data or tokens. Run it from your project's directory,
|
|
452
|
+
or add `--game <game-id>`.
|
|
453
|
+
|
|
454
|
+
The order matters, and two of the steps are the ones people skip:
|
|
455
|
+
|
|
456
|
+
1. **Close the game first.** A running game still holds the old state, and its next autosave
|
|
457
|
+
writes it straight back over the reset.
|
|
458
|
+
2. **Run the reset.**
|
|
459
|
+
3. **Clear the site data of the page the game runs in, with the game still closed.** That is
|
|
460
|
+
the Arcade, or the page `gemctl serve` opened, not the game's own origin. `gem.deviceSettings`
|
|
461
|
+
lives in that page's storage on this device, and no reset reaches it. Skip this and the old
|
|
462
|
+
volume, quality and dismissed tips come back. Clearing the Arcade's site data also signs you
|
|
463
|
+
out of it and clears the device settings of every game you have played there on this device;
|
|
464
|
+
the page `gemctl serve` opened holds only the game you are serving.
|
|
465
|
+
4. **Reopen the game.** It should see what a new player sees: `null` from
|
|
466
|
+
`gem.me.saves.get`, an empty `gem.me.settings.list()`, and `null` device settings.
|
|
467
|
+
|
|
468
|
+
The reset clears this player's state **in this game, on that one channel**: saves,
|
|
469
|
+
`gem.me.settings`, inventory and progressions, and the rest of what the game keeps for them.
|
|
470
|
+
It does not touch your other channels, your other games, achievements, or your Gems and
|
|
471
|
+
Tickets. Your purchases and what you paid stay on record, but an item a purchase delivered on
|
|
472
|
+
that channel is cleared with the rest, and the answer lists those purchases.
|
|
473
|
+
`gemctl support reset --help` has the full list, and the answer names whatever it left alone.
|
|
474
|
+
|
|
475
|
+
**It never runs on `release`.** gemctl refuses before sending anything, and so does the
|
|
476
|
+
platform. It is a tool for testing your own game, not something a game ships. A "New game" or
|
|
477
|
+
"Reset progress" button for players is game code: it overwrites or clears the player's slots
|
|
478
|
+
with `gem.me.saves` and their keys with `gem.me.settings`, as described above.
|
|
479
|
+
|
|
439
480
|
## What the platform already does with these, so you do not
|
|
440
481
|
|
|
441
482
|
**A player's erasure covers their saves.** When an account is erased the stored payloads go with
|
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/v8.1.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v8.1.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -33,8 +33,9 @@ message and the stack go to the platform's log for the invocation, never to the
|
|
|
33
33
|
`handleJson` whose input is not JSON, or whose return value JSON cannot carry, fails the same way.
|
|
34
34
|
|
|
35
35
|
**`console.*` reaches the invocation's log** at the matching level (`debug`, `info`, `warn`,
|
|
36
|
-
`error`); `console.log` and every other method log at `info`. As with a component's `log`,
|
|
37
|
-
reads it back
|
|
36
|
+
`error`); `console.log` and every other method log at `info`. As with a component's `log`,
|
|
37
|
+
`gemctl function logs` reads it back for a call on `dev`, and a call on `test` or `release` keeps
|
|
38
|
+
no line.
|
|
38
39
|
|
|
39
40
|
## 2. Writing one
|
|
40
41
|
|
|
@@ -81,7 +81,7 @@ everything a function can reach:
|
|
|
81
81
|
| `call-api` | One request to the platform API, made **as your game's server**: a method, a `/v1/` path and a JSON body, and nothing else |
|
|
82
82
|
| `download-link` | A save's contents, fetched from the download link the save's record carries |
|
|
83
83
|
| `upload-link` | A save's new contents, sent to the upload link the platform issued for them |
|
|
84
|
-
| `log` | A line kept
|
|
84
|
+
| `log` | A line kept 24 hours under this call's invocation id, on `dev` only, cut at 1 KiB |
|
|
85
85
|
| `random-bytes` | Randomness. The function has no other source |
|
|
86
86
|
| `now-ms` | Unix time in milliseconds. The function has no other clock |
|
|
87
87
|
|
|
@@ -120,9 +120,9 @@ Anything that changes several players at once — settling a match, trading betw
|
|
|
120
120
|
granting a whole room its rewards — is your **game server's** job, or the job of a function your
|
|
121
121
|
server calls, which is not bound to a player.
|
|
122
122
|
|
|
123
|
-
**`
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
**`gemctl function logs` reads back what `log` wrote** (section 7), with a line saying how each
|
|
124
|
+
call ended — but only for a call on `dev`, and only for 24 hours. A call on `test` or `release`
|
|
125
|
+
keeps no line, so to see what a tester or a player hit, make the same call on `dev`.
|
|
126
126
|
|
|
127
127
|
**A function cannot call a function.** `call-api` to `/v1/functions/invoke` is refused with
|
|
128
128
|
`function_cannot_invoke`.
|
|
@@ -732,8 +732,10 @@ It reports:
|
|
|
732
732
|
- `function_message`: your `Err` text, on a `function_error`;
|
|
733
733
|
- `reason`: on a `function_failed`, which limit or fault ended the call;
|
|
734
734
|
- `round_trip_ms`: measured by gemctl, from sending the call to the last byte back;
|
|
735
|
-
- `invocation_id`: what the platform files the call under. **Keep it.** It is
|
|
736
|
-
|
|
735
|
+
- `invocation_id`: what the platform files the call under. **Keep it.** It is what you pass to
|
|
736
|
+
`--invocation-id` to read that call's lines, and what you quote when you report the call;
|
|
737
|
+
- `logs`: on a `test` or `release` call that reached your function, that it left no log lines. A
|
|
738
|
+
`dev` call that reached it prints the `gemctl function logs` command that reads its lines instead.
|
|
737
739
|
|
|
738
740
|
Anything but `returned` exits 1.
|
|
739
741
|
|
|
@@ -744,6 +746,48 @@ If the call is refused before anything is invoked, the problem is your sign-in:
|
|
|
744
746
|
be a developer on the game, `test` needs a grant on it, and `release` needs a game your account may
|
|
745
747
|
play.
|
|
746
748
|
|
|
749
|
+
### Reading what it logged
|
|
750
|
+
|
|
751
|
+
`gemctl function logs` reads what your function wrote with `log`, newest first, with one closing
|
|
752
|
+
line per call saying how it ended.
|
|
753
|
+
|
|
754
|
+
```bash
|
|
755
|
+
gemctl function logs --invocation-id <invocation-id> # one call: what it logged, how it ended
|
|
756
|
+
gemctl function logs --outcome error # every call whose handle returned Err
|
|
757
|
+
gemctl function logs --level warn # what your function warned about
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
⚠️ **Only a call on `dev` leaves lines.** A call on `test` or `release` leaves none, so there is
|
|
761
|
+
no channel to choose and nothing here shows what a tester or a player hit. Make the same call on
|
|
762
|
+
`dev`, with the input that failed, and read that.
|
|
763
|
+
|
|
764
|
+
Each row is one of two kinds, and both carry the call's `invocation_id` and its `caller`, `player`
|
|
765
|
+
or `game_server`:
|
|
766
|
+
|
|
767
|
+
- **A line your function logged**: its `level` and its `message`.
|
|
768
|
+
- **The closing line**: `outcome` is `ok`, `error`, `timeout`, `cpu_exceeded` or `failed`, with the
|
|
769
|
+
call's `wall_ms` and `cpu_ms`. On `error`, `message` is your `Err` text. On `failed`, `reason`
|
|
770
|
+
says what ended the call, and when your own code trapped, `message` is the trap's message.
|
|
771
|
+
|
|
772
|
+
Newest first means a call's closing line comes before what it logged. **A call's logging is
|
|
773
|
+
capped**: once it reaches its per-call log limit, that `log` call and every later one go
|
|
774
|
+
unrecorded, and the closing line's `dropped_log_lines` says how many. A log that stops part-way
|
|
775
|
+
with a nonzero count means your function logged too much, not that it went quiet.
|
|
776
|
+
|
|
777
|
+
`--invocation-id`, `--level` (`debug`, `info`, `warn` or `error`) and `--outcome` narrow the page.
|
|
778
|
+
`--level` picks what your function logged and `--outcome` picks closing lines, so the two cannot be
|
|
779
|
+
combined: to see everything a failed call logged, pass its invocation id. The window is the last 24
|
|
780
|
+
hours unless `--start-ns` and `--end-ns` (Unix nanoseconds) narrow it, and a full page ends with a
|
|
781
|
+
hint carrying `--cursor` for the next, older one. There is no follow mode.
|
|
782
|
+
|
|
783
|
+
**An empty page does not prove your function was not called.** Lines take a few seconds to arrive
|
|
784
|
+
and are kept for 24 hours. A call the platform refused before your function ran leaves none — a bad
|
|
785
|
+
credential, a malformed request, a channel serving nothing, a full pool, a component that could not
|
|
786
|
+
be downloaded — and its `invoke` answer is its only record.
|
|
787
|
+
|
|
788
|
+
Every `message` was written by your function's code, and any member of your game's team can read
|
|
789
|
+
it. Log ids, not anything personal about a player.
|
|
790
|
+
|
|
747
791
|
## 8. When a call is refused
|
|
748
792
|
|
|
749
793
|
Every answer from a call that reached your function carries an invocation id: success,
|
|
@@ -766,7 +810,9 @@ the last column: it is `true` exactly where this table says to retry.
|
|
|
766
810
|
| `403 forbidden` | The call carried an account sign-in rather than a game-scoped or game-server token | Call from your game or its server, or through `gemctl function invoke` |
|
|
767
811
|
|
|
768
812
|
A `trap` is a panic, an `unreachable`, an out-of-bounds access or a stack overflow, and no stack
|
|
769
|
-
trace comes back with it.
|
|
813
|
+
trace comes back with it. On `dev`, the call's closing line in `gemctl function logs` carries the
|
|
814
|
+
trap's message when your own code trapped (section 7). The other three reasons are the limits in
|
|
815
|
+
section 2.
|
|
770
816
|
|
|
771
817
|
Publishing has its own refusals, and nothing is stored when one happens:
|
|
772
818
|
|