@indiegems/gem-web-sdk 7.9.0 → 8.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 +99 -0
- package/README.md +1 -0
- package/dist/function.d.ts +26 -1
- package/dist/game.d.ts +32 -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 +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/sdk.d.ts +32 -1
- package/docs/api/api-reference-game.md +435 -15
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +443 -7
- package/docs/changelog.md +99 -0
- package/docs/friends.md +86 -11
- package/docs/getting-started.md +10 -6
- package/docs/manifest.json +21 -16
- 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 +106 -63
- package/package.json +1 -1
- package/schema/bridge.v1.json +60 -0
- package/schema/capabilities.public.json +9 -0
package/docs/changelog.md
CHANGED
|
@@ -16,6 +16,105 @@ 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.0.0
|
|
20
|
+
|
|
21
|
+
*Written from the API facts alone; no summary was generated for this release.*
|
|
22
|
+
|
|
23
|
+
Upgrading from 7.9.1. Impact: required.
|
|
24
|
+
|
|
25
|
+
### Must change
|
|
26
|
+
|
|
27
|
+
- BucketConfig changed its signature.
|
|
28
|
+
- FRAME_TO_PARENT changed its signature.
|
|
29
|
+
- FrameMessage changed its signature.
|
|
30
|
+
- GemCapability changed its signature.
|
|
31
|
+
|
|
32
|
+
### New capability
|
|
33
|
+
|
|
34
|
+
- FriendChallenge was added.
|
|
35
|
+
- FriendChallengesPage was added.
|
|
36
|
+
- FriendsApi.inbox was added.
|
|
37
|
+
- FriendsApi.launchedChallenge was added.
|
|
38
|
+
- FriendsApi.markAnswered was added.
|
|
39
|
+
- FriendsApiDependencies.launchedChallengeId was added.
|
|
40
|
+
- ListFriendChallengesOptions was added.
|
|
41
|
+
- Gem.vibration was added.
|
|
42
|
+
- GemFrame.requestVibrate was added.
|
|
43
|
+
- VibrationApi was added.
|
|
44
|
+
- GemVibrate was added.
|
|
45
|
+
- HostCallbacks.onVibrate was added.
|
|
46
|
+
- HostOptions.onVibrate was added.
|
|
47
|
+
- validateGemVibrate was added.
|
|
48
|
+
|
|
49
|
+
## 7.9.1
|
|
50
|
+
|
|
51
|
+
Upgrading from 7.9.0. Impact: none.
|
|
52
|
+
|
|
53
|
+
Nothing in this release asks anything of a game.
|
|
54
|
+
|
|
55
|
+
## 7.9.0
|
|
56
|
+
|
|
57
|
+
Upgrading from 7.8.1. Impact: required.
|
|
58
|
+
|
|
59
|
+
### Must change
|
|
60
|
+
|
|
61
|
+
- A link your game shares can no longer carry a parameter named build. The Arcade now reads that key for itself and removes it before your game starts, so the share request refuses it with a TypeError that names the key. Rename the parameter if your game shares one by that name.
|
|
62
|
+
|
|
63
|
+
### Rebuild
|
|
64
|
+
|
|
65
|
+
- GemCapability gained gameMenu, and the bridge's message lists and unions in both directions gained the two new menu messages. A switch that assumes it knows every GemCapability value must handle the new one. Other code that compiles today keeps compiling.
|
|
66
|
+
|
|
67
|
+
### New capability
|
|
68
|
+
|
|
69
|
+
- gem.setMenu adds to the Arcade's menu, the one control the Arcade keeps over a game in its top right corner. Give onSettings and the menu gains a settings entry that opens your own settings screen, and give items for up to four entries of your own, each with an id, a label you have already translated and an onSelect handler. Each call is the whole answer and replaces the last, so call it again when the entries change. It returns true when the Arcade was told. After awaitBridge has settled, draw a settings button of your own only when it returns false. The Arcade pauses your game while its menu is open and resumes it before your handler runs.
|
|
70
|
+
|
|
71
|
+
## 7.8.1
|
|
72
|
+
|
|
73
|
+
Upgrading from 7.8.0. Impact: none.
|
|
74
|
+
|
|
75
|
+
Nothing in this release asks anything of a game.
|
|
76
|
+
|
|
77
|
+
## 7.8.0
|
|
78
|
+
|
|
79
|
+
Upgrading from 7.7.0. Impact: recommended.
|
|
80
|
+
|
|
81
|
+
### May stop
|
|
82
|
+
|
|
83
|
+
- The SDK now stops accidental text selection, the long-press copy menu and the tap highlight across your game's document, so a game can drop its own rules for that. Your own style rules always win, and inputs, text areas and editable content stay typeable and selectable. Text a player could select and copy before no longer is, unless your own styles make it selectable. Pass selectableText as true to createGem only if your game is mostly text a player should be able to copy.
|
|
84
|
+
|
|
85
|
+
### New capability
|
|
86
|
+
|
|
87
|
+
- A room says whether it is self-hosted in selfHosted: its dedicated server runs on the machine of the player who created it, as it does in the Gem editor. The SDK asks for such a room on your behalf where the embedder hosts locally, and there is no option for a game to set. It is false on every room a player in the Arcade is in.
|
|
88
|
+
|
|
89
|
+
## 7.7.0
|
|
90
|
+
|
|
91
|
+
Upgrading from 7.6.0. Impact: required.
|
|
92
|
+
|
|
93
|
+
### Must change
|
|
94
|
+
|
|
95
|
+
- In the server API types, occurred_at on a leaderboard row and on an entity leaderboard row is now optional and may be null. Code that assumes it is always a number must handle it missing. LeaderboardResponse and EntityLeaderboardResponse are now written as the shared LeaderboardPeriodFields joined with their own fields, so every field they had is still there.
|
|
96
|
+
|
|
97
|
+
### Rebuild
|
|
98
|
+
|
|
99
|
+
- Server API unions gained values. OrderSource has earning_reset, and InventoryChange has a new earning_limits_reset change. A switch that assumes it knows every OrderSource value or every InventoryChange type must handle the new one. Other code that compiles today keeps compiling.
|
|
100
|
+
- The server API reads getLeaderboard and getEntityLeaderboard accept three more options, periods_back, period and player_id, and the option types GetLeaderboardOptions and EntityLeaderboardOptions now extend the new PeriodOptions. Code that compiles today keeps compiling.
|
|
101
|
+
|
|
102
|
+
### New capability
|
|
103
|
+
|
|
104
|
+
- gem.leaderboards can read a board scoped to one period of a challenge, such as today's board or this week's. Every read takes periodsBack, where 0 is the current period and 1 is the one before, or period, the id of a period from an earlier result. Name at most one of them. A result from such a board carries period, which says which period it is, when it opens and closes, and whether it is final. A serverless function or a dedicated server passes playerId so the board answers for that player's own day. On a board that is not scoped to periods, both selectors are refused rather than ignored.
|
|
105
|
+
|
|
106
|
+
## 7.6.0
|
|
107
|
+
|
|
108
|
+
Upgrading from 7.5.0. Impact: required.
|
|
109
|
+
|
|
110
|
+
### Must change
|
|
111
|
+
|
|
112
|
+
- createGem now throws GemUnframedError when the page is opened on its own instead of inside a frame. Before, such a game constructed and then waited for a bridge that never came, with no player, no saves and no config. Run your game where it is framed and handed an identity: the Arcade, the Gem editor or the Gem dev host. A game that is only ever played there changes nothing. When the framing page is not one the game allows, the refusal is now final: awaitBridge, awaitIdentity, awaitPlayer and awaitCohorts answer at once instead of timing out.
|
|
113
|
+
|
|
114
|
+
### New capability
|
|
115
|
+
|
|
116
|
+
- A serverless function written in JavaScript can use the SDK through a new function entry of the package. createGem there returns the same platform-backed calls a browser game makes, bound to the player who called the function. Nothing is retried, a conditional save is refused before it is sent, and gem.functions and gem.gameConfig throw because a function has neither. This entry is in the package only.
|
|
117
|
+
|
|
19
118
|
## 7.5.0
|
|
20
119
|
|
|
21
120
|
Upgrading from 7.4.2. Impact: required.
|
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.0.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
|
|
|
@@ -365,9 +368,10 @@ What reaches the frame:
|
|
|
365
368
|
letter, so the arcade keeps that space for itself.
|
|
366
369
|
|
|
367
370
|
- **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
|
|
371
|
+
opens a friend challenge — but it is the platform's, not yours: read the challenge with
|
|
372
|
+
`gem.friends.launchedChallenge()`, never from the URL. Do not define a key of that name, and
|
|
373
|
+
treat every key beginning with `gem` as reserved: a friend-challenge template refuses a param
|
|
374
|
+
named that way.
|
|
371
375
|
- **Bounded.** At most 16 parameters. A key is a letter followed by up to 63 letters, digits,
|
|
372
376
|
`_` or `-`. A value is at most 512 characters with no control or format characters. An entry
|
|
373
377
|
that breaks a rule is dropped on its own; the rest of the link still arrives.
|
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": "37bae4170e89587ee8c498891fa2dac008f09fb01fcfbcf87b1310340d2cfc62",
|
|
37
|
+
"updated": "2026-10-06T19:43:21Z"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"file": "cross-promotion.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": "77ce7b2223d18267ac3ca2f49e09481752539f2674910e2cb12832825c945000",
|
|
72
|
+
"updated": "2026-10-06T19:43:21Z"
|
|
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": "49f51b37e969b6120d2df6cf6d2f33d2856c1602fd7504a7b308a9ea21c9b502",
|
|
122
|
+
"updated": "2026-10-06T19:43:21Z"
|
|
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": "436a8b61c46a3de6c7d5db65293bdcd17426412742dffd21f976bf87d1418d88",
|
|
147
|
+
"updated": "2026-10-06T17:36:25Z"
|
|
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.0.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v8.0.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
|
|