@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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -0
- package/dist/function.d.ts +1 -1
- package/dist/game.d.ts +1 -1
- package/dist/gem-web-sdk.esm.js +1 -1
- package/dist/gem-web-sdk.function.js +1 -1
- 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/sdk.d.ts +1 -1
- package/dist/server.d.ts +1 -1
- package/docs/api/api-reference-game.md +69 -24
- package/docs/api/api-reference-server.md +6 -2
- package/docs/api/reference.json +20 -20
- package/docs/changelog.md +14 -0
- package/docs/cross-promotion.md +103 -44
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +23 -18
- package/docs/match-session.md +1 -1
- package/docs/rewarded-ads.md +616 -0
- package/docs/room-shapes.md +54 -76
- package/docs/rooms-and-matches.md +2 -3
- package/docs/server-bundles.md +2 -2
- package/docs/serverless-functions.md +42 -32
- package/docs/what-a-game-must-do.md +14 -3
- package/package.json +1 -1
- package/schema/bridge.v1.json +42 -6
package/docs/room-shapes.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
# Room shapes — the
|
|
1
|
+
# Room shapes — the three things a game room is for
|
|
2
2
|
|
|
3
|
-
A game room is one mechanism with
|
|
4
|
-
in, `gem.match.connect()` to reach the authority) and the same server entry point
|
|
5
|
-
serve all
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
20
|
-
|
|
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
|
-
|
|
24
|
+
3. Otherwise a lobby fills and plays one match → **match**.
|
|
25
25
|
|
|
26
|
-
## The
|
|
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 |
|
|
95
|
-
| ------------------------ | ----------------- | --------------------- |
|
|
96
|
-
| `match_type` | `dedicated`/`p2p` | `dedicated` or `none` | `dedicated
|
|
97
|
-
| `auto_start_host` | `true` | `true` |
|
|
98
|
-
| `min_players` | the match size | `1` | `1`
|
|
99
|
-
| `allow_join_in_progress` | for backfill | **`true`** |
|
|
100
|
-
| `show_in_browser` | `false` | `true` |
|
|
101
|
-
| `transports` | the ladder | the ladder | the ladder
|
|
102
|
-
| How a player gets in | `quickJoin` | `quickJoin` | `
|
|
103
|
-
| What starts the match | `min_players` | `min_players` |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
**
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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
|
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/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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
|
449
|
-
|
|
|
450
|
-
|
|
|
451
|
-
|
|
|
452
|
-
|
|
|
453
|
-
|
|
|
454
|
-
|
|
|
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
|
|
799
|
-
| ---------------------------------------------------------- |
|
|
800
|
-
| `422 function_error` | Your `handle` returned `Err`. The text is `err.functionMessage`
|
|
801
|
-
| `422 function_failed` | It did not finish. `err.failure` is `trap`, `memory_exceeded`, `cpu_budget_exceeded` or `
|
|
802
|
-
| `504 function_timeout` | It ran past the wall clock
|
|
803
|
-
| `404 function_not_published` | Nothing is promoted on the caller's channel
|
|
804
|
-
| `413 function_input_too_large` | The input is over 256 KiB
|
|
805
|
-
| `429 rate_limited` | This caller's per-minute budget is spent
|
|
806
|
-
| `503 function_capacity_unavailable` | The platform was too busy to run it. Your function is not known to be broken
|
|
807
|
-
| `503 function_host_unreachable`, `503 function_host_error` | The platform could not run it. Nothing about your function
|
|
808
|
-
| `503 service_unavailable` | The platform could not check the caller's budget, so it refused the call. The function did not run
|
|
809
|
-
| `503 functions_not_configured` | Functions are switched off on the backend you called
|
|
810
|
-
| `403 forbidden` | The call carried an account sign-in rather than a game-scoped or game-server token
|
|
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
|
|
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
|
|
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
package/schema/bridge.v1.json
CHANGED
|
@@ -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
|
-
"
|
|
3228
|
-
" guarantee you hold: a hostile frame posts on the port directly.
|
|
3229
|
-
" matched against nothing
|
|
3230
|
-
"
|
|
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', '
|
|
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
|
|
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",
|