@indiegems/gem-web-sdk 11.1.2 → 13.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.
@@ -1,9 +1,5 @@
1
1
  # Serverless functions in JavaScript
2
2
 
3
- **The platform does not run JavaScript functions yet.** This page describes how one is written
4
- and how it behaves; today a function you publish is a WebAssembly component, as
5
- [serverless-functions.md](serverless-functions.md) describes.
6
-
7
3
  A serverless function can be written in JavaScript or TypeScript instead of Rust: the language your
8
4
  game is already written in, so the verification code in your browser game moves into a function
9
5
  with no change beyond bundling. You upload one bundled ES module, and the platform runs it on a
@@ -51,31 +47,46 @@ are read and written:
51
47
  | `handle(input)` | `Uint8Array`: the request body, byte for byte | `Uint8Array`, `ArrayBuffer` or `string` (sent as UTF-8); anything else is an error |
52
48
  | `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 |
53
49
 
54
- Either may be `async`; the platform waits for the promise. Exporting neither, or both, is refused
55
- when you upload. A caller cannot tell which kind of function answered: `gem.functions.invoke`, the
56
- limits and the refusals in serverless-functions.md are identical for both.
50
+ Either may be `async`; the platform waits for the promise, and a promise that never settles fails
51
+ the call. Exporting neither, or both, is refused when you upload. A caller cannot tell which kind of
52
+ function answered: `gem.functions.invoke`, the limits and the refusals in serverless-functions.md
53
+ are identical for both.
54
+
55
+ **Each call gets the same limits as a component:** 5 seconds of wall clock, 1 second of CPU,
56
+ 256 MiB of memory, 32 platform calls, 256 KiB of input and 1 MiB of output, and the module itself
57
+ may be at most 32 MiB. The full list is
58
+ [the limits on one call](serverless-functions.md#the-limits-on-one-call).
57
59
 
58
60
  **A thrown exception is `Err`.** The call fails with `function_error`, and `err.functionMessage` is
59
61
  the exception's `name: message` (a thrown value that is not an `Error` is its `String()`). The
60
- message and the stack go to the platform's log for the invocation, never to the caller. A
61
- `handleJson` whose input is not JSON, or whose return value JSON cannot carry, fails the same way.
62
+ message and the stack are also logged for the invocation at `error`; the stack never reaches the
63
+ caller. A `handleJson` whose input is not JSON, or whose return value JSON cannot carry, fails the
64
+ same way.
62
65
 
63
66
  **`console.*` reaches the invocation's log** at the matching level (`debug`, `info`, `warn`,
64
67
  `error`); `console.log` and every other method log at `info`. As with a component's `log`,
65
68
  `gemctl function logs` reads it back for a call on `dev`, and a call on `test` or `release` keeps
66
- no line.
69
+ no line. Each line is cut at 1 KiB, and one call keeps at most **100 lines or 64 KiB** of them,
70
+ whichever comes first. Every `console` call past that goes unrecorded, and the call's closing log
71
+ line counts them in `dropped_log_lines` (serverless-functions.md section 7).
67
72
 
68
73
  ## 2. Writing one
69
74
 
70
75
  ```bash
71
76
  gemctl function init --lang js functions/score-check
72
- cd functions/score-check && npm install && npm run build
73
- gemctl function upload dist/function.js
77
+ (cd functions/score-check && npm install && npm run build)
78
+ gemctl function upload --file functions/score-check/dist/function.js --version 1.0.0
79
+ gemctl function promote 1.0.0 --channel dev
74
80
  ```
75
81
 
76
82
  **`init --lang js`** writes a `package.json` whose `build` script type-checks and then bundles to
77
- one ES module, a TypeScript starter handler and a `tsconfig.json`. gemctl runs neither step:
78
- `npm run build` is yours, the way `cargo build` is for a component.
83
+ one ES module in `dist`, a TypeScript starter handler and a `tsconfig.json`. gemctl runs neither
84
+ step: `npm run build` is yours, the way `cargo build` is for a component. The starter lists the SDK
85
+ in `devDependencies`, and its build bundles the SDK into the one module you upload. Nothing is
86
+ provided to a function you upload now, so that module is the whole of your function. That is why
87
+ the SDK is a dev dependency here while [getting-started.md](getting-started.md) puts it in
88
+ `dependencies` for a game: what a game ships is built from its install, and what a function ships
89
+ is the bundle.
79
90
 
80
91
  A handler looks like this:
81
92
 
@@ -96,23 +107,38 @@ The package is the one your game already depends on, installed as
96
107
  serverless-functions.md section 1, and it applies unchanged: a save or a setting the player's own
97
108
  browser can write is the player's claim, not evidence.
98
109
 
99
- **Imports.** The module may import the SDK and nothing else, under any of the three names a game
100
- already uses: `@indiegems/gem-web-sdk/function`, `@indiegems/gem-web-sdk/sdk`, or
101
- `@indiegems/gem-web-sdk/game`. Inside a function all three are the same module, the function
102
- entry, so shared code keeps its import. A name that module does not export, such as
103
- `defineServer`, fails every call with `function_error`. Everything else your code needs is bundled
104
- in, which is what the starter's build does. An `import()` at runtime is refused at upload.
105
- Top-level `await` is allowed and runs on every call.
106
-
107
- **What the engine gives you.** `Date`, `performance`, `Math.random`, `crypto.getRandomValues`,
108
- `crypto.randomUUID`, `TextEncoder`, `URL` and `fetch` all work. `fetch` reaches only the platform
109
- API, through the SDK, and the save links the platform hands you; any other URL is refused. A timer
110
- waits at most 10 ms: a function has no reason to sleep, and a long one would run out the wall
111
- clock.
112
-
113
- **The SDK version your function runs is the one the platform bundles**, not the one in your
114
- `package.json`; the starter pins the matching version. A method added in a newer SDK reaches a
115
- function when the platform's engine picks that version up.
110
+ **Imports.** The module you upload imports nothing. Everything it uses, the SDK included, is
111
+ bundled into it, which is what the starter's build does. A file shared with your game may keep its
112
+ `@indiegems/gem-web-sdk/sdk` or `@indiegems/gem-web-sdk/game` import, because the starter's build
113
+ resolves both to `@indiegems/gem-web-sdk/function` when it bundles your function. A name the
114
+ function entry does not export, such as `defineServer`, then fails the build rather than a call.
115
+ For a function still on the engine's built-in copy (below) the same name fails the call with
116
+ `function_error`. An `import()` at runtime is refused at upload. Your module is evaluated afresh
117
+ for every call, so its top level, including any top-level `await`, runs on every call and inside
118
+ that call's limits; the engine's built-in copy, by contrast, was evaluated once when the engine was
119
+ built, which is one reason it stays at 7.6.0.
120
+
121
+ **What the platform gives your function.** `fetch`, `URL`, `URLSearchParams`, `TextEncoder`,
122
+ `TextDecoder`, `AbortController`, `AbortSignal.timeout`, `setTimeout`, `setInterval`,
123
+ `crypto.randomUUID`, `crypto.getRandomValues`, `atob`, `btoa`, `console`, `Date` and `Math.random`
124
+ all work. `fetch` reaches only the platform API, through the SDK, and the save links the platform
125
+ hands you; any other URL is refused before anything is sent, and every request counts toward the
126
+ call's platform-call limit. Timers and `AbortSignal.timeout` wait their full length, and the engine
127
+ sets no limit of its own: what bounds a wait is the call's wall clock, and a call still waiting when
128
+ it runs out fails with `function_timeout`. A wait spends wall clock, not CPU.
129
+
130
+ **The SDK your function runs is the one you bundle.** A function published importing the SDK from
131
+ the engine runs the engine's built-in copy instead (the next paragraph). The starter's build
132
+ bundles the version your `package.json` names, so pin the version your game pins and the same code
133
+ runs in both places. A method added in a newer SDK reaches your function when you bundle that
134
+ version: update the pin, build, upload and promote.
135
+
136
+ **A function published importing the SDK from the engine** (the earlier starter marked it external)
137
+ keeps running on the engine's built-in copy, gem-web-sdk 7.6.0, which is not updated and is removed
138
+ once no published function imports it. Nothing fails at an engine roll. A new upload cannot import
139
+ it: the platform refuses every import, so a function moves off the built-in by a rebuild with the
140
+ current starter's build (which bundles the SDK), an upload of a new version and a promote. A method
141
+ added in a newer SDK reaches a function only when that version is bundled.
116
142
 
117
143
  ## 3. The SDK inside a function
118
144
 
@@ -120,8 +146,47 @@ function when the platform's engine picks that version up.
120
146
  `awaitIdentity()` resolve at once, and `gem.me` is the player who called. So a game's boot sequence
121
147
  runs unchanged, and the same `gem.me.saves.get`, `gem.leaderboards.get` and `gem.profiles.of` calls
122
148
  work, with the same types and the same errors. The function's game and channel come from the call;
123
- nothing you write names them. `gem.context` is who called, for which game and channel, and
124
- `gem.identity` is the same three fields a browser game reads from its own `gem.identity`.
149
+ nothing you write names them.
150
+
151
+ ```ts
152
+ function createGem(options?: unknown): GemFunction; // options is accepted and ignored
153
+ ```
154
+
155
+ | Member | Type | What it is |
156
+ | ----------------- | ------------------------------ | --------------------------------------------------------------------- |
157
+ | `context` | `FunctionContext` | Who called, for which game and channel, and this call's invocation id |
158
+ | `identity` | `GemFunctionIdentity \| null` | `playerId`, `gameId` and `channel`; `null` when a game server called |
159
+ | `awaitIdentity()` | `Promise<GemFunctionIdentity>` | `identity`, at once; rejects when a game server called |
160
+ | `awaitBridge()` | `Promise<'connected'>` | `'connected'`, at once |
161
+ | `me` | `EntityScope` | The calling player's settings, inventory, currencies and saves |
162
+ | `characters` | `CharactersApi` | The calling player's characters |
163
+ | `transfers` | `TransfersApi` | Moving items and currency between players |
164
+ | `analytics` | `AnalyticsApi` | Your game's analytics, as the calling player |
165
+ | `leaderboards` | `LeaderboardsApi` | The leaderboards your game config declares |
166
+ | `challenges` | `ChallengesApi` | Daily challenge periods and subjects |
167
+ | `showcase` | `ShowcaseApi` | Display-only values other players may read |
168
+ | `friends` | `FriendsApi` | The calling player's accepted friends |
169
+ | `platform` | `PlatformApi` | The calling player's platform-currency balances and earn allowance |
170
+ | `profiles` | `ProfilesApi` | Another player's per-game profile |
171
+ | `client` | `GemClient` | The platform API by path, for what the members above do not cover |
172
+
173
+ ```ts
174
+ interface FunctionContext {
175
+ readonly gameId: string;
176
+ readonly channel: 'dev' | 'test' | 'release';
177
+ readonly invocationId: string; // the id `gemctl function invoke` reports
178
+ readonly caller:
179
+ | { readonly type: 'player'; readonly playerId: string }
180
+ | { readonly type: 'game-server'; readonly serverId: string | null };
181
+ }
182
+ ```
183
+
184
+ `gem.identity` is the same three fields a browser game reads from its own `gem.identity`, so code
185
+ that keys records on `playerId` runs unchanged. The entry also exports the error classes a game
186
+ already catches (`GemApiError`, `AuthError`, `TransportRefusal`, `GemSaveError` and
187
+ `FriendChallengeError`), so an `instanceof` check reads the same in both places. The
188
+ [function API reference](api/api-reference-function.md) documents each member above, and the
189
+ methods, types and errors behind all of them but `characters`, `transfers` and `client`.
125
190
 
126
191
  What a browser game has that is backed by the page or the arcade (ads, purchases, rooms, device
127
192
  settings, sharing) is not on `gem` inside a function. Four more differences, each following from
@@ -149,20 +214,29 @@ events may be lost.
149
214
  ## 4. Check before you upload
150
215
 
151
216
  ```bash
152
- gemctl function check dist/function.js
217
+ gemctl function check functions/score-check/dist/function.js
153
218
  ```
154
219
 
155
220
  `check` runs the platform's publish-time validation for JavaScript on your machine, and an upload
156
- answers `400 source_invalid` with the same `details.reason`:
221
+ runs it first and answers `400 source_invalid` with the same `details.reason`:
157
222
 
158
- | Reason | What it usually is | What to change |
159
- | ------------------ | ------------------------------------------------------------------------- | ------------------------------------------------- |
160
- | `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 |
161
- | `forbidden_import` | An `import` or `export … from` of anything but the SDK, or an `import()` | Bundle the dependency in; drop the dynamic import |
162
- | `missing_handle` | Neither `handle` nor `handleJson` is exported (`export *` does not count) | Export one of them from the module itself |
163
- | `ambiguous_handle` | Both are exported | Keep one |
164
- | `too_large` | Over the size limit, the same one a component has | Bundle less; check what your build pulled in |
223
+ | Reason | What it usually is | What to change |
224
+ | ------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------------------- |
225
+ | `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 |
226
+ | `forbidden_import` | Any `import` or `export … from`, or an `import()` | Bundle all of it in, the SDK too; drop `import()` |
227
+ | `missing_handle` | Neither `handle` nor `handleJson` is a named export (`export *` and `default` do not count) | Export one of them from the module itself |
228
+ | `ambiguous_handle` | Both are exported | Keep one |
229
+ | `too_large` | Over the size limit, the same one a component has | Bundle less; check what your build pulled in |
165
230
 
166
231
  An upload also answers `source_invalid`, rather than `component_invalid`, for `not_uploaded`,
167
- `size_mismatch` and `sha256_mismatch`: upload again. Promoting, channels and invoking are the same
168
- as for a component (serverless-functions.md sections 4 to 7).
232
+ `size_mismatch` and `sha256_mismatch`: upload again. gemctl uploads a `.js` or `.mjs` file as
233
+ JavaScript; it does not accept TypeScript, so upload what your build wrote.
234
+
235
+ ## 5. Publishing
236
+
237
+ Uploading, promoting, rolling back and channels are the same as for a component
238
+ (serverless-functions.md sections 4 and 5). On the website, the **Functions** tab of your game's
239
+ **Builds** page promotes a version to a channel, rolls back by promoting an earlier one, and shows
240
+ each channel's calls and daily usage and the last 24 hours of `dev` logs; a JavaScript version is
241
+ marked there as running as JavaScript. Uploading is gemctl's alone. Invoking a function from your
242
+ game or the terminal is serverless-functions.md sections 6 and 7.
@@ -55,8 +55,10 @@ possible.
55
55
 
56
56
  ## 2. What a function is
57
57
 
58
- A function is one WebAssembly **component** per game per channel, built against the platform's
59
- `gem:functions` world. It exports exactly one function:
58
+ A function is one WebAssembly **component** or one JavaScript module per game per channel. A
59
+ JavaScript module is written against the SDK instead, and
60
+ [serverless-functions-js.md](serverless-functions-js.md) covers what differs for one. A component
61
+ is built against the platform's `gem:functions` world, and exports exactly one function:
60
62
 
61
63
  ```wit
62
64
  export handle: func(input: list<u8>) -> result<list<u8>, string>;
@@ -450,12 +452,13 @@ reason to move the work to a game server.
450
452
  | ------------------------------------------------------------------------------ | -------------------------------------- |
451
453
  | Wall clock | 5 seconds |
452
454
  | CPU | 1 second of that 5 |
453
- | Memory | 64 MiB |
455
+ | Memory | 256 MiB |
454
456
  | `call-api`, `download-link` and `upload-link` calls | 32, all three together |
455
457
  | A value handed to one `log`, `call-api`, `download-link` or `upload-link` call | 16 MiB |
456
458
  | Input | 256 KiB |
457
459
  | Output | 1 MiB |
458
- | Component size | 32 MiB |
460
+ | Lines your function logs, kept per call (`dev` only) | 100 lines or 64 KiB, whichever first |
461
+ | Component or JavaScript module size | 32 MiB |
459
462
  | Calls, per caller per minute | 30 for a player, 120 for a game server |
460
463
 
461
464
  **The per-minute budget belongs to the caller, not to your game.** A player who spends theirs is
@@ -464,8 +467,9 @@ decision point, such as the end of a run or a claim, and never on a timer or onc
464
467
 
465
468
  ## 3. Writing one
466
469
 
467
- The reference toolchain is Rust. gemctl writes the starter crate and wraps what your build
468
- produces, and the build in between is yours:
470
+ The reference toolchain for a component is Rust; to write a function in JavaScript or TypeScript,
471
+ see [serverless-functions-js.md](serverless-functions-js.md). gemctl writes the starter crate and
472
+ wraps what your build produces, and the build in between is yours:
469
473
 
470
474
  ```bash
471
475
  gemctl function init functions/score-check
@@ -618,6 +622,15 @@ been purged yet.
618
622
  `gemctl function delete <version> --yes` is refused while any channel serves the version, and
619
623
  gemctl cannot undo it.
620
624
 
625
+ **The website does the same from the Functions tab of your game's Builds page.** It lists every
626
+ version with its description, size, SHA-256 and the channels serving it, and promotes, unassigns and
627
+ deletes, asking you to confirm before it promotes to `release`. Rolling back there is the same act:
628
+ promote the earlier version. It cannot upload; that is `gemctl function upload`. The tab also shows
629
+ each channel's calls today and its daily usage over the last 7, 28 or 90 days (calls, unsuccessful
630
+ calls, CPU time, the longest call and peak memory), and the last 24 hours of `dev` logs, filtered by
631
+ time, level, outcome or invocation id. Promoting, unassigning and deleting there take an Owner,
632
+ Admin or Contributor role on the game; anyone else on the team can only look.
633
+
621
634
  ## 5. Channels
622
635
 
623
636
  Each channel (`dev`, `test` and `release`) serves at most one function version. A call never names
@@ -674,7 +687,8 @@ it is the progression's own state, or a key the platform decides (section 2).
674
687
  platform's code from section 8, and the error adds what that code needs:
675
688
 
676
689
  - `err.functionMessage` is the text your `handle` returned in `Err`, on `function_error`. Your
677
- function wrote it and it may repeat what the caller sent, so log it and never render it as markup;
690
+ function wrote it and it may repeat what the caller sent, so log it and never render it as markup.
691
+ For a JavaScript function it is the thrown exception's `name: message`;
678
692
  - `err.failure` says which limit or fault ended the call, on `function_failed`;
679
693
  - `err.retryable` is decided by the code. It is `false` for `function_timeout`, because the same
680
694
  input runs out of time again, and for `functions_not_configured`, because functions are switched
@@ -777,9 +791,10 @@ or `game_server`:
777
791
  exceeded. The platform writes it and never copies your function's data into it.
778
792
 
779
793
  Newest first means a call's closing line comes before what it logged. **A call's logging is
780
- capped**: once it reaches its per-call log limit, that `log` call and every later one go
781
- unrecorded, and the closing line's `dropped_log_lines` says how many. A log that stops part-way
782
- with a nonzero count means your function logged too much, not that it went quiet.
794
+ capped**: once it reaches 100 lines or 64 KiB of logged text, whichever comes first, that `log`
795
+ call and every later one go unrecorded, and the closing line's `dropped_log_lines` says how many.
796
+ A log that stops part-way with a nonzero count means your function logged too much, not that it
797
+ went quiet.
783
798
 
784
799
  `--invocation-id`, `--level` (`debug`, `info`, `warn` or `error`) and `--outcome` narrow the page.
785
800
  `--level` picks what your function logged and `--outcome` picks closing lines, so the two cannot be
@@ -804,7 +819,7 @@ the last column: it is `true` exactly where this table says to retry.
804
819
 
805
820
  | What you get | What happened | What to do |
806
821
  | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
807
- | `422 function_error` | Your `handle` returned `Err`. The text is `err.functionMessage` | Fix your code or the input. Return verdicts in `Ok` |
822
+ | `422 function_error` | Your `handle` returned `Err`, or your JavaScript function threw. The text is `err.functionMessage` | Fix your code or the input. Return verdicts in `Ok` |
808
823
  | `422 function_failed` | It did not finish. `err.failure` is `trap`, `memory_exceeded`, `cpu_budget_exceeded`, `host_call_limit`, `output_too_large` or `argument_too_large` | A bug or a limit in the function. Reproduce with the same input |
809
824
  | `504 function_timeout` | It ran past the wall clock | Do not retry: the same input takes as long again |
810
825
  | `404 function_not_published` | Nothing is promoted on the caller's channel | `gemctl function promote <version> --channel <channel>` |
@@ -891,13 +891,31 @@ arrives.
891
891
  ## Game settings
892
892
 
893
893
  **Your settings belong to your game: draw a settings button of your own, and the screen it opens.**
894
- The Arcade does not put your settings in its menu any more — its menu holds the player's
895
- _Arcade-wide_ settings instead — so a game with no button of its own has settings nobody can reach.
896
-
897
- **Put the button in the top-right corner.** That is a recommendation, not a rule the Arcade
898
- enforces, but a player who finds settings in the same corner in every game never has to look. Place
899
- it with the reserved-area properties so it never sits under an Arcade control, whatever the Arcade
900
- keeps in that corner on this device:
894
+ The Arcade's menu never opens your settings — it holds the player's _Arcade-wide_ settings
895
+ instead — so a game with no button of its own has settings nobody can reach.
896
+
897
+ This is the whole model, and every other page that mentions settings links here:
898
+
899
+ - **The Arcade's settings are the player's Arcade-wide defaults for this device.** Your game reads
900
+ them from `gem.prefs.recommended` (and `muted` and `reducedMotion` from `gem.prefs`) and applies
901
+ them again on `onPrefs`. The Arcade never asks your game anything about them, and never shows
902
+ anything your game says.
903
+ - **Every game draws its own settings button, top-right, and its own settings screen.** The
904
+ Arcade's menu never opens it.
905
+ - **A setting starts from `recommended` until the player changes it in your game, and then the
906
+ player's choice in your game wins.** Save only what the player chose, never a recommended value.
907
+ - **Where a choice is kept.** Device configuration — volume, graphics quality, look sensitivity, a
908
+ touch-stick style — goes in `gem.deviceSettings`; a choice that must follow the player across
909
+ devices — difficulty, key bindings, accessibility toggles the game owns — goes in
910
+ `gem.me.settings`.
911
+ - **Mute and vibration are in your settings screen too**, wired both ways: your mute follows
912
+ `prefs.muted` and reports the player's own with `gem.reportMuted`
913
+ ([sound](#sound-your-own-mute-control-and-the-arcades)), and your vibration toggle shows only
914
+ while `gem.vibration.available` ([vibration](vibration.md)).
915
+
916
+ **The button goes in the top-right corner**, the same corner in every game, so a player never has
917
+ to look for it. Place it with the reserved-area properties so it never sits under an Arcade
918
+ control, whatever the Arcade keeps in that corner on this device:
901
919
 
902
920
  ```css
903
921
  #settings-button {
@@ -931,8 +949,8 @@ same push as `muted` and `reducedMotion`, and again whenever the player changes
931
949
  | `graphicsQualityAuto` | `true` / `false` | `true`: the Arcade picked that tier for this device, so you may adapt from it. `false`: the player picked it, so hold it. |
932
950
 
933
951
  `lookSensitivity` to `touchMovement` are the camera and controls settings: see
934
- [camera and look controls](#camera-and-look-controls) for telling the Arcade which of them your game
935
- uses, and for `gem.controls.look()`, which applies the two sensitivities and the invert for you. An
952
+ [camera and look controls](#camera-and-look-controls) for `gem.controls.look()`, which applies the
953
+ two sensitivities and the invert for you. An
936
954
  older Arcade sends `recommended` without them; the SDK then fills each with its default (`100`,
937
955
  `100`, `false`, `'floating'`). The last two are the starting graphics quality: see
938
956
  [graphics quality](#graphics-quality-the-arcade-picks-the-start-your-game-adapts). An older Arcade
@@ -981,8 +999,11 @@ volumeSlider.oninput = () => {
981
999
  };
982
1000
  ```
983
1001
 
984
- [Device settings](#device-settings-what-this-device-remembers-about-your-game) above is where the
985
- player's choices are kept between sessions.
1002
+ `chosen` here is this device's configuration, so it is kept in
1003
+ [device settings](#device-settings-what-this-device-remembers-about-your-game). A choice that must
1004
+ follow the player to another device — a difficulty, key bindings, an accessibility toggle your game
1005
+ owns — starts from `recommended` the same way and is kept in `gem.me.settings` instead
1006
+ ([saving player progress](saving-player-progress.md#for-something-small-reach-for-settings-instead)).
986
1007
 
987
1008
  **Read `muted` and `reducedMotion` as before.** They are not recommendations and they did not move:
988
1009
  `muted` is the Arcade's mute, which the SDK applies for you
@@ -995,30 +1016,12 @@ If your game turns a camera — a drag on a phone, a mouse under pointer lock, a
995
1016
  stick — the player has four settings for it in the Arcade, per device, the way Roblox players do:
996
1017
  **look sensitivity**, **gamepad look sensitivity**, **invert look up/down** and **touch movement**
997
1018
  (a floating or a fixed thumbstick). They reach you in `gem.prefs.recommended`
998
- ([the table above](#start-from-the-players-arcade-wide-defaults-gemprefsrecommended)). Two calls
999
- make them work.
1000
-
1001
- **1. Declare which ones your game uses — `gem.controls.declare`.** For each setting, `'player'`
1002
- means you apply the Arcade's value (or the player's own choice in your settings, which still wins);
1003
- `'game'` means your game decides it, and the Arcade's Settings shows that row as "Set by this game";
1004
- leave out a setting your game does not use, and the Arcade says so beside it. Call it once at boot:
1005
-
1006
- ```js
1007
- gem.controls.declare({
1008
- lookSensitivity: 'player',
1009
- gamepadLookSensitivity: 'player',
1010
- invertLookY: 'player',
1011
- touchMovement: 'game', // this game always draws a fixed stick
1012
- });
1013
- ```
1014
-
1015
- It is screen state, like [`gem.setWalletVisible`](#the-arcades-wallet-in-the-gem-menu-never-drawn-by-you):
1016
- each call is the whole answer, the latest wins, and the SDK holds it and sends it again after a
1017
- reconnect. It returns `false` before the bridge is up (it is held and sent on connect) and under an
1018
- embedder that does not show these settings; it never throws. A key or a value other than these is
1019
- dropped.
1019
+ ([the table above](#start-from-the-players-arcade-wide-defaults-gemprefsrecommended)) as the
1020
+ player's Arcade-wide defaults, and, like every [game setting](#game-settings), they are where your
1021
+ own settings start. There is nothing to tell the Arcade about them: its Settings show the player's
1022
+ defaults, never what a game uses.
1020
1023
 
1021
- **2. Turn look input into a camera turn — `gem.controls.look()`.** It is pure math: it listens to
1024
+ **Turn look input into a camera turn — `gem.controls.look()`.** It is pure math: it listens to
1022
1025
  nothing and moves no camera. Give it the deltas your input code already has; it returns radians to
1023
1026
  **add**, `yaw` positive turning right and `pitch` positive looking up, at Roblox's base speeds
1024
1027
  (`gem.controls.LOOK_BASE`: touch 1° across and 0.66° up/down per CSS px, mouse 0.5° and 0.385°,
@@ -1067,15 +1070,16 @@ function frame(dt) {
1067
1070
  Do not copy them into variables at boot.
1068
1071
  - **The player's choice in your game wins.** If your settings screen has its own look speed or
1069
1072
  invert, pass it — `gem.controls.look({ sensitivity: 1.5, invertY: true })`, or later
1070
- `look.set({ sensitivity: 1.5 })` — and that input stops following the Arcade.
1073
+ `look.set({ sensitivity: 1.5 })` — and that input stops following the Arcade. Keep it in
1074
+ `gem.deviceSettings`: a look speed is this device's configuration.
1071
1075
  `look.set({ sensitivity: undefined })` hands it back. `sensitivity` and `gamepadSensitivity` are
1072
1076
  multipliers (`1` is the base speed); the Arcade's percentages are divided by 100 for you.
1073
1077
  - **Pass `pitch` for touch**, the camera's current pitch in radians (0 at the horizon, positive
1074
1078
  looking up), or the slowdown near vertical is skipped. A mouse and a stick are never slowed.
1075
1079
  - **`touchMovement` is yours to draw.** The SDK has no thumbstick: read
1076
- `gem.prefs.recommended?.touchMovement` and draw a floating or a fixed one, or declare
1077
- `touchMovement: 'game'` if your game only has one kind.
1078
- - **No camera, no call.** A game with no camera to turn declares nothing and never calls `look`.
1080
+ `gem.prefs.recommended?.touchMovement` and draw a floating or a fixed one. A game with only one
1081
+ kind draws that one and ignores the field.
1082
+ - **No camera, no call.** A game with no camera to turn never calls `look`.
1079
1083
 
1080
1084
  ## Graphics quality: the Arcade picks the start, your game adapts
1081
1085
 
@@ -1150,10 +1154,8 @@ gem.setMenu({
1150
1154
  });
1151
1155
  ```
1152
1156
 
1153
- - **The Arcade no longer shows a "Game settings" row for `onSettings`.** Your settings open from
1154
- [your own button](#game-settings). `gem.setMenu({ onSettings })` is still accepted, so a game that
1155
- passes it keeps working, and another embedder may still show an entry for it — but in the Arcade
1156
- it adds nothing.
1157
+ - **Your settings are not in it.** They open from [your own button](#game-settings), and the
1158
+ Arcade's menu never opens them.
1157
1159
  - **`items` are up to four entries of your own**, shown in a group below the Arcade's. `id` is your
1158
1160
  name for the entry (1 to 32 characters); `label` is what the player reads, **already in their
1159
1161
  language** (`gem.prefs.locale`) — the Arcade does not translate it, cuts it at 40 characters, and
@@ -1460,9 +1462,12 @@ This is a platform rule, not advice — see
1460
1462
 
1461
1463
  ### Arcade avatars: the character the player wears, and changing it mid-game
1462
1464
 
1463
- Some games draw **Arcade avatars** (store tag `arcade-avatars`): the player's look in your game is
1464
- something they own and wear in the Arcade. The Arcade's avatar picker has **two tabs**, and they
1465
- are **two separate worn choices** — wearing one never changes the other:
1465
+ Some games draw **Arcade avatars**: the player's look in your game is something they own and wear
1466
+ in the Arcade. **The whole guide for 3D avatars — rig families, declaring the ones your game
1467
+ accepts, uploading and selling your own, the purchase flow and loading the model — is
1468
+ [arcade-avatars.md](arcade-avatars.md).** This section is the summary. The Arcade's avatar picker
1469
+ has **two tabs**, and they are **two separate worn choices** — wearing one never changes the
1470
+ other:
1466
1471
 
1467
1472
  | Tab | What the player wears | On `gem.player` and every room member |
1468
1473
  |---|---|---|
@@ -1476,6 +1481,8 @@ are **two separate worn choices** — wearing one never changes the other:
1476
1481
  (an idle, say). It never changes at its URL, so cache it by URL.
1477
1482
  - **`avatar3dIconUrl`** is a rendered portrait of that character, for a roster or a menu where
1478
1483
  loading the model is too much. It is **not** the 2D portrait.
1484
+ - **`avatars3d`** lists every worn 3D character, one per **rig family**; the three fields above
1485
+ are your game's first declared family's. `gem.avatars3d.worn()` reads it for you.
1479
1486
 
1480
1487
  **Draw the 3D character, falling back to what you did before.** A game that drew characters
1481
1488
  before the 3D tab existed keyed its own models on `avatarIconId`. Keep that mapping as the
@@ -1522,7 +1529,7 @@ is missing now (a player who takes off their 3D character has no `avatar3dId`).
1522
1529
 
1523
1530
  **A "Change avatar" button belongs in your own UI**, and `gem.openAvatarPicker()` opens the
1524
1531
  Arcade's picker from it. Pass `{ tab: '3d' }` or `{ tab: '2d' }` to open on a tab; omit it and
1525
- the Arcade decides (a game tagged `arcade-avatars` gets the 3D tab). The Arcade pauses your game
1532
+ the Arcade decides (a game that declares avatar families gets the 3D tab). The Arcade pauses your game
1526
1533
  while the picker is open, saves the player's choice, sends the player again, and resumes you:
1527
1534
 
1528
1535
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "11.1.2",
3
+ "version": "13.0.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",