@indiegems/gem-web-sdk 13.3.3 → 13.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  # The browser surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v13.3.3, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v13.4.1, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
4
4
 
5
5
  `@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/game` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
6
 
@@ -333,6 +333,15 @@ from whether the player holds one, which is on [EntityScope](#entityscope).
333
333
  Indexed rather than scanned, so a bag render asking for forty definitions
334
334
  in a frame stays cheap.
335
335
 
336
+ Its `icon_url` is the `icon:` file you uploaded with your config, hosted
337
+ by the platform — the picture the platform's purchase sheet shows. To show
338
+ it in your own shop, pass it through [platformIconUrl](#platformiconurl) and draw your
339
+ placeholder on `undefined`, which is what a development backend answers:
340
+
341
+ ```js
342
+ const src = platformIconUrl(config.item('iron-sword')?.icon_url ?? undefined);
343
+ ```
344
+
336
345
  | Parameter | Type |
337
346
  | --- | --- |
338
347
  | `idOrSlug` | `string` |
@@ -7579,7 +7588,9 @@ numbers to `--gem-safe-top`, `--gem-safe-right`, `--gem-safe-bottom` and
7579
7588
  stylesheet honours them with `var(--gem-safe-top, env(safe-area-inset-top, 0px))`
7580
7589
  and no code — and the rectangles the arcade's controls cover to
7581
7590
  `--gem-reserved-tl-width`, `--gem-reserved-tl-height` and the same pair for
7582
- `tr`, `bl` and `br`. Handle it only for a HUD you position from script — a
7591
+ `tr`, `bl` and `br`, with each corner's control centre in
7592
+ `--gem-reserved-tl-center-y` and its height in
7593
+ `--gem-reserved-tl-control-height`. Handle it only for a HUD you position from script — a
7583
7594
  canvas you draw into, an engine's own UI layer.
7584
7595
 
7585
7596
  <a id="gemoptions-manageaudio"></a>
@@ -8439,8 +8450,13 @@ beside it while the player shows it; an embedder that draws wallets over
8439
8450
  your game adds theirs (see `setWalletVisible`). Keep essential UI and touch
8440
8451
  targets out of them. Empty when the arcade covers nothing.
8441
8452
 
8453
+ Each is a keep-out area, grown out to the frame edges it sits nearest — not
8454
+ the control's own box, and not something to line anything up with. Where
8455
+ the arcade says, `control` on each is the control itself.
8456
+
8442
8457
  A stylesheet reads the per-corner custom properties the SDK stamps from
8443
- these instead — `--gem-reserved-tl-height` and friends.
8458
+ these instead — `--gem-reserved-tl-height` and friends, and
8459
+ `--gem-reserved-tl-center-y` to line a top bar up with the Gem.
8444
8460
 
8445
8461
  <a id="gemreservedarea"></a>
8446
8462
 
@@ -8494,6 +8510,72 @@ readonly height: number
8494
8510
 
8495
8511
  The rectangle's height.
8496
8512
 
8513
+ <a id="gemreservedarea-control"></a>
8514
+
8515
+ ## GemReservedArea.control
8516
+
8517
+ ```ts
8518
+ readonly control?: GemControlBox
8519
+ ```
8520
+
8521
+ The box of the control this area keeps you out of, as it is drawn — the
8522
+ Gem button itself, not grown to the edges — in the same space. Line your
8523
+ own controls up with this, never with the area: on a notched phone the
8524
+ Gem's area runs up to the top edge, so its middle sits well above the
8525
+ button's. Absent under an embedder that does not say, which leaves you the
8526
+ keep-out area alone.
8527
+
8528
+ <a id="gemcontrolbox"></a>
8529
+
8530
+ ## GemControlBox
8531
+
8532
+ ```ts
8533
+ interface GemControlBox
8534
+ ```
8535
+
8536
+ The box of one of the arcade's controls over your frame, in the same CSS
8537
+ pixels and space as `GemReservedArea`.
8538
+
8539
+ <a id="gemcontrolbox-x"></a>
8540
+
8541
+ ## GemControlBox.x
8542
+
8543
+ ```ts
8544
+ readonly x: number
8545
+ ```
8546
+
8547
+ From your frame's left edge to the control's left edge.
8548
+
8549
+ <a id="gemcontrolbox-y"></a>
8550
+
8551
+ ## GemControlBox.y
8552
+
8553
+ ```ts
8554
+ readonly y: number
8555
+ ```
8556
+
8557
+ From your frame's top edge to the control's top edge.
8558
+
8559
+ <a id="gemcontrolbox-width"></a>
8560
+
8561
+ ## GemControlBox.width
8562
+
8563
+ ```ts
8564
+ readonly width: number
8565
+ ```
8566
+
8567
+ The control's width.
8568
+
8569
+ <a id="gemcontrolbox-height"></a>
8570
+
8571
+ ## GemControlBox.height
8572
+
8573
+ ```ts
8574
+ readonly height: number
8575
+ ```
8576
+
8577
+ The control's height.
8578
+
8497
8579
  <a id="walletvisibleoptions"></a>
8498
8580
 
8499
8581
  ## WalletVisibleOptions
@@ -16955,6 +17037,34 @@ readonly members: readonly RoomMember[]
16955
17037
 
16956
17038
  Everyone in the current room, including this player. Empty when in none.
16957
17039
 
17040
+ <a id="roommanager-playwith"></a>
17041
+
17042
+ ## RoomManager.playWith
17043
+
17044
+ ```ts
17045
+ readonly playWith: PlayWithOutcome | null
17046
+ ```
17047
+
17048
+ What the last [RoomManager.quickJoin](#roommanager-quickjoin) did when the Arcade opened
17049
+ your game to play with someone — a friend whose Join the player pressed,
17050
+ or their party's leader. Read-only.
17051
+
17052
+ - `'joined'` — the player was seated in that person's room, as a member
17053
+ sharing their `groupId`.
17054
+ - `'unavailable'` — that room could not be joined (no seat, a match that
17055
+ admits nobody late, another mode), so the quick-join went on as it
17056
+ always does. No reason is given.
17057
+ - `null` — no quick-join has asked: the Arcade did not open the game to
17058
+ play with anyone, or nothing has quick-joined yet.
17059
+
17060
+ Your game makes no call for this, names nobody and is told nobody's name:
17061
+ the Arcade adds a launch key of its own, an opaque ticket, and every
17062
+ `quickJoin` passes it on. Nothing else
17063
+ changes — the room is the one `quickJoin` answers with, `joined` fires as
17064
+ for any quick-join, and no `seated` follows, because your own call made
17065
+ the seat. Use it for a line of text ("You joined your friend's game") or
17066
+ to skip a mode picker, never to decide anything of value.
17067
+
16958
17068
  <a id="roommanager-on"></a>
16959
17069
 
16960
17070
  ## RoomManager.on
@@ -17461,6 +17571,23 @@ Typed as an open string because the platform may add states. Match the ones
17461
17571
  you handle and treat anything else as unknown rather than switching
17462
17572
  exhaustively.
17463
17573
 
17574
+ <a id="playwithoutcome"></a>
17575
+
17576
+ ## PlayWithOutcome
17577
+
17578
+ ```ts
17579
+ type PlayWithOutcome = "joined" | "unavailable"
17580
+ ```
17581
+
17582
+ What a quick-join did when the Arcade opened your game to play with someone
17583
+ — see `gem.rooms.playWith`.
17584
+
17585
+ - `joined` — the player was seated in the room of the friend or party mate
17586
+ the Arcade opened the game to play with.
17587
+ - `unavailable` — that room could not be joined (no seat, a match that
17588
+ admits nobody late, another mode), so the quick-join went on as it always
17589
+ does. No reason is given, by design.
17590
+
17464
17591
  <a id="matchconnectionstate"></a>
17465
17592
 
17466
17593
  ## MatchConnectionState
@@ -1,6 +1,6 @@
1
1
  # The server surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v13.3.3, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v13.4.1, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
4
4
 
5
5
  `@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/server` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
6
 
@@ -2,7 +2,7 @@
2
2
  "schema_version": 2,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "13.3.3"
5
+ "version": "13.4.1"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -430,7 +430,7 @@
430
430
  "group": "config",
431
431
  "signature": "item(idOrSlug: string): SnapshotItem | null",
432
432
  "documented": true,
433
- "summary": "One item definition, by id or by slug — either works.\n\n```js\nconst config = await gem.gameConfig.load();\nconst sword = config?.item('iron-sword');\n```\n\n`null` means your config does not define it. That is a different question\nfrom whether the player holds one, which is on [EntityScope](api-reference-game.md#entityscope).\n\nIndexed rather than scanned, so a bag render asking for forty definitions\nin a frame stays cheap.",
433
+ "summary": "One item definition, by id or by slug — either works.\n\n```js\nconst config = await gem.gameConfig.load();\nconst sword = config?.item('iron-sword');\n```\n\n`null` means your config does not define it. That is a different question\nfrom whether the player holds one, which is on [EntityScope](api-reference-game.md#entityscope).\n\nIndexed rather than scanned, so a bag render asking for forty definitions\nin a frame stays cheap.\n\nIts `icon_url` is the `icon:` file you uploaded with your config, hosted\nby the platform — the picture the platform's purchase sheet shows. To show\nit in your own shop, pass it through [platformIconUrl](api-reference-game.md#platformiconurl) and draw your\nplaceholder on `undefined`, which is what a development backend answers:\n\n```js\nconst src = platformIconUrl(config.item('iron-sword')?.icon_url ?? undefined);\n```",
434
434
  "deprecated": null,
435
435
  "params": [
436
436
  {
@@ -8678,7 +8678,7 @@
8678
8678
  "group": "start",
8679
8679
  "signature": "readonly onViewport?: (viewport: GemViewport) => void",
8680
8680
  "documented": true,
8681
- "summary": "Which part of your game's own box is unsafe to draw essential UI in arrived,\nor changed — the display cutout, the home indicator, the arcade's own\ncontrols beside your frame.\n\n**Most games need no handler.** The SDK has already written the same four\nnumbers to `--gem-safe-top`, `--gem-safe-right`, `--gem-safe-bottom` and\n`--gem-safe-left` on your document's root before this fires, so a\nstylesheet honours them with `var(--gem-safe-top, env(safe-area-inset-top, 0px))`\nand no code — and the rectangles the arcade's controls cover to\n`--gem-reserved-tl-width`, `--gem-reserved-tl-height` and the same pair for\n`tr`, `bl` and `br`. Handle it only for a HUD you position from script — a\ncanvas you draw into, an engine's own UI layer.",
8681
+ "summary": "Which part of your game's own box is unsafe to draw essential UI in arrived,\nor changed — the display cutout, the home indicator, the arcade's own\ncontrols beside your frame.\n\n**Most games need no handler.** The SDK has already written the same four\nnumbers to `--gem-safe-top`, `--gem-safe-right`, `--gem-safe-bottom` and\n`--gem-safe-left` on your document's root before this fires, so a\nstylesheet honours them with `var(--gem-safe-top, env(safe-area-inset-top, 0px))`\nand no code — and the rectangles the arcade's controls cover to\n`--gem-reserved-tl-width`, `--gem-reserved-tl-height` and the same pair for\n`tr`, `bl` and `br`, with each corner's control centre in\n`--gem-reserved-tl-center-y` and its height in\n`--gem-reserved-tl-control-height`. Handle it only for a HUD you position from script — a\ncanvas you draw into, an engine's own UI layer.",
8682
8682
  "deprecated": null,
8683
8683
  "params": [],
8684
8684
  "returns": null,
@@ -9556,7 +9556,7 @@
9556
9556
  "group": "player",
9557
9557
  "signature": "readonly reserved: readonly GemReservedArea[]",
9558
9558
  "documented": true,
9559
- "summary": "Rectangles of your frame the arcade's own controls cover — in the Gem\nArcade, its Gem button in whichever corner it sits, and the frame-rate readout\nbeside it while the player shows it; an embedder that draws wallets over\nyour game adds theirs (see `setWalletVisible`). Keep essential UI and touch\ntargets out of them. Empty when the arcade covers nothing.\n\nA stylesheet reads the per-corner custom properties the SDK stamps from\nthese instead — `--gem-reserved-tl-height` and friends.",
9559
+ "summary": "Rectangles of your frame the arcade's own controls cover — in the Gem\nArcade, its Gem button in whichever corner it sits, and the frame-rate readout\nbeside it while the player shows it; an embedder that draws wallets over\nyour game adds theirs (see `setWalletVisible`). Keep essential UI and touch\ntargets out of them. Empty when the arcade covers nothing.\n\nEach is a keep-out area, grown out to the frame edges it sits nearest — not\nthe control's own box, and not something to line anything up with. Where\nthe arcade says, `control` on each is the control itself.\n\nA stylesheet reads the per-corner custom properties the SDK stamps from\nthese instead — `--gem-reserved-tl-height` and friends, and\n`--gem-reserved-tl-center-y` to line a top bar up with the Gem.",
9560
9560
  "deprecated": null,
9561
9561
  "params": [],
9562
9562
  "returns": null,
@@ -9645,6 +9645,102 @@
9645
9645
  "defaultValue": null,
9646
9646
  "guides": []
9647
9647
  },
9648
+ {
9649
+ "id": "GemReservedArea.control",
9650
+ "name": "control",
9651
+ "anchor": "gemreservedarea-control",
9652
+ "group": "player",
9653
+ "signature": "readonly control?: GemControlBox",
9654
+ "documented": true,
9655
+ "summary": "The box of the control this area keeps you out of, as it is drawn — the\nGem button itself, not grown to the edges — in the same space. Line your\nown controls up with this, never with the area: on a notched phone the\nGem's area runs up to the top edge, so its middle sits well above the\nbutton's. Absent under an embedder that does not say, which leaves you the\nkeep-out area alone.",
9656
+ "deprecated": null,
9657
+ "params": [],
9658
+ "returns": null,
9659
+ "throws": null,
9660
+ "example": null,
9661
+ "defaultValue": null,
9662
+ "guides": []
9663
+ },
9664
+ {
9665
+ "id": "GemControlBox",
9666
+ "name": "GemControlBox",
9667
+ "anchor": "gemcontrolbox",
9668
+ "group": "player",
9669
+ "signature": "interface GemControlBox",
9670
+ "documented": true,
9671
+ "summary": "The box of one of the arcade's controls over your frame, in the same CSS\npixels and space as `GemReservedArea`.",
9672
+ "deprecated": null,
9673
+ "params": [],
9674
+ "returns": null,
9675
+ "throws": null,
9676
+ "example": null,
9677
+ "defaultValue": null,
9678
+ "guides": []
9679
+ },
9680
+ {
9681
+ "id": "GemControlBox.x",
9682
+ "name": "x",
9683
+ "anchor": "gemcontrolbox-x",
9684
+ "group": "player",
9685
+ "signature": "readonly x: number",
9686
+ "documented": true,
9687
+ "summary": "From your frame's left edge to the control's left edge.",
9688
+ "deprecated": null,
9689
+ "params": [],
9690
+ "returns": null,
9691
+ "throws": null,
9692
+ "example": null,
9693
+ "defaultValue": null,
9694
+ "guides": []
9695
+ },
9696
+ {
9697
+ "id": "GemControlBox.y",
9698
+ "name": "y",
9699
+ "anchor": "gemcontrolbox-y",
9700
+ "group": "player",
9701
+ "signature": "readonly y: number",
9702
+ "documented": true,
9703
+ "summary": "From your frame's top edge to the control's top edge.",
9704
+ "deprecated": null,
9705
+ "params": [],
9706
+ "returns": null,
9707
+ "throws": null,
9708
+ "example": null,
9709
+ "defaultValue": null,
9710
+ "guides": []
9711
+ },
9712
+ {
9713
+ "id": "GemControlBox.width",
9714
+ "name": "width",
9715
+ "anchor": "gemcontrolbox-width",
9716
+ "group": "player",
9717
+ "signature": "readonly width: number",
9718
+ "documented": true,
9719
+ "summary": "The control's width.",
9720
+ "deprecated": null,
9721
+ "params": [],
9722
+ "returns": null,
9723
+ "throws": null,
9724
+ "example": null,
9725
+ "defaultValue": null,
9726
+ "guides": []
9727
+ },
9728
+ {
9729
+ "id": "GemControlBox.height",
9730
+ "name": "height",
9731
+ "anchor": "gemcontrolbox-height",
9732
+ "group": "player",
9733
+ "signature": "readonly height: number",
9734
+ "documented": true,
9735
+ "summary": "The control's height.",
9736
+ "deprecated": null,
9737
+ "params": [],
9738
+ "returns": null,
9739
+ "throws": null,
9740
+ "example": null,
9741
+ "defaultValue": null,
9742
+ "guides": []
9743
+ },
9648
9744
  {
9649
9745
  "id": "WalletVisibleOptions",
9650
9746
  "name": "WalletVisibleOptions",
@@ -18878,6 +18974,22 @@
18878
18974
  "defaultValue": null,
18879
18975
  "guides": []
18880
18976
  },
18977
+ {
18978
+ "id": "RoomManager.playWith",
18979
+ "name": "playWith",
18980
+ "anchor": "roommanager-playwith",
18981
+ "group": "rooms",
18982
+ "signature": "readonly playWith: PlayWithOutcome | null",
18983
+ "documented": true,
18984
+ "summary": "What the last [RoomManager.quickJoin](api-reference-game.md#roommanager-quickjoin) did when the Arcade opened\nyour game to play with someone — a friend whose Join the player pressed,\nor their party's leader. Read-only.\n\n- `'joined'` — the player was seated in that person's room, as a member\n sharing their `groupId`.\n- `'unavailable'` — that room could not be joined (no seat, a match that\n admits nobody late, another mode), so the quick-join went on as it\n always does. No reason is given.\n- `null` — no quick-join has asked: the Arcade did not open the game to\n play with anyone, or nothing has quick-joined yet.\n\nYour game makes no call for this, names nobody and is told nobody's name:\nthe Arcade adds a launch key of its own, an opaque ticket, and every\n`quickJoin` passes it on. Nothing else\nchanges — the room is the one `quickJoin` answers with, `joined` fires as\nfor any quick-join, and no `seated` follows, because your own call made\nthe seat. Use it for a line of text (\"You joined your friend's game\") or\nto skip a mode picker, never to decide anything of value.",
18985
+ "deprecated": null,
18986
+ "params": [],
18987
+ "returns": null,
18988
+ "throws": null,
18989
+ "example": null,
18990
+ "defaultValue": null,
18991
+ "guides": []
18992
+ },
18881
18993
  {
18882
18994
  "id": "RoomManager.on",
18883
18995
  "name": "on",
@@ -19474,6 +19586,22 @@
19474
19586
  "defaultValue": null,
19475
19587
  "guides": []
19476
19588
  },
19589
+ {
19590
+ "id": "PlayWithOutcome",
19591
+ "name": "PlayWithOutcome",
19592
+ "anchor": "playwithoutcome",
19593
+ "group": "rooms",
19594
+ "signature": "type PlayWithOutcome = \"joined\" | \"unavailable\"",
19595
+ "documented": true,
19596
+ "summary": "What a quick-join did when the Arcade opened your game to play with someone\n— see `gem.rooms.playWith`.\n\n- `joined` — the player was seated in the room of the friend or party mate\n the Arcade opened the game to play with.\n- `unavailable` — that room could not be joined (no seat, a match that\n admits nobody late, another mode), so the quick-join went on as it always\n does. No reason is given, by design.",
19597
+ "deprecated": null,
19598
+ "params": [],
19599
+ "returns": null,
19600
+ "throws": null,
19601
+ "example": null,
19602
+ "defaultValue": null,
19603
+ "guides": []
19604
+ },
19477
19605
  {
19478
19606
  "id": "MatchConnectionState",
19479
19607
  "name": "MatchConnectionState",
package/docs/changelog.md CHANGED
@@ -16,6 +16,26 @@ 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
+ ## 13.4.1
20
+
21
+ Upgrading from 13.4.0. Impact: none.
22
+
23
+ Nothing in this release asks anything of a game.
24
+
25
+ ## 13.4.0
26
+
27
+ Upgrading from 13.3.3. Impact: required.
28
+
29
+ ### Rebuild
30
+
31
+ - The reserved areas accepted by GemHost.sendViewport and the reserved list on GemViewport now allow an optional control box on each area. The field is additive and optional, so existing code keeps working. Recompile your game; no code change is needed unless you want to use the new field.
32
+
33
+ ### New capability
34
+
35
+ - Each reserved area can now carry the box of the control inside it, with x, y, width and height. Reserved areas are keep-out zones that extend to the frame edges, so their middle is not the middle of the control. When you want to align your own interface with the Arcade control, for example to centre a top bar on it, read the control box from the area instead of deriving it from the keep-out zone. The field is absent for an area with no control, so check for it first.
36
+ - The room manager now exposes a read-only playWith value. When the Arcade opens your game to play with a friend or party member, a quick-join may land the player in that person's room. The value tells your game whether the player joined somebody or no such room was available. It is null when the game was not opened this way, and it never says who the other player is. Use it to adjust your lobby or welcome messaging.
37
+ - Catalog recipes now include an icon_url field with the platform-hosted icon. The Arcade purchase sheet already draws this icon, and your game can show the same icon in its own shop or inventory screens instead of shipping a separate copy.
38
+
19
39
  ## 13.3.3
20
40
 
21
41
  Upgrading from 13.3.2. Impact: none.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "13.3.3"
53
+ "@indiegems/gem-web-sdk": "13.4.1"
54
54
  }
55
55
  ```
56
56
 
@@ -347,13 +347,18 @@ const jarIndex = Number.isInteger(n) && n >= 1 && n <= JAR_COUNT ? n : null;
347
347
 
348
348
  That is the whole contract for your own keys. The bridge carries no launch message, and the SDK
349
349
  has no API for them: the arcade does the forwarding, so a build you published on any SDK version
350
- gets it without a code change. The one key that is not yours is the platform's `gemChallenge`
351
- (below), which belongs to [friend challenges](friends.md#friend-challenges): never define it, and
352
- never parse it yourself. The SDK reads it for you — `gem.friends.launchedChallenge()` answers the
353
- challenge it names, or `null` — and that is the only launch key it will ever read. It is read once,
354
- when `createGem` runs, so cleaning your URL afterwards does not lose it. Call `createGem` before
355
- you clean the URL, or leave `gemChallenge` in place: a key removed before `createGem` runs is never
356
- seen, and `launchedChallenge()` then resolves `null` with no error.
350
+ gets it without a code change. Two keys are not yours, and they are the platform's (below):
351
+ `gemChallenge`, which belongs to [friend challenges](friends.md#friend-challenges), and
352
+ `gemPlayWith`, an opaque ticket the arcade adds when it opens your game to play with a friend or a
353
+ party mate, which names nobody your game can recognise. Never define either, and never parse
354
+ either yourself. The SDK reads them for you —
355
+ `gem.friends.launchedChallenge()` answers the challenge `gemChallenge` names, or `null`, and every
356
+ `gem.rooms.quickJoin()` passes `gemPlayWith` on
357
+ ([Landing in a friend's room](rooms-and-matches.md#5e-landing-in-a-friends-room-play-with)) — and
358
+ they are the only launch keys it reads. Each is read once, when `createGem` runs, so cleaning your
359
+ URL afterwards does not lose it. Call `createGem` before you clean the URL, or leave both keys in
360
+ place: a key removed before `createGem` runs is never seen, so `launchedChallenge()` resolves
361
+ `null` with no error and a quick-join plays with nobody.
357
362
 
358
363
  What reaches the frame:
359
364
 
@@ -375,11 +380,12 @@ What reaches the frame:
375
380
  A key starting with `_` can never be one of yours: the key rule below requires a leading
376
381
  letter, so the arcade keeps that space for itself.
377
382
 
378
- - **One platform key.** `gemChallenge` does reach the frame — the arcade adds it when a player
379
- opens a friend challenge — but it is the platform's, not yours: read the challenge with
380
- `gem.friends.launchedChallenge()`, never from the URL. Do not define a key of that name, and
381
- treat every key beginning with `gem` as reserved: a friend-challenge template refuses a param
382
- named that way.
383
+ - **Two platform keys.** `gemChallenge` and `gemPlayWith` do reach the frame — the arcade adds the
384
+ first when a player opens a friend challenge, and the second when it opens your game to play
385
+ with someone — but they are the platform's, not yours: read the challenge with
386
+ `gem.friends.launchedChallenge()`, let `gem.rooms.quickJoin()` carry `gemPlayWith`, and never
387
+ read either from the URL. Do not define a key of either name, and treat every key beginning
388
+ with `gem` as reserved: a friend-challenge template refuses a param named that way.
383
389
  - **Bounded.** At most 16 parameters. A key is a letter followed by up to 63 letters, digits,
384
390
  `_` or `-`. A value is at most 512 characters with no control or format characters. An entry
385
391
  that breaks a rule is dropped on its own; the rest of the link still arrives.
@@ -696,6 +696,27 @@ game `busy` for a short, growing interval — do not re-ask on `cancelled`.
696
696
  Hide this specific affordance when `gem.frame.parentSupports('purchases')` is false. That check does
697
697
  not apply to the direct SDK path.
698
698
 
699
+ The sheet shows the item's or recipe's picture from its `icon_url`. You never write that field: it
700
+ is the `icon:` file you upload with your config (`icon: icons/extra-life.png` in the YAML, uploaded
701
+ by `gemctl config upload`), which the platform hosts and fills in. A recipe with no `icon:` of its
702
+ own shows the icon of the one item it grants. With neither, the sheet shows its default picture.
703
+
704
+ Your own shop can show the same picture. Pass the URL through `platformIconUrl` first, from the
705
+ package you installed in [Getting started](getting-started.md), and draw your own placeholder when
706
+ that answers `undefined`:
707
+
708
+ ```js
709
+ import { platformIconUrl } from '@indiegems/gem-web-sdk/sdk';
710
+
711
+ const src = platformIconUrl(config.item('extra-life')?.icon_url ?? undefined);
712
+ if (src) img.src = src;
713
+ else drawPlaceholder();
714
+ ```
715
+
716
+ A recipe's icon is the `icon_url` on the recipe in `config.catalogs[].recipes`; read it the same
717
+ way. `platformIconUrl` passes only the platform's production image host, so against a development
718
+ backend it answers `undefined` by design: there, your placeholder is what you see.
719
+
699
720
  #### Game-owned direct purchase
700
721
 
701
722
  Use the direct path when your game provides its own purchase UI and does not want the platform sheet:
@@ -13,8 +13,8 @@
13
13
  },
14
14
  {
15
15
  "file": "analytics.md",
16
- "sha256": "10d8155b80d2e8233ef9b489a4ad20a46e89e01447ebbc23acc86aaeb2dbc73e",
17
- "updated": "2026-10-16T18:11:54Z"
16
+ "sha256": "94baa26268843ba926462633ddec8aaca56aabf79ee6639aaf77df68474debc7",
17
+ "updated": "2026-10-17T00:33:31Z"
18
18
  },
19
19
  {
20
20
  "file": "arcade-avatars.md",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  {
40
40
  "file": "changelog.md",
41
- "sha256": "854389e510b125afc6fdd075d19db655388330faa676a82a888a1447b9973e2b",
42
- "updated": "2026-10-16T20:36:38Z"
41
+ "sha256": "68726bcd339a18e0fc6b5b9fdfe5ca3666e1c1fb79293a64c361e09616c87a85",
42
+ "updated": "2026-10-18T17:04:31Z"
43
43
  },
44
44
  {
45
45
  "file": "daily-challenges.md",
@@ -68,13 +68,13 @@
68
68
  },
69
69
  {
70
70
  "file": "getting-started.md",
71
- "sha256": "32cc41ca951169e902f39493d9d912ca55d67f906e83066217fb38b62680ab1e",
72
- "updated": "2026-10-16T20:36:38Z"
71
+ "sha256": "5f950537b4a4d1f952adbc294fdba8bdd05ebaf6d8a924951129ff604b31f3d8",
72
+ "updated": "2026-10-18T17:04:31Z"
73
73
  },
74
74
  {
75
75
  "file": "inventory-and-catalogs.md",
76
- "sha256": "4251f451c48ab80bb7a9c50a6a717dfca6e057ff128d32ecb5e2cd13d28c5b7d",
77
- "updated": "2026-10-14T21:18:16Z"
76
+ "sha256": "fc2dbf113d2602fff90d03fc24e03d468c7b4ab3633844ed9d0df6b985e69385",
77
+ "updated": "2026-10-18T00:39:23Z"
78
78
  },
79
79
  {
80
80
  "file": "leaderboards.md",
@@ -88,8 +88,8 @@
88
88
  },
89
89
  {
90
90
  "file": "p2p-and-parties.md",
91
- "sha256": "3887bc706d098014cefab84e9b8bd5b964841888c242a7ccb1f799a9b9723fc0",
92
- "updated": "2026-09-24T01:00:22Z"
91
+ "sha256": "4cfe2a47def51eb617f154eaac39faf176d8aa7ca2c36907084b6349048ab1b9",
92
+ "updated": "2026-10-17T22:11:15Z"
93
93
  },
94
94
  {
95
95
  "file": "platform-moments.md",
@@ -108,8 +108,8 @@
108
108
  },
109
109
  {
110
110
  "file": "rewarded-ads.md",
111
- "sha256": "a99363a8eb6e8e5978c24a283e2a606efce1308b05039b9fb82ec06f39a3213c",
112
- "updated": "2026-10-16T19:17:27Z"
111
+ "sha256": "a111ae68fb0d443f8c183c7a963668078a6d155124cf19da45391218c0716e1f",
112
+ "updated": "2026-10-17T17:23:21Z"
113
113
  },
114
114
  {
115
115
  "file": "room-shapes.md",
@@ -118,8 +118,8 @@
118
118
  },
119
119
  {
120
120
  "file": "rooms-and-matches.md",
121
- "sha256": "29cf42a4334d3d9702e6a7f6e04c52fb2b414af3340043c2f98b2bfafa7dc64d",
122
- "updated": "2026-10-12T19:59:08Z"
121
+ "sha256": "8f5879028034a75dfd14dc6afa71d96e1c3203ba1969f00b39c2dad043818963",
122
+ "updated": "2026-10-17T22:11:15Z"
123
123
  },
124
124
  {
125
125
  "file": "saving-player-progress.md",
@@ -128,8 +128,8 @@
128
128
  },
129
129
  {
130
130
  "file": "server-bundles.md",
131
- "sha256": "a53c08888a4eff98e84d2876f461c119125fd44a74a6f4f787ba910920bf59fc",
132
- "updated": "2026-10-16T20:36:38Z"
131
+ "sha256": "056d725c06e61dc6ebdda3fe082809a416ce7f87f2e84879bc0971c6065ba4f9",
132
+ "updated": "2026-10-18T17:04:31Z"
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": "e97ca7a5b01a40d57900eb1116e961768f62bccfd412afd02bca245cdb7beeab",
157
- "updated": "2026-10-15T20:36:02Z"
156
+ "sha256": "97f97817fb087dbe2c637b4ea7f22f1ab624496bd32e427e4619c211c65d4f21",
157
+ "updated": "2026-10-18T15:05:29Z"
158
158
  }
159
159
  ]
160
160
  }
@@ -568,9 +568,10 @@ one this SDK calls. A v2 member row carries `group_id`; no party id appears
568
568
  anywhere in the response, at any level — not even the requester's own.
569
569
 
570
570
  `RoomMember.groupId` is that value, read off the wire. Two members sharing it
571
- arrived together; a member with no value arrived alone. It is opaque: draw a badge
572
- with it, group a roster by it, compare two members' values — but do not parse it,
573
- key anything off it, or keep it once the room is gone.
571
+ arrived together — as a party, or one coming to play with the other (§7.5 and
572
+ `rooms-and-matches.md` §5e); a member with no value arrived alone. It is opaque:
573
+ draw a badge with it, group a roster by it, compare two members' values — but do
574
+ not parse it, key anything off it, or keep it once the room is gone.
574
575
 
575
576
  The stability across rooms is precisely what was removed: the marker is scoped to
576
577
  one room, so the same party in two rooms carries two unrelated values. The useful
@@ -708,6 +709,34 @@ cleared, and the SDK raises a refusal that names the party cause — because a g
708
709
  that just ran the documented recovery has every reason to think the room is gone.
709
710
  Adopting is the answer there too: the room to be in is the one the party is in.
710
711
 
712
+ ### 7.5 A member who opens the game after the leader: play-with
713
+
714
+ A member who was not in your game when their leader started it — browsing the
715
+ Arcade, or in another game — is not left to find it. The Arcade shows them a
716
+ prompt, and its Join opens your game **to play with the leader**: the launch key
717
+ `gemPlayWith` that `rooms-and-matches.md` §5e describes, an opaque ticket that
718
+ names nobody your game can recognise. Your game makes no call for it. Its own
719
+ `quickJoin()` carries the ticket, and the platform seats the member in the
720
+ leader's room when it has a seat, because the two are in one party.
721
+
722
+ What the member's game sees is an ordinary quick-join:
723
+
724
+ - **The room it asked for answers, and `gem.rooms.playWith` reads `'joined'`.**
725
+ `'unavailable'` means the leader's room could not be joined — full, of another
726
+ mode, or in a match that admits nobody late — and the quick-join went on as
727
+ usual.
728
+ - **No `seated` fires for it**, because the member's own call made the seat.
729
+ Keep the `seated` handler (§7.4) all the same: it is still how a member who
730
+ was already in your game when the leader started is brought in.
731
+ - **The member shares the party's `groupId`**, so a roster that groups the party
732
+ shows them with it.
733
+ - **It holds its own seat.** A member seated by the leader's call leaves only
734
+ with the group (§7.4); one who came in this way arrived by their own call, so
735
+ they can leave alone, and the leader taking the party out does not take them.
736
+
737
+ The leader's game does nothing new either. Its quick-join is the party's room
738
+ call, as before, and plays with nobody else.
739
+
711
740
  ---
712
741
 
713
742
  ## 8. Capabilities