@indiegems/gem-web-sdk 10.4.0 → 11.1.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 +26 -0
- package/dist/function.d.ts +3 -0
- package/dist/game.d.ts +29 -2
- 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.js +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/sdk.d.ts +35 -2
- package/dist/server-api.d.ts +6 -0
- package/dist/server.d.ts +6 -0
- package/docs/api/api-reference-game.md +423 -19
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +392 -10
- package/docs/changelog.md +26 -0
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +10 -10
- package/docs/rooms-and-matches.md +43 -0
- package/docs/server-bundles.md +2 -2
- package/docs/what-a-game-must-do.md +69 -19
- package/package.json +1 -1
- package/schema/bridge.v1.json +79 -6
|
@@ -872,11 +872,54 @@ seat stands, so `myRooms()` shows the room and `join(id)` enters it.
|
|
|
872
872
|
| `matchmaking_not_enabled` (400) | A named template has no enabled `matchmaking` block. A config fault, not a player one. |
|
|
873
873
|
| `matchmaking_unavailable` (503) | Matchmaking is turned off for the whole platform, or its store is not answering. |
|
|
874
874
|
|
|
875
|
+
**A party leader can meet three more.** The platform checks that a party can be seated together
|
|
876
|
+
before it queues it, so a party that could not play is told at once rather than matched and then
|
|
877
|
+
dropped. Only the leader queues, so only the leader's game sees these. No ticket was made, and the
|
|
878
|
+
button can be pressed again.
|
|
879
|
+
|
|
880
|
+
| Code | Meaning | What to show the leader |
|
|
881
|
+
| --- | --- | --- |
|
|
882
|
+
| `version_data_required` (400) | The party has not launched this game together, so the platform has no record of what each member is running. | "Start the game with your party from the Arcade to queue together." Pressing again will not help until they do. |
|
|
883
|
+
| `party_versions_incomplete` (409) | A member's game has not finished starting, so their version has not arrived yet. | "Waiting for your party to finish loading." This one clears by itself: queue again after a few seconds, on the player's press or on a short timer. |
|
|
884
|
+
| `party_version_mismatch` (409) | A member is running a build that cannot play with the leader's. | "Someone in your party is on a different version. Everyone reload the game." |
|
|
885
|
+
|
|
886
|
+
A party's `quickJoin()` and `create()` are refused the same way, with the same codes.
|
|
887
|
+
|
|
875
888
|
⚠️ **Nothing falls back to quick-join, and a game should not either.** Not on
|
|
876
889
|
`matchmaking_unavailable`, not on `expired`. Quick-join puts the player in a different kind of room —
|
|
877
890
|
alone, in a mode that was queued for precisely because it needs a lobby. Tell the player matchmaking is
|
|
878
891
|
unavailable and leave the button where it is.
|
|
879
892
|
|
|
893
|
+
## 5d. Quick-joining any of several modes
|
|
894
|
+
|
|
895
|
+
A drop-in game with more than one mode can ask for whichever has players, in one call:
|
|
896
|
+
|
|
897
|
+
```js
|
|
898
|
+
const room = await gem.rooms.quickJoin({ templateSlugs: ['duel', 'brawl'] });
|
|
899
|
+
startMode(room.templateSlug); // which one the player landed in
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
- **Open rooms of every listed template are candidates together**, ranked as a single-template
|
|
903
|
+
quick-join ranks them: the fullest first, then the closest cluster.
|
|
904
|
+
- **The order matters only when no room is open.** The room that gets created is made from the
|
|
905
|
+
**first** slug listed, so put the mode an empty lobby should start in first.
|
|
906
|
+
- **One to eight slugs, in place of `templateSlug`, never beside it.** A list together with
|
|
907
|
+
`templateSlug` or `templateId`, an empty list, or more than eight is refused locally, before
|
|
908
|
+
the platform is asked.
|
|
909
|
+
- **Every listed template must be one players can quick-join.** One that is not refuses the whole
|
|
910
|
+
call; it is not skipped.
|
|
911
|
+
- **`room.templateSlug` says which template the room is from.** It is on every room, however it was
|
|
912
|
+
entered.
|
|
913
|
+
|
|
914
|
+
A call that passes `templateSlug` behaves exactly as it always has.
|
|
915
|
+
|
|
916
|
+
Under a developer's tool that runs the game's server on the developer's own machine, only the first
|
|
917
|
+
slug is sent. Such a room is always created rather than joined, so it would be made from the first
|
|
918
|
+
template either way.
|
|
919
|
+
|
|
920
|
+
This is still quick-join: the player is in a room at once, possibly alone. For "any of these modes,
|
|
921
|
+
but never start short", name the same slugs on `queueForMatch()` instead.
|
|
922
|
+
|
|
880
923
|
## 6. Scope
|
|
881
924
|
|
|
882
925
|
**In:** rooms, membership, host, ready state, the match lifecycle, and the dedicated-server connect
|
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.1.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v11.1.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -1457,41 +1457,91 @@ This is a platform rule, not advice — see
|
|
|
1457
1457
|
|
|
1458
1458
|
### Arcade avatars: the character the player wears, and changing it mid-game
|
|
1459
1459
|
|
|
1460
|
-
Some games draw **Arcade avatars
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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:
|
|
1463
|
+
|
|
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`) |
|
|
1468
|
+
|
|
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:
|
|
1464
1480
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1481
|
+
```js
|
|
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`).
|
|
1469
1519
|
|
|
1470
1520
|
**A "Change avatar" button belongs in your own UI**, and `gem.openAvatarPicker()` opens the
|
|
1471
|
-
Arcade's picker from it.
|
|
1472
|
-
|
|
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:
|
|
1473
1524
|
|
|
1474
1525
|
```js
|
|
1475
|
-
const gem = createGem({ onPlayer: (p) => hero.wear(avatarOfItem(p.avatarIconId)) });
|
|
1476
|
-
|
|
1477
1526
|
changeAvatar.hidden = !gem.avatarPickerAvailable; // read it live
|
|
1478
1527
|
changeAvatar.onclick = () => {
|
|
1479
|
-
if (!gem.openAvatarPicker()) changeAvatar.hidden = true;
|
|
1528
|
+
if (!gem.openAvatarPicker({ tab: '3d' })) changeAvatar.hidden = true;
|
|
1480
1529
|
};
|
|
1481
1530
|
```
|
|
1482
1531
|
|
|
1483
1532
|
- **`gem.avatarPickerAvailable`** says whether the Arcade will open its picker here. It is `false`
|
|
1484
1533
|
outside the Arcade, before the bridge is up, and under an embedder that has no picker.
|
|
1485
1534
|
- **`gem.openAvatarPicker()`** returns `true` when the Arcade was asked and `false` when it was not
|
|
1486
|
-
(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.
|
|
1487
1537
|
- **Nothing answers it.** The outcome is the new player through `onPlayer`. A player who closes the
|
|
1488
1538
|
picker unchanged produces nothing, so never wait for an answer or poll `gem.player`.
|
|
1489
1539
|
|
|
1490
|
-
**Your server is not told.** A server-authoritative game reads each member's `
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
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.
|
|
1495
1545
|
|
|
1496
1546
|
## Which backend — no longer yours, and what the override is for
|
|
1497
1547
|
|
package/package.json
CHANGED
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,
|
|
@@ -2888,11 +2940,14 @@
|
|
|
2888
2940
|
"ASKS, and the parent, which holds the account session, renders the",
|
|
2889
2941
|
"picker in its own chrome and saves the choice.",
|
|
2890
2942
|
"",
|
|
2891
|
-
"
|
|
2892
|
-
"
|
|
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",
|
|
2893
2946
|
"picker says are the parent's. And nothing answers this message. THE",
|
|
2894
|
-
"OUTCOME IS A NEW `gem.player`: when the worn portrait
|
|
2895
|
-
"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",
|
|
2896
2951
|
"it does for a change the player makes from the parent's menu without",
|
|
2897
2952
|
"the game asking. A reply would be a second copy of an identity fact",
|
|
2898
2953
|
"(`gem.signInResult`'s argument), and the picker may stay open for as",
|
|
@@ -2922,7 +2977,25 @@
|
|
|
2922
2977
|
"picker is the control, and the account-session save is the",
|
|
2923
2978
|
"enforcement."
|
|
2924
2979
|
],
|
|
2925
|
-
"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
|
+
},
|
|
2926
2999
|
"rateLimit": {
|
|
2927
3000
|
"perMinute": 6,
|
|
2928
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."
|