@indiegems/gem-web-sdk 10.3.1 → 11.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 +31 -0
- package/dist/function.d.ts +3 -0
- package/dist/game.d.ts +14 -3
- 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.quality.js +2 -0
- package/dist/gem-web-sdk.sdk.js +2 -2
- package/dist/gem-web-sdk.server.js +1 -1
- package/dist/index.d.ts +18 -2
- package/dist/quality.d.ts +49 -0
- package/dist/sdk.d.ts +20 -3
- package/dist/server-api.d.ts +6 -0
- package/dist/server.d.ts +6 -0
- package/docs/api/api-reference-game.md +435 -21
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +424 -10
- package/docs/changelog.md +31 -0
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +8 -8
- package/docs/server-bundles.md +2 -2
- package/docs/what-a-game-must-do.md +138 -21
- package/package.json +5 -1
- package/schema/bridge.v1.json +116 -7
package/docs/manifest.json
CHANGED
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"file": "changelog.md",
|
|
36
|
-
"sha256": "
|
|
37
|
-
"updated": "2026-10-
|
|
36
|
+
"sha256": "49abc51f04333f12f5b88687a4d5cd702bac1ab28a8f19533f9253ba015ec41a",
|
|
37
|
+
"updated": "2026-10-11T20:42:08Z"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"file": "cross-promotion.md",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"file": "getting-started.md",
|
|
71
|
-
"sha256": "
|
|
72
|
-
"updated": "2026-10-
|
|
71
|
+
"sha256": "f46be1e65cc5936ad3c06c27f5eb33b250651003f8880c36bf4f0deb50b30e09",
|
|
72
|
+
"updated": "2026-10-11T20:42:08Z"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"file": "inventory-and-catalogs.md",
|
|
@@ -123,8 +123,8 @@
|
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"file": "server-bundles.md",
|
|
126
|
-
"sha256": "
|
|
127
|
-
"updated": "2026-10-
|
|
126
|
+
"sha256": "74b7d0964881a74ee12097bdee41afec12ca03d3feddb5a935bea7483a79967d",
|
|
127
|
+
"updated": "2026-10-11T20:42:08Z"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
"file": "serverless-functions-js.md",
|
|
@@ -148,8 +148,8 @@
|
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"file": "what-a-game-must-do.md",
|
|
151
|
-
"sha256": "
|
|
152
|
-
"updated": "2026-10-
|
|
151
|
+
"sha256": "ab4f18f2c1cfccf54b01027dea77d229f0a9f086e373d6cfd4c741fd0004557f",
|
|
152
|
+
"updated": "2026-10-11T19:48:44Z"
|
|
153
153
|
}
|
|
154
154
|
]
|
|
155
155
|
}
|
package/docs/server-bundles.md
CHANGED
|
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
|
|
|
80
80
|
also publishes on its download host, with the hash beside it:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
curl -fsSL -O https://downloads.gemarcade.com/sdk/
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v11.0.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v11.0.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -924,12 +924,18 @@ same push as `muted` and `reducedMotion`, and again whenever the player changes
|
|
|
924
924
|
| `gamepadLookSensitivity` | `20` to `400` | Look speed for a gamepad stick, as a percentage: `100` is 1×. |
|
|
925
925
|
| `invertLookY` | `true` / `false` | Up/down look inverted: pushing up or dragging up looks down. Pitch only, never yaw. |
|
|
926
926
|
| `touchMovement` | `'floating'` / `'fixed'` | On a touch screen, a movement stick that appears where the thumb lands, or one fixed in place. |
|
|
927
|
+
| `graphicsQuality` | `'low'` / `'medium'` / `'high'` | The graphics quality to start at on this device. |
|
|
928
|
+
| `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. |
|
|
927
929
|
|
|
928
|
-
|
|
930
|
+
`lookSensitivity` to `touchMovement` are the camera and controls settings: see
|
|
929
931
|
[camera and look controls](#camera-and-look-controls) for telling the Arcade which of them your game
|
|
930
932
|
uses, and for `gem.controls.look()`, which applies the two sensitivities and the invert for you. An
|
|
931
933
|
older Arcade sends `recommended` without them; the SDK then fills each with its default (`100`,
|
|
932
|
-
`100`, `false`, `'floating'`)
|
|
934
|
+
`100`, `false`, `'floating'`). The last two are the starting graphics quality: see
|
|
935
|
+
[graphics quality](#graphics-quality-the-arcade-picks-the-start-your-game-adapts). An older Arcade
|
|
936
|
+
sends neither; the SDK then fills `graphicsQuality` from `batterySaver` (`'low'` when it is on,
|
|
937
|
+
`'medium'` otherwise) and `graphicsQualityAuto` with `true`, and reads a tier it does not know as
|
|
938
|
+
`'medium'`. So whenever `recommended` is not `null`, all eleven fields are there.
|
|
933
939
|
|
|
934
940
|
**They are recommendations, and the player's own choice in your game always wins:**
|
|
935
941
|
|
|
@@ -949,6 +955,7 @@ older Arcade sends `recommended` without them; the SDK then fills each with its
|
|
|
949
955
|
const DEFAULTS = {
|
|
950
956
|
volume: 100, batterySaver: false, keyboardLayout: 'wasd', gamepadConfirm: 'south', vibration: true,
|
|
951
957
|
lookSensitivity: 100, gamepadLookSensitivity: 100, invertLookY: false, touchMovement: 'floating',
|
|
958
|
+
graphicsQuality: 'medium',
|
|
952
959
|
};
|
|
953
960
|
let chosen = {}; // ONLY what the player picked in your settings screen
|
|
954
961
|
|
|
@@ -1067,6 +1074,66 @@ function frame(dt) {
|
|
|
1067
1074
|
`touchMovement: 'game'` if your game only has one kind.
|
|
1068
1075
|
- **No camera, no call.** A game with no camera to turn declares nothing and never calls `look`.
|
|
1069
1076
|
|
|
1077
|
+
## Graphics quality: the Arcade picks the start, your game adapts
|
|
1078
|
+
|
|
1079
|
+
Players open a lot of games they have never played. Each one should open at a quality that suits
|
|
1080
|
+
the device instead of guessing and stuttering for the first minute. The work is split three ways:
|
|
1081
|
+
|
|
1082
|
+
- **The Arcade picks the starting tier, per device**, and sends it as
|
|
1083
|
+
`gem.prefs.recommended.graphicsQuality`: `'low'`, `'medium'` or `'high'`. On Auto, the player's
|
|
1084
|
+
default, it is the tier the Arcade measured for this browser on this device. Otherwise it is the
|
|
1085
|
+
player's own choice in the Arcade's Settings: Battery saver (`'low'`, and cap your frame rate and
|
|
1086
|
+
cut effects too), Balanced (`'medium'`) or Best looking (`'high'`).
|
|
1087
|
+
- **Your game adapts from there while it runs**: one level down when frames stay slow, one level up
|
|
1088
|
+
when there is plenty of room. `createQualityGovernor` does this for you, so you do not write a
|
|
1089
|
+
frame-time monitor of your own.
|
|
1090
|
+
- **A choice the player made by hand is held.** `graphicsQualityAuto` is `false` when the player
|
|
1091
|
+
picked the tier, and then you render at exactly that tier and never move it yourself. Your own
|
|
1092
|
+
quality option, if your settings screen has one, still wins over the Arcade's, as every
|
|
1093
|
+
[recommended setting](#start-from-the-players-arcade-wide-defaults-gemprefsrecommended) does.
|
|
1094
|
+
|
|
1095
|
+
The governor is a separate import, so a game that does not use it carries none of it:
|
|
1096
|
+
|
|
1097
|
+
```js
|
|
1098
|
+
import { createQualityGovernor } from '@indiegems/gem-web-sdk/quality';
|
|
1099
|
+
|
|
1100
|
+
// Your own levels, lowest first. Names, or a count: `levels: 4` means levels 0 to 3.
|
|
1101
|
+
const governor = createQualityGovernor({
|
|
1102
|
+
gem,
|
|
1103
|
+
levels: ['low', 'medium', 'high'],
|
|
1104
|
+
onChange: (level) => applyQuality(level), // shadows, resolution scale, particles…
|
|
1105
|
+
});
|
|
1106
|
+
applyQuality(governor.level); // where it starts; onChange is not called for that
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
What it does, so you know what to expect:
|
|
1110
|
+
|
|
1111
|
+
- **It starts at the Arcade's tier.** A tier whose name is one of your levels starts there;
|
|
1112
|
+
otherwise `'low'` is your lowest level, `'high'` your highest and `'medium'` your middle one. With
|
|
1113
|
+
no Arcade (`gem.prefs` is `null`, or no `gem`), it starts in the middle.
|
|
1114
|
+
- **It measures your frames itself**, with `requestAnimationFrame`. If your loop already knows its
|
|
1115
|
+
frame time, pass `manual: true` and call `governor.frame(dtMs)` once a frame instead; the first
|
|
1116
|
+
`frame` call switches it to your numbers anyway, so frames are never counted twice.
|
|
1117
|
+
- **Down fast, up slow.** One level down after about 2 seconds of slow frames (the 90th-percentile
|
|
1118
|
+
frame over 1.25 times your frame budget), one level up after about 10 seconds with room to spare
|
|
1119
|
+
(under 0.7 times the budget), and never more than one level above where it started. A single
|
|
1120
|
+
hitch is not slow frames. After every change it ignores 2 seconds of frames, and a level it
|
|
1121
|
+
already had to leave takes twice as long to climb back into.
|
|
1122
|
+
- **Your budget is `1000 / targetFps`**, and `targetFps` is 60 unless you say. A game that caps
|
|
1123
|
+
itself at 30 frames a second on battery saver passes `targetFps: 30`, or every frame reads as
|
|
1124
|
+
slow.
|
|
1125
|
+
- **It never counts a paused game**: not while the Arcade has you paused (`gem.paused`), and not
|
|
1126
|
+
while the tab is hidden. It also ignores the first 2 seconds, which are your game loading.
|
|
1127
|
+
- **It holds a hand-picked tier.** While `graphicsQualityAuto` is `false` it stays at the start and
|
|
1128
|
+
never calls `onChange` on its own. Pass `auto: false` to hold whatever the Arcade says, or
|
|
1129
|
+
`start: 'high'` to start from a level the player chose in your own settings.
|
|
1130
|
+
- **A new recommendation starts it over.** When the player changes Graphics in the Arcade's
|
|
1131
|
+
Settings, the governor goes back to the new start and calls `onChange(level, 'reset')`. It reads
|
|
1132
|
+
`gem.prefs` live, so you do not have to tell it.
|
|
1133
|
+
- **`governor.stats`** has what it measured (`fps`, `p90Ms` and `budgetMs`, the level's index,
|
|
1134
|
+
whether it is holding or paused), for your own debug overlay. Call `governor.dispose()` when your
|
|
1135
|
+
game stops rendering for good.
|
|
1136
|
+
|
|
1070
1137
|
## The Arcade's menu
|
|
1071
1138
|
|
|
1072
1139
|
The Arcade keeps one button over every game, on every device: the Gem, at the top-left. Its menu is
|
|
@@ -1390,41 +1457,91 @@ This is a platform rule, not advice — see
|
|
|
1390
1457
|
|
|
1391
1458
|
### Arcade avatars: the character the player wears, and changing it mid-game
|
|
1392
1459
|
|
|
1393
|
-
Some games draw **Arcade avatars
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
member — rather than on the thumbnail.
|
|
1460
|
+
Some games draw **Arcade avatars** (store tag `arcade-avatars`): the player's look in your game is
|
|
1461
|
+
something they own and wear in the Arcade. The Arcade's avatar picker has **two tabs**, and they
|
|
1462
|
+
are **two separate worn choices** — wearing one never changes the other:
|
|
1397
1463
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1464
|
+
| Tab | What the player wears | On `gem.player` and every room member |
|
|
1465
|
+
|---|---|---|
|
|
1466
|
+
| **2D** | a portrait — a picture | `avatarIconId`, `avatarIconUrl` (`avatar_icon_id`, `avatar_icon_url` on the wire) |
|
|
1467
|
+
| **3D** | a 3D character — a rigged model | `avatar3dId`, `avatar3dModelUrl`, `avatar3dIconUrl` (`avatar_3d_id`, `avatar_3d_model_url`, `avatar_3d_icon_url`) |
|
|
1402
1468
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1469
|
+
- **`avatar3dId`** is the worn 3D character's item id. Absent means none is worn — draw your
|
|
1470
|
+
game's default look.
|
|
1471
|
+
- **`avatar3dModelUrl`** is its model: a binary glTF (`.glb`, a few hundred kB) with one rig,
|
|
1472
|
+
`root > body > (head, arm_l, arm_r > hand_socket, leg_l, leg_r)`, sometimes with animation clips
|
|
1473
|
+
(an idle, say). It never changes at its URL, so cache it by URL.
|
|
1474
|
+
- **`avatar3dIconUrl`** is a rendered portrait of that character, for a roster or a menu where
|
|
1475
|
+
loading the model is too much. It is **not** the 2D portrait.
|
|
1476
|
+
|
|
1477
|
+
**Draw the 3D character, falling back to what you did before.** A game that drew characters
|
|
1478
|
+
before the 3D tab existed keyed its own models on `avatarIconId`. Keep that mapping as the
|
|
1479
|
+
fallback, and prefer the 3D character when one is worn:
|
|
1406
1480
|
|
|
1407
1481
|
```js
|
|
1408
|
-
|
|
1482
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
1483
|
+
import { platformIconUrl } from '@indiegems/gem-web-sdk/sdk';
|
|
1484
|
+
|
|
1485
|
+
const loader = new GLTFLoader();
|
|
1486
|
+
|
|
1487
|
+
async function characterFor(p) {
|
|
1488
|
+
// 1. The 3D character the player wears — the platform's own model.
|
|
1489
|
+
const url = platformIconUrl(p.avatar3dModelUrl); // pins the host, as for pictures
|
|
1490
|
+
if (p.avatar3dId && url) {
|
|
1491
|
+
try {
|
|
1492
|
+
const gltf = await loader.loadAsync(url);
|
|
1493
|
+
return { scene: gltf.scene, clips: gltf.animations };
|
|
1494
|
+
} catch {
|
|
1495
|
+
// A model that fails to load is not a reason to show nothing.
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
// 2. Your old mapping of the worn 2D portrait, then your default.
|
|
1499
|
+
return myModelForPortrait(p.avatarIconId) ?? myDefaultCharacter();
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const gem = createGem({ onPlayer: async (p) => hero.wear(await characterFor(p)) });
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
- **Pass every URL through `platformIconUrl` first** — the model too. Loading it is a request that
|
|
1506
|
+
carries your player's `Referer`, so only the platform's own item host is allowed; anything else
|
|
1507
|
+
reads as absent. Against a development backend that includes the development image host, so
|
|
1508
|
+
there the fallback is what you see.
|
|
1509
|
+
- **Attach things by the rig's names**, not by your old model's: a held item goes on
|
|
1510
|
+
`hand_socket`. Play the model's own clips when it has them; your game's animations may not fit
|
|
1511
|
+
its rig.
|
|
1512
|
+
- **Keep loading off the critical path.** Start the boot with your default look and swap the
|
|
1513
|
+
character in when the model arrives; a player on a slow connection should not wait for it.
|
|
1514
|
+
|
|
1515
|
+
**Redraw from `onPlayer`, not from boot alone.** The Arcade sends the player again whenever what
|
|
1516
|
+
they wear changes — either tab — including mid-game from the Arcade's own menu, without your game
|
|
1517
|
+
asking. `onPlayer` runs and `gem.player` holds the new values; a field missing from the new player
|
|
1518
|
+
is missing now (a player who takes off their 3D character has no `avatar3dId`).
|
|
1409
1519
|
|
|
1520
|
+
**A "Change avatar" button belongs in your own UI**, and `gem.openAvatarPicker()` opens the
|
|
1521
|
+
Arcade's picker from it. Pass `{ tab: '3d' }` or `{ tab: '2d' }` to open on a tab; omit it and
|
|
1522
|
+
the Arcade decides (a game tagged `arcade-avatars` gets the 3D tab). The Arcade pauses your game
|
|
1523
|
+
while the picker is open, saves the player's choice, sends the player again, and resumes you:
|
|
1524
|
+
|
|
1525
|
+
```js
|
|
1410
1526
|
changeAvatar.hidden = !gem.avatarPickerAvailable; // read it live
|
|
1411
1527
|
changeAvatar.onclick = () => {
|
|
1412
|
-
if (!gem.openAvatarPicker()) changeAvatar.hidden = true;
|
|
1528
|
+
if (!gem.openAvatarPicker({ tab: '3d' })) changeAvatar.hidden = true;
|
|
1413
1529
|
};
|
|
1414
1530
|
```
|
|
1415
1531
|
|
|
1416
1532
|
- **`gem.avatarPickerAvailable`** says whether the Arcade will open its picker here. It is `false`
|
|
1417
1533
|
outside the Arcade, before the bridge is up, and under an embedder that has no picker.
|
|
1418
1534
|
- **`gem.openAvatarPicker()`** returns `true` when the Arcade was asked and `false` when it was not
|
|
1419
|
-
(the same cases, or more than six asks a minute). It never throws and never waits.
|
|
1535
|
+
(the same cases, or more than six asks a minute). It never throws and never waits. An Arcade
|
|
1536
|
+
older than the tabs ignores `tab` and opens its picker as before.
|
|
1420
1537
|
- **Nothing answers it.** The outcome is the new player through `onPlayer`. A player who closes the
|
|
1421
1538
|
picker unchanged produces nothing, so never wait for an answer or poll `gem.player`.
|
|
1422
1539
|
|
|
1423
|
-
**Your server is not told.** A server-authoritative game reads each member's `
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
never trust
|
|
1540
|
+
**Your server is not told.** A server-authoritative game reads each member's `avatar_3d_id`,
|
|
1541
|
+
`avatar_3d_model_url` and `avatar_icon_id` from the room read
|
|
1542
|
+
(`GET /v1/games/{game_id}/game-rooms/{game_room_id}`, or `gem.room.get()` on a dedicated server),
|
|
1543
|
+
and nothing pushes a change to it. So when `onPlayer` brings a different avatar, have your client
|
|
1544
|
+
tell your server, and have the server **re-read the room** — never trust an id the client sends.
|
|
1428
1545
|
|
|
1429
1546
|
## Which backend — no longer yours, and what the override is for
|
|
1430
1547
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indiegems/gem-web-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"description": "postMessage bridge contract between gem-web and browser-playable games",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"types": "./dist/function.d.ts",
|
|
36
36
|
"import": "./dist/gem-web-sdk.function.js"
|
|
37
37
|
},
|
|
38
|
+
"./quality": {
|
|
39
|
+
"types": "./dist/quality.d.ts",
|
|
40
|
+
"import": "./dist/gem-web-sdk.quality.js"
|
|
41
|
+
},
|
|
38
42
|
"./schema": "./schema/bridge.v1.json",
|
|
39
43
|
"./brand/*": "./brand/*"
|
|
40
44
|
},
|
package/schema/bridge.v1.json
CHANGED
|
@@ -557,7 +557,13 @@
|
|
|
557
557
|
"from the parent's menu. A frame replaces its whole copy with each one; a",
|
|
558
558
|
"field absent from the latest message is absent now. There is no other",
|
|
559
559
|
"signal that the portrait changed, and no reply to the request that",
|
|
560
|
-
"opened the picker: this message is the outcome."
|
|
560
|
+
"opened the picker: this message is the outcome.",
|
|
561
|
+
"",
|
|
562
|
+
"The WORN 3D CHARACTER (`avatar3dId`, `avatar3dModelUrl`,",
|
|
563
|
+
"`avatar3dIconUrl`) is re-sent the same way: whenever it changes, from",
|
|
564
|
+
"either tab of the parent's picker, the whole message goes again. It is",
|
|
565
|
+
"a separate worn choice from the portrait — wearing one never changes",
|
|
566
|
+
"the other — so a message may carry either, both or neither."
|
|
561
567
|
],
|
|
562
568
|
"fields": {
|
|
563
569
|
"untrusted": {
|
|
@@ -620,6 +626,52 @@
|
|
|
620
626
|
"the way it drops a non-digit `discriminator`."
|
|
621
627
|
]
|
|
622
628
|
},
|
|
629
|
+
"avatar3dId": {
|
|
630
|
+
"kind": "string",
|
|
631
|
+
"maxLength": 64,
|
|
632
|
+
"optional": true,
|
|
633
|
+
"$comment": [
|
|
634
|
+
"The item-definition id of the 3D CHARACTER the player wears (gem-api",
|
|
635
|
+
"`avatar_3d_id`, a UUID; item type `avatar_3d`) — the same id a",
|
|
636
|
+
"game-room member's `avatar_3d_id` carries. A game that draws Arcade",
|
|
637
|
+
"avatars keys its character on this first, and falls back to its own",
|
|
638
|
+
"mapping of `avatarIconId` when it is absent.",
|
|
639
|
+
"",
|
|
640
|
+
"INDEPENDENT of `avatarIconId`: the portrait and the 3D character are",
|
|
641
|
+
"two worn choices, and wearing one never changes the other. Absent",
|
|
642
|
+
"means no 3D character is worn (the game's default look), or an older",
|
|
643
|
+
"parent; a parent never sends an empty string. Sent with",
|
|
644
|
+
"`avatar3dModelUrl` and `avatar3dIconUrl`, all three describing the",
|
|
645
|
+
"same item.",
|
|
646
|
+
"",
|
|
647
|
+
"Bounded here and shape-checked by the frame, exactly as `avatarIconId`",
|
|
648
|
+
"is and for the same reason: the frame keeps it only when it is a UUID."
|
|
649
|
+
]
|
|
650
|
+
},
|
|
651
|
+
"avatar3dModelUrl": {
|
|
652
|
+
"kind": "string",
|
|
653
|
+
"maxLength": 2048,
|
|
654
|
+
"format": "url",
|
|
655
|
+
"optional": true,
|
|
656
|
+
"$comment": [
|
|
657
|
+
"The worn 3D character's MODEL (gem-api `avatar_3d_model_url`): a",
|
|
658
|
+
"binary glTF (`.glb`, `model/gltf-binary`) on the platform's item CDN,",
|
|
659
|
+
"one rig `root > body > (head, arm_l, arm_r > hand_socket, leg_l,",
|
|
660
|
+
"leg_r)`, possibly with animation clips. Immutable at its URL. Absent",
|
|
661
|
+
"when no 3D character is worn. Structurally validated only, like",
|
|
662
|
+
"`avatarIconUrl`, with the same pin caveat: THE CONSUMER MUST PIN THE",
|
|
663
|
+
"ORIGIN before fetching it — the SDK's `platformIconUrl` does, for",
|
|
664
|
+
"models as for pictures — or a hostile parent supplies a request-time",
|
|
665
|
+
"beacon carrying the Referer."
|
|
666
|
+
]
|
|
667
|
+
},
|
|
668
|
+
"avatar3dIconUrl": {
|
|
669
|
+
"kind": "string",
|
|
670
|
+
"maxLength": 2048,
|
|
671
|
+
"format": "url",
|
|
672
|
+
"optional": true,
|
|
673
|
+
"$comment": "The worn 3D character's PORTRAIT RENDER (gem-api `avatar_3d_icon_url`) — a picture of the model, for a roster or a menu where loading the model is too much. NOT the 2D portrait (`avatarIconUrl`), which is a separate worn choice. Absent when no 3D character is worn. Same pin caveat as `avatarIconUrl`."
|
|
674
|
+
},
|
|
623
675
|
"badges": {
|
|
624
676
|
"kind": "array",
|
|
625
677
|
"maxItems": 2,
|
|
@@ -804,7 +856,8 @@
|
|
|
804
856
|
"$comment": [
|
|
805
857
|
"The player's ARCADE-WIDE DEFAULTS for this device, which the Arcade",
|
|
806
858
|
"keeps in its own Settings: a starting volume, battery saver, a keyboard",
|
|
807
|
-
"layout, the gamepad's confirm button,
|
|
859
|
+
"layout, the gamepad's confirm button, vibration, the camera and",
|
|
860
|
+
"controls settings and a starting graphics quality. RECOMMENDATIONS",
|
|
808
861
|
"ONLY. The game's own saved settings win: a game starts a setting from",
|
|
809
862
|
"here only while the player has made no choice of their own in that",
|
|
810
863
|
"game, and never overwrites one the player made because this changed.",
|
|
@@ -824,6 +877,16 @@
|
|
|
824
877
|
"the game side all four are present whenever the object is. A game says",
|
|
825
878
|
"which of them it leaves to the player with `gem.controlsDeclare`.",
|
|
826
879
|
"",
|
|
880
|
+
"THE GRAPHICS TWO ARE OPTIONAL INSIDE IT TOO (owner decision",
|
|
881
|
+
"2026-10-11): `graphicsQuality` and `graphicsQualityAuto`, the quality",
|
|
882
|
+
"a game STARTS at on this device. The Arcade picks the start (the",
|
|
883
|
+
"player's Graphics setting, or the tier it detected for this device on",
|
|
884
|
+
"Auto); the game adapts from there while it runs, and holds the start",
|
|
885
|
+
"when the player picked it by hand. A parent built against an older SDK",
|
|
886
|
+
"omits them, and the SDK fills `graphicsQuality` from `batterySaver`",
|
|
887
|
+
"(`low` when it is on, `medium` otherwise) and `graphicsQualityAuto`",
|
|
888
|
+
"with true, so on the game side both are present whenever the object is.",
|
|
889
|
+
"",
|
|
827
890
|
"`muted`, `reducedMotion` and `showFrameStats` stay where they are, at",
|
|
828
891
|
"the top of this message, and keep their meaning: they are the Arcade's",
|
|
829
892
|
"own state the frame acts on, not a recommendation. The Arcade starts",
|
|
@@ -900,6 +963,31 @@
|
|
|
900
963
|
"only `floating` or `fixed` today, and the SDK maps any other value to",
|
|
901
964
|
"the default, `floating`, so a game only ever sees those two."
|
|
902
965
|
]
|
|
966
|
+
},
|
|
967
|
+
"graphicsQuality": {
|
|
968
|
+
"kind": "string",
|
|
969
|
+
"maxLength": 16,
|
|
970
|
+
"format": "code",
|
|
971
|
+
"optional": true,
|
|
972
|
+
"$comment": [
|
|
973
|
+
"The graphics quality a game should START at on this device: `low`,",
|
|
974
|
+
"`medium` or `high`. The player's Graphics setting decides it --",
|
|
975
|
+
"Battery saver sends `low`, Balanced `medium`, Best looking `high`,",
|
|
976
|
+
"and Auto (the default) sends the tier the Arcade detected for this",
|
|
977
|
+
"device. A starting point, not a cap: a game adapts from here while it",
|
|
978
|
+
"runs unless `graphicsQualityAuto` is false.",
|
|
979
|
+
"",
|
|
980
|
+
"A CODE, NOT AN ENUM, so the vocabulary can grow without costing an",
|
|
981
|
+
"older frame the whole message (see WIDENING above): a parent sends",
|
|
982
|
+
"only `low`, `medium` or `high` today, and the SDK maps any other",
|
|
983
|
+
"value to `medium`, so a game only ever sees those three. When it is",
|
|
984
|
+
"absent the SDK fills `low` if `batterySaver` is true, else `medium`."
|
|
985
|
+
]
|
|
986
|
+
},
|
|
987
|
+
"graphicsQualityAuto": {
|
|
988
|
+
"kind": "boolean",
|
|
989
|
+
"optional": true,
|
|
990
|
+
"$comment": "True when the Arcade detected `graphicsQuality` for this device (the player left Graphics on Auto), so the game may step its quality up or down from there as it measures frames; false when the player picked it by hand, so the game holds it and changes it only when the player does (in the Arcade or in the game's own settings). Default true when absent."
|
|
903
991
|
}
|
|
904
992
|
}
|
|
905
993
|
}
|
|
@@ -2852,11 +2940,14 @@
|
|
|
2852
2940
|
"ASKS, and the parent, which holds the account session, renders the",
|
|
2853
2941
|
"picker in its own chrome and saves the choice.",
|
|
2854
2942
|
"",
|
|
2855
|
-
"
|
|
2856
|
-
"
|
|
2943
|
+
"ONE OPTIONAL FIELD, AND NO REPLY. The game names nothing but which tab",
|
|
2944
|
+
"it would like the picker to open on (`tab`, below) — not an item, not",
|
|
2945
|
+
"a filter, not a line of text: which avatars are offered and what the",
|
|
2857
2946
|
"picker says are the parent's. And nothing answers this message. THE",
|
|
2858
|
-
"OUTCOME IS A NEW `gem.player`: when the worn portrait
|
|
2859
|
-
"parent re-sends it with the new `avatarIconId` and
|
|
2947
|
+
"OUTCOME IS A NEW `gem.player`: when the worn portrait or 3D character",
|
|
2948
|
+
"changes, the parent re-sends it with the new `avatarIconId` and",
|
|
2949
|
+
"`avatarIconUrl`, or `avatar3dId`, `avatar3dModelUrl` and",
|
|
2950
|
+
"`avatar3dIconUrl`, as",
|
|
2860
2951
|
"it does for a change the player makes from the parent's menu without",
|
|
2861
2952
|
"the game asking. A reply would be a second copy of an identity fact",
|
|
2862
2953
|
"(`gem.signInResult`'s argument), and the picker may stay open for as",
|
|
@@ -2886,7 +2977,25 @@
|
|
|
2886
2977
|
"picker is the control, and the account-session save is the",
|
|
2887
2978
|
"enforcement."
|
|
2888
2979
|
],
|
|
2889
|
-
"fields": {
|
|
2980
|
+
"fields": {
|
|
2981
|
+
"tab": {
|
|
2982
|
+
"kind": "enum",
|
|
2983
|
+
"values": ["2d", "3d"],
|
|
2984
|
+
"optional": true,
|
|
2985
|
+
"$comment": [
|
|
2986
|
+
"Which tab the picker opens on: `2d` the portrait (`avatarIconId`),",
|
|
2987
|
+
"`3d` the 3D character (`avatar3dId`). Absent",
|
|
2988
|
+
"means the parent decides — for a game that draws Arcade avatars it",
|
|
2989
|
+
"opens the 3D tab. A HINT, not a restriction: the player may switch",
|
|
2990
|
+
"tabs, and a parent with one tab ignores it.",
|
|
2991
|
+
"",
|
|
2992
|
+
"ADDITIVE, so no `v` bump: an older parent validated this message",
|
|
2993
|
+
"with no fields and ignores an unknown one (rule 4), so it opens its",
|
|
2994
|
+
"picker as before. A CLOSED enum, so a third value is a breaking",
|
|
2995
|
+
"change under the freeze — an older parent would refuse the whole ask."
|
|
2996
|
+
]
|
|
2997
|
+
}
|
|
2998
|
+
},
|
|
2890
2999
|
"rateLimit": {
|
|
2891
3000
|
"perMinute": 6,
|
|
2892
3001
|
"$comment": "Bounds a BUGGY game. NOT a security control: a hostile game posts on the port it holds rather than calling the SDK. What stops a game nagging is the parent's one-at-a-time rule and its freedom to drop an ask."
|