@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.
- package/CHANGELOG.md +37 -0
- package/README.md +3 -1
- package/dist/function.d.ts +9 -1
- package/dist/game.d.ts +88 -13
- package/dist/gem-web-sdk.bridge.js +1 -1
- package/dist/gem-web-sdk.esm.js +2 -2
- package/dist/gem-web-sdk.function.js +2 -2
- package/dist/gem-web-sdk.game.js +2 -2
- package/dist/gem-web-sdk.min.js +2 -2
- package/dist/gem-web-sdk.sdk.js +2 -2
- package/dist/gem-web-sdk.server-api.js +1 -1
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +100 -16
- package/dist/sdk.d.ts +98 -14
- package/dist/server-api.d.ts +9 -1
- package/dist/server.d.ts +10 -1
- package/docs/api/api-reference-function.md +8076 -0
- package/docs/api/api-reference-game.md +719 -165
- package/docs/api/api-reference-server.md +5 -3
- package/docs/api/reference.json +10310 -225
- package/docs/arcade-avatars.md +303 -0
- package/docs/changelog.md +37 -0
- package/docs/getting-started.md +5 -2
- package/docs/manifest.json +21 -16
- package/docs/rooms-and-matches.md +64 -1
- package/docs/saving-player-progress.md +16 -10
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions-js.md +118 -44
- package/docs/serverless-functions.md +26 -11
- package/docs/what-a-game-must-do.md +53 -46
- package/package.json +1 -1
- package/schema/bridge.v1.json +282 -18
- package/schema/capabilities.public.json +12 -0
|
@@ -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
|
|
55
|
-
when you upload. A caller cannot tell which kind of
|
|
56
|
-
limits and the refusals in serverless-functions.md
|
|
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
|
|
61
|
-
`handleJson` whose input is not JSON, or whose return value JSON cannot carry, fails the
|
|
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
|
|
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
|
|
100
|
-
|
|
101
|
-
`@indiegems/gem-web-sdk/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
124
|
-
|
|
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
|
|
159
|
-
| ------------------ |
|
|
160
|
-
| `not_a_module` | A parse error, with its line and column, or a file that is not UTF-8
|
|
161
|
-
| `forbidden_import` |
|
|
162
|
-
| `missing_handle` | Neither `handle` nor `handleJson` is
|
|
163
|
-
| `ambiguous_handle` | Both are exported
|
|
164
|
-
| `too_large` | Over the size limit, the same one a component has
|
|
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.
|
|
168
|
-
|
|
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
|
|
59
|
-
|
|
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 |
|
|
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
|
-
|
|
|
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
|
|
468
|
-
|
|
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
|
|
781
|
-
unrecorded, and the closing line's `dropped_log_lines` says how many.
|
|
782
|
-
with a nonzero count means your function logged too much, not that it
|
|
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
|
|
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
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
935
|
-
|
|
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
|
-
|
|
985
|
-
|
|
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))
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
**
|
|
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
|
|
1077
|
-
|
|
1078
|
-
- **No camera, no call.** A game with no camera to turn
|
|
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
|
-
- **
|
|
1154
|
-
|
|
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
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
|
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
|