@indiegems/gem-web-sdk 7.4.2 → 7.6.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 +886 -0
- package/README.md +1 -0
- package/dist/function.d.ts +978 -0
- package/dist/game.d.ts +11 -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 +3 -0
- 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 +16 -3
- package/dist/sdk.d.ts +11 -1
- package/docs/analytics.md +15 -5
- package/docs/api/api-reference-game.md +60 -33
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +29 -12
- package/docs/changelog.md +886 -0
- package/docs/getting-started.md +8 -6
- package/docs/manifest.json +18 -8
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions-js.md +139 -0
- package/docs/what-a-game-must-do.md +46 -26
- package/package.json +7 -2
- package/schema/bridge.v1.json +62 -1
- package/schema/capabilities.public.json +6 -0
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": "7.
|
|
53
|
+
"@indiegems/gem-web-sdk": "7.6.0"
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -157,11 +157,13 @@ your reports arrive with no picture. Two-dimensional canvas games need nothing h
|
|
|
157
157
|
covers overriding the capture and attaching your own debug state, which is the part actually
|
|
158
158
|
worth spending effort on.
|
|
159
159
|
|
|
160
|
-
**There is no offline mode.** A game must be framed by
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
160
|
+
**There is no offline mode.** A game must be framed by a page that speaks the bridge: opened
|
|
161
|
+
at the top level, `createGem` throws `GemUnframedError`, which says how to run it. To run framed
|
|
162
|
+
without publishing, use the Gem dev host, signed in to `gemctl`: `npm run dev` in a project the
|
|
163
|
+
Gem tools scaffolded (`gemctl serve start . --dev`, your vite server with hot reload), or
|
|
164
|
+
`gemctl serve start .` for a build. The Gem Editor's Play frames it too, and the SDK
|
|
165
|
+
repository's `examples/dev-harness/` is a local parent page that frames your dev server. Never
|
|
166
|
+
open the dev server's own address.
|
|
165
167
|
|
|
166
168
|
## 5. Tell the arcade when you are ready
|
|
167
169
|
|
package/docs/manifest.json
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"file": "analytics.md",
|
|
16
|
-
"sha256": "
|
|
17
|
-
"updated": "2026-
|
|
16
|
+
"sha256": "57fb07c3d649c2758a35660707d31b05164b363e9a425156978aed394d086339",
|
|
17
|
+
"updated": "2026-10-02T14:04:37Z"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"file": "arcade-economy.md",
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"sha256": "cf54939a0fcee1f5a6e11b9de52b5ed498ec8c373683e92aa5bc6654b12eb0ab",
|
|
32
32
|
"updated": "2026-10-01T03:49:20Z"
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
"file": "changelog.md",
|
|
36
|
+
"sha256": "02083c9ed1d62baed5a297ca71b602608afa90320afb73a21ff03187da11346f",
|
|
37
|
+
"updated": "2026-10-02T04:41:46Z"
|
|
38
|
+
},
|
|
34
39
|
{
|
|
35
40
|
"file": "cross-promotion.md",
|
|
36
41
|
"sha256": "3a8121a9c973e4a1bfed076a3226d139f8b4e38a173a06ea9f08000c72bca4b4",
|
|
@@ -63,8 +68,8 @@
|
|
|
63
68
|
},
|
|
64
69
|
{
|
|
65
70
|
"file": "getting-started.md",
|
|
66
|
-
"sha256": "
|
|
67
|
-
"updated": "2026-10-
|
|
71
|
+
"sha256": "fd859a01988ecf57c857dcfba9958a8a32a65ff2a8ecfaf55f33f9045d368180",
|
|
72
|
+
"updated": "2026-10-02T17:28:15Z"
|
|
68
73
|
},
|
|
69
74
|
{
|
|
70
75
|
"file": "inventory-and-catalogs.md",
|
|
@@ -113,8 +118,13 @@
|
|
|
113
118
|
},
|
|
114
119
|
{
|
|
115
120
|
"file": "server-bundles.md",
|
|
116
|
-
"sha256": "
|
|
117
|
-
"updated": "2026-10-
|
|
121
|
+
"sha256": "8190da0484b7e1ae6f636068b06a43729f3ef132488f5a0cfd2fc5c2988bc55b",
|
|
122
|
+
"updated": "2026-10-02T17:28:15Z"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"file": "serverless-functions-js.md",
|
|
126
|
+
"sha256": "86b0f9f0b742a662ebb78b1538cbbc399309725faf22f46f613b63b5e0864082",
|
|
127
|
+
"updated": "2026-10-02T17:17:29Z"
|
|
118
128
|
},
|
|
119
129
|
{
|
|
120
130
|
"file": "serverless-functions.md",
|
|
@@ -128,8 +138,8 @@
|
|
|
128
138
|
},
|
|
129
139
|
{
|
|
130
140
|
"file": "what-a-game-must-do.md",
|
|
131
|
-
"sha256": "
|
|
132
|
-
"updated": "2026-10-
|
|
141
|
+
"sha256": "a0a2952a2b16b43b0af831ff58ad8fabe47b414f57fccd2cb2d866861a65465e",
|
|
142
|
+
"updated": "2026-10-02T14:04:37Z"
|
|
133
143
|
}
|
|
134
144
|
]
|
|
135
145
|
}
|
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/v7.
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/v7.
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v7.6.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v7.6.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Serverless functions in JavaScript
|
|
2
|
+
|
|
3
|
+
<!-- gem-web: unpublished — the platform does not run JavaScript functions yet -->
|
|
4
|
+
|
|
5
|
+
A serverless function can be written in JavaScript or TypeScript instead of Rust: the language your
|
|
6
|
+
game is already written in, so the verification code in your browser game moves into a function
|
|
7
|
+
with no change beyond bundling. You upload one bundled ES module, and the platform runs it on a
|
|
8
|
+
JavaScript engine of its own.
|
|
9
|
+
|
|
10
|
+
Everything [serverless-functions.md](serverless-functions.md) says about what a function is for,
|
|
11
|
+
what it may change, its limits, channels, and calling it from your game holds here unchanged. This
|
|
12
|
+
page covers only what is different: how you write one, how the SDK behaves inside it, and how
|
|
13
|
+
publishing checks it.
|
|
14
|
+
|
|
15
|
+
## 1. The contract
|
|
16
|
+
|
|
17
|
+
The same as a component's: bytes in, bytes out, one fresh instance per call, nothing kept between
|
|
18
|
+
calls. Your module exports **exactly one** of two functions, and which one decides how the bytes
|
|
19
|
+
are read and written:
|
|
20
|
+
|
|
21
|
+
| Export | Input | Return |
|
|
22
|
+
| ------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `handle(input)` | `Uint8Array`: the request body, byte for byte | `Uint8Array`, `ArrayBuffer` or `string` (sent as UTF-8); anything else is an error |
|
|
24
|
+
| `handleJson(input)` | the body parsed as JSON; an empty body is `undefined` | any JSON-serializable value, sent as its JSON text; `undefined` is sent as `null`; a `BigInt` is an error |
|
|
25
|
+
|
|
26
|
+
Either may be `async`; the platform waits for the promise. Exporting neither, or both, is refused
|
|
27
|
+
when you upload. A caller cannot tell which kind of function answered: `gem.functions.invoke`, the
|
|
28
|
+
limits and the refusals in serverless-functions.md are identical for both.
|
|
29
|
+
|
|
30
|
+
**A thrown exception is `Err`.** The call fails with `function_error`, and `err.functionMessage` is
|
|
31
|
+
the exception's `name: message` (a thrown value that is not an `Error` is its `String()`). The
|
|
32
|
+
message and the stack go to the platform's log for the invocation, never to the caller. A
|
|
33
|
+
`handleJson` whose input is not JSON, or whose return value JSON cannot carry, fails the same way.
|
|
34
|
+
|
|
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`, nothing
|
|
37
|
+
reads it back to you: the invocation id is what you quote about one call.
|
|
38
|
+
|
|
39
|
+
## 2. Writing one
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
gemctl function init --lang js functions/score-check
|
|
43
|
+
cd functions/score-check && npm install && npm run build
|
|
44
|
+
gemctl function upload dist/function.js
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**`init --lang js`** writes a `package.json` whose `build` script type-checks and then bundles to
|
|
48
|
+
one ES module, a TypeScript starter handler and a `tsconfig.json`. gemctl runs neither step:
|
|
49
|
+
`npm run build` is yours, the way `cargo build` is for a component.
|
|
50
|
+
|
|
51
|
+
A handler looks like this:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { createGem } from '@indiegems/gem-web-sdk/function';
|
|
55
|
+
|
|
56
|
+
export async function handleJson(claim: { level: number }) {
|
|
57
|
+
const gem = createGem();
|
|
58
|
+
console.info(`level ${claim.level} claimed by ${gem.identity?.playerId ?? 'a game server'}`);
|
|
59
|
+
// Decide from what the player cannot forge, and make any write through `gem`
|
|
60
|
+
// here, before answering: the answer itself is only advice to the browser.
|
|
61
|
+
return { accepted: false };
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The package is the one your game already depends on, installed as
|
|
66
|
+
[getting-started.md](getting-started.md) shows. What makes a check worth running is
|
|
67
|
+
serverless-functions.md section 1, and it applies unchanged: a save or a setting the player's own
|
|
68
|
+
browser can write is the player's claim, not evidence.
|
|
69
|
+
|
|
70
|
+
**Imports.** The module may import the SDK and nothing else, under any of the three names a game
|
|
71
|
+
already uses: `@indiegems/gem-web-sdk/function`, `@indiegems/gem-web-sdk/sdk`, or
|
|
72
|
+
`@indiegems/gem-web-sdk/game`. Inside a function all three are the same module, the function
|
|
73
|
+
entry, so shared code keeps its import. A name that module does not export, such as
|
|
74
|
+
`defineServer`, fails every call with `function_error`. Everything else your code needs is bundled
|
|
75
|
+
in, which is what the starter's build does. An `import()` at runtime is refused at upload.
|
|
76
|
+
Top-level `await` is allowed and runs on every call.
|
|
77
|
+
|
|
78
|
+
**What the engine gives you.** `Date`, `performance`, `Math.random`, `crypto.getRandomValues`,
|
|
79
|
+
`crypto.randomUUID`, `TextEncoder`, `URL` and `fetch` all work. `fetch` reaches only the platform
|
|
80
|
+
API, through the SDK, and the save links the platform hands you; any other URL is refused. A timer
|
|
81
|
+
waits at most 10 ms: a function has no reason to sleep, and a long one would run out the wall
|
|
82
|
+
clock.
|
|
83
|
+
|
|
84
|
+
**The SDK version your function runs is the one the platform bundles**, not the one in your
|
|
85
|
+
`package.json`; the starter pins the matching version. A method added in a newer SDK reaches a
|
|
86
|
+
function when the platform's engine picks that version up.
|
|
87
|
+
|
|
88
|
+
## 3. The SDK inside a function
|
|
89
|
+
|
|
90
|
+
`createGem()` from `@indiegems/gem-web-sdk/function` is synchronous, `awaitBridge()` and
|
|
91
|
+
`awaitIdentity()` resolve at once, and `gem.me` is the player who called. So a game's boot sequence
|
|
92
|
+
runs unchanged, and the same `gem.me.saves.get`, `gem.leaderboards.get` and `gem.profiles.of` calls
|
|
93
|
+
work, with the same types and the same errors. The function's game and channel come from the call;
|
|
94
|
+
nothing you write names them. `gem.context` is who called, for which game and channel, and
|
|
95
|
+
`gem.identity` is the same three fields a browser game reads from its own `gem.identity`.
|
|
96
|
+
|
|
97
|
+
What a browser game has that is backed by the page or the arcade (ads, purchases, rooms, device
|
|
98
|
+
settings, sharing) is not on `gem` inside a function. Four more differences, each following from
|
|
99
|
+
how a function reaches the platform:
|
|
100
|
+
|
|
101
|
+
- **`gem.me` is the invoker.** When a game server invoked the function there is no player, and
|
|
102
|
+
reading `gem.me` or `gem.transfers` throws saying so; `gem.identity` is `null`. Which writes a
|
|
103
|
+
player-invoked function may make is unchanged (serverless-functions.md section 2).
|
|
104
|
+
- **No conditional requests, and no retries.** The platform call a function makes carries no
|
|
105
|
+
headers, so an `ifMatch` or `ifNoneMatch` cannot travel. A call carrying one is refused with
|
|
106
|
+
`TransportRefusal` naming the option, rather than sent as an overwrite that checks nothing.
|
|
107
|
+
Nothing is retried either: a retry is another platform call and a wait is your function's wall
|
|
108
|
+
clock, so your code decides.
|
|
109
|
+
- **`gem.functions` is absent.** A function may not invoke a function.
|
|
110
|
+
- **`gem.gameConfig` is absent.** The config document is not reachable from a function, so
|
|
111
|
+
`gem.me.currencies.get()` takes a currency id rather than a slug.
|
|
112
|
+
|
|
113
|
+
`gem.friends.challenge()` answers `unavailable` without asking anything, because there is nobody in
|
|
114
|
+
front of a screen to confirm it, and `gem.platform.earnAllowance.on()` never fires.
|
|
115
|
+
|
|
116
|
+
⚠️ **Flush analytics before you return.** A page flushes on a timer and as it closes; a function
|
|
117
|
+
ends the moment your handler settles, so `await gem.analytics.flush()` before you return, or the
|
|
118
|
+
events may be lost.
|
|
119
|
+
|
|
120
|
+
## 4. Check before you upload
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
gemctl function check dist/function.js
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`check` runs the platform's publish-time validation for JavaScript on your machine, and an upload
|
|
127
|
+
answers `400 source_invalid` with the same `details.reason`:
|
|
128
|
+
|
|
129
|
+
| Reason | What it usually is | What to change |
|
|
130
|
+
| ------------------ | ------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
131
|
+
| `not_a_module` | A parse error, with its line and column, or a file that is not UTF-8 | Fix the syntax; bundle to ES2022 |
|
|
132
|
+
| `forbidden_import` | An `import` or `export … from` of anything but the SDK, or an `import()` | Bundle the dependency in; drop the dynamic import |
|
|
133
|
+
| `missing_handle` | Neither `handle` nor `handleJson` is exported (`export *` does not count) | Export one of them from the module itself |
|
|
134
|
+
| `ambiguous_handle` | Both are exported | Keep one |
|
|
135
|
+
| `too_large` | Over the size limit, the same one a component has | Bundle less; check what your build pulled in |
|
|
136
|
+
|
|
137
|
+
An upload also answers `source_invalid`, rather than `component_invalid`, for `not_uploaded`,
|
|
138
|
+
`size_mismatch` and `sha256_mismatch`: upload again. Promoting, channels and invoking are the same
|
|
139
|
+
as for a component (serverless-functions.md sections 4 to 7).
|
|
@@ -187,9 +187,9 @@ try {
|
|
|
187
187
|
|
|
188
188
|
`awaitBridge()` resolves when the bridge leaves `waiting` — connected, refused or closed — or at a
|
|
189
189
|
bounded timeout (default 10s), with the state it settled on. **Proceed on every outcome.**
|
|
190
|
-
`connected` is
|
|
191
|
-
anyway. The wait costs nothing when the handshake is quick: it settles the moment the frame
|
|
192
|
-
connects
|
|
190
|
+
`connected` is a Gem host listening; anything else is a boot with no session, which your game must
|
|
191
|
+
survive anyway. The wait costs nothing when the handshake is quick: it settles the moment the frame
|
|
192
|
+
connects, and at once when the page that framed it is refused.
|
|
193
193
|
|
|
194
194
|
The measured stakes, from the first game to hit this on an iPhone: listener up at ~300ms, then a
|
|
195
195
|
main-thread block — the parent's offer, queued behind the block, was delivered twenty seconds later
|
|
@@ -515,8 +515,8 @@ them**, so the whole migration for a stylesheet is one declaration per edge:
|
|
|
515
515
|
```
|
|
516
516
|
|
|
517
517
|
The `env()` fallback is not decoration. Under a parent that has not sent the message — an older
|
|
518
|
-
arcade, a third-party embedder
|
|
519
|
-
|
|
518
|
+
arcade or dev host, a third-party embedder — the properties are never set and the browser's own
|
|
519
|
+
answer stands, which is what your game did before.
|
|
520
520
|
Under a parent that has, the variable wins. One line, correct in both worlds. The four names are
|
|
521
521
|
`--gem-safe-top`, `--gem-safe-right`, `--gem-safe-bottom` and `--gem-safe-left`.
|
|
522
522
|
|
|
@@ -538,17 +538,21 @@ the `var()` above, or read `gem.viewport`. The release checklist looks for the b
|
|
|
538
538
|
|
|
539
539
|
### The arcade's own controls: reserved areas
|
|
540
540
|
|
|
541
|
-
On a phone your game is full-bleed, and the arcade keeps one
|
|
542
|
-
|
|
543
|
-
the player's wallets (Gems, Tickets, their achievement score) while they are
|
|
544
|
-
[shown](#the-arcades-wallet-show-it-never-draw-it). The cluster stays in the top-left and runs along
|
|
545
|
-
the top row at most about three quarters of the way across on the narrowest phone — but do not
|
|
546
|
-
hard-code a size: read it, as below. Those rectangles of your frame are **reserved**: keep essential
|
|
547
|
-
UI and every touch target out of them.
|
|
541
|
+
On a phone your game is full-bleed, and the arcade keeps one row of its own across the top of it,
|
|
542
|
+
with the middle left open for a centred camera:
|
|
548
543
|
|
|
549
|
-
**
|
|
550
|
-
|
|
551
|
-
|
|
544
|
+
- **Top-left: the Back tab** — the Gem Arcade wordmark, always there. **Do not put a back arrow of
|
|
545
|
+
your own top-left**: it would sit under the arcade's tab, which leaves the game, while yours would
|
|
546
|
+
mean something else.
|
|
547
|
+
- **Top-right: the player's wallets** (Gems, Tickets, their achievement score) while your screen
|
|
548
|
+
[shows them](#the-arcades-wallet-show-it-never-draw-it). Put your own settings, pause or menu
|
|
549
|
+
button top-right **below** them, at `top: var(--gem-reserved-tr-height, …)` — and when gameplay
|
|
550
|
+
hides the wallets with `gem.setWalletVisible(false)`, that rectangle shrinks to nothing and your
|
|
551
|
+
button moves up into the corner on its own.
|
|
552
|
+
|
|
553
|
+
Those rectangles of your frame are **reserved**: keep essential UI and every touch target out of
|
|
554
|
+
them. **Never hard-code their size or height.** They differ by phone, and in native fullscreen the
|
|
555
|
+
whole row may rise into the camera band — read them, as below.
|
|
552
556
|
|
|
553
557
|
An inset cannot say that — as a top inset, a tab a few dozen pixels wide would take your whole top
|
|
554
558
|
strip. So `gem.viewport` also carries the rectangles, in the same CSS pixels of your frame, measured
|
|
@@ -567,13 +571,21 @@ from its top-left corner:
|
|
|
567
571
|
reserved rectangles reach in from its two edges. Push the corner's UI clear of it:
|
|
568
572
|
|
|
569
573
|
```css
|
|
570
|
-
/*
|
|
574
|
+
/* settings in the top-right corner, below the wallets — and up in the corner when they are hidden */
|
|
575
|
+
#settings {
|
|
576
|
+
top: max(var(--gem-safe-top, env(safe-area-inset-top, 0px)), var(--gem-reserved-tr-height, 0px));
|
|
577
|
+
right: var(--gem-safe-right, 0px);
|
|
578
|
+
}
|
|
579
|
+
/* a score in the top-left corner, below the tab */
|
|
571
580
|
#score {
|
|
572
581
|
top: max(var(--gem-safe-top, env(safe-area-inset-top, 0px)), var(--gem-reserved-tl-height, 0px));
|
|
573
582
|
left: var(--gem-safe-left, 0px);
|
|
574
583
|
}
|
|
575
|
-
/* or a row along the top,
|
|
576
|
-
#top-bar {
|
|
584
|
+
/* or a row along the top, between the two */
|
|
585
|
+
#top-bar {
|
|
586
|
+
padding-left: var(--gem-reserved-tl-width, 0px);
|
|
587
|
+
padding-right: var(--gem-reserved-tr-width, 0px);
|
|
588
|
+
}
|
|
577
589
|
```
|
|
578
590
|
|
|
579
591
|
All eight are written on every push, `0px` for a clear corner, so a rotation never leaves a stale
|
|
@@ -581,9 +593,9 @@ value; they are never set under an embedder that sends no viewport, which is wha
|
|
|
581
593
|
fallback is for. A rectangle belongs to the corner its centre lies nearest to. For a HUD laid out
|
|
582
594
|
from script, read `gem.viewport.reserved` — `[]` when nothing is covered.
|
|
583
595
|
|
|
584
|
-
**Lay out against it whenever it changes.** Shown wallets
|
|
585
|
-
|
|
586
|
-
are hidden never reserves anything — it floats over your game and moves nothing.
|
|
596
|
+
**Lay out against it whenever it changes.** Shown wallets reserve the top-right corner; hiding them
|
|
597
|
+
shrinks it to nothing, and only the tab's top-left rectangle is left. A wallet the arcade pops up for
|
|
598
|
+
a moment while they are hidden never reserves anything — it floats over your game and moves nothing.
|
|
587
599
|
|
|
588
600
|
**Older SDKs get it folded into the top inset.** A game on an SDK from before reserved areas cannot
|
|
589
601
|
read the rectangles, so the arcade sends it none and instead raises `insets.top` — and so
|
|
@@ -746,6 +758,13 @@ window.addEventListener('keydown', resume);
|
|
|
746
758
|
Resume on the first input and carry on — never block the first frame on audio, never show an error
|
|
747
759
|
for it, and do not ask the player to "click to start" just to unlock sound.
|
|
748
760
|
|
|
761
|
+
**Such a launch is not counted as a play until the player touches it.** The Arcade tells the SDK it
|
|
762
|
+
started your game on its own, and the SDK holds your analytics — the platform's session and boot
|
|
763
|
+
events and your own `track` calls — until the first real input inside your game, then sends them
|
|
764
|
+
as a session that starts there. You write nothing for it; [analytics.md](analytics.md) has the
|
|
765
|
+
detail. So design the first seconds to be **watchable**: a game that sits on a "press any key"
|
|
766
|
+
screen autoplays as a still picture.
|
|
767
|
+
|
|
749
768
|
### Doing it yourself instead
|
|
750
769
|
|
|
751
770
|
`createGem({ manageAudio: false })` turns the SDK's silencing off — for a game that would rather fade
|
|
@@ -757,7 +776,8 @@ and the release checklist checks that it does.
|
|
|
757
776
|
|
|
758
777
|
The player's platform balances — Gems, Tickets (`ARCADE_TICKETS`) and their achievement score —
|
|
759
778
|
belong to the Arcade. **Your game never draws one**, and never shows a pending state for one: the
|
|
760
|
-
Arcade takes every figure from the platform and draws the wallets itself,
|
|
779
|
+
Arcade takes every figure from the platform and draws the wallets itself, in the top-right corner
|
|
780
|
+
of your frame on a phone.
|
|
761
781
|
|
|
762
782
|
**They are shown unless your game hides them.** A game that does nothing has every wallet on show
|
|
763
783
|
the whole time. The one rule: **hide them during gameplay, show them on your menus.**
|
|
@@ -773,10 +793,10 @@ pauseMenu.onOpen = () => gem.setWalletVisible(true);
|
|
|
773
793
|
- **It is screen state, not a toggle.** Each call is the whole answer and replaces the last. Call it
|
|
774
794
|
as often as you like: before the Arcade connects the state is held and sent when it does, and
|
|
775
795
|
again after any reconnect; a repeat of the current state is not re-sent.
|
|
776
|
-
- **Shown wallets take space; a pop does not.** Shown, they
|
|
777
|
-
[
|
|
778
|
-
|
|
779
|
-
and never moves its layout.
|
|
796
|
+
- **Shown wallets take space; a pop does not.** Shown, they reserve the top-right
|
|
797
|
+
[corner](#the-arcades-own-controls-reserved-areas), so lay your menu out against
|
|
798
|
+
`--gem-reserved-tr-*` — your settings button sits below them; hidden, the corner is yours again and
|
|
799
|
+
the same `var()` moves the button up. A pop floats over your game and never moves its layout.
|
|
780
800
|
- **Narrowing is optional.** On a screen that sells in one currency — a ticket store — you may show
|
|
781
801
|
only that one: `gem.setWalletVisible(true, { only: ['ARCADE_TICKETS'] })`, with `'score'` for the
|
|
782
802
|
achievement score. Use it only there; leaving it out shows every wallet, which is always safe.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indiegems/gem-web-sdk",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.0",
|
|
4
4
|
"description": "postMessage bridge contract between gem-web and browser-playable games",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"types": "./dist/server-api.d.ts",
|
|
32
32
|
"import": "./dist/gem-web-sdk.server-api.js"
|
|
33
33
|
},
|
|
34
|
+
"./function": {
|
|
35
|
+
"types": "./dist/function.d.ts",
|
|
36
|
+
"import": "./dist/gem-web-sdk.function.js"
|
|
37
|
+
},
|
|
34
38
|
"./schema": "./schema/bridge.v1.json",
|
|
35
39
|
"./brand/*": "./brand/*"
|
|
36
40
|
},
|
|
@@ -45,6 +49,7 @@
|
|
|
45
49
|
"docs/*.md",
|
|
46
50
|
"docs/manifest.json",
|
|
47
51
|
"docs/api/*.md",
|
|
48
|
-
"docs/api/reference.json"
|
|
52
|
+
"docs/api/reference.json",
|
|
53
|
+
"CHANGELOG.md"
|
|
49
54
|
]
|
|
50
55
|
}
|
package/schema/bridge.v1.json
CHANGED
|
@@ -205,6 +205,33 @@
|
|
|
205
205
|
"maxLength": 2048,
|
|
206
206
|
"format": "origin",
|
|
207
207
|
"$comment": "The parent's own origin, so the frame can check who is embedding it."
|
|
208
|
+
},
|
|
209
|
+
"unbidden": {
|
|
210
|
+
"kind": "boolean",
|
|
211
|
+
"optional": true,
|
|
212
|
+
"$comment": [
|
|
213
|
+
"True when the parent started this game WITHOUT the player asking — it",
|
|
214
|
+
"autoplays on an information page, say — rather than because the player",
|
|
215
|
+
"pressed Play. Absent or false is an ordinary launch.",
|
|
216
|
+
"",
|
|
217
|
+
"ON THE HANDSHAKE because it must reach the frame before anything the frame",
|
|
218
|
+
"counts: the SDK opens its play session the moment the handshake lands, and",
|
|
219
|
+
"a fact that arrived on any later message would arrive after the session it",
|
|
220
|
+
"is about. A per-launch FIELD rather than a capability, because a",
|
|
221
|
+
"capability is a deployment-wide fact about what a peer implements and this",
|
|
222
|
+
"is a fact about one launch.",
|
|
223
|
+
"",
|
|
224
|
+
"Sent to every frame, since the parent cannot know which grammar a frame",
|
|
225
|
+
"speaks before it answers; a frame that predates the field ignores it, as a",
|
|
226
|
+
"receiver ignores any unknown field. What a frame that understands it does —",
|
|
227
|
+
"holds every analytics event that would count a play until the first real",
|
|
228
|
+
"input inside the frame, and then sends `gem.engaged` — it says by",
|
|
229
|
+
"advertising `engagement` in `gem.handshake.accept`.",
|
|
230
|
+
"",
|
|
231
|
+
"A presentation and measurement fact, NOT AN AUTHORIZATION CONTROL: nothing",
|
|
232
|
+
"may be granted or withheld on it, and a frame cannot use it to tell a",
|
|
233
|
+
"player apart from anyone else."
|
|
234
|
+
]
|
|
208
235
|
}
|
|
209
236
|
},
|
|
210
237
|
"rateLimit": { "perMinute": 30 }
|
|
@@ -867,7 +894,7 @@
|
|
|
867
894
|
"AMENDED, DELIBERATELY: this message once carried insets only. It now also",
|
|
868
895
|
"carries `reserved`, rectangles of the frame covered by the parent's own",
|
|
869
896
|
"controls floating over the game (a Back tab in a corner, the wallet chips",
|
|
870
|
-
"shown
|
|
897
|
+
"shown in another). An inset can only describe a full-width or full-height band,",
|
|
871
898
|
"and a corner tab expressed as a top inset would take the whole top strip",
|
|
872
899
|
"from a full-bleed game to clear a control a few dozen pixels wide. A",
|
|
873
900
|
"rectangle says exactly which part, and still says nothing about what the",
|
|
@@ -2022,6 +2049,40 @@
|
|
|
2022
2049
|
"rateLimit": { "perMinute": 5 }
|
|
2023
2050
|
},
|
|
2024
2051
|
|
|
2052
|
+
{
|
|
2053
|
+
"type": "gem.engaged",
|
|
2054
|
+
"direction": "frameToParent",
|
|
2055
|
+
"transport": "port",
|
|
2056
|
+
"$comment": [
|
|
2057
|
+
"The player has given this frame its first REAL input: a pointer press, a",
|
|
2058
|
+
"key press or a gamepad button press that the browser itself reported",
|
|
2059
|
+
"(`isTrusted`). Focus, hover, wheel and synthetic events do not count. Sent",
|
|
2060
|
+
"once per frame document, and only to a parent that advertised",
|
|
2061
|
+
"`engagement`.",
|
|
2062
|
+
"",
|
|
2063
|
+
"WHY THE FRAME SAYS IT. Input inside a cross-origin frame never reaches the",
|
|
2064
|
+
"parent's own listeners, so a parent can only guess at engagement from focus",
|
|
2065
|
+
"and blur — which a game autoplaying on an information page defeats, since",
|
|
2066
|
+
"nothing about watching it moves focus. The frame is the only party that",
|
|
2067
|
+
"sees the press.",
|
|
2068
|
+
"",
|
|
2069
|
+
"SENT ON EVERY LAUNCH, not only an unbidden one: the first input is a fact",
|
|
2070
|
+
"whatever started the game, and a parent that already knows (the player",
|
|
2071
|
+
"pressed its Play) is free to ignore it. On an unbidden launch it is also",
|
|
2072
|
+
"the moment the SDK releases the analytics it held, so it marks where the",
|
|
2073
|
+
"play session it reports begins.",
|
|
2074
|
+
"",
|
|
2075
|
+
"NO FIELDS, AND IT SHOULD STAY THAT WAY. Which key, where on screen, how",
|
|
2076
|
+
"long after load — none of it is the parent's business, and a field here",
|
|
2077
|
+
"would be one a game could fill with anything. Its occurrence is the whole",
|
|
2078
|
+
"message, like `gem.ready`. A cooperative signal from untrusted code, so",
|
|
2079
|
+
"NOT AN AUTHORIZATION CONTROL: a parent may stop autoplay-related chrome",
|
|
2080
|
+
"and count the launch as engaged on it, and must grant nothing."
|
|
2081
|
+
],
|
|
2082
|
+
"fields": {},
|
|
2083
|
+
"rateLimit": { "perMinute": 5 }
|
|
2084
|
+
},
|
|
2085
|
+
|
|
2025
2086
|
{
|
|
2026
2087
|
"type": "gem.progress",
|
|
2027
2088
|
"direction": "frameToParent",
|