@indiegems/gem-web-sdk 13.4.2 → 14.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/docs/changelog.md CHANGED
@@ -16,6 +16,25 @@ 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
+ ## 14.0.0
20
+
21
+ Upgrading from 13.4.3. Impact: required.
22
+
23
+ ### Rebuild
24
+
25
+ - The message name lists, the frame and parent message unions, and the capability union each gained new members for game settings (the gameSettings capability among them). Recompiling is enough for most games. If your code switches exhaustively over message kinds or capability names, add handling for the new members.
26
+
27
+ ### New capability
28
+
29
+ - A game can now mirror up to eight of its own settings into the Arcade's in-game menu using Gem.settings. A setting is a toggle, a slider or a choice. The Arcade shows up to two game toggles in the quick row next to its own sound and vibration controls, and lists the game's settings at the top of its settings screen. The game receives a change whenever the player alters one of them, but only for a setting it declared and only with a value of that setting's kind. Slider values are clamped and stepped. Keep drawing your own settings button and screen, because the declaration only mirrors a few settings. Do not declare the Arcade's own settings such as master volume, mute, vibration, graphics or controls.
30
+ - A game that talks to the Arcade through the frame object directly can now declare its settings with GemFrame.declareSettings and receive player changes through the onSettingChange option or callback. The declaration is held until the connection is ready and re-sent after each handshake. Updates are paced to about two per second.
31
+
32
+ ## 13.4.3
33
+
34
+ Upgrading from 13.4.2. Impact: none.
35
+
36
+ Nothing in this release asks anything of a game.
37
+
19
38
  ## 13.4.2
20
39
 
21
40
  Upgrading from 13.4.1. Impact: none.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "13.4.2"
53
+ "@indiegems/gem-web-sdk": "14.0.0"
54
54
  }
55
55
  ```
56
56
 
@@ -149,19 +149,12 @@ reports. A `clientVersion` option still exists and is deprecated; see
149
149
  [what a game must do](what-a-game-must-do.md#2-your-version-number-which-you-no-longer-declare)
150
150
  for when an older arcade still needs it.
151
151
 
152
- **One more line if you render with WebGL**, and it is not part of `createGem`:
153
-
154
- ```js
155
- const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true });
156
- ```
157
-
158
- Players can attach a screenshot to a bug report, and without that flag the browser may clear
159
- your drawing buffer before the platform can read it. The SDK detects that and refuses rather
160
- than sending your team **a fully black image with no error attached** — but the cost is that
161
- your reports arrive with no picture. Two-dimensional canvas games need nothing here.
162
- [The full section](what-a-game-must-do.md#bug-reports-players-send-you-and-the-one-webgl-flag-that-decides-them)
163
- covers overriding the capture and attaching your own debug state, which is the part actually
164
- worth spending effort on.
152
+ **Screenshots need nothing from you**, in 2D, WebGL or WebGPU, and that includes
153
+ `preserveDrawingBuffer`, which you should leave off. Players can attach a screenshot to a bug
154
+ report, and the SDK reads your canvas right after a frame you draw, so call `createGem` before you
155
+ create your renderer and draw inside `requestAnimationFrame`, as every engine does.
156
+ [The full section](what-a-game-must-do.md#bug-reports-players-send-you) covers overriding the
157
+ capture and attaching your own debug state, which is the part actually worth spending effort on.
165
158
 
166
159
  **There is no offline mode.** A game must be framed by a page that speaks the bridge: opened
167
160
  at the top level, `createGem` throws `GemUnframedError`, which says how to run it. To run framed
@@ -185,7 +178,8 @@ And draw a settings button of your own — top-right, with your Back button just
185
178
  ([the top of the screen](what-a-game-must-do.md#the-top-of-the-screen)) — whose values start from
186
179
  the player's Arcade-wide defaults, `gem.prefs.recommended`, until the player chooses their own
187
180
  ([what a game must do](what-a-game-must-do.md#game-settings)). The Arcade's menu does not open
188
- your settings. Outside the Arcade there is no Arcade menu at all, so also give players a way out — a
181
+ your settings screen; you may also mirror a few of your settings into it with
182
+ `gem.settings.declare`, but never instead of your own button. Outside the Arcade there is no Arcade menu at all, so also give players a way out — a
189
183
  control that calls `gem.exit()`. A game a player cannot leave is one they leave by closing the tab.
190
184
 
191
185
  ## 6. Play it
@@ -38,8 +38,8 @@
38
38
  },
39
39
  {
40
40
  "file": "changelog.md",
41
- "sha256": "4e64fa3d2f8aeeae3e9fb31b9adb500bbad34fcdfe9fa5ba0a2067d29f35bfd2",
42
- "updated": "2026-10-19T17:54:55Z"
41
+ "sha256": "3ccaf70d80a6164736920b4df6f817d600b2a80287d4a14783f72fbb470863f6",
42
+ "updated": "2026-10-21T13:51:48Z"
43
43
  },
44
44
  {
45
45
  "file": "daily-challenges.md",
@@ -68,8 +68,8 @@
68
68
  },
69
69
  {
70
70
  "file": "getting-started.md",
71
- "sha256": "bbfe1f9550c8af5378a779cab63a5a7936e948def64aa451ec9f98c09dd8e059",
72
- "updated": "2026-10-19T17:54:55Z"
71
+ "sha256": "dbda9199712f4112bfe316b7af6e63a11fcce6842e778672459e1532947cf21f",
72
+ "updated": "2026-10-21T13:51:48Z"
73
73
  },
74
74
  {
75
75
  "file": "inventory-and-catalogs.md",
@@ -128,8 +128,8 @@
128
128
  },
129
129
  {
130
130
  "file": "server-bundles.md",
131
- "sha256": "5fcb2a32a21c47586645fa16a9593f312171f1d794ed9b5602fc820ffd4627c3",
132
- "updated": "2026-10-19T17:54:55Z"
131
+ "sha256": "2312a25bb11ce29f50b97b0737c708b31f6e0290f99fe2520179a9dbac67fb8c",
132
+ "updated": "2026-10-21T13:51:48Z"
133
133
  },
134
134
  {
135
135
  "file": "serverless-functions-js.md",
@@ -153,8 +153,8 @@
153
153
  },
154
154
  {
155
155
  "file": "what-a-game-must-do.md",
156
- "sha256": "97f97817fb087dbe2c637b4ea7f22f1ab624496bd32e427e4619c211c65d4f21",
157
- "updated": "2026-10-18T15:05:29Z"
156
+ "sha256": "3a9c0e307b78cfb8b959c7b47a1ad0349699be944903a24834ee785c3823b402",
157
+ "updated": "2026-10-21T13:31:36Z"
158
158
  }
159
159
  ]
160
160
  }
@@ -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/v13.4.2/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v13.4.2/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v14.0.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v14.0.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -1016,10 +1016,13 @@ This is the whole model, and every other page that mentions settings links here:
1016
1016
 
1017
1017
  - **The Arcade's settings are the player's Arcade-wide defaults for this device.** Your game reads
1018
1018
  them from `gem.prefs.recommended` (and `muted` and `reducedMotion` from `gem.prefs`) and applies
1019
- them again on `onPrefs`. The Arcade never asks your game anything about them, and never shows
1020
- anything your game says.
1019
+ them again on `onPrefs`. The Arcade never asks your game anything about them.
1021
1020
  - **Every game draws its own settings button, top-right ([the top of the
1022
1021
  screen](#the-top-of-the-screen)), and its own settings screen.** The Arcade's menu never opens it.
1022
+ - **A game may also mirror up to eight of its own settings into the Arcade's menu**, with
1023
+ `gem.settings.declare` — a second way to reach a few of them, never the only one, and never a
1024
+ setting the Arcade already has ([mirror a few settings into the Arcade's
1025
+ menu](#mirror-a-few-settings-into-the-arcades-menu-gemsettings)).
1023
1026
  - **A setting starts from `recommended` until the player changes it in your game, and then the
1024
1027
  player's choice in your game wins.** Save only what the player chose, never a recommended value.
1025
1028
  - **Where a choice is kept.** Device configuration — volume, graphics quality, look sensitivity, a
@@ -1135,6 +1138,90 @@ owns — starts from `recommended` the same way and is kept in `gem.me.settings`
1135
1138
  ([sound](#sound-your-own-mute-control-and-the-arcades)), and `reducedMotion` is the player's
1136
1139
  accessibility setting, which your game honours.
1137
1140
 
1141
+ ### Mirror a few settings into the Arcade's menu: `gem.settings`
1142
+
1143
+ Optional. The Arcade's Gem menu can show a few of your game's own settings beside the player's
1144
+ Arcade-wide ones: in its Settings screen, in a group named for your game above the player's
1145
+ defaults for all games, and — for up to two toggles — in the menu's quick row, next to the
1146
+ Arcade's own Sound and Vibration buttons. A player who opens the menu mid-game can turn your music
1147
+ off without hunting for your settings screen.
1148
+
1149
+ **It is a mirror, never a replacement: keep your own settings button and screen**, with every
1150
+ setting you declare on it too. An embedder that draws no game settings shows none of this, and the
1151
+ Arcade may leave an entry out when it is short of room — your own screen is what remains. A game
1152
+ that dropped its button because the Arcade's menu had its settings would have settings nobody can
1153
+ reach.
1154
+
1155
+ ```js
1156
+ // Built from the same state your own settings screen shows.
1157
+ const declared = () => [
1158
+ { id: 'music', label: t('Music'), kind: 'toggle', value: music.on, quick: true, icon: 'music' },
1159
+ { id: 'sfx', label: t('Sound effects'), kind: 'toggle', value: sfx.on, quick: true, icon: 'sfx' },
1160
+ { id: 'colors', label: t('Color help'), kind: 'toggle', value: colorHelp, icon: 'eye' },
1161
+ { id: 'speed', label: t('Game speed'), kind: 'slider', value: speed, min: 0.5, max: 2, step: 0.25 },
1162
+ {
1163
+ id: 'difficulty',
1164
+ label: t('Difficulty'),
1165
+ kind: 'choice',
1166
+ value: difficulty,
1167
+ options: [
1168
+ { id: 'easy', label: t('Easy') },
1169
+ { id: 'normal', label: t('Normal') },
1170
+ { id: 'hard', label: t('Hard') },
1171
+ ],
1172
+ },
1173
+ ];
1174
+
1175
+ gem.settings.declare(declared());
1176
+
1177
+ // The player changed one in the Arcade's menu. Your game is usually paused: apply it anyway.
1178
+ gem.settings.onChange(({ id, value }) => {
1179
+ if (id === 'music') setMusic(value === true);
1180
+ if (id === 'sfx') setSfx(value === true);
1181
+ if (id === 'colors') setColorHelp(value === true);
1182
+ if (id === 'speed') setSpeed(value);
1183
+ if (id === 'difficulty') setDifficulty(value);
1184
+ keepChoice(id, value); // where your own screen keeps the same choice
1185
+ });
1186
+
1187
+ // Your own screen changed one: declare again, so the menu never shows a stale value.
1188
+ musicToggle.onchange = () => {
1189
+ setMusic(musicToggle.checked);
1190
+ keepChoice('music', musicToggle.checked);
1191
+ gem.settings.declare(declared());
1192
+ };
1193
+ ```
1194
+
1195
+ - **Three kinds, up to eight settings, shown in your order.** A `toggle`'s `value` is `true` or
1196
+ `false`; a `slider`'s is a number from `min` to `max` on `step` (defaults `0`, `100`, `1`); a
1197
+ `choice` has two to four `options` and its `value` is one of their ids. `id` is your own name
1198
+ for the setting, 1 to 40 of `A-Z a-z 0-9 _ -`, unique. An entry that breaks a rule is left out
1199
+ with a warning on the console, and nothing throws.
1200
+ - **Mark at most two toggles `quick`** — the ones a player reaches for mid-game: Music, Sound
1201
+ effects, Color help. A third that asks is shown only in the list. `icon` is optional: `music`,
1202
+ `sfx`, `voice`, `eye`, `text`, `hint`, `speed`, `bright`, `controls` or `star`.
1203
+ - **Labels are already in the player's language** (`gem.prefs.locale`): the Arcade does not
1204
+ translate them, and cuts them at 40 characters. Declare again when the language changes.
1205
+ - **Never declare a setting the Arcade already has.** Master volume and mute, vibration, graphics
1206
+ quality and the controls defaults (look sensitivity, invert, touch stick, keyboard layout,
1207
+ confirm button) are in the Arcade's own settings and reach you as `gem.prefs` and
1208
+ `gem.prefs.recommended`. A "Vibration", "Volume" or "Mute" of yours beside them is a second
1209
+ switch for the same thing, and the player cannot tell which one wins. Your own music and
1210
+ sound-effect levels are yours to declare.
1211
+ - **It is screen state, not a toggle.** Each call is the whole list and replaces the last;
1212
+ `gem.settings.declare([])` takes them all back out. **Declare again whenever your own screen
1213
+ changes a value**, so the menu never shows a stale one. Before the Arcade connects the list is
1214
+ held and sent when it does, and again after any reconnect; a burst — a slider being dragged — is
1215
+ sent at most twice a second, always ending on the last value.
1216
+ - **Apply a change at once, even while paused.** The Arcade's menu holds your game paused
1217
+ (`onPause(true)`) while it is open, and a player who turns Music off expects the music to stop
1218
+ while the menu is still open. The change is the player's own choice, exactly as if they had made
1219
+ it on your screen: keep it where you keep that setting (device settings or `gem.me.settings`,
1220
+ [above](#game-settings)). The SDK has already written it into your declaration, so you do not
1221
+ need to declare again for it.
1222
+ - **`declare` returns `false`** before the Arcade connects (the list is held) and under an
1223
+ embedder that draws no game settings. Do nothing differently: your own screen has them all.
1224
+
1138
1225
  ## Camera and look controls
1139
1226
 
1140
1227
  If your game turns a camera — a drag on a phone, a mouse under pointer lock, a gamepad's right
@@ -1289,8 +1376,10 @@ gem.setMenu({
1289
1376
  });
1290
1377
  ```
1291
1378
 
1292
- - **Your settings are not in it.** They open from [your own button](#game-settings), and the
1293
- Arcade's menu never opens them.
1379
+ - **Your settings screen is not in it.** It opens from [your own button](#game-settings), and the
1380
+ Arcade's menu never opens it. A few of your settings can also appear in the Arcade's menu, with
1381
+ [`gem.settings`](#mirror-a-few-settings-into-the-arcades-menu-gemsettings) — a mirror of your
1382
+ screen, never a replacement for it.
1294
1383
  - **`items` are up to four entries of your own**, shown in a group below the Arcade's. `id` is your
1295
1384
  name for the entry (1 to 32 characters); `label` is what the player reads, **already in their
1296
1385
  language** (`gem.prefs.locale`) — the Arcade does not translate it, cuts it at 40 characters, and
@@ -1951,34 +2040,40 @@ advice to a client you do not trust, so whatever it decides it must also carry o
1951
2040
  spending currency or granting an item is a recipe, which the platform already runs where a client
1952
2041
  cannot change it. [`serverless-functions.md`](serverless-functions.md) is the page.
1953
2042
 
1954
- ## Bug reports players send you, and the one WebGL flag that decides them
2043
+ ## Bug reports players send you
1955
2044
 
1956
2045
  The arcade has a **Feedback** control. A player taps it, the platform asks your game for a picture
1957
2046
  of itself, the player draws on it and writes what happened, and it arrives on your game's feedback
1958
2047
  list. You get this for nothing — there is no option to set.
1959
2048
 
1960
- There is one thing to get right, and it is easy to miss because getting it wrong produces no error
1961
- anywhere.
1962
-
1963
- ### ⚠️ WebGL games: `preserveDrawingBuffer: true`
1964
-
1965
- ```js
1966
- const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true });
1967
- ```
1968
-
1969
- Without it the browser is free to clear your drawing buffer once the frame is composited. Read it
1970
- after that and you get **a fully black image and no error** — `createImageBitmap` resolves, the
1971
- transfer succeeds, every layer reports success.
1972
-
1973
- The SDK checks the flag before it captures and **refuses** rather than sending you black. That is
1974
- deliberate: a black screenshot costs the player their whole report and costs you the time to work
1975
- out why the platform sent you a rectangle of nothing. The refusal reaches you instead, as
1976
- `unsupported` in the report's diagnostics.
1977
-
1978
- The flag has a real cost — it can stop the browser discarding the buffer after compositing — so it
1979
- is your call. But the default is to get no screenshots, silently, which is rarely what anyone wants.
1980
-
1981
- 2D canvas games need nothing. `OffscreenCanvas` games need `onScreenshot`, below.
2049
+ ### WebGL and WebGPU games: leave `preserveDrawingBuffer` off
2050
+
2051
+ **You do not need `preserveDrawingBuffer: true` for screenshots, and should not set it.** With it
2052
+ on, the browser copies the whole drawing buffer every frame instead of swapping it (measured at
2053
+ about 0.4 ms of GPU time a frame on a 2021 Android flagship) to keep pixels readable that the SDK
2054
+ only needs once, when a player files a report.
2055
+
2056
+ A WebGL canvas without the flag, and every WebGPU canvas, can be read only right after a frame is
2057
+ drawn: read it later and you get **a fully black image and no error**. So the SDK reads it there.
2058
+ It watches `requestAnimationFrame` and, when a capture is asked for, reads your canvas right after
2059
+ your next frame. When your game has stopped asking for frames, because nothing on screen is moving,
2060
+ it uses the picture it kept from the last frame you drew before you stopped. Every read is checked,
2061
+ and a blank one is never sent.
2062
+
2063
+ Two things keep that working, and an ordinary game does both already:
2064
+
2065
+ - **Call `createGem` before you create your renderer.** The SDK records each context as it is made,
2066
+ so that it never has to ask a canvas for one. Asking would create a context on a canvas you had
2067
+ not started yet, and take it from you.
2068
+ - **Draw inside `requestAnimationFrame`** (three.js's `setAnimationLoop`, Phaser, Babylon.js and
2069
+ PixiJS all do), **from a loop that stops by not asking for the next frame.** A frame drawn from
2070
+ an input handler or a timer has no moment the SDK can read it in, and a loop stopped with
2071
+ `cancelAnimationFrame` from outside a frame leaves no last frame to keep.
2072
+
2073
+ When neither read produced a picture, the report says so in its diagnostics: `unsupported` for
2074
+ WebGL (`preserveDrawingBuffer: true` would fix it, at the cost above), `unreadable` for WebGPU (only
2075
+ `onScreenshot` can). 2D canvas games need nothing. `OffscreenCanvas` games need `onScreenshot`,
2076
+ below.
1982
2077
 
1983
2078
  ### When the default capture is not right
1984
2079
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "13.4.2",
3
+ "version": "14.0.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -2155,8 +2155,8 @@
2155
2155
  "$comment": [
2156
2156
  "`not_wired` -- the capability was advertised but nothing implements it.",
2157
2157
  "`no_source` -- the default strategy found no visible canvas, or one of zero area.",
2158
- "`unsupported` -- the WebGL context has `preserveDrawingBuffer: false`, or was lost. THE ONE THE FLAG FIXES, and it is kept narrow on purpose: it used to also cover the case below, so a developer whose canvas simply cannot be read was told to set a flag that would change nothing.",
2159
- "`unreadable` -- this page cannot read that canvas AT ALL: control was transferred to an OffscreenCanvas, or the context is one the SDK has no way to snapshot (WebGPU). No flag fixes it; the game must supply `onScreenshot`.",
2158
+ "`unsupported` -- the WebGL context has `preserveDrawingBuffer: false` and no read produced a picture (no frame was drawn while the capture waited, and none was kept when the loop last stopped), or the context was lost. THE ONE THE FLAG FIXES, and it is kept narrow on purpose: it used to also cover the case below, so a developer whose canvas simply cannot be read was told to set a flag that would change nothing.",
2159
+ "`unreadable` -- this page cannot read that canvas: control was transferred to an OffscreenCanvas, or a WebGPU canvas produced no picture in the frame or at rest. No flag fixes it; the game must supply `onScreenshot`.",
2160
2160
  "`tainted` -- the canvas origin-clean flag is false. THE ONE A DEVELOPER CAN ACT ON (add `crossorigin` to the offending image), so it must stay distinguishable from `failed`.",
2161
2161
  "`too_large` -- the source exceeds the requested caps and cannot be downscaled.",
2162
2162
  "`busy` -- a capture is already in flight. Single-flight is enforced by the frame.",
@@ -3076,6 +3076,205 @@
3076
3076
  "rateLimit": { "perMinute": 12 }
3077
3077
  },
3078
3078
 
3079
+ {
3080
+ "type": "gem.settingsDeclare",
3081
+ "direction": "frameToParent",
3082
+ "transport": "port",
3083
+ "$comment": [
3084
+ "A few of the game's OWN settings, mirrored into the arcade's menu - in",
3085
+ "the Gem Arcade, a group headed with the game's name in its Settings",
3086
+ "screen (\"only this game\"), above the player's Arcade-wide defaults,",
3087
+ "and up to two toggles in the menu's quick row beside the arcade's own",
3088
+ "Sound and Vibration (owner decision 2026-10-21).",
3089
+ "",
3090
+ "A MIRROR, NEVER A REPLACEMENT. A game that declares settings still draws",
3091
+ "its own settings button and the screen it opens, and every one of these",
3092
+ "settings is on that screen too. That is the answer to why the earlier",
3093
+ "game-to-arcade settings channels (`gem.controlsDeclare`, `gem.menu`'s",
3094
+ "`settings` flag) were withdrawn: games hid their own button, and their",
3095
+ "settings became unreachable wherever the arcade showed nothing. A parent may",
3096
+ "draw none of this - any embedder that did not advertise `gameSettings`,",
3097
+ "a parent short of room - and the game's own screen is what remains.",
3098
+ "",
3099
+ " settings 0..8 entries, in the order the game wants them shown;",
3100
+ " empty means the game shows none",
3101
+ "",
3102
+ "Each entry is one of three kinds:",
3103
+ "",
3104
+ " toggle `value` a boolean",
3105
+ " slider `value` a number in [min, max], on `step` from `min`;",
3106
+ " `min`, `max` and `step` default to 0, 100 and 1",
3107
+ " choice `value` one of `options`' ids; 2..4 options",
3108
+ "",
3109
+ "`id` is the game's own name for the setting, 1..40 characters of",
3110
+ "A-Z a-z 0-9 _ -, and is what comes back in `gem.settingChange`;",
3111
+ "unique within one message, and the parent keeps the first of a",
3112
+ "repeated id. `label` (and each option's) is what the player reads,",
3113
+ "1..40 characters, ALREADY IN THE PLAYER'S LANGUAGE (`gem.prefs.locale`):",
3114
+ "the parent does not translate it. A parent drops an entry whose id,",
3115
+ "kind, value or options break these rules rather than guessing; the SDK",
3116
+ "never sends one.",
3117
+ "",
3118
+ "`quick` asks for a toggle in the menu's quick row. A parent honours it",
3119
+ "on the first two toggles that ask, in order, and ignores it on any other",
3120
+ "kind. `icon` is a CODE, not an enum: one of music, sfx, voice, eye,",
3121
+ "text, hint, speed, bright, controls, star - and a parent draws its",
3122
+ "generic glyph for one it does not know, so the vocabulary can grow",
3123
+ "without an older parent refusing the message. The SDK drops a code it",
3124
+ "does not know before sending.",
3125
+ "",
3126
+ "NOT THE ARCADE'S OWN SETTINGS. Master volume and mute, vibration,",
3127
+ "graphics and the controls defaults are the arcade's, reaching the game",
3128
+ "as `gem.prefs` and `gem.prefs.recommended`, and a game declares none of",
3129
+ "them.",
3130
+ "",
3131
+ "A LABEL IS UNTRUSTED TEXT IN THE PARENT'S CHROME. A parent renders it as",
3132
+ "text and never as markup, in the game's own group and never among its",
3133
+ "own entries, truncated to fit, and may decline to show an entry at all.",
3134
+ "",
3135
+ "SCREEN STATE, NOT A TOGGLE, like `gem.menu`. Each message is the whole",
3136
+ "answer and the latest one wins; nothing accumulates. The SDK holds it",
3137
+ "from before the bridge is up and re-sends it after every handshake; a",
3138
+ "game declares again whenever its own screen changes a value, and the",
3139
+ "SDK writes a `gem.settingChange` into the declaration it holds, so a",
3140
+ "reconnect never shows a stale value. A declaration is per game",
3141
+ "session: the parent forgets it when the game is left.",
3142
+ "",
3143
+ "A PRESENTATION REQUEST ONLY, and not an authorization control. It",
3144
+ "carries no player data and changes nothing but what the parent draws;",
3145
+ "a change the player makes there reaches the game as",
3146
+ "`gem.settingChange` and does nothing else.",
3147
+ "",
3148
+ "Gated by `gameSettings`: sent only to a parent that advertised it. To",
3149
+ "any other, `gem.settings.declare` returns false."
3150
+ ],
3151
+ "fields": {
3152
+ "settings": {
3153
+ "kind": "array",
3154
+ "maxItems": 8,
3155
+ "items": {
3156
+ "kind": "object",
3157
+ "fields": {
3158
+ "id": {
3159
+ "kind": "string",
3160
+ "maxLength": 40,
3161
+ "$comment": "The game's own name for the setting, 1..40 characters of A-Z a-z 0-9 _ -, echoed in `gem.settingChange`."
3162
+ },
3163
+ "label": {
3164
+ "kind": "string",
3165
+ "maxLength": 40,
3166
+ "$comment": "What the player reads, 1..40 characters, already translated. Untrusted text: rendered as text only."
3167
+ },
3168
+ "kind": {
3169
+ "kind": "enum",
3170
+ "values": ["toggle", "slider", "choice"]
3171
+ },
3172
+ "value": {
3173
+ "kind": "scalar",
3174
+ "maxLength": 40,
3175
+ "min": -1000000,
3176
+ "max": 1000000,
3177
+ "$comment": "The setting's current value: a boolean for a toggle, a number in [min, max] for a slider, one of `options`' ids for a choice."
3178
+ },
3179
+ "quick": {
3180
+ "kind": "boolean",
3181
+ "optional": true,
3182
+ "$comment": "Toggles only: ask to appear in the menu's quick row. A parent honours the first two that ask."
3183
+ },
3184
+ "icon": {
3185
+ "kind": "string",
3186
+ "maxLength": 16,
3187
+ "format": "code",
3188
+ "optional": true,
3189
+ "$comment": "A code: music, sfx, voice, eye, text, hint, speed, bright, controls or star. Unknown means the parent's generic glyph."
3190
+ },
3191
+ "min": {
3192
+ "kind": "number",
3193
+ "min": -1000000,
3194
+ "max": 1000000,
3195
+ "optional": true,
3196
+ "$comment": "Sliders only: the lowest value, 0 when absent. Below `max`."
3197
+ },
3198
+ "max": {
3199
+ "kind": "number",
3200
+ "min": -1000000,
3201
+ "max": 1000000,
3202
+ "optional": true,
3203
+ "$comment": "Sliders only: the highest value, 100 when absent. Above `min`."
3204
+ },
3205
+ "step": {
3206
+ "kind": "number",
3207
+ "min": 0,
3208
+ "max": 1000000,
3209
+ "optional": true,
3210
+ "$comment": "Sliders only: the increment from `min`, 1 when absent. Greater than 0."
3211
+ },
3212
+ "options": {
3213
+ "kind": "array",
3214
+ "maxItems": 4,
3215
+ "optional": true,
3216
+ "$comment": "Choices only: 2..4 options, ids unique, each with the same id and label rules as a setting.",
3217
+ "items": {
3218
+ "kind": "object",
3219
+ "fields": {
3220
+ "id": { "kind": "string", "maxLength": 40 },
3221
+ "label": { "kind": "string", "maxLength": 40 }
3222
+ }
3223
+ }
3224
+ }
3225
+ }
3226
+ }
3227
+ }
3228
+ },
3229
+ "rateLimit": { "perMinute": 120 }
3230
+ },
3231
+
3232
+ {
3233
+ "type": "gem.settingChange",
3234
+ "direction": "parentToFrame",
3235
+ "transport": "port",
3236
+ "$comment": [
3237
+ "The player changed one of the game's declared settings in the arcade's",
3238
+ "own chrome: the setting with this `id`, now at `value`.",
3239
+ "",
3240
+ "Sent only for an id the frame's LATEST `gem.settingsDeclare` holds,",
3241
+ "with a value of that setting's kind - a boolean for a toggle, a slider's",
3242
+ "number clamped into [min, max] and onto its step, one of a choice's",
3243
+ "option ids - and only to a frame that advertised `gameSettings`. A",
3244
+ "parent coalesces a drag: it sends a slider's value when the player",
3245
+ "lets go, or at most a few times a second while they hold it.",
3246
+ "",
3247
+ "THE GAME IS USUALLY PAUSED when this arrives - the menu holds the",
3248
+ "`gem.pause` it opened with - and applies the change anyway, so the",
3249
+ "player hears the music stop while the menu is still open. A frame",
3250
+ "that hears an id it no longer declares ignores it: the menu may have",
3251
+ "been open across a change.",
3252
+ "",
3253
+ "The SDK writes the new value into the declaration it holds before the",
3254
+ "game's handlers run, so the next handshake re-sends what the player",
3255
+ "chose rather than what the game last said.",
3256
+ "",
3257
+ "A PRESENTATION EVENT, NOT AN AUTHORIZATION CONTROL. It tells the game",
3258
+ "what the player picked in a mirror of the game's own screen, and",
3259
+ "nothing else; the game keeps the choice where it keeps any other."
3260
+ ],
3261
+ "fields": {
3262
+ "id": {
3263
+ "kind": "string",
3264
+ "maxLength": 40,
3265
+ "$comment": "The setting's id, from the latest `gem.settingsDeclare`."
3266
+ },
3267
+ "value": {
3268
+ "kind": "scalar",
3269
+ "maxLength": 40,
3270
+ "min": -1000000,
3271
+ "max": 1000000,
3272
+ "$comment": "The new value, of the setting's kind."
3273
+ }
3274
+ },
3275
+ "rateLimit": { "perMinute": 120 }
3276
+ },
3277
+
3079
3278
  {
3080
3279
  "type": "gem.requestShare",
3081
3280
  "direction": "frameToParent",
@@ -243,6 +243,13 @@
243
243
  "gem.gemCorner"
244
244
  ]
245
245
  },
246
+ "gameSettings": {
247
+ "implemented": true,
248
+ "gates": [
249
+ "gem.settingsDeclare",
250
+ "gem.settingChange"
251
+ ]
252
+ },
246
253
  "moments": {
247
254
  "implemented": true,
248
255
  "gates": [