@bountyboard/arcade-sdk 1.3.0 → 1.4.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 +13 -1
- package/README.md +8 -8
- package/docs/external-authoritative-servers.md +21 -21
- package/docs/game-design-playbook.md +25 -25
- package/docs/llms.txt +147 -102
- package/docs/relay-rooms.md +22 -22
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @bountyboard/arcade-sdk
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- publish workflow and docs cleanup
|
|
8
|
+
|
|
9
|
+
## 1.3.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 7273667: Stop prompting flexible-orientation Arcade games to rotate on mobile.
|
|
14
|
+
|
|
3
15
|
## 1.3.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -64,7 +76,7 @@
|
|
|
64
76
|
- 145d951: Document the built-in relay room tier: every multiplayer-approved game gets
|
|
65
77
|
hosted casual lobbies (public quick match, invite codes, host succession,
|
|
66
78
|
rate-guarded public message fan-out) with zero server code, via the existing
|
|
67
|
-
`joinRoom()` API. New `docs/relay-rooms.md` covers the wire contract
|
|
79
|
+
`joinRoom()` API. New `docs/relay-rooms.md` covers the wire contract: the
|
|
68
80
|
`joinData.roomSize` founding rule (2–64, default 8), `relay`/`relay_host`
|
|
69
81
|
events, the 1 KiB / 15 msg/s / 120 msg/room/s guardrails, and the
|
|
70
82
|
client-trusted outcome model (relay rooms never emit `end` or report results).
|
package/README.md
CHANGED
|
@@ -144,8 +144,8 @@ Hosted uploads run in an opaque-origin sandbox without `localStorage`; SDK
|
|
|
144
144
|
cloud save is primary there. URL embeds and standalone builds need their own
|
|
145
145
|
same-origin storage. Oversized blobs reject `too_large` immediately (a
|
|
146
146
|
client-side precheck against the same 1 MiB cap the server enforces), and a
|
|
147
|
-
host that never answers rejects `error` after a 15-second request timeout
|
|
148
|
-
|
|
147
|
+
host that never answers rejects `error` after a 15-second request timeout.
|
|
148
|
+
The same timeout covers load, variant, and multiplayer ticket requests. Every
|
|
149
149
|
save/load rejection is a real `Error` with a typed `code`:
|
|
150
150
|
|
|
151
151
|
```text
|
|
@@ -156,7 +156,7 @@ unsupported | unauthenticated | too_large | rejected | error
|
|
|
156
156
|
|
|
157
157
|
Prefer `save()`/`load()` when you control the source. The shim is for engine
|
|
158
158
|
runtimes whose storage layer can't be rewired without patching engine
|
|
159
|
-
internals
|
|
159
|
+
internals: GameMaker HTML5 (`ini_open`, `ini_write_*`, and `game_save` all sit
|
|
160
160
|
on `localStorage`), Godot, Unity, Construct. It replaces the throwing
|
|
161
161
|
`localStorage` with a Storage-shaped object backed by your cloud save, so those
|
|
162
162
|
builds persist per player and across devices with no engine changes.
|
|
@@ -181,7 +181,7 @@ startGame();
|
|
|
181
181
|
|
|
182
182
|
Writes made before hydration are kept and merged with the cloud read (your
|
|
183
183
|
write wins; a `removeItem` isn't resurrected), and nothing is pushed to the
|
|
184
|
-
server until that read has SUCCEEDED
|
|
184
|
+
server until that read has SUCCEEDED. A failed read is retried, and a write
|
|
185
185
|
that still can't confirm what the player had is refused rather than allowed to
|
|
186
186
|
overwrite it blind, so neither a fresh-start write at boot nor a transport blip
|
|
187
187
|
can wipe an existing save. `sessionStorage` is shimmed too, but memory-only. Guests and
|
|
@@ -325,11 +325,11 @@ Two tuning hooks: `joinRoom({ timeoutMs })` overrides how long a join (and
|
|
|
325
325
|
each reconnect attempt) waits for the server's welcome before rejecting with
|
|
326
326
|
`code: 'error'` (default 10000, clamped to 1000–60000), and `room.latencyMs`
|
|
327
327
|
reports the join-handshake latency (socket open to server welcome, refreshed
|
|
328
|
-
on every reconnect; `null` until the first welcome)
|
|
329
|
-
render-side smoothing
|
|
328
|
+
on every reconnect; `null` until the first welcome). Treat it as an estimate
|
|
329
|
+
for tuning render-side smoothing, not a measured RTT.
|
|
330
330
|
|
|
331
331
|
Rooms are lobby-based: each game module declares its player range (up to 64
|
|
332
|
-
per room). `match: true` is public quick match
|
|
332
|
+
per room). `match: true` is public quick match: Bounty Board fills the game's
|
|
333
333
|
open public room and starts a fresh one when no seat is free; the matched
|
|
334
334
|
room's `room.code` still works as an invite code. When the last free seat is
|
|
335
335
|
lost to a race the SDK re-matchmakes automatically (up to two retries) before
|
|
@@ -344,7 +344,7 @@ snapshots at the room tick rate, and every input takes a network round trip
|
|
|
344
344
|
before its effect appears in a snapshot. There is no built-in client-side
|
|
345
345
|
prediction, interpolation, or rollback. Design for it: turn-based, timing-duel,
|
|
346
346
|
score-race, and party games feel native; twitch physics (fighting games,
|
|
347
|
-
precision platform duels) need your own render-side smoothing
|
|
347
|
+
precision platform duels) need your own render-side smoothing: interpolate
|
|
348
348
|
between snapshots and animate optimistic feedback for the local player's input,
|
|
349
349
|
but treat the next snapshot as truth. Assume 50-150 ms of input-to-snapshot
|
|
350
350
|
latency on real connections when tuning game feel. (Relay rooms differ: game
|
|
@@ -7,7 +7,7 @@ adapter contract; it does not relay traffic or run a second simulation.
|
|
|
7
7
|
|
|
8
8
|
External authorities are reviewed and enabled per game. Registration is not
|
|
9
9
|
self-service. Contact Bounty Board before implementing the adapter so the game
|
|
10
|
-
slug, endpoints, room
|
|
10
|
+
slug, endpoints, room code rules, ticket keys, and result model can be agreed
|
|
11
11
|
for staging and production.
|
|
12
12
|
|
|
13
13
|
## What stays authoritative
|
|
@@ -23,7 +23,7 @@ Your server remains the only source of truth. It must:
|
|
|
23
23
|
- keep endless rooms endless instead of inventing a terminal result.
|
|
24
24
|
|
|
25
25
|
The SDK handles ticket acquisition, WebSocket connection, reconnection, and a
|
|
26
|
-
small room
|
|
26
|
+
small room message envelope. It never grants a client authority over identity,
|
|
27
27
|
state, roles, or results.
|
|
28
28
|
|
|
29
29
|
## Registration information
|
|
@@ -32,9 +32,9 @@ Provide Bounty Board with the following for each environment:
|
|
|
32
32
|
|
|
33
33
|
- the exact Arcade game slug;
|
|
34
34
|
- one fixed `wss:` endpoint dedicated to authenticated SDK rooms;
|
|
35
|
-
- the room
|
|
35
|
+
- the room code format and maximum length your server accepts;
|
|
36
36
|
- whether missing room codes may create rooms;
|
|
37
|
-
- any validated, game
|
|
37
|
+
- any validated, game defined `joinData` fields;
|
|
38
38
|
- whether the game has finite matches or endless sessions; and
|
|
39
39
|
- an operational contact for key rotation or incident response.
|
|
40
40
|
|
|
@@ -42,9 +42,9 @@ The registered endpoint must not contain userinfo, query parameters, or a
|
|
|
42
42
|
fragment. Use `ws:` only for loopback development. Staging and production need
|
|
43
43
|
different endpoints and different secrets.
|
|
44
44
|
|
|
45
|
-
Bounty Board provisions a dedicated ticket
|
|
45
|
+
Bounty Board provisions a dedicated ticket verification secret for the game
|
|
46
46
|
and environment. Store it only on servers. Do not put it in the game bundle,
|
|
47
|
-
browser storage, logs, analytics, crash reports, or client
|
|
47
|
+
browser storage, logs, analytics, crash reports, or client visible environment
|
|
48
48
|
variables. Do not reuse credentials from another game or room service.
|
|
49
49
|
|
|
50
50
|
## Connection flow
|
|
@@ -100,7 +100,7 @@ Before accepting a connection, verify all of the following:
|
|
|
100
100
|
- `sub` is non-empty; and
|
|
101
101
|
- required claim types and lengths are valid.
|
|
102
102
|
|
|
103
|
-
Treat `sub` as an opaque game
|
|
103
|
+
Treat `sub` as an opaque game scoped identity. Never attempt to map it to a
|
|
104
104
|
Bounty Board account or correlate it with another game. `name` and `avatarUrl`
|
|
105
105
|
are display data, not authorization data. `avatarUrl` may be `null` for any
|
|
106
106
|
player.
|
|
@@ -129,12 +129,12 @@ if (!room.trySend({ type: 'move', x: 1, y: 0 })) {
|
|
|
129
129
|
}
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
Script
|
|
132
|
+
Script tag builds use `BBArcade.multiplayer.joinRoom(...)`.
|
|
133
133
|
|
|
134
134
|
`joinData` is an optional JSON object capped at 1 KiB. Validate every field,
|
|
135
135
|
reject unknown or oversized values, and never accept credentials or identity
|
|
136
136
|
claims from it. If your server distinguishes create from join, use a documented
|
|
137
|
-
game
|
|
137
|
+
game defined intent field and rate limit room creation. That field is still
|
|
138
138
|
untrusted and grants no role or permission by itself.
|
|
139
139
|
|
|
140
140
|
## WebSocket URL
|
|
@@ -147,7 +147,7 @@ wss://multiplayer.example.com/bountyboard?ticket=<encoded>&join=<encoded-json>
|
|
|
147
147
|
|
|
148
148
|
The `join` parameter is omitted when no `joinData` was supplied. Keep this path
|
|
149
149
|
separate from any anonymous or legacy socket endpoint so a guessed room code
|
|
150
|
-
cannot cross the authentication boundary. Redact or disable request
|
|
150
|
+
cannot cross the authentication boundary. Redact or disable request URI logging
|
|
151
151
|
on this route because the short-lived ticket is in the query string.
|
|
152
152
|
|
|
153
153
|
## SDK room envelope
|
|
@@ -174,7 +174,7 @@ Send authoritative state snapshots as:
|
|
|
174
174
|
}
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
Send game
|
|
177
|
+
Send game defined transient events without changing their payload:
|
|
178
178
|
|
|
179
179
|
```json
|
|
180
180
|
{ "t": "event", "data": { "type": "round_started", "round": 2 } }
|
|
@@ -204,23 +204,23 @@ Reject an invalid ticket before `welcome` with an error and close the socket:
|
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
Use `rejected` for invalid admission or `joinData`. After admission, error codes
|
|
207
|
-
remain game
|
|
207
|
+
remain game defined strings and are surfaced through `room.on('error', ...)`.
|
|
208
208
|
|
|
209
209
|
## Reconnection
|
|
210
210
|
|
|
211
211
|
The SDK automatically makes a small number of reconnect attempts. It requests
|
|
212
212
|
a fresh ticket for the same room and reuses the original `joinData`. Rebind the
|
|
213
213
|
seat using the verified `(ticket.sub, ticket.roomId)` pair. Never trust a
|
|
214
|
-
client
|
|
214
|
+
client supplied player id or reconnect token as the proof of identity.
|
|
215
215
|
|
|
216
|
-
Send the latest complete viewer
|
|
216
|
+
Send the latest complete viewer safe state in the new `welcome` frame. If your
|
|
217
217
|
client uses prediction, include the last processed input sequence in that state
|
|
218
218
|
so it can discard acknowledged inputs. Clear held controls while the socket is
|
|
219
219
|
absent.
|
|
220
220
|
|
|
221
221
|
## Finite matches and endless rooms
|
|
222
222
|
|
|
223
|
-
Only a server
|
|
223
|
+
Only a server authoritative finite game may emit `match_end`:
|
|
224
224
|
|
|
225
225
|
```json
|
|
226
226
|
{
|
|
@@ -239,7 +239,7 @@ Only a server-authoritative finite game may emit `match_end`:
|
|
|
239
239
|
|
|
240
240
|
The client receives this as `room.on('end', ({ results }) => ...)`. Persistent
|
|
241
241
|
Bounty Board results, when enabled, use a separately provisioned
|
|
242
|
-
server
|
|
242
|
+
server to server reporting credential. Never accept a client originated result
|
|
243
243
|
or reporting credential.
|
|
244
244
|
|
|
245
245
|
Endless rooms must not emit `match_end`. Death, respawn, a round transition, or
|
|
@@ -248,18 +248,18 @@ a player leaving is not automatically a terminal match result.
|
|
|
248
248
|
## Production checklist
|
|
249
249
|
|
|
250
250
|
- Use an isolated staging endpoint and secrets before production.
|
|
251
|
-
- Confirm tampered, expired, wrong
|
|
252
|
-
- Test both guest and logged
|
|
251
|
+
- Confirm tampered, expired, wrong slug, and wrong room tickets fail closed.
|
|
252
|
+
- Test both guest and logged in opaque subjects.
|
|
253
253
|
- Reject malformed, unknown, or oversized `joinData`.
|
|
254
254
|
- Send `welcome` only after ticket and admission validation succeeds.
|
|
255
255
|
- Verify every snapshot is safe for its specific viewer.
|
|
256
|
-
- Test disconnect, held
|
|
257
|
-
- Rate
|
|
256
|
+
- Test disconnect, held input cleanup, fresh ticket reconnect, and `leave()`.
|
|
257
|
+
- Rate limit upgrades, room creation, joins, and inputs.
|
|
258
258
|
- Redact tickets and join payloads from access logs and error telemetry.
|
|
259
259
|
- Rotate the dedicated ticket secret without reusing another environment's key.
|
|
260
260
|
- Emit final results only from a real authoritative terminal condition.
|
|
261
261
|
- Keep the game playable when multiplayer is unsupported or temporarily down.
|
|
262
262
|
|
|
263
263
|
For SDK integration basics, see https://www.bountyboard.gg/arcade/sdk. For the
|
|
264
|
-
agent
|
|
264
|
+
agent readable API contract, see
|
|
265
265
|
https://www.bountyboard.gg/arcade/sdk/llms.txt.
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Bounty Board game design playbook (the "design brain")
|
|
2
2
|
|
|
3
3
|
What actually performs on the Bounty Board arcade, distilled from operating it. Read this
|
|
4
|
-
BEFORE designing a new game or porting one in
|
|
4
|
+
BEFORE designing a new game or porting one in. The SDK wiring is the easy part; these choices
|
|
5
5
|
decide whether the game earns plays, retention, and revenue.
|
|
6
6
|
|
|
7
|
-
> Draft co-owned with our partner studios
|
|
7
|
+
> Draft co-owned with our partner studios. Challenge anything here with data.
|
|
8
8
|
|
|
9
9
|
## Session shape
|
|
10
10
|
|
|
11
|
-
- **Target a 60
|
|
11
|
+
- **Target a 60 to 180 second core loop.** Arcade traffic arrives mid browse; games that deliver a
|
|
12
12
|
complete emotional arc (start → tension → payoff) in under three minutes get replays, and
|
|
13
|
-
replays drive feed ranking. Longer
|
|
14
|
-
- **Time
|
|
13
|
+
replays drive feed ranking. Longer form games need checkpointed sessions via cloud saves.
|
|
14
|
+
- **Time to first input under 5 seconds.** Call `gameLoadingFinished()` honestly; players who
|
|
15
15
|
bounce on a spinner never come back. Defer heavy assets past the first playable moment.
|
|
16
16
|
- **Instant restart.** Death → new run should be ONE input and under a second. Restart friction
|
|
17
17
|
is the top killer of "one more run".
|
|
@@ -19,31 +19,31 @@ decide whether the game earns plays, retention, and revenue.
|
|
|
19
19
|
## Score design (this is leaderboard design)
|
|
20
20
|
|
|
21
21
|
- **Scores must be integers with a meaningful gradient.** A good score curve separates a casual
|
|
22
|
-
run from a great one by 10
|
|
22
|
+
run from a great one by 10 to 100x, not 2x. That's what makes a board worth climbing.
|
|
23
23
|
- **Skill ceiling over grind ceiling.** If score scales with time played rather than skill, the
|
|
24
|
-
board saturates and goes stale. Cap or decay pure
|
|
25
|
-
- **Design the "one point short" feeling.** Near
|
|
24
|
+
board saturates and goes stale. Cap or decay pure survival scoring; reward risk.
|
|
25
|
+
- **Design the "one point short" feeling.** Near miss visibility (show the player's best and the
|
|
26
26
|
next board rank in-game via your own UI) measurably lifts replays.
|
|
27
|
-
- **Daily mode**: if your game has procedural content, ship a shared
|
|
27
|
+
- **Daily mode**: if your game has procedural content, ship a shared seed daily run and submit
|
|
28
28
|
it with `{ mode: 'daily' }`. Daily boards reset at midnight UTC and are the strongest
|
|
29
|
-
retention surface on the platform
|
|
29
|
+
retention surface on the platform. Everyone plays the SAME level, so the board is fair chat.
|
|
30
30
|
|
|
31
|
-
## Multiplayer design (for room
|
|
31
|
+
## Multiplayer design (for room based games)
|
|
32
32
|
|
|
33
|
-
- **Latency
|
|
33
|
+
- **Latency tolerant mechanics win.** Positional games at 10 to 20Hz snapshots with client
|
|
34
34
|
interpolation feel great for chase/tag/social deduction; twitch duels don't. Design around
|
|
35
|
-
prediction
|
|
35
|
+
prediction friendly movement (momentum, grid steps) rather than instant hit actions.
|
|
36
36
|
- **Information asymmetry is a server feature.** The room server sends each player only what
|
|
37
37
|
they may know (hiders invisible to the seeker). Lean into designs where hidden information IS
|
|
38
|
-
the game
|
|
39
|
-
- **2-minute rounds, drop
|
|
40
|
-
forgive mid
|
|
38
|
+
the game. It's cheat proof by construction here.
|
|
39
|
+
- **2-minute rounds, drop in lobbies.** Rooms fill from friends sharing codes; short rounds
|
|
40
|
+
forgive mid round joins as spectators and keep groups cycling.
|
|
41
41
|
- **Send inputs, not outcomes.** If your design needs the client to decide who got tagged, the
|
|
42
|
-
design is wrong
|
|
42
|
+
design is wrong. Move the rule server-side.
|
|
43
43
|
|
|
44
44
|
## Monetization etiquette (rewarded ads)
|
|
45
45
|
|
|
46
|
-
- **Ads are a player's trade, never a toll.** Best
|
|
46
|
+
- **Ads are a player's trade, never a toll.** Best performing placements: revive ("continue this
|
|
47
47
|
run?"), doubler ("2x this run's coins"), cosmetic unlock. Never gate core progression.
|
|
48
48
|
- **One organic placement beats three pushy ones.** Interrupting flow trains players to leave;
|
|
49
49
|
prepare one rewarded placement at a natural fail state and offer it once.
|
|
@@ -54,13 +54,13 @@ decide whether the game earns plays, retention, and revenue.
|
|
|
54
54
|
|
|
55
55
|
## Platform fit
|
|
56
56
|
|
|
57
|
-
- **Mobile
|
|
58
|
-
targets, no hover dependence, portrait
|
|
59
|
-
- **Performance budget: 60fps on a mid
|
|
57
|
+
- **Mobile first inputs.** Most arcade sessions are touch. One thumb controls, generous hit
|
|
58
|
+
targets, no hover dependence, portrait friendly if possible. Keyboard is the enhancement.
|
|
59
|
+
- **Performance budget: 60fps on a mid range phone.** Cap DPR, pool objects, avoid layout
|
|
60
60
|
thrash. Players don't report jank, they just leave.
|
|
61
|
-
- **Own your standalone build.** The same bundle must run off
|
|
62
|
-
fork builds; feature
|
|
63
|
-
- **Cloud saves make your game feel native.** Load on boot, save on checkpoint/game
|
|
61
|
+
- **Own your standalone build.** The same bundle must run off platform (the SDK no-ops). Don't
|
|
62
|
+
fork builds; feature detect through the SDK's own fallbacks.
|
|
63
|
+
- **Cloud saves make your game feel native.** Load on boot, save on checkpoint/game over, and
|
|
64
64
|
greet returning players with their progress (pair with `getPlayer()` for the name). Hosted
|
|
65
65
|
builds have no localStorage, so wire this early, not as a retrofit.
|
|
66
66
|
|
|
@@ -71,5 +71,5 @@ decide whether the game earns plays, retention, and revenue.
|
|
|
71
71
|
- [ ] The score of a great run embarrasses the score of a lucky run
|
|
72
72
|
- [ ] Daily mode if content is procedural
|
|
73
73
|
- [ ] Rewarded placement is a trade the player initiates
|
|
74
|
-
- [ ] Playable one
|
|
74
|
+
- [ ] Playable one thumb on a phone at 60fps
|
|
75
75
|
- [ ] Boots and plays with the SDK fully offline
|
package/docs/llms.txt
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
# Bounty Board Arcade SDK
|
|
1
|
+
# Bounty Board Arcade SDK: agent readable integration contract
|
|
2
2
|
|
|
3
3
|
Human guide: https://www.bountyboard.gg/arcade/sdk
|
|
4
|
-
Package: @bountyboard/arcade-sdk
|
|
5
|
-
Stable npm release: 1.
|
|
4
|
+
Package: @bountyboard/arcade-sdk (https://www.npmjs.com/package/@bountyboard/arcade-sdk)
|
|
5
|
+
Stable npm release: 1.4.0
|
|
6
6
|
Wire protocol: 1
|
|
7
7
|
|
|
8
8
|
This file is the complete integration contract for coding agents integrating an
|
|
9
9
|
HTML5 game. The package TypeScript declarations remain the exact public type
|
|
10
|
-
reference. npm 1.
|
|
10
|
+
reference. npm 1.4.0, the /arcade-sdk/v1.js browser artifact, and repository
|
|
11
11
|
source all expose the same surface. Rooms open on all three approved rails:
|
|
12
|
-
Bounty shared
|
|
13
|
-
host
|
|
14
|
-
match: true works on both Bounty shared
|
|
12
|
+
Bounty shared service referee modules, Bounty shared service relay rooms, and
|
|
13
|
+
host routed registered external authorities. code/create works on every rail;
|
|
14
|
+
match: true works on both Bounty shared service tiers. The external authority
|
|
15
15
|
adapter guide ships in the package and is public at the URL above.
|
|
16
16
|
|
|
17
17
|
## Non-negotiable integration rules
|
|
18
18
|
|
|
19
19
|
1. The SDK is never load-bearing. A game must boot and remain fully playable
|
|
20
|
-
with no Bounty Board parent, no logged
|
|
20
|
+
with no Bounty Board parent, no logged in player, no ad inventory, no cloud
|
|
21
21
|
save, and no multiplayer authority.
|
|
22
22
|
2. Call gameOver() exactly once per run and use integer scores. The host/server
|
|
23
23
|
enforces per-game plausibility caps.
|
|
@@ -25,17 +25,19 @@ adapter guide ships in the package and is public at the URL above.
|
|
|
25
25
|
death prompts, and ad breaks are not active play.
|
|
26
26
|
4. getPlayer() can be null and avatarUrl can be null. The SDK never exposes
|
|
27
27
|
account ids, emails, or roles.
|
|
28
|
-
5. Store all progress in one save(string) blob (about 1 MB).
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
5. Store all progress in one save(string) blob (about 1 MB). Gate the account
|
|
29
|
+
backed calls on getPlayer() rather than discovering the guest case through a
|
|
30
|
+
thrown unauthenticated. Save at checkpoints/game over, not in a frame loop,
|
|
31
|
+
and catch every rejection. When the game is an engine export whose storage
|
|
32
|
+
layer you cannot rewire, use storage.install() instead and let the shim
|
|
33
|
+
carry localStorage to the cloud.
|
|
34
|
+
6. For new rewarded ad work, prepare first, enable the game's button only when
|
|
33
35
|
status is ready, call prepared.show() directly from the click/tap handler,
|
|
34
36
|
and grant only when the final status is viewed.
|
|
35
37
|
7. Multiplayer has three rails. Referee modules and external authorities own
|
|
36
38
|
simulation/results and receive client inputs. The casual relay owns signed
|
|
37
39
|
admission, roster, capacity, host succession, reconnect grace, and public
|
|
38
|
-
message fan
|
|
40
|
+
message fan out, but it does not referee game state or outcomes. Catch every
|
|
39
41
|
joinRoom() rejection and keep solo/standalone play available.
|
|
40
42
|
8. lockToHost() is an optional anti-rehosting deterrent, not DRM. Call it before
|
|
41
43
|
boot. allow lists the domains the STUDIO hosts the build on, so a build that
|
|
@@ -49,16 +51,21 @@ Preferred for games with a build step:
|
|
|
49
51
|
npm install @bountyboard/arcade-sdk
|
|
50
52
|
import { BBArcade } from '@bountyboard/arcade-sdk';
|
|
51
53
|
|
|
52
|
-
The package is zero
|
|
54
|
+
The package is zero dependency, typed, ESM + CommonJS, and SSR safe. Importing
|
|
53
55
|
the module does not install a window.BBArcade global.
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
Script tag, no build step:
|
|
56
58
|
|
|
57
59
|
<script src="https://www.bountyboard.gg/arcade-sdk/v1.js"></script>
|
|
58
60
|
|
|
59
61
|
This installs window.BBArcade. Standalone declarations:
|
|
60
62
|
https://www.bountyboard.gg/arcade-sdk.d.ts
|
|
61
63
|
|
|
64
|
+
The hosted file is served with Cross-Origin-Resource-Policy: cross-origin and
|
|
65
|
+
Access-Control-Allow-Origin: *, so the tag also loads on a cross-origin-isolated
|
|
66
|
+
page (Cross-Origin-Embedder-Policy: require-corp). Vendoring a copy of the file
|
|
67
|
+
into the build and loading it same-origin is equally supported.
|
|
68
|
+
|
|
62
69
|
Do not mix npm and the script tag in one page. A bundler that specifically
|
|
63
70
|
wants the global may import @bountyboard/arcade-sdk/global.
|
|
64
71
|
|
|
@@ -66,7 +73,7 @@ Multiplayer module import:
|
|
|
66
73
|
|
|
67
74
|
import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';
|
|
68
75
|
|
|
69
|
-
Script
|
|
76
|
+
Script tag multiplayer is BBArcade.multiplayer.joinRoom(...).
|
|
70
77
|
|
|
71
78
|
Public package exports:
|
|
72
79
|
|
|
@@ -81,12 +88,12 @@ Public package exports:
|
|
|
81
88
|
|
|
82
89
|
Distribution format does not determine capabilities; the embedding host does.
|
|
83
90
|
|
|
84
|
-
- Bounty
|
|
91
|
+
- Bounty hosted upload:
|
|
85
92
|
lifecycle/scores supported; player identity/variants supported; cloud
|
|
86
|
-
save/load available to logged
|
|
93
|
+
save/load available to logged in players; native localStorage/sessionStorage/
|
|
87
94
|
IndexedDB/cookies all THROW (opaque origin), so engine exports need
|
|
88
95
|
storage.install(); ads available to approved games; multiplayer available
|
|
89
|
-
after per-game approval, with relay as the default Bounty shared
|
|
96
|
+
after per-game approval, with relay as the default Bounty shared service tier
|
|
90
97
|
when no bespoke referee module is registered.
|
|
91
98
|
- Approved external URL embed inside the Bounty Board player:
|
|
92
99
|
lifecycle/scores, identity, variants, and approved multiplayer (including the
|
|
@@ -94,11 +101,11 @@ Distribution format does not determine capabilities; the embedding host does.
|
|
|
94
101
|
per-game attribution yet); cloud save/load is unsupported.
|
|
95
102
|
- Standalone or opened directly on the game's own site:
|
|
96
103
|
fire-and-forget calls no-op; init resolves immediately; getPlayer returns
|
|
97
|
-
null; getVariant returns the alphabetical control; host
|
|
104
|
+
null; getVariant returns the alphabetical control; host only promise APIs
|
|
98
105
|
reject unsupported or return an unavailable outcome. An unanswered non-Bounty
|
|
99
106
|
embed uses an approximately 1.5-second init/getPlayer grace instead.
|
|
100
107
|
|
|
101
|
-
Guests are normal. getPlayer resolves null and account
|
|
108
|
+
Guests are normal. getPlayer resolves null and account backed calls may reject
|
|
102
109
|
with code unauthenticated.
|
|
103
110
|
|
|
104
111
|
Leaderboards need one thing outside the code: the studio must declare
|
|
@@ -122,7 +129,7 @@ ceilings live beside that declaration and reject implausible values.
|
|
|
122
129
|
BBArcade.gameplayStop(); // pause/death/menu/ad
|
|
123
130
|
BBArcade.gameOver(Math.trunc(finalScore)); // exactly once per run
|
|
124
131
|
|
|
125
|
-
For a shared
|
|
132
|
+
For a shared seed Daily, pass { mode: 'daily' } to submitScore and gameOver.
|
|
126
133
|
|
|
127
134
|
## Exact shared types and configuration
|
|
128
135
|
|
|
@@ -164,8 +171,8 @@ Core configuration shapes:
|
|
|
164
171
|
lockToHost defaults allow bountyboard.gg and its subdomains, the fixed Bounty
|
|
165
172
|
staging host, localhost, and 127.0.0.1. allow extends rather than replaces that
|
|
166
173
|
list. signed mode requests an ECDSA origin attestation; it falls back to the
|
|
167
|
-
best
|
|
168
|
-
Web Crypto is unavailable. A missing host or present
|
|
174
|
+
best effort browser origin check only when the host reports no signing key or
|
|
175
|
+
Web Crypto is unavailable. A missing host or present but invalid attestation
|
|
169
176
|
blocks.
|
|
170
177
|
|
|
171
178
|
Player and score shapes:
|
|
@@ -187,7 +194,7 @@ Storage compatibility shapes:
|
|
|
187
194
|
readonly mode: BBArcadeStorageMode;
|
|
188
195
|
}
|
|
189
196
|
|
|
190
|
-
Rewarded
|
|
197
|
+
Rewarded ad options and results:
|
|
191
198
|
|
|
192
199
|
interface BBArcadeRewardedAdOptions {
|
|
193
200
|
placement?: string; name?: string; reward?: string; adBreakId?: string;
|
|
@@ -279,24 +286,26 @@ Lifecycle and scoring:
|
|
|
279
286
|
Player data and experiments:
|
|
280
287
|
|
|
281
288
|
- save(blob: string): Promise<void>
|
|
282
|
-
One blob, about 1 MB. Requires a Bounty
|
|
289
|
+
One blob, about 1 MB. Requires a Bounty hosted upload and logged in player.
|
|
283
290
|
Oversized blobs reject too_large immediately via a client-side precheck
|
|
284
291
|
against the same 1 MiB cap the server enforces.
|
|
285
292
|
- load(): Promise<string | null>
|
|
286
|
-
Returns null when no save exists. Rejects like save().
|
|
293
|
+
Returns null when no save exists. Rejects like save(). Gate on getPlayer() so
|
|
294
|
+
the guest case is a branch rather than a catch.
|
|
287
295
|
- storage: BBArcadeStorage
|
|
288
296
|
localStorage compatibility for hosted builds. install(): BBArcadeStorageMode
|
|
289
|
-
replaces a throwing window.localStorage with a Storage
|
|
297
|
+
replaces a throwing window.localStorage with a Storage shaped object backed
|
|
290
298
|
by the cloud save, and is a no-op where a real Storage works. ready():
|
|
291
299
|
Promise<BBArcadeStorageMode> resolves once the cloud read has landed.
|
|
292
300
|
flush(): Promise<void> forces the debounced write out now. mode is the
|
|
293
|
-
current BBArcadeStorageMode ('native' | 'cloud' | 'memory'). The script
|
|
301
|
+
current BBArcadeStorageMode ('native' | 'cloud' | 'memory'). The script tag
|
|
294
302
|
build calls install() for you at load; module consumers call it themselves
|
|
295
303
|
before boot.
|
|
296
304
|
- getPlayer(): Promise<{ name: string, avatarUrl: string | null } | null>
|
|
297
|
-
Display identity only; always handle null.
|
|
305
|
+
Display identity only; always handle null. Never rejects and never hangs, so
|
|
306
|
+
it is also the cheapest signed in check to gate save()/load() on.
|
|
298
307
|
- onPlayerChange(handler): () => void
|
|
299
|
-
Subscribes to CHANGES in the display identity (mid
|
|
308
|
+
Subscribes to CHANGES in the display identity (mid session login/logout).
|
|
300
309
|
The handler receives the same { name, avatarUrl } | null shape as
|
|
301
310
|
getPlayer() and runs only when the identity actually changes; subscribing
|
|
302
311
|
does not replay the current value. Returns an unsubscribe function.
|
|
@@ -307,9 +316,11 @@ Player data and experiments:
|
|
|
307
316
|
item. Do not re-randomize client-side.
|
|
308
317
|
|
|
309
318
|
save/load can reject with every BBArcadeErrorCode. load resolves null only when
|
|
310
|
-
the logged
|
|
311
|
-
|
|
312
|
-
|
|
319
|
+
the logged in hosted player has no save; a guest can reject unauthenticated.
|
|
320
|
+
Gate on getPlayer() so account state is a branch you took rather than an error
|
|
321
|
+
you caught, and keep the catch for transport failures. Every host request
|
|
322
|
+
(save, load, variant, multiplayer ticket) rejects with code error after a
|
|
323
|
+
15-second timeout when no answer arrives.
|
|
313
324
|
|
|
314
325
|
unsupported | unauthenticated | too_large | rejected | error
|
|
315
326
|
|
|
@@ -318,7 +329,7 @@ Rewarded ads:
|
|
|
318
329
|
- prepareRewardedAd(options?): Promise<BBArcadeRewardedAdPreparation>
|
|
319
330
|
Recommended. Alias: prepareRewardedBreak(). Prepared show() is one-shot.
|
|
320
331
|
- rewardedAd(options?): Promise<BBArcadeRewardedAdResult>
|
|
321
|
-
Low-level structured
|
|
332
|
+
Low-level structured result API. Alias: showRewardedAd().
|
|
322
333
|
- rewardedBreak(options | onStart): Promise<boolean>
|
|
323
334
|
Deprecated compatibility helper. New games must use the prepared flow.
|
|
324
335
|
- preloadRewardedAds(options?): Promise<boolean>
|
|
@@ -341,7 +352,7 @@ Multiplayer:
|
|
|
341
352
|
type BBArcadeMpJoinOptions = {
|
|
342
353
|
code?: string;
|
|
343
354
|
create?: boolean;
|
|
344
|
-
match?: boolean; // public quick match; Bounty shared
|
|
355
|
+
match?: boolean; // public quick match; Bounty shared service tiers only
|
|
345
356
|
joinData?: Readonly<Record<string, unknown>>;
|
|
346
357
|
roomUrl?: string; // local development override; provide with ticket
|
|
347
358
|
ticket?: string; // local development override; provide with roomUrl
|
|
@@ -379,7 +390,7 @@ Multiplayer:
|
|
|
379
390
|
players: BBArcadeMpPlayer[];
|
|
380
391
|
state: unknown;
|
|
381
392
|
connected: boolean;
|
|
382
|
-
latencyMs: number | null; // join
|
|
393
|
+
latencyMs: number | null; // join handshake estimate, refreshed on reconnect
|
|
383
394
|
send(input: unknown): void;
|
|
384
395
|
trySend(input: unknown): boolean;
|
|
385
396
|
on<K extends keyof BBArcadeMpRoomEvents>(
|
|
@@ -393,7 +404,7 @@ Multiplayer:
|
|
|
393
404
|
joinRoom(options?: BBArcadeMpJoinOptions): Promise<BBArcadeMpRoom>;
|
|
394
405
|
}
|
|
395
406
|
|
|
396
|
-
Relay runtime payloads use these exact shapes through the otherwise
|
|
407
|
+
Relay runtime payloads use these exact shapes through the otherwise unknown
|
|
397
408
|
room.state and room.on('event') values:
|
|
398
409
|
|
|
399
410
|
interface BBArcadeRelayState {
|
|
@@ -411,16 +422,16 @@ room.state and room.on('event') values:
|
|
|
411
422
|
| { type: 'relay_host'; hostId: string | null };
|
|
412
423
|
|
|
413
424
|
BBArcadeRelayState and BBArcadeRelayEvent are documentation names, not package
|
|
414
|
-
exports; the public SDK intentionally types game/tier
|
|
425
|
+
exports; the public SDK intentionally types game/tier defined state and events
|
|
415
426
|
as unknown.
|
|
416
427
|
|
|
417
428
|
- joinRoom(options?): Promise<BBArcadeMpRoom>
|
|
418
429
|
No options and { create: true } both generate a 4-character invite code from
|
|
419
430
|
ABCDEFGHJKLMNPQRSTUVWXYZ23456789. A supplied code is uppercased. match: true
|
|
420
|
-
is Bounty
|
|
431
|
+
is Bounty hosted public quick match and excludes code, create, roomUrl, and
|
|
421
432
|
ticket. roomUrl+ticket bypass the host handshake for local development only;
|
|
422
433
|
provide both (a lone value is not an override).
|
|
423
|
-
- joinData must be a non-null, non-array JSON
|
|
434
|
+
- joinData must be a non-null, non-array JSON serializable object whose UTF-8
|
|
424
435
|
JSON encoding is at most 1 KiB. Cycles, arrays, primitives, and oversized data
|
|
425
436
|
reject with code rejected. Never include credentials or secrets.
|
|
426
437
|
- joinRoom resolves only after the authority sends welcome (10-second default
|
|
@@ -429,17 +440,17 @@ as unknown.
|
|
|
429
440
|
At resolution, code/playerId/players/state/connected already hold the initial
|
|
430
441
|
lobby state. There is no replayed initial snapshot: render those fields first,
|
|
431
442
|
then subscribe to future events.
|
|
432
|
-
- room.latencyMs reports the join
|
|
443
|
+
- room.latencyMs reports the join handshake latency in ms (socket open to
|
|
433
444
|
server welcome: ticket verification plus one round trip), refreshed on every
|
|
434
445
|
successful (re)connect and null until the first welcome. Use it to tune
|
|
435
|
-
render
|
|
436
|
-
- Values passed to send/trySend must be JSON
|
|
446
|
+
render side smoothing; it is an estimate, not a measured RTT.
|
|
447
|
+
- Values passed to send/trySend must be JSON serializable. A referee room
|
|
437
448
|
treats the value as an input; a relay room treats it as a public game
|
|
438
449
|
message. send discards local write status. trySend
|
|
439
450
|
returns true only when JSON was written to an open socket; it does NOT mean
|
|
440
451
|
the authority accepted the input. false means disconnected, raced closed, or
|
|
441
452
|
serialization/WebSocket.send failed. The authority still validates,
|
|
442
|
-
sequences, and may
|
|
453
|
+
sequences, and may drop inputs above its rate limit.
|
|
443
454
|
- on returns an unsubscribe function. The SDK updates room.players before
|
|
444
455
|
playerJoin/playerLeave handlers and room.state before snapshot handlers.
|
|
445
456
|
- leave intentionally closes the Room and permanently disables reconnect for it.
|
|
@@ -455,28 +466,49 @@ BBArcade.version and the exported PROTOCOL_VERSION are the core bb-arcade
|
|
|
455
466
|
postMessage wire-protocol version. They are not npm semver and are not a
|
|
456
467
|
version field on multiplayer WebSocket frames.
|
|
457
468
|
|
|
458
|
-
## Cloud
|
|
469
|
+
## Cloud save recipe
|
|
459
470
|
|
|
460
471
|
Hosted uploads have an opaque origin where a real localStorage is not merely
|
|
461
|
-
empty
|
|
472
|
+
empty. Reading window.localStorage THROWS a SecurityError, as do
|
|
462
473
|
sessionStorage, IndexedDB, and document.cookie. SDK save is the primary store
|
|
463
474
|
there. URL embeds and standalone builds need their own same-origin fallback.
|
|
464
475
|
|
|
476
|
+
Ask who is playing before reaching for the account backed calls. getPlayer()
|
|
477
|
+
resolves null for guests, standalone play, and embeds off Bounty Board, and it
|
|
478
|
+
never rejects and never hangs, so it turns the guest case into a branch instead
|
|
479
|
+
of a thrown error:
|
|
480
|
+
|
|
481
|
+
const player = await BBArcade.getPlayer();
|
|
482
|
+
|
|
483
|
+
if (player) {
|
|
484
|
+
const blob = await BBArcade.load(); // null = signed in, no save yet
|
|
485
|
+
restore(blob ? JSON.parse(blob) : defaults);
|
|
486
|
+
} else {
|
|
487
|
+
restoreStandaloneProgress(); // and offer "Sign in to save"
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
Still catch save()/load(), but for transport failures, not for account state
|
|
491
|
+
you already branched on:
|
|
492
|
+
|
|
465
493
|
const blob = JSON.stringify(progress);
|
|
466
494
|
try {
|
|
467
495
|
await BBArcade.save(blob);
|
|
468
496
|
} catch (error) {
|
|
469
497
|
if (error.code === 'unsupported') localStorage.setItem('progress', blob);
|
|
498
|
+
else if (error.code !== 'unauthenticated') reportSaveFailure(error.code);
|
|
470
499
|
}
|
|
471
500
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
501
|
+
save() rejecting for a guest is deliberate, not an oversight. A write that did
|
|
502
|
+
not happen must never resolve as if it had, or the game reports "Saved" over
|
|
503
|
+
progress that is already gone. It is also the only place "Sign in to keep your
|
|
504
|
+
progress" can be offered in context, which is worth more than a silent no-op.
|
|
505
|
+
|
|
506
|
+
Do not assume load() resolves null for a guest either. null means the logged in
|
|
507
|
+
player has no save yet; a guest rejects unauthenticated.
|
|
478
508
|
|
|
479
|
-
|
|
509
|
+
A game that does not want to model accounts at all should use the storage shim
|
|
510
|
+
below instead. It folds guest and standalone play into 'memory' mode and never
|
|
511
|
+
rejects on either.
|
|
480
512
|
|
|
481
513
|
## localStorage shim for engine exports
|
|
482
514
|
|
|
@@ -485,13 +517,19 @@ runtimes whose storage layer cannot be rewired without patching engine
|
|
|
485
517
|
internals: GameMaker HTML5 (ini_open/ini_write_*/game_save all sit on
|
|
486
518
|
localStorage), Godot, Unity, and Construct.
|
|
487
519
|
|
|
488
|
-
|
|
520
|
+
It doubles as the tier for any game that does not want to model accounts at
|
|
521
|
+
all. Guests, standalone play, and embeds off Bounty Board settle into 'memory'
|
|
522
|
+
mode, so nothing rejects and no auth state has to be handled. The cost is that
|
|
523
|
+
the game never learns a save was not persisted, so it cannot offer the player a
|
|
524
|
+
chance to sign in and keep it. Check storage.mode for that.
|
|
525
|
+
|
|
526
|
+
With the script tag, nothing is required beyond load order. The SDK installs
|
|
489
527
|
the shim at load and window.localStorage starts working:
|
|
490
528
|
|
|
491
529
|
<script src="https://www.bountyboard.gg/arcade-sdk/v1.js"></script>
|
|
492
530
|
<script src="html5game/YourGame.js"></script>
|
|
493
531
|
|
|
494
|
-
Module consumers keep a side
|
|
532
|
+
Module consumers keep a side effect free import and install it themselves,
|
|
495
533
|
before any engine code runs:
|
|
496
534
|
|
|
497
535
|
import { BBArcade } from '@bountyboard/arcade-sdk';
|
|
@@ -505,13 +543,13 @@ play and URL embeds are untouched. Behavior once installed:
|
|
|
505
543
|
- Progress becomes per-player and cross-device, not per-browser.
|
|
506
544
|
- setItem throws QuotaExceededError when the write would exceed the ~1 MB save
|
|
507
545
|
cap, matching a real Storage.
|
|
508
|
-
- sessionStorage is shimmed too, but memory
|
|
546
|
+
- sessionStorage is shimmed too, but memory only. It is per session by
|
|
509
547
|
definition and is never synced.
|
|
510
548
|
- Guests and standalone play settle in 'memory' mode: storage still works for
|
|
511
549
|
the session, it is just never persisted. Nothing rejects; nothing hangs.
|
|
512
550
|
|
|
513
551
|
The one real limitation is that getItem is synchronous while the cloud read is
|
|
514
|
-
not. Gate boot
|
|
552
|
+
not. Gate boot time reads on ready():
|
|
515
553
|
|
|
516
554
|
await BBArcade.storage.ready(); // 'cloud' | 'memory' | 'native'
|
|
517
555
|
startGame();
|
|
@@ -520,8 +558,8 @@ Writes made before hydration are kept and merged with the cloud read (the
|
|
|
520
558
|
game's own write wins, and a removeItem is not resurrected), and nothing is
|
|
521
559
|
pushed to the server until that read has SUCCEEDED. A failed read is retried on
|
|
522
560
|
the next write, and a write that still cannot confirm what the player had is
|
|
523
|
-
refused (rejecting with code error) rather than allowed to overwrite it blind
|
|
524
|
-
so neither a fresh
|
|
561
|
+
refused (rejecting with code error) rather than allowed to overwrite it blind,
|
|
562
|
+
so neither a fresh start write at boot nor a transport blip can wipe an existing
|
|
525
563
|
save. A game that reads
|
|
526
564
|
at boot WITHOUT awaiting ready() may still see an empty map on the first frame.
|
|
527
565
|
|
|
@@ -531,9 +569,9 @@ unwraps it and returns only what save() wrote. A raw blob written before the
|
|
|
531
569
|
shim existed is read back untouched, so turning the shim on never orphans a
|
|
532
570
|
save.
|
|
533
571
|
|
|
534
|
-
## Safe rewarded
|
|
572
|
+
## Safe rewarded ad recipe
|
|
535
573
|
|
|
536
|
-
Prepare at the natural break. Keep the game
|
|
574
|
+
Prepare at the natural break. Keep the game owned button disabled until ready.
|
|
537
575
|
Call show() as the first operation in the direct click/tap handler: no await,
|
|
538
576
|
timer, microtask, animation, state transition, or network call before it.
|
|
539
577
|
|
|
@@ -604,79 +642,79 @@ Bounty referee module, or registered external authority. The signed ticket
|
|
|
604
642
|
binds that tier. joinData cannot select or downgrade it; a ticket/registry
|
|
605
643
|
mismatch fails closed, and a registered external slug never falls back to
|
|
606
644
|
relay. All tiers use the same joinRoom transport, room codes, and client
|
|
607
|
-
connection lifecycle; tier/game
|
|
645
|
+
connection lifecycle; tier/game specific state and event schemas still differ.
|
|
608
646
|
|
|
609
647
|
create/code/match select a room. Relay supplies the host contract below, but
|
|
610
648
|
games build any ready/team/start/kick/rematch protocol on top and those rules
|
|
611
|
-
remain client
|
|
649
|
+
remain client trusted. Referee modules and external authorities define their
|
|
612
650
|
own state, input, event, spectator, lobby, and rematch schemas. A module's
|
|
613
651
|
minPlayers does not automatically gate simulation or start a match. Quick match
|
|
614
|
-
may place a player into a joinable room already in progress; late
|
|
615
|
-
is tier/game
|
|
652
|
+
may place a player into a joinable room already in progress; late join behavior
|
|
653
|
+
is tier/game defined.
|
|
616
654
|
|
|
617
655
|
joinData is always untrusted JSON. The SDK only validates its shape and 1 KiB
|
|
618
656
|
cap. A relay reads roomSize only from the founding seat. Referee modules and
|
|
619
|
-
external authorities validate any game
|
|
657
|
+
external authorities validate any game specific admission fields themselves.
|
|
620
658
|
|
|
621
659
|
### Built-in Bounty relay tier
|
|
622
660
|
|
|
623
|
-
The relay is the default Bounty shared
|
|
661
|
+
The relay is the default Bounty shared service tier for a multiplayer approved
|
|
624
662
|
slug that has no bespoke referee module and is not routed to an external
|
|
625
|
-
authority. It is a casual
|
|
663
|
+
authority. It is a casual lobby with no server code, not an authoritative game
|
|
626
664
|
simulation or anti-cheat boundary.
|
|
627
665
|
|
|
628
666
|
- The first admitted seat fixes capacity from joinData.roomSize. It must be an
|
|
629
|
-
integer from 2 through 64; missing, invalid, or out
|
|
667
|
+
integer from 2 through 64; missing, invalid, or out of range means 8. The room
|
|
630
668
|
may operate with one current occupant. Later joiners cannot change size. Once
|
|
631
669
|
the room is completely empty, the next founder may choose it again. Ship the
|
|
632
|
-
same roomSize from every client so quick
|
|
670
|
+
same roomSize from every client so quick matched rooms found consistently.
|
|
633
671
|
- Welcome and later 1 Hz metadata snapshots are identical for every viewer and
|
|
634
672
|
expose exactly
|
|
635
673
|
{ mode: 'relay', hostId: string | null, size: number, dropped: number }.
|
|
636
674
|
- The oldest retained seat is host. A transient disconnect preserves its seat
|
|
637
675
|
and hostId through the 15-second grace. After the host actually leaves or its
|
|
638
|
-
seat expires, the next
|
|
676
|
+
seat expires, the next oldest retained seat becomes host and live clients get
|
|
639
677
|
{ type: 'relay_host', hostId } through room.on('event'). The founding welcome
|
|
640
678
|
already carries hostId, so no initial relay_host event is required.
|
|
641
679
|
- room.send(data) and room.trySend(data) publicly fan the JSON payload to EVERY
|
|
642
680
|
player, including the sender. There are no private messages, hidden state, or
|
|
643
681
|
viewer filtering. Batches arrive at 20 Hz through room.on('event') as
|
|
644
|
-
{ type: 'relay', messages: [{ from, data }] }, where from is a room
|
|
682
|
+
{ type: 'relay', messages: [{ from, data }] }, where from is a room scoped
|
|
645
683
|
player id. Tick batching adds at most about 50 ms before network latency.
|
|
646
684
|
- Each relay payload's UTF-8 JSON encoding is capped at 1024 bytes. The relay
|
|
647
685
|
accepts at most 15 messages per player per second and 120 per room per second.
|
|
648
686
|
Byte/rate excess is silently dropped and increments cumulative state.dropped,
|
|
649
687
|
visible on a later metadata snapshot. state.dropped does not count malformed,
|
|
650
|
-
stale
|
|
688
|
+
stale sequence, or outer envelope drops.
|
|
651
689
|
- The common outer room guard still caps a client frame at 4096 characters and
|
|
652
690
|
90 accepted envelopes per player per second; server frames are capped at
|
|
653
691
|
256 KiB.
|
|
654
692
|
- Relay rooms are endless. They never emit match_end/room.on('end'), never close
|
|
655
693
|
because a game outcome was claimed, and never report results to Bounty Board.
|
|
656
|
-
All game rules and outcomes are client
|
|
694
|
+
All game rules and outcomes are client trusted; do not use relay claims for
|
|
657
695
|
trusted rewards, standings, or anti-cheat decisions. Implement round boundaries
|
|
658
696
|
in game messages and call room.leave() when the player exits.
|
|
659
697
|
|
|
660
698
|
### Refereed Bounty modules and external authorities
|
|
661
699
|
|
|
662
700
|
A referee validates every input, runs the only game simulation, clears held
|
|
663
|
-
controls on disconnect when its game requires it, and sends viewer
|
|
701
|
+
controls on disconnect when its game requires it, and sends viewer safe state.
|
|
664
702
|
Clients never report authoritative results. A finite referee may emit one
|
|
665
|
-
server
|
|
703
|
+
server authored match_end; the SDK treats it as terminal and does not reconnect.
|
|
666
704
|
There is no SDK reset/rematch method. Endless authorities do not fabricate
|
|
667
705
|
match_end. Registered external authorities define their own socket shutdown and
|
|
668
|
-
subsequent
|
|
706
|
+
subsequent join behavior.
|
|
669
707
|
|
|
670
708
|
Bounty referee modules declare integer min/max players with
|
|
671
709
|
1 <= min <= max <= 64. tickHz is 1-60. snapshotHz may be lower, from 1 through
|
|
672
|
-
tickHz; not every simulation tick emits a snapshot. Simulation
|
|
710
|
+
tickHz; not every simulation tick emits a snapshot. Simulation backed sync has
|
|
673
711
|
a full network round trip, and the SDK provides no client-side prediction,
|
|
674
|
-
interpolation, or rollback. Expect roughly 50-150 ms input
|
|
675
|
-
on real connections; smooth locally, then correct to the next viewer
|
|
712
|
+
interpolation, or rollback. Expect roughly 50-150 ms from input to snapshot
|
|
713
|
+
on real connections; smooth locally, then correct to the next viewer safe
|
|
676
714
|
snapshot. Relay game payloads instead arrive in the public 20 Hz event batches
|
|
677
715
|
described above; its 1 Hz snapshot contains metadata only.
|
|
678
716
|
|
|
679
|
-
### Common Bounty shared
|
|
717
|
+
### Common Bounty shared service room behavior
|
|
680
718
|
|
|
681
719
|
- Generated rooms use four-character invite codes. match: true works for relay
|
|
682
720
|
and referee rooms: it uses the current public room while live occupancy plus
|
|
@@ -684,12 +722,12 @@ described above; its 1 Hz snapshot contains metadata only.
|
|
|
684
722
|
failed occupancy probes, or reservations roll a new room, so live occupancy
|
|
685
723
|
can roll before reaching the room's capacity. A matched room.code is also an
|
|
686
724
|
invite code.
|
|
687
|
-
- A lost last
|
|
725
|
+
- A lost last seat or ended room race triggers at most 2 re-matchmaking retries
|
|
688
726
|
(3 total attempts). Final rejection has code rejected and detail room_full or
|
|
689
727
|
room_over, whichever the last attempt returned.
|
|
690
728
|
- When a finite Bounty referee module ends, it broadcasts match_end once, closes
|
|
691
729
|
its sockets, and subsequent joins to that room reject with room_over. This
|
|
692
|
-
|
|
730
|
+
closing rule does not apply to the endless relay tier.
|
|
693
731
|
- Disconnected seats are reserved for 15 seconds. The SDK obtains fresh tickets
|
|
694
732
|
and makes up to 3 reconnect attempts. During grace, the player remains in
|
|
695
733
|
room.players and playerLeave is delayed until the seat expires. room.leave()
|
|
@@ -701,7 +739,7 @@ described above; its 1 Hz snapshot contains metadata only.
|
|
|
701
739
|
|
|
702
740
|
Public quick match is unavailable on registered external authorities, which
|
|
703
741
|
own their capacity, lobby lifecycle, and any matchmaking. Their reviewed
|
|
704
|
-
limits can differ from the Bounty shared
|
|
742
|
+
limits can differ from the Bounty shared service defaults, and an external
|
|
705
743
|
authority may be endless.
|
|
706
744
|
|
|
707
745
|
joinRoom rejection map:
|
|
@@ -721,12 +759,12 @@ or room_over. Generic rejected has multiple causes; do not blind retry or show
|
|
|
721
759
|
Multiplayer is curated per game slug. After approval, Bounty Board assigns one
|
|
722
760
|
of three rails: (a) the built-in casual relay, (b) a bespoke Bounty referee
|
|
723
761
|
module, or (c) a registered external authority with a dedicated ticket key.
|
|
724
|
-
Relay is the shared
|
|
762
|
+
Relay is the shared service default when no bespoke module or external route is
|
|
725
763
|
registered; clients cannot select the tier. External servers keep their existing
|
|
726
|
-
simulation and add only an isolated signed
|
|
727
|
-
envelope
|
|
764
|
+
simulation and add only an isolated signed ticket adapter plus the SDK room
|
|
765
|
+
envelope. Never create a second simulation beside one.
|
|
728
766
|
|
|
729
|
-
Local Bounty shared
|
|
767
|
+
Local Bounty shared service development bypasses the host ticket handshake only
|
|
730
768
|
with paired roomUrl and ticket values (for example wrangler dev with
|
|
731
769
|
DEV_ALLOW_UNSIGNED=1). A registered slug resolves to its referee module; another
|
|
732
770
|
well-formed slug resolves to relay:
|
|
@@ -754,28 +792,34 @@ https://www.bountyboard.gg/arcade/sdk/external-authoritative-servers.txt
|
|
|
754
792
|
- Mock rewarded ready, unavailable, dismissed, error, and viewed; only viewed grants.
|
|
755
793
|
- Assert prepared.show() is called directly from the player gesture.
|
|
756
794
|
- Test initial room.state/players rendering before the first later event.
|
|
757
|
-
- Test multiplayer reconnect/grace, trySend false, tier
|
|
795
|
+
- Test multiplayer reconnect/grace, trySend false, tier appropriate events/state,
|
|
758
796
|
and leave.
|
|
759
|
-
- Test create/code, and match on the Bounty shared
|
|
760
|
-
- For relay, test roomSize default/range, public fan
|
|
797
|
+
- Test create/code, and match on the Bounty shared service tiers.
|
|
798
|
+
- For relay, test roomSize default/range, public fan out (including sender), host
|
|
761
799
|
succession after grace, byte/rate drops, state.dropped, and absence of end.
|
|
762
|
-
- For referee/external rooms, test viewer
|
|
763
|
-
and game
|
|
800
|
+
- For referee/external rooms, test viewer safe snapshots, authoritative results,
|
|
801
|
+
and game defined start/ready/late join rules against that authority.
|
|
764
802
|
- Test the identical artifact in its standalone location and inside Bounty Board.
|
|
765
803
|
|
|
766
804
|
## Troubleshooting map
|
|
767
805
|
|
|
806
|
+
- BBArcade is not defined, script blocked with
|
|
807
|
+
ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep -> the
|
|
808
|
+
embedding page is cross-origin isolated -> the hosted file already sends CORP
|
|
809
|
+
cross-origin; if a proxy strips it, add crossorigin="anonymous" to the tag or
|
|
810
|
+
vendor the file into the build.
|
|
768
811
|
- Boot waits forever -> game startup depends on an SDK promise -> start init
|
|
769
812
|
fire-and-forget and give every promise feature a standalone outcome.
|
|
770
|
-
- save/load unsupported -> not a Bounty
|
|
771
|
-
- save/load unauthenticated -> guest player -> continue with defaults/fallback
|
|
813
|
+
- save/load unsupported -> not a Bounty hosted upload -> use own same-origin store.
|
|
814
|
+
- save/load unauthenticated -> guest player -> continue with defaults/fallback,
|
|
815
|
+
and gate on getPlayer() so the guest case is a branch instead of a catch.
|
|
772
816
|
- SecurityError touching localStorage/sessionStorage/document.cookie -> hosted
|
|
773
817
|
builds run on an opaque origin -> use save()/load(), or storage.install() for
|
|
774
818
|
an engine export that cannot be rewired.
|
|
775
819
|
- Shimmed storage reads empty at boot -> the cloud read had not landed yet ->
|
|
776
820
|
await BBArcade.storage.ready() before restoring progress.
|
|
777
|
-
- storage.mode is 'memory' -> guest, standalone, or off
|
|
778
|
-
for the session but is never persisted; keep first
|
|
821
|
+
- storage.mode is 'memory' -> guest, standalone, or off host -> storage works
|
|
822
|
+
for the session but is never persisted; keep first run defaults sane.
|
|
779
823
|
- direct_user_action_required -> show() lost browser activation -> make show()
|
|
780
824
|
the first line of the click/tap handler.
|
|
781
825
|
- host_disabled/unavailable -> host or inventory is not ready -> keep the normal
|
|
@@ -787,14 +831,15 @@ https://www.bountyboard.gg/arcade/sdk/external-authoritative-servers.txt
|
|
|
787
831
|
- trySend false -> disconnected/raced socket or local serialization/send failure
|
|
788
832
|
-> stop sending, show reconnecting when disconnected, and resume only after
|
|
789
833
|
connection.connected is true. A true result still does not prove acceptance.
|
|
790
|
-
- match rejected on an external authority -> external matchmaking is authority
|
|
834
|
+
- match rejected on an external authority -> external matchmaking is authority
|
|
791
835
|
owned -> use code/create or that authority's separately documented flow.
|
|
792
836
|
|
|
793
837
|
## More
|
|
794
838
|
|
|
795
839
|
- Human guide: https://www.bountyboard.gg/arcade/sdk
|
|
840
|
+
- npm package: https://www.npmjs.com/package/@bountyboard/arcade-sdk
|
|
796
841
|
- Relay room guide: https://www.bountyboard.gg/arcade/sdk/relay-rooms.txt
|
|
797
842
|
- Standalone types: https://www.bountyboard.gg/arcade-sdk.d.ts
|
|
798
843
|
- Submit a game: https://www.bountyboard.gg/arcade/submit
|
|
799
|
-
- Package README, changelog, AGENTS.md, design playbook, relay
|
|
800
|
-
external
|
|
844
|
+
- Package README, changelog, AGENTS.md, design playbook, relay room guide, and
|
|
845
|
+
external authority guide all ship in the npm package.
|
package/docs/relay-rooms.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Relay rooms
|
|
2
2
|
|
|
3
|
-
Relay rooms are the
|
|
4
|
-
multiplayer approved gets them automatically: hosted lobbies, shareable
|
|
5
|
-
codes, public quick match, reconnect handling, and message fan
|
|
6
|
-
registering a server module or running any backend. The same
|
|
7
|
-
drives all tiers, so a game can start on relay rooms and
|
|
8
|
-
refereed module or an external authoritative server later without
|
|
9
|
-
rewrites.
|
|
3
|
+
Relay rooms are the multiplayer tier that needs no server code. Every game with
|
|
4
|
+
SDK multiplayer approved gets them automatically: hosted lobbies, shareable
|
|
5
|
+
invite codes, public quick match, reconnect handling, and message fan out,
|
|
6
|
+
without registering a server module or running any backend. The same
|
|
7
|
+
`joinRoom()` API drives all tiers, so a game can start on relay rooms and
|
|
8
|
+
graduate to a refereed module or an external authoritative server later without
|
|
9
|
+
client rewrites.
|
|
10
10
|
|
|
11
11
|
## What the server owns (and what it refuses to)
|
|
12
12
|
|
|
@@ -15,13 +15,13 @@ The relay server authoritatively owns everything it can own *generically*:
|
|
|
15
15
|
- the roster and seat cap,
|
|
16
16
|
- host designation and succession,
|
|
17
17
|
- admission (signed tickets, room binding, per-seat connection caps),
|
|
18
|
-
- message fan
|
|
18
|
+
- message fan out with rate and size guardrails.
|
|
19
19
|
|
|
20
20
|
It deliberately does not interpret game payloads, so it cannot referee them.
|
|
21
|
-
Relay outcomes are client
|
|
21
|
+
Relay outcomes are client trusted: relay rooms never emit `match_end`, never
|
|
22
22
|
report results to Bounty Board, and never feed win/loss records or
|
|
23
|
-
leaderboards. Games that need authoritative results
|
|
24
|
-
recorded, or reward
|
|
23
|
+
leaderboards. Games that need authoritative results (anything ranked,
|
|
24
|
+
recorded, or reward adjacent) must use a refereed Bounty hosted module or a
|
|
25
25
|
registered external authority instead.
|
|
26
26
|
|
|
27
27
|
## Joining
|
|
@@ -39,7 +39,7 @@ const room = await joinRoom({
|
|
|
39
39
|
- `code` joins a friend's room.
|
|
40
40
|
- `match: true` enters the game's open public room, or founds one.
|
|
41
41
|
|
|
42
|
-
All three go through the standard signed
|
|
42
|
+
All three go through the standard signed ticket flow; nothing about relay
|
|
43
43
|
rooms weakens admission.
|
|
44
44
|
|
|
45
45
|
### Room size
|
|
@@ -52,7 +52,7 @@ next arrival re-founds it. Ship the same `roomSize` from every client of
|
|
|
52
52
|
your game so matchmade rooms are founded consistently.
|
|
53
53
|
|
|
54
54
|
Quick match fills rooms to the founded size. Because matchmaking reserves
|
|
55
|
-
seats conservatively, a burst of simultaneous quick
|
|
55
|
+
seats conservatively, a burst of simultaneous quick match joins can briefly
|
|
56
56
|
overshoot a small room; losers of that race receive `room_full` and the SDK
|
|
57
57
|
automatically retries into the next room.
|
|
58
58
|
|
|
@@ -61,7 +61,7 @@ automatically retries into the next room.
|
|
|
61
61
|
`room.send(payload)` relays `payload` to **every** player in the room,
|
|
62
62
|
including the sender. There are no private messages: every client sees every
|
|
63
63
|
payload, so never send secrets (hidden roles, private hands) through a relay
|
|
64
|
-
room. Payload semantics are entirely yours
|
|
64
|
+
room. Payload semantics are entirely yours. The server never reads them.
|
|
65
65
|
|
|
66
66
|
Messages are batched per server tick and delivered through `room.on('event')`:
|
|
67
67
|
|
|
@@ -78,7 +78,7 @@ room.on('event', event => {
|
|
|
78
78
|
});
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Guardrails (over
|
|
81
|
+
Guardrails (traffic over the limit is dropped silently; the state snapshot's
|
|
82
82
|
`dropped` counter is the debugging breadcrumb):
|
|
83
83
|
|
|
84
84
|
| Limit | Value |
|
|
@@ -101,9 +101,9 @@ grace keeps both the seat and the host role, so a brief network blip does not
|
|
|
101
101
|
thrash host succession.
|
|
102
102
|
|
|
103
103
|
Use the host as your game's coordinator: it can own spawn timing, level
|
|
104
|
-
seeds, or authoritative
|
|
105
|
-
model
|
|
106
|
-
couch
|
|
104
|
+
seeds, or authoritative enough game state for casual play. Remember the trust
|
|
105
|
+
model. A modified client can lie, which is acceptable for casual co-op and
|
|
106
|
+
couch style games and not acceptable for anything with stakes.
|
|
107
107
|
|
|
108
108
|
## State snapshots
|
|
109
109
|
|
|
@@ -123,12 +123,12 @@ current.
|
|
|
123
123
|
Relay rooms never settle: there is no `end` event, and a room lives while it
|
|
124
124
|
has players (seats survive a 15-second reconnect grace). Implement your own
|
|
125
125
|
notion of rounds or matches in game messages, and call `room.leave()` when
|
|
126
|
-
the player exits
|
|
126
|
+
the player exits. Leaving is immediate and never reconnects.
|
|
127
127
|
|
|
128
128
|
## Graduating to a refereed tier
|
|
129
129
|
|
|
130
|
-
If your game outgrows client trust
|
|
131
|
-
paid
|
|
130
|
+
If your game outgrows client trust (ranked results, tournaments, anything
|
|
131
|
+
paid), the ticket flow, room codes, and quick match all stay the same; the
|
|
132
132
|
room's simulation moves server-side. Contact Bounty Board about a refereed
|
|
133
|
-
Bounty
|
|
133
|
+
Bounty hosted module, or keep your own server and register it under the
|
|
134
134
|
external authority contract (see `external-authoritative-servers.md`).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bountyboard/arcade-sdk",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Bounty Board Arcade SDK
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Bounty Board Arcade SDK: leaderboards, cloud saves, rewarded ads, A/B variants, and multiplayer rooms for games on bountyboard.gg",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bountyboard",
|
|
7
7
|
"arcade",
|