@indiegems/gem-web-sdk 8.1.0 → 9.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.
@@ -1,12 +1,14 @@
1
- # Room shapes — the four things a game room is for
1
+ # Room shapes — the three things a game room is for
2
2
 
3
- A game room is one mechanism with four useful shapes. The same browser surface (`gem.rooms` to get
4
- in, `gem.match.connect()` to reach the authority) and the same server entry point (`defineServer`)
5
- serve all four; what differs is the room template and what the hosting server chooses to do with
6
- its time.
3
+ A game room is one mechanism with three useful shapes. The same browser surface (`gem.rooms` to
4
+ get in, `gem.match.connect()` to reach the authority) and the same server entry point
5
+ (`defineServer`) serve all three; what differs is the room template and what the hosting server
6
+ chooses to do with its time.
7
7
 
8
- Only three of the four need a second player. The fourth is for a game that has no multiplayer at
9
- all and still needs something a browser cannot be trusted to do.
8
+ A room is for players who share something live. A game with no real-time match that still needs
9
+ something a browser cannot be trusted to do — a run verified, a score checked before it counts, a
10
+ reward granted — does not need a room or a game server. That is a serverless function:
11
+ [serverless-functions.md](serverless-functions.md).
10
12
 
11
13
  This page is the decision. [rooms-and-matches.md](rooms-and-matches.md) is the mechanism behind
12
14
  it — the room state machine, the event stream, room args, reconnection — and is what to read once
@@ -16,14 +18,12 @@ you know which shape you want.
16
18
 
17
19
  Answer in this order. The first yes wins.
18
20
 
19
- 1. **Does the server need to run a match at all**, or only answer requests from clients who are
20
- not playing each other? Only answer requests → **authoritative worker**.
21
- 2. **Does one player decide when a round starts, and with what settings?** → **custom**.
22
- 3. **Do players arrive and leave continuously, with the world outliving any one of them?** →
21
+ 1. **Does one player decide when a round starts, and with what settings?** → **custom**.
22
+ 2. **Do players arrive and leave continuously, with the world outliving any one of them?** →
23
23
  **hub**.
24
- 4. Otherwise a lobby fills and plays one match → **match**.
24
+ 3. Otherwise a lobby fills and plays one match → **match**.
25
25
 
26
- ## The four
26
+ ## The three
27
27
 
28
28
  ### Match
29
29
 
@@ -53,31 +53,6 @@ A hub is **served** or **unserved**, and `match_type` decides which:
53
53
  Reach for `none` when nothing needs simulating. A lobby that needs a member list and a settings
54
54
  object does not need a server, and a served hub costs one.
55
55
 
56
- ### Authoritative worker
57
-
58
- A room whose server **runs no match**. It accepts no player input, replicates no world state,
59
- simulates nothing. It answers requests from members who are not playing each other, and each
60
- member's session is unrelated to every other member's.
61
-
62
- This is how a browser game reaches an execution context a modified client cannot rewrite. Use it
63
- when something must be decided on your behalf rather than the player's, and the platform has no
64
- built-in rule for it:
65
-
66
- - Confirming an offline, single-player run really was finished, and to the score claimed.
67
- - Granting an award whose condition is game state — level reached, boss defeated, run completed
68
- under a time — that only your game code can evaluate.
69
- - Checking a score before it counts.
70
-
71
- **A game with no multiplayer can need this shape.** The room is not a lobby; it is a way to get
72
- your own code running somewhere the player does not control.
73
-
74
- If a check out of the player's reach is all you want the room for, a serverless function is the
75
- smaller tool: no room, no seat and no connection, one call at a time. See
76
- [serverless-functions.md](serverless-functions.md).
77
-
78
- Two things decide whether you need it at all. See
79
- [what a worker owes you, and what it does not](#what-a-worker-owes-you-and-what-it-does-not).
80
-
81
56
  ### Custom
82
57
 
83
58
  A room one player owns, where a group gathers and plays round after round. The owner picks the
@@ -91,16 +66,16 @@ from a list hard-coded in your client — then the config can add a mode without
91
66
 
92
67
  ## The settings that produce each
93
68
 
94
- | | Match | Hub | Worker | Custom |
95
- | ------------------------ | ----------------- | --------------------- | ----------- | --------------------- |
96
- | `match_type` | `dedicated`/`p2p` | `dedicated` or `none` | `dedicated` | `dedicated`/`p2p` |
97
- | `auto_start_host` | `true` | `true` | `true` | **`false`** |
98
- | `min_players` | the match size | `1` | `1` | `1` |
99
- | `allow_join_in_progress` | for backfill | **`true`** | **`true`** | `false` |
100
- | `show_in_browser` | `false` | `true` | `false` | when public |
101
- | `transports` | the ladder | the ladder | the ladder | the ladder |
102
- | How a player gets in | `quickJoin` | `quickJoin` | `quickJoin` | `create`/`joinByCode` |
103
- | What starts the match | `min_players` | `min_players` | first join | the owner |
69
+ | | Match | Hub | Custom |
70
+ | ------------------------ | ----------------- | --------------------- | --------------------- |
71
+ | `match_type` | `dedicated`/`p2p` | `dedicated` or `none` | `dedicated`/`p2p` |
72
+ | `auto_start_host` | `true` | `true` | **`false`** |
73
+ | `min_players` | the match size | `1` | `1` |
74
+ | `allow_join_in_progress` | for backfill | **`true`** | `false` |
75
+ | `show_in_browser` | `false` | `true` | when public |
76
+ | `transports` | the ladder | the ladder | the ladder |
77
+ | How a player gets in | `quickJoin` | `quickJoin` | `create`/`joinByCode` |
78
+ | What starts the match | `min_players` | `min_players` | the owner |
104
79
 
105
80
  Templates live in your game config under `game_room_templates`, and are uploaded and promoted per
106
81
  channel like the rest of it. `transports` is the same on every shape because it is orthogonal to
@@ -119,8 +94,7 @@ quietly becomes one world per player.
119
94
 
120
95
  ## What the platform decides for you
121
96
 
122
- Four limits bind every shape, and three of the four shapes exist partly to work around one of
123
- them.
97
+ Four limits bind every shape.
124
98
 
125
99
  **Seats belong to the template, not to the room.** A create request carrying its own
126
100
  `max_players` is refused rather than having the field ignored. The template's count is not
@@ -130,7 +104,7 @@ from what one process can carry.
130
104
 
131
105
  **A match has a maximum duration.** `max_match_duration_seconds` defaults to 21600 — six hours —
132
106
  after which the platform ends the match the way any end happens: members get the ordinary
133
- `match_ended` push and the server is released. **Nothing here runs forever.** A hub or a worker is
107
+ `match_ended` push and the server is released. **Nothing here runs forever.** A hub is
134
108
  recycled, not permanent, and has to be written that way: durable state belongs in player save
135
109
  data or your own store, written as it changes rather than at shutdown, and the client re-enters
136
110
  on the room's `matchEnded` event (reason `expired`, `roomReopened: true`) rather than treating it
@@ -150,37 +124,39 @@ server per player.
150
124
  behind a shared fence, so an `auto_start_host` template and a client that also calls start cannot
151
125
  race into two matches.
152
126
 
153
- ## What a worker owes you, and what it does not
154
-
155
- The worker shape is the one most easily got wrong, in both directions.
156
-
157
- **It is not the answer to economy arithmetic.** Spending a currency and granting an item is a
158
- **recipe**, and a recipe is already evaluated on the platform's side where a modified client
159
- cannot change the arithmetic — no server of yours, no allocation, no seat. See
160
- [inventory-and-catalogs.md](inventory-and-catalogs.md). A worker room is for validating game state
161
- the platform knows nothing about. A worker that grants _after_ validating is doing both: decide
162
- here, then let the grant itself be a recipe or an order.
127
+ ## What a server in a room owes you
163
128
 
164
129
  **Its server must check its own roster.** A server bundle's credential is scoped to your game and
165
130
  channel, not to its match, and the room roster is not a boundary the platform enforces on it. A
166
131
  bundle can therefore act on any player of your game, including one who never joined its room.
167
- That is needed by an ordinary match server writing results — and it means a worker that skips its
168
- own membership check is an open grant endpoint wearing a room as decoration. Read the roster and
169
- check the requesting player against it before acting; not against an id the client sent.
170
-
171
- **A result against a daily's board is checked against the platform's copy, never the client's.**
172
- When your studio stores each period's content with the challenge, the worker reads it with
173
- `gem.challenges.list({ playerId })` — for the player who submitted, whose period is their own, for
174
- the current period and, with `periodsBack: 1`, the one before, so a run finished just before the
175
- rollover still verifies — and validates against the entry whose `periodLocalDate` (a cron
176
- challenge's `seed`) and `dataHash` match what the client submitted, never its `periodStartAt`. No
177
- match is a rejection. [daily-challenges.md](daily-challenges.md#verifying-a-result-against-the-content) has
132
+ That is needed by an ordinary match server writing results — and it means a server that acts
133
+ because a client asked it to must read the roster and check the requesting player against it
134
+ first; not against an id the client sent.
135
+
136
+ **It should hold no player state between joins.** Each join is a session, not an identity. A
137
+ returning player may be in a completely different state than when they left, and the process may
138
+ have served hundreds of others in between.
139
+
140
+ ## When you do not need a room
141
+
142
+ A room, and the server behind it, is for a real-time match: the server is the authority, corrects
143
+ what players do, and replicates the state of the game to everyone in it. Such a server can also
144
+ verify results and grant progression and rewards for the match it ran.
145
+
146
+ Everything else that must happen out of the player's reach is a **serverless function**:
147
+ confirming a single-player run really was finished and to the score claimed, checking a score
148
+ before it reaches a leaderboard, anti-cheat, updating progression, granting a reward whose
149
+ condition only your game code can evaluate. A function needs no room, no seat and no connection,
150
+ costs less and scales further. See [serverless-functions.md](serverless-functions.md).
151
+
152
+ Two things are not a function's job either. Spending a currency and granting an item is a
153
+ **recipe**, already evaluated on the platform's side where a modified client cannot change the
154
+ arithmetic — see [inventory-and-catalogs.md](inventory-and-catalogs.md); a function that has
155
+ decided a player earned something still makes the grant itself a recipe or an order. And a result
156
+ against a daily's board is checked against the platform's copy of that period's content, never the
157
+ client's — [daily-challenges.md](daily-challenges.md#verifying-a-result-against-the-content) has
178
158
  the recipe.
179
159
 
180
- **It should hold nothing between joins.** Each join is an unrelated session. A returning player
181
- may be in a completely different state than when they left, and the process may have served
182
- hundreds of others in between.
183
-
184
160
  ## Where to go next
185
161
 
186
162
  - [rooms-and-matches.md](rooms-and-matches.md) — the browser half in full: the room state
@@ -193,3 +169,5 @@ hundreds of others in between.
193
169
  - [p2p-and-parties.md](p2p-and-parties.md) — the shapes that run without a server of ours.
194
170
  - [inventory-and-catalogs.md](inventory-and-catalogs.md) — what a server calls once it has
195
171
  authority.
172
+ - [serverless-functions.md](serverless-functions.md) — your own check out of the player's reach,
173
+ with no room and no game server.
@@ -6,9 +6,8 @@ carrying the engine's model should transfer with no friction, and where the web
6
6
  engine this document says why.
7
7
 
8
8
  > **Which SHAPE of room you want is decided first, on
9
- > [`room-shapes.md`](room-shapes.md)** — a match, a hub players drift through, a custom lobby one
10
- > player owns, or an authoritative worker whose server runs no match at all. This page is the
11
- > mechanism underneath all four.
9
+ > [`room-shapes.md`](room-shapes.md)** — a match, a hub players drift through, or a custom lobby one
10
+ > player owns. This page is the mechanism underneath all three.
12
11
 
13
12
  > **This page is the SDK's model. The platform's own rules are
14
13
  > [`platform-rules.md`](platform-rules.md)** — what gem-api enforces underneath, none of which is
@@ -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/v8.1.0/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v8.1.0/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v9.1.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v9.1.0/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -35,10 +35,11 @@ Two cases look like a function and are not:
35
35
  a dedicated server, and [server-bundles.md](server-bundles.md) is how you write one. A function
36
36
  holds nothing between calls.
37
37
 
38
- A function is also the smaller answer to a need [room-shapes.md](room-shapes.md#authoritative-worker)
39
- meets with an **authoritative worker** room. If you were going to allocate a worker room only to
40
- run your own check out of the player's reach, a function does the same job with no room, no seat
41
- and no connection to open, and each call is its own run.
38
+ A function is the answer whenever your own check has to run out of the player's reach and no
39
+ real-time match is running: verifying a run or a score, anti-cheat, updating progression, granting
40
+ a reward. It needs no room, no seat and no connection to open, and each call is its own run. A game
41
+ with no multiplayer does not need a game server for any of this.
42
+ [room-shapes.md](room-shapes.md#when-you-do-not-need-a-room) draws the line from the other side.
42
43
 
43
44
  ⚠️ **A verdict you return to the browser is advice to the client you do not trust.** If your
44
45
  function decides a player earned something and returns `{ "ok": true }`, a modified client can skip
@@ -440,18 +441,22 @@ on every request your function makes.
440
441
 
441
442
  ### The limits on one call
442
443
 
443
- These are the platform's limits, the same for every game, and a game cannot raise its own.
444
-
445
- | Limit | Value |
446
- | --------------------------------------------------- | -------------------------------------- |
447
- | Wall clock | 5 seconds |
448
- | CPU | 1 second of that 5 |
449
- | Memory | 64 MiB |
450
- | `call-api`, `download-link` and `upload-link` calls | 32, all three together |
451
- | Input | 256 KiB |
452
- | Output | 1 MiB |
453
- | Component size | 32 MiB |
454
- | Calls, per caller per minute | 30 for a player, 120 for a game server |
444
+ These are the platform's limits, the same for every game, and a game cannot raise its own. If your
445
+ game needs more memory, CPU, time or calls than they allow, ask for the limit to be raised: the
446
+ platform raises a function's limits so the game can stay on a function. Reaching one is never a
447
+ reason to move the work to a game server.
448
+
449
+ | Limit | Value |
450
+ | ------------------------------------------------------------------------------ | -------------------------------------- |
451
+ | Wall clock | 5 seconds |
452
+ | CPU | 1 second of that 5 |
453
+ | Memory | 64 MiB |
454
+ | `call-api`, `download-link` and `upload-link` calls | 32, all three together |
455
+ | A value handed to one `log`, `call-api`, `download-link` or `upload-link` call | 16 MiB |
456
+ | Input | 256 KiB |
457
+ | Output | 1 MiB |
458
+ | Component size | 32 MiB |
459
+ | Calls, per caller per minute | 30 for a player, 120 for a game server |
455
460
 
456
461
  **The per-minute budget belongs to the caller, not to your game.** A player who spends theirs is
457
462
  refused and nobody else is affected. Thirty a minute is also why a function call belongs at a
@@ -767,7 +772,9 @@ or `game_server`:
767
772
  - **A line your function logged**: its `level` and its `message`.
768
773
  - **The closing line**: `outcome` is `ok`, `error`, `timeout`, `cpu_exceeded` or `failed`, with the
769
774
  call's `wall_ms` and `cpu_ms`. On `error`, `message` is your `Err` text. On `failed`, `reason`
770
- says what ended the call, and when your own code trapped, `message` is the trap's message.
775
+ says what ended the call, and when your own code trapped, `message` is the trap's message. When
776
+ `reason` is `output_too_large` or `argument_too_large`, `detail` names the limit the call
777
+ exceeded. The platform writes it and never copies your function's data into it.
771
778
 
772
779
  Newest first means a call's closing line comes before what it logged. **A call's logging is
773
780
  capped**: once it reaches its per-call log limit, that `log` call and every later one go
@@ -795,24 +802,27 @@ Every answer from a call that reached your function carries an invocation id: su
795
802
  refused before that point never became an invocation and carries none. `err.retryable` agrees with
796
803
  the last column: it is `true` exactly where this table says to retry.
797
804
 
798
- | What you get | What happened | What to do |
799
- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
800
- | `422 function_error` | Your `handle` returned `Err`. The text is `err.functionMessage` | Fix your code or the input. Return verdicts in `Ok` |
801
- | `422 function_failed` | It did not finish. `err.failure` is `trap`, `memory_exceeded`, `cpu_budget_exceeded` or `host_call_limit` | A bug or a limit in the function. Reproduce with the same input |
802
- | `504 function_timeout` | It ran past the wall clock | Do not retry: the same input takes as long again |
803
- | `404 function_not_published` | Nothing is promoted on the caller's channel | `gemctl function promote <version> --channel <channel>` |
804
- | `413 function_input_too_large` | The input is over 256 KiB | Send a request, not a file |
805
- | `429 rate_limited` | This caller's per-minute budget is spent | Wait `err.retryAfterSeconds` |
806
- | `503 function_capacity_unavailable` | The platform was too busy to run it. Your function is not known to be broken | Retry after `err.retryAfterSeconds` |
807
- | `503 function_host_unreachable`, `503 function_host_error` | The platform could not run it. Nothing about your function | Retry shortly. Quote the invocation id if it persists |
808
- | `503 service_unavailable` | The platform could not check the caller's budget, so it refused the call. The function did not run | Retry shortly |
809
- | `503 functions_not_configured` | Functions are switched off on the backend you called | Do not retry. Nothing about your function or your game |
810
- | `403 forbidden` | The call carried an account sign-in rather than a game-scoped or game-server token | Call from your game or its server, or through `gemctl function invoke` |
805
+ | What you get | What happened | What to do |
806
+ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
807
+ | `422 function_error` | Your `handle` returned `Err`. The text is `err.functionMessage` | Fix your code or the input. Return verdicts in `Ok` |
808
+ | `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
+ | `504 function_timeout` | It ran past the wall clock | Do not retry: the same input takes as long again |
810
+ | `404 function_not_published` | Nothing is promoted on the caller's channel | `gemctl function promote <version> --channel <channel>` |
811
+ | `413 function_input_too_large` | The input is over 256 KiB | Send a request, not a file |
812
+ | `429 rate_limited` | This caller's per-minute budget is spent | Wait `err.retryAfterSeconds` |
813
+ | `503 function_capacity_unavailable` | The platform was too busy to run it. Your function is not known to be broken | Retry after `err.retryAfterSeconds` |
814
+ | `503 function_host_unreachable`, `503 function_host_error` | The platform could not run it. Nothing about your function | Retry shortly. Quote the invocation id if it persists |
815
+ | `503 service_unavailable` | The platform could not check the caller's budget, so it refused the call. The function did not run | Retry shortly |
816
+ | `503 functions_not_configured` | Functions are switched off on the backend you called | Do not retry. Nothing about your function or your game |
817
+ | `403 forbidden` | The call carried an account sign-in rather than a game-scoped or game-server token | Call from your game or its server, or through `gemctl function invoke` |
811
818
 
812
819
  A `trap` is a panic, an `unreachable`, an out-of-bounds access or a stack overflow, and no stack
813
820
  trace comes back with it. On `dev`, the call's closing line in `gemctl function logs` carries the
814
- trap's message when your own code trapped (section 7). The other three reasons are the limits in
815
- section 2.
821
+ trap's message when your own code trapped (section 7). The other five reasons are the limits in
822
+ section 2. Two of them are about size: `output_too_large` is a result over the 1 MiB output cap,
823
+ and `argument_too_large` is a value your function handed to `log`, `call-api`, `download-link` or
824
+ `upload-link` that was over the 16 MiB one call accepts. On `dev`, the closing line of either
825
+ carries `detail`, naming the limit the call exceeded.
816
826
 
817
827
  Publishing has its own refusals, and nothing is stored when one happens:
818
828
 
@@ -53,8 +53,9 @@ No `baseUrl`. The Arcade names the backend on every token and the SDK routes the
53
53
  tokens only if its origin is allowed, and the pages that matter are always allowed without a word
54
54
  from you:
55
55
 
56
- - **the Gem Arcade website and app** — the SDK carries their origins as `ARCADE_EMBEDDERS`, on every
57
- platform the app runs on;
56
+ - **the platform's websites (Gem Arcade and Meme Games) and the Gem Arcade app** — the SDK carries
57
+ their origins as `ARCADE_EMBEDDERS`, on every platform the app runs on. Your game is listed on
58
+ one of the websites, but it accepts every origin in that list;
58
59
  - **a dev host on your own machine** — `gemctl serve` and the Gem Editor frame a locally-served game
59
60
  from a loopback origin whose port changes every run, and the SDK admits that shape by rule, only
60
61
  while your game itself is served from loopback. Once published, nothing about the rule applies.
@@ -527,7 +528,10 @@ credit for it until you say so.
527
528
  inventory and dialog operable on the standard gamepad mapping with the pointer unused, and a focus
528
529
  cue thicker than a hairline — a 1px outline disappears at ten feet. TV remotes arrive as
529
530
  **keyboard-class events** (arrows, Enter, Back), so a game that calls itself a keyboard game is a
530
- D-pad game on a TV whether it planned to be or not.
531
+ D-pad game on a TV whether it planned to be or not. One control a gamepad cannot work: a
532
+ rewarded-ad button. Browsers do not count gamepad input as a press, so a rewarded ask made from
533
+ one is refused ([`rewarded-ads.md` §3](rewarded-ads.md#3-ask-from-the-press-then-re-read)). A
534
+ remote's keys do count. Keep the ordinary way on beside the button working.
531
535
  - **Title-safe layout.** HUD and critical UI inset at least ~5% of the frame — roughly 48–96px at
532
536
  1080p. `env(safe-area-inset-*)` resolves to **0 on every TV**: overscan is a hardware crop the
533
537
  browser cannot see, so no platform can hand you the number and your game must apply its own
@@ -1281,6 +1285,13 @@ rather than a failure; nothing rejects, so there is no error branch to write; an
1281
1285
  nothing, so a reward must never be handed out on the strength of it.
1282
1286
  [`cross-promotion.md`](cross-promotion.md) is the page.
1283
1287
 
1288
+ A button the player presses for a reward — a revive, an extra attempt — is the same call with
1289
+ `format: 'rewarded'`, made from that press itself. The button says it is an ad and names the
1290
+ reward, and the Arcade starts the ad as soon as the ask arrives. Your game config declares what
1291
+ each such button grants, the platform writes the reward into your game's inventory, and your game
1292
+ reads it there rather than from the answer.
1293
+ [`rewarded-ads.md`](rewarded-ads.md) is that page.
1294
+
1284
1295
  ## An account upsell, if your game has something to offer one
1285
1296
 
1286
1297
  Not on the list above either. When a player reaches for something only an account gives them,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "8.1.0",
3
+ "version": "9.1.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -3203,6 +3203,21 @@
3203
3203
  "",
3204
3204
  " RENDER IT IN YOUR OWN CHROME. Labelled as a promotion, dismissible, and",
3205
3205
  " focus-trapped over the paused game. Nothing in this message is rendered.",
3206
+ " For `rewarded`, the player pressed the game's button, and THAT PRESS IS",
3207
+ " THE OPT-IN: the game's label says it is an ad and names the reward, so",
3208
+ " start the ad without a prompt of your own. Label it as an ad for as long",
3209
+ " as it plays, and let the player close it; closing early is a skip and",
3210
+ " pays nothing. Where no ad could play and nothing was granted, say so",
3211
+ " briefly in your own chrome ('No ad available right now').",
3212
+ "",
3213
+ " DO NOT RELY ON THE FRAME HAVING CHECKED FOR A PRESS. SDK releases after",
3214
+ " 9.0.0 send a rewarded ask only while the frame holds user activation,",
3215
+ " where the browser reports it. Every SDK from 5.1.0 to 9.0.0 sends one",
3216
+ " with no such check, so a game pinned to one of those can ask from a",
3217
+ " timer and start an ad with no press, and a hostile frame posts on the",
3218
+ " port directly. Even the check cannot say which press caused an ask:",
3219
+ " activation belongs to the whole frame for a few seconds, so an ask from",
3220
+ " the game's jump button passes it too.",
3206
3221
  "",
3207
3222
  " PAUSE WITH `gem.pause`. That message already means 'stop running and stop",
3208
3223
  " making noise', which is exactly what is wanted while a panel is up. Do",
@@ -3224,10 +3239,14 @@
3224
3239
  " generated validator checks type and length only - it may carry `../`,",
3225
3240
  " NUL, newlines, RTL overrides and astral Unicode. The SDK will only SEND",
3226
3241
  " `^[a-z0-9_-]{1,32}$`, which is the grammar the platform's own ad",
3227
- " endpoint accepts - but that is the sender keeping a promise, not a",
3228
- " guarantee you hold: a hostile frame posts on the port directly. It is",
3229
- " matched against nothing, never interpolated into a path or a log line,",
3230
- " and never shown to a player.",
3242
+ " endpoints accept - but that is the sender keeping a promise, not a",
3243
+ " guarantee you hold: a hostile frame posts on the port directly. For an",
3244
+ " interstitial it is matched against nothing. For `rewarded` it is the key",
3245
+ " of the reward the game's config declares, so check it against that",
3246
+ " grammar YOURSELF before forwarding it, in a request body, to the",
3247
+ " platform's rewarded offer. Never interpolate it into a path or an",
3248
+ " unchecked log line, never show it to a player, and NEVER SEND IT TO AN",
3249
+ " AD NETWORK: it stops at the platform, as every game-authored string does.",
3231
3250
  "",
3232
3251
  " THE `perMinute` BELOW BOUNDS A BUGGY GAME, NOT A HOSTILE ONE. A hostile",
3233
3252
  " game posts on the port it holds rather than calling the SDK, so a",
@@ -3243,12 +3262,12 @@
3243
3262
  "placement": {
3244
3263
  "kind": "string",
3245
3264
  "maxLength": 32,
3246
- "$comment": "The game's own name for the break it is at - 'level-complete', 'between-rounds'. UNTRUSTED TEXT, MATCHED AGAINST NOTHING BY TODAY'S PARENT: it does not look it up, does not vary its decision on it and does not render it. THE BOUND IS THE PLATFORM'S OWN, not a number chosen here: the ad endpoint behind the arcade accepts `^[a-z0-9_-]{1,32}$` and answers 400 otherwise, so 32 is what this carries and the SDK refuses the charset before sending. A real ad network keys its ad units BY placement - that is what the field is for - so the day one is swapped in the parent starts forwarding this string unchanged, and a game that shipped 'Boss Fight' or a 40-character name would have to be rebuilt. The grammar is narrow NOW, while nothing has shipped against it, so a game learns at development time instead. Codegen emits type and length only; the charset is the sender's to keep."
3265
+ "$comment": "The game's own name for the break it is at - 'level-complete', 'classic_revive'. UNTRUSTED TEXT, never rendered. FOR AN INTERSTITIAL IT IS MATCHED AGAINST NOTHING: the parent does not look it up and does not vary its decision on it. FOR `rewarded` IT NAMES THE REWARD the game's config declares in `rewarded_placements`: the parent forwards it unchanged to the platform's rewarded offer, which matches it against that config, and a placement the config does not declare is answered `no_ad`. IT NEVER REACHES AN AD NETWORK, for either format, because a game-authored string that fits this grammar fits a dashless UUID too and would be an identifier channel to a third party. THE BOUND IS THE PLATFORM'S OWN, not a number chosen here: the platform's ad endpoints accept `^[a-z0-9_-]{1,32}$` and answer 400 otherwise, and config upload holds a declared placement to the same grammar, so 32 is what this carries and the SDK refuses the charset before sending. A game that shipped 'Boss Fight' or a 40-character name would have to be rebuilt to reach its reward, so the SDK refuses it at development time instead. Codegen emits type and length only; the charset is the sender's to keep."
3247
3266
  },
3248
3267
  "format": {
3249
3268
  "kind": "enum",
3250
3269
  "values": ["interstitial", "rewarded"],
3251
- "$comment": "A closed set, so a typo is refused at the boundary rather than left for the parent to interpret. `rewarded` IS IN THE GRAMMAR AND IS NOT SERVED: it is answered `no_ad` today, and a `shown` would grant nothing even when it is not - see gem.adResult's outcome. A game that grants a reward on an outcome has a reward any page that can post on this port can forge."
3270
+ "$comment": "A closed set, so a typo is refused at the boundary rather than left for the parent to interpret. `rewarded` IS SERVED WHERE THE PARENT SERVES IT, and answered `no_ad` where it does not. Either way a `shown` grants nothing - see gem.adResult's outcome: the platform grants a rewarded placement's reward itself, into the game's own inventory, against its own record. A game that grants a reward on an outcome has a reward any page that can post on this port can forge."
3252
3271
  }
3253
3272
  },
3254
3273
  "rateLimit": {
@@ -3277,6 +3296,23 @@
3277
3296
  " unavailable - the shell could not render at all just now. NOT a softer",
3278
3297
  " no; a game reads it identically to `no_ad`.",
3279
3298
  "",
3299
+ "AFTER A `rewarded` ASK the player pressed a button, so two of those",
3300
+ "readings change. `shown` means the parent painted something for the ask",
3301
+ "and it has closed - the ad, however it ended - and still grants nothing.",
3302
+ "`no_ad` can arrive with the reward already granted - where no ad serves",
3303
+ "and the game's config says to grant without one, for one - so a game",
3304
+ "reads its own inventory, never the outcome, to learn whether a reward",
3305
+ "landed. And where nothing new landed, a neutral 'no reward this time'",
3306
+ "line from the game is right, because the player is waiting on their own",
3307
+ "press; it must never guess a reason.",
3308
+ "",
3309
+ "THAT INVENTORY IS A SECOND CHANNEL, AND THE RULE BELOW DOES NOT CLOSE IT.",
3310
+ "The rule closes the outcome. Whether a reward landed is visible to the",
3311
+ "game by design, so which `no_ad` branches grant one is a disclosure the",
3312
+ "platform decides where it makes the grant. A parent adds nothing to it:",
3313
+ "it never grants, and it answers a rewarded ask exactly as below whatever",
3314
+ "was granted.",
3315
+ "",
3280
3316
  "THREE VALUES, AND THAT IS THE COMPLETE VOCABULARY. `no_ad` absorbs the",
3281
3317
  "feature being switched off, an empty catalogue once the running game is",
3282
3318
  "excluded, a frequency cap that has not expired, and a player whose",