@bountyboard/arcade-sdk 1.2.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/AGENTS.md +12 -3
- package/CHANGELOG.md +48 -1
- package/README.md +56 -9
- package/dist/{chunk-KDBBR532.js → chunk-PYW5F45R.js} +334 -2
- package/dist/chunk-PYW5F45R.js.map +1 -0
- package/dist/global-sdk.d.ts +47 -0
- package/dist/global.js +343 -10
- package/dist/index.cjs +333 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/multiplayer.cjs +342 -10
- package/dist/multiplayer.cjs.map +1 -1
- package/dist/multiplayer.d.cts +2 -2
- package/dist/multiplayer.d.ts +2 -2
- package/dist/multiplayer.js +1 -1
- package/dist/{types-dtsNXYZW.d.cts → types-B5bqWnHH.d.cts} +46 -1
- package/dist/{types-dtsNXYZW.d.ts → types-B5bqWnHH.d.ts} +46 -1
- package/docs/external-authoritative-servers.md +21 -21
- package/docs/game-design-playbook.md +25 -25
- package/docs/llms.txt +257 -127
- package/docs/relay-rooms.md +22 -22
- package/package.json +2 -2
- package/dist/chunk-KDBBR532.js.map +0 -1
package/docs/llms.txt
CHANGED
|
@@ -1,26 +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.
|
|
6
|
-
Current browser v1/source addition: public quick match (queued for npm 1.2.0)
|
|
4
|
+
Package: @bountyboard/arcade-sdk (https://www.npmjs.com/package/@bountyboard/arcade-sdk)
|
|
5
|
+
Stable npm release: 1.4.0
|
|
7
6
|
Wire protocol: 1
|
|
8
7
|
|
|
9
8
|
This file is the complete integration contract for coding agents integrating an
|
|
10
9
|
HTML5 game. The package TypeScript declarations remain the exact public type
|
|
11
|
-
reference.
|
|
12
|
-
on all three approved rails:
|
|
13
|
-
shared
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
external-authority adapter guide is public now and will also join the packaged
|
|
18
|
-
docs in 1.2.0.
|
|
10
|
+
reference. npm 1.4.0, the /arcade-sdk/v1.js browser artifact, and repository
|
|
11
|
+
source all expose the same surface. Rooms open on all three approved rails:
|
|
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
|
+
adapter guide ships in the package and is public at the URL above.
|
|
19
16
|
|
|
20
17
|
## Non-negotiable integration rules
|
|
21
18
|
|
|
22
19
|
1. The SDK is never load-bearing. A game must boot and remain fully playable
|
|
23
|
-
with no Bounty Board parent, no logged
|
|
20
|
+
with no Bounty Board parent, no logged in player, no ad inventory, no cloud
|
|
24
21
|
save, and no multiplayer authority.
|
|
25
22
|
2. Call gameOver() exactly once per run and use integer scores. The host/server
|
|
26
23
|
enforces per-game plausibility caps.
|
|
@@ -28,17 +25,24 @@ docs in 1.2.0.
|
|
|
28
25
|
death prompts, and ad breaks are not active play.
|
|
29
26
|
4. getPlayer() can be null and avatarUrl can be null. The SDK never exposes
|
|
30
27
|
account ids, emails, or roles.
|
|
31
|
-
5. Store all progress in one save(string) blob (about 1 MB).
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
34
35
|
status is ready, call prepared.show() directly from the click/tap handler,
|
|
35
36
|
and grant only when the final status is viewed.
|
|
36
37
|
7. Multiplayer has three rails. Referee modules and external authorities own
|
|
37
38
|
simulation/results and receive client inputs. The casual relay owns signed
|
|
38
39
|
admission, roster, capacity, host succession, reconnect grace, and public
|
|
39
|
-
message fan
|
|
40
|
+
message fan out, but it does not referee game state or outcomes. Catch every
|
|
40
41
|
joinRoom() rejection and keep solo/standalone play available.
|
|
41
|
-
8. lockToHost() is an optional anti-rehosting deterrent, not DRM. Call it before
|
|
42
|
+
8. lockToHost() is an optional anti-rehosting deterrent, not DRM. Call it before
|
|
43
|
+
boot. allow lists the domains the STUDIO hosts the build on, so a build that
|
|
44
|
+
only runs on Bounty Board passes no allow entries (or skips the call);
|
|
45
|
+
play.yourgame.com in every example is a placeholder, never a real value.
|
|
42
46
|
|
|
43
47
|
## Install and distribution
|
|
44
48
|
|
|
@@ -47,16 +51,21 @@ Preferred for games with a build step:
|
|
|
47
51
|
npm install @bountyboard/arcade-sdk
|
|
48
52
|
import { BBArcade } from '@bountyboard/arcade-sdk';
|
|
49
53
|
|
|
50
|
-
The package is zero
|
|
54
|
+
The package is zero dependency, typed, ESM + CommonJS, and SSR safe. Importing
|
|
51
55
|
the module does not install a window.BBArcade global.
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
Script tag, no build step:
|
|
54
58
|
|
|
55
59
|
<script src="https://www.bountyboard.gg/arcade-sdk/v1.js"></script>
|
|
56
60
|
|
|
57
61
|
This installs window.BBArcade. Standalone declarations:
|
|
58
62
|
https://www.bountyboard.gg/arcade-sdk.d.ts
|
|
59
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
|
+
|
|
60
69
|
Do not mix npm and the script tag in one page. A bundler that specifically
|
|
61
70
|
wants the global may import @bountyboard/arcade-sdk/global.
|
|
62
71
|
|
|
@@ -64,7 +73,7 @@ Multiplayer module import:
|
|
|
64
73
|
|
|
65
74
|
import { joinRoom } from '@bountyboard/arcade-sdk/multiplayer';
|
|
66
75
|
|
|
67
|
-
Script
|
|
76
|
+
Script tag multiplayer is BBArcade.multiplayer.joinRoom(...).
|
|
68
77
|
|
|
69
78
|
Public package exports:
|
|
70
79
|
|
|
@@ -79,29 +88,39 @@ Public package exports:
|
|
|
79
88
|
|
|
80
89
|
Distribution format does not determine capabilities; the embedding host does.
|
|
81
90
|
|
|
82
|
-
- Bounty
|
|
91
|
+
- Bounty hosted upload:
|
|
83
92
|
lifecycle/scores supported; player identity/variants supported; cloud
|
|
84
|
-
save/load available to logged
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
save/load available to logged in players; native localStorage/sessionStorage/
|
|
94
|
+
IndexedDB/cookies all THROW (opaque origin), so engine exports need
|
|
95
|
+
storage.install(); ads available to approved games; multiplayer available
|
|
96
|
+
after per-game approval, with relay as the default Bounty shared service tier
|
|
97
|
+
when no bespoke referee module is registered.
|
|
87
98
|
- Approved external URL embed inside the Bounty Board player:
|
|
88
99
|
lifecycle/scores, identity, variants, and approved multiplayer (including the
|
|
89
100
|
Bounty relay tier) are supported; rewarded ads are unavailable (no payable
|
|
90
101
|
per-game attribution yet); cloud save/load is unsupported.
|
|
91
102
|
- Standalone or opened directly on the game's own site:
|
|
92
103
|
fire-and-forget calls no-op; init resolves immediately; getPlayer returns
|
|
93
|
-
null; getVariant returns the alphabetical control; host
|
|
104
|
+
null; getVariant returns the alphabetical control; host only promise APIs
|
|
94
105
|
reject unsupported or return an unavailable outcome. An unanswered non-Bounty
|
|
95
106
|
embed uses an approximately 1.5-second init/getPlayer grace instead.
|
|
96
107
|
|
|
97
|
-
Guests are normal. getPlayer resolves null and account
|
|
108
|
+
Guests are normal. getPlayer resolves null and account backed calls may reject
|
|
98
109
|
with code unauthenticated.
|
|
99
110
|
|
|
111
|
+
Leaderboards need one thing outside the code: the studio must declare
|
|
112
|
+
Leaderboards for the game in its Arcade submission (or later by editing it).
|
|
113
|
+
Until then the host rejects every posted score and the game sees nothing,
|
|
114
|
+
because submitScore()/gameOver() are fire-and-forget. Optional per-game score
|
|
115
|
+
ceilings live beside that declaration and reject implausible values.
|
|
116
|
+
|
|
100
117
|
## Minimal correct lifecycle
|
|
101
118
|
|
|
102
119
|
import { BBArcade } from '@bountyboard/arcade-sdk';
|
|
103
120
|
|
|
104
|
-
|
|
121
|
+
// Optional, before boot. Bounty Board and localhost are always allowed, so
|
|
122
|
+
// allow lists only domains YOU host on; omit it when Bounty Board hosts the build.
|
|
123
|
+
BBArcade.lockToHost({ allow: ['play.yourgame.com'] }); // placeholder domain
|
|
105
124
|
void BBArcade.init(); // never gate boot on it
|
|
106
125
|
BBArcade.gameLoadingFinished(); // first playable scene ready
|
|
107
126
|
|
|
@@ -110,11 +129,11 @@ with code unauthenticated.
|
|
|
110
129
|
BBArcade.gameplayStop(); // pause/death/menu/ad
|
|
111
130
|
BBArcade.gameOver(Math.trunc(finalScore)); // exactly once per run
|
|
112
131
|
|
|
113
|
-
For a shared
|
|
132
|
+
For a shared seed Daily, pass { mode: 'daily' } to submitScore and gameOver.
|
|
114
133
|
|
|
115
134
|
## Exact shared types and configuration
|
|
116
135
|
|
|
117
|
-
All rejected SDK promises use a real Error
|
|
136
|
+
All rejected SDK promises use a real Error:
|
|
118
137
|
|
|
119
138
|
type BBArcadeErrorCode =
|
|
120
139
|
| 'unsupported' // no compatible host/capability
|
|
@@ -123,12 +142,6 @@ All rejected SDK promises use a real Error. Stable npm 1.1.0 has this shape:
|
|
|
123
142
|
| 'rejected' // host/authority refused the payload or room
|
|
124
143
|
| 'error'; // timeout, transport, or server failure
|
|
125
144
|
|
|
126
|
-
interface BBArcadeError extends Error {
|
|
127
|
-
code: BBArcadeErrorCode;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
The current browser v1/source build, and npm 1.2.0 when released, add:
|
|
131
|
-
|
|
132
145
|
interface BBArcadeError extends Error {
|
|
133
146
|
code: BBArcadeErrorCode;
|
|
134
147
|
detail?: string; // raw authority reason when available
|
|
@@ -158,8 +171,8 @@ Core configuration shapes:
|
|
|
158
171
|
lockToHost defaults allow bountyboard.gg and its subdomains, the fixed Bounty
|
|
159
172
|
staging host, localhost, and 127.0.0.1. allow extends rather than replaces that
|
|
160
173
|
list. signed mode requests an ECDSA origin attestation; it falls back to the
|
|
161
|
-
best
|
|
162
|
-
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
|
|
163
176
|
blocks.
|
|
164
177
|
|
|
165
178
|
Player and score shapes:
|
|
@@ -167,7 +180,21 @@ Player and score shapes:
|
|
|
167
180
|
interface BBArcadePlayer { name: string; avatarUrl: string | null }
|
|
168
181
|
// submitScore/gameOver use the inline option shape { mode?: 'daily' }.
|
|
169
182
|
|
|
170
|
-
|
|
183
|
+
Storage compatibility shapes:
|
|
184
|
+
|
|
185
|
+
type BBArcadeStorageMode =
|
|
186
|
+
| 'native' // a real Storage works here; the SDK changed nothing
|
|
187
|
+
| 'cloud' // shim installed, syncing to the player's Bounty Board save
|
|
188
|
+
| 'memory'; // shim installed but nothing can persist (guest/standalone)
|
|
189
|
+
|
|
190
|
+
interface BBArcadeStorage {
|
|
191
|
+
install(): BBArcadeStorageMode; // idempotent; no-op when native works
|
|
192
|
+
ready(): Promise<BBArcadeStorageMode>;
|
|
193
|
+
flush(): Promise<void>; // rejects with the save() error codes
|
|
194
|
+
readonly mode: BBArcadeStorageMode;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Rewarded ad options and results:
|
|
171
198
|
|
|
172
199
|
interface BBArcadeRewardedAdOptions {
|
|
173
200
|
placement?: string; name?: string; reward?: string; adBreakId?: string;
|
|
@@ -259,15 +286,26 @@ Lifecycle and scoring:
|
|
|
259
286
|
Player data and experiments:
|
|
260
287
|
|
|
261
288
|
- save(blob: string): Promise<void>
|
|
262
|
-
One blob, about 1 MB. Requires a Bounty
|
|
289
|
+
One blob, about 1 MB. Requires a Bounty hosted upload and logged in player.
|
|
263
290
|
Oversized blobs reject too_large immediately via a client-side precheck
|
|
264
291
|
against the same 1 MiB cap the server enforces.
|
|
265
292
|
- load(): Promise<string | null>
|
|
266
|
-
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.
|
|
295
|
+
- storage: BBArcadeStorage
|
|
296
|
+
localStorage compatibility for hosted builds. install(): BBArcadeStorageMode
|
|
297
|
+
replaces a throwing window.localStorage with a Storage shaped object backed
|
|
298
|
+
by the cloud save, and is a no-op where a real Storage works. ready():
|
|
299
|
+
Promise<BBArcadeStorageMode> resolves once the cloud read has landed.
|
|
300
|
+
flush(): Promise<void> forces the debounced write out now. mode is the
|
|
301
|
+
current BBArcadeStorageMode ('native' | 'cloud' | 'memory'). The script tag
|
|
302
|
+
build calls install() for you at load; module consumers call it themselves
|
|
303
|
+
before boot.
|
|
267
304
|
- getPlayer(): Promise<{ name: string, avatarUrl: string | null } | null>
|
|
268
|
-
Display identity only; always handle null.
|
|
269
|
-
|
|
270
|
-
|
|
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.
|
|
307
|
+
- onPlayerChange(handler): () => void
|
|
308
|
+
Subscribes to CHANGES in the display identity (mid session login/logout).
|
|
271
309
|
The handler receives the same { name, avatarUrl } | null shape as
|
|
272
310
|
getPlayer() and runs only when the identity actually changes; subscribing
|
|
273
311
|
does not replay the current value. Returns an unsubscribe function.
|
|
@@ -278,9 +316,11 @@ Player data and experiments:
|
|
|
278
316
|
item. Do not re-randomize client-side.
|
|
279
317
|
|
|
280
318
|
save/load can reject with every BBArcadeErrorCode. load resolves null only when
|
|
281
|
-
the logged
|
|
282
|
-
|
|
283
|
-
|
|
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.
|
|
284
324
|
|
|
285
325
|
unsupported | unauthenticated | too_large | rejected | error
|
|
286
326
|
|
|
@@ -289,7 +329,7 @@ Rewarded ads:
|
|
|
289
329
|
- prepareRewardedAd(options?): Promise<BBArcadeRewardedAdPreparation>
|
|
290
330
|
Recommended. Alias: prepareRewardedBreak(). Prepared show() is one-shot.
|
|
291
331
|
- rewardedAd(options?): Promise<BBArcadeRewardedAdResult>
|
|
292
|
-
Low-level structured
|
|
332
|
+
Low-level structured result API. Alias: showRewardedAd().
|
|
293
333
|
- rewardedBreak(options | onStart): Promise<boolean>
|
|
294
334
|
Deprecated compatibility helper. New games must use the prepared flow.
|
|
295
335
|
- preloadRewardedAds(options?): Promise<boolean>
|
|
@@ -307,16 +347,16 @@ ad_break_unavailable, ad_break_timeout, no_rewarded_ad, ad_in_progress,
|
|
|
307
347
|
show_ad_error, direct_user_action_required, before_reward_error, and
|
|
308
348
|
ad_break_error.
|
|
309
349
|
|
|
310
|
-
Multiplayer
|
|
350
|
+
Multiplayer:
|
|
311
351
|
|
|
312
352
|
type BBArcadeMpJoinOptions = {
|
|
313
353
|
code?: string;
|
|
314
354
|
create?: boolean;
|
|
315
|
-
match?: boolean;
|
|
355
|
+
match?: boolean; // public quick match; Bounty shared service tiers only
|
|
316
356
|
joinData?: Readonly<Record<string, unknown>>;
|
|
317
357
|
roomUrl?: string; // local development override; provide with ticket
|
|
318
358
|
ticket?: string; // local development override; provide with roomUrl
|
|
319
|
-
timeoutMs?: number; //
|
|
359
|
+
timeoutMs?: number; // welcome timeout override, clamped 1000-60000 ms
|
|
320
360
|
};
|
|
321
361
|
|
|
322
362
|
interface BBArcadeMpPlayer {
|
|
@@ -350,7 +390,7 @@ Multiplayer (current browser v1/source shape; stable npm 1.1.0 omits match):
|
|
|
350
390
|
players: BBArcadeMpPlayer[];
|
|
351
391
|
state: unknown;
|
|
352
392
|
connected: boolean;
|
|
353
|
-
latencyMs: number | null; //
|
|
393
|
+
latencyMs: number | null; // join handshake estimate, refreshed on reconnect
|
|
354
394
|
send(input: unknown): void;
|
|
355
395
|
trySend(input: unknown): boolean;
|
|
356
396
|
on<K extends keyof BBArcadeMpRoomEvents>(
|
|
@@ -364,7 +404,7 @@ Multiplayer (current browser v1/source shape; stable npm 1.1.0 omits match):
|
|
|
364
404
|
joinRoom(options?: BBArcadeMpJoinOptions): Promise<BBArcadeMpRoom>;
|
|
365
405
|
}
|
|
366
406
|
|
|
367
|
-
Relay runtime payloads use these exact shapes through the otherwise
|
|
407
|
+
Relay runtime payloads use these exact shapes through the otherwise unknown
|
|
368
408
|
room.state and room.on('event') values:
|
|
369
409
|
|
|
370
410
|
interface BBArcadeRelayState {
|
|
@@ -382,16 +422,16 @@ room.state and room.on('event') values:
|
|
|
382
422
|
| { type: 'relay_host'; hostId: string | null };
|
|
383
423
|
|
|
384
424
|
BBArcadeRelayState and BBArcadeRelayEvent are documentation names, not package
|
|
385
|
-
exports; the public SDK intentionally types game/tier
|
|
425
|
+
exports; the public SDK intentionally types game/tier defined state and events
|
|
386
426
|
as unknown.
|
|
387
427
|
|
|
388
428
|
- joinRoom(options?): Promise<BBArcadeMpRoom>
|
|
389
429
|
No options and { create: true } both generate a 4-character invite code from
|
|
390
430
|
ABCDEFGHJKLMNPQRSTUVWXYZ23456789. A supplied code is uppercased. match: true
|
|
391
|
-
is Bounty
|
|
431
|
+
is Bounty hosted public quick match and excludes code, create, roomUrl, and
|
|
392
432
|
ticket. roomUrl+ticket bypass the host handshake for local development only;
|
|
393
433
|
provide both (a lone value is not an override).
|
|
394
|
-
- joinData must be a non-null, non-array JSON
|
|
434
|
+
- joinData must be a non-null, non-array JSON serializable object whose UTF-8
|
|
395
435
|
JSON encoding is at most 1 KiB. Cycles, arrays, primitives, and oversized data
|
|
396
436
|
reject with code rejected. Never include credentials or secrets.
|
|
397
437
|
- joinRoom resolves only after the authority sends welcome (10-second default
|
|
@@ -400,26 +440,25 @@ as unknown.
|
|
|
400
440
|
At resolution, code/playerId/players/state/connected already hold the initial
|
|
401
441
|
lobby state. There is no replayed initial snapshot: render those fields first,
|
|
402
442
|
then subscribe to future events.
|
|
403
|
-
- room.latencyMs reports the join
|
|
443
|
+
- room.latencyMs reports the join handshake latency in ms (socket open to
|
|
404
444
|
server welcome: ticket verification plus one round trip), refreshed on every
|
|
405
445
|
successful (re)connect and null until the first welcome. Use it to tune
|
|
406
|
-
render
|
|
407
|
-
- 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
|
|
408
448
|
treats the value as an input; a relay room treats it as a public game
|
|
409
449
|
message. send discards local write status. trySend
|
|
410
450
|
returns true only when JSON was written to an open socket; it does NOT mean
|
|
411
451
|
the authority accepted the input. false means disconnected, raced closed, or
|
|
412
452
|
serialization/WebSocket.send failed. The authority still validates,
|
|
413
|
-
sequences, and may
|
|
453
|
+
sequences, and may drop inputs above its rate limit.
|
|
414
454
|
- on returns an unsubscribe function. The SDK updates room.players before
|
|
415
455
|
playerJoin/playerLeave handlers and room.state before snapshot handlers.
|
|
416
456
|
- leave intentionally closes the Room and permanently disables reconnect for it.
|
|
417
457
|
A welcome that arrives after leave() (e.g. racing a reconnect) is ignored and
|
|
418
458
|
never flips the room back to connected.
|
|
419
|
-
- A pre-welcome room error rejects joinRoom with BBArcadeError.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
error.code. An error after welcome emits room.on('error', { code }).
|
|
459
|
+
- A pre-welcome room error rejects joinRoom with BBArcadeError. Raw reasons such
|
|
460
|
+
as room_full/room_over are carried in error.detail when available.
|
|
461
|
+
An error after welcome emits room.on('error', { code }).
|
|
423
462
|
Connection loss emits connection/close events and may start automatic
|
|
424
463
|
reconnect.
|
|
425
464
|
|
|
@@ -427,31 +466,112 @@ BBArcade.version and the exported PROTOCOL_VERSION are the core bb-arcade
|
|
|
427
466
|
postMessage wire-protocol version. They are not npm semver and are not a
|
|
428
467
|
version field on multiplayer WebSocket frames.
|
|
429
468
|
|
|
430
|
-
## Cloud
|
|
469
|
+
## Cloud save recipe
|
|
470
|
+
|
|
471
|
+
Hosted uploads have an opaque origin where a real localStorage is not merely
|
|
472
|
+
empty. Reading window.localStorage THROWS a SecurityError, as do
|
|
473
|
+
sessionStorage, IndexedDB, and document.cookie. SDK save is the primary store
|
|
474
|
+
there. URL embeds and standalone builds need their own same-origin fallback.
|
|
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();
|
|
431
482
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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:
|
|
435
492
|
|
|
436
493
|
const blob = JSON.stringify(progress);
|
|
437
494
|
try {
|
|
438
495
|
await BBArcade.save(blob);
|
|
439
496
|
} catch (error) {
|
|
440
497
|
if (error.code === 'unsupported') localStorage.setItem('progress', blob);
|
|
498
|
+
else if (error.code !== 'unauthenticated') reportSaveFailure(error.code);
|
|
441
499
|
}
|
|
442
500
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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.
|
|
449
508
|
|
|
450
|
-
|
|
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.
|
|
451
512
|
|
|
452
|
-
##
|
|
513
|
+
## localStorage shim for engine exports
|
|
453
514
|
|
|
454
|
-
|
|
515
|
+
Prefer save()/load() when you control the source. The shim exists for engine
|
|
516
|
+
runtimes whose storage layer cannot be rewired without patching engine
|
|
517
|
+
internals: GameMaker HTML5 (ini_open/ini_write_*/game_save all sit on
|
|
518
|
+
localStorage), Godot, Unity, and Construct.
|
|
519
|
+
|
|
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
|
|
527
|
+
the shim at load and window.localStorage starts working:
|
|
528
|
+
|
|
529
|
+
<script src="https://www.bountyboard.gg/arcade-sdk/v1.js"></script>
|
|
530
|
+
<script src="html5game/YourGame.js"></script>
|
|
531
|
+
|
|
532
|
+
Module consumers keep a side effect free import and install it themselves,
|
|
533
|
+
before any engine code runs:
|
|
534
|
+
|
|
535
|
+
import { BBArcade } from '@bountyboard/arcade-sdk';
|
|
536
|
+
BBArcade.storage.install();
|
|
537
|
+
|
|
538
|
+
install() is idempotent and a no-op wherever a real Storage works, so standalone
|
|
539
|
+
play and URL embeds are untouched. Behavior once installed:
|
|
540
|
+
|
|
541
|
+
- Reads and writes are synchronous and in-memory; the map is persisted to the
|
|
542
|
+
player's cloud save on a short debounce, on pagehide, and on flush().
|
|
543
|
+
- Progress becomes per-player and cross-device, not per-browser.
|
|
544
|
+
- setItem throws QuotaExceededError when the write would exceed the ~1 MB save
|
|
545
|
+
cap, matching a real Storage.
|
|
546
|
+
- sessionStorage is shimmed too, but memory only. It is per session by
|
|
547
|
+
definition and is never synced.
|
|
548
|
+
- Guests and standalone play settle in 'memory' mode: storage still works for
|
|
549
|
+
the session, it is just never persisted. Nothing rejects; nothing hangs.
|
|
550
|
+
|
|
551
|
+
The one real limitation is that getItem is synchronous while the cloud read is
|
|
552
|
+
not. Gate boot time reads on ready():
|
|
553
|
+
|
|
554
|
+
await BBArcade.storage.ready(); // 'cloud' | 'memory' | 'native'
|
|
555
|
+
startGame();
|
|
556
|
+
|
|
557
|
+
Writes made before hydration are kept and merged with the cloud read (the
|
|
558
|
+
game's own write wins, and a removeItem is not resurrected), and nothing is
|
|
559
|
+
pushed to the server until that read has SUCCEEDED. A failed read is retried on
|
|
560
|
+
the next write, and a write that still cannot confirm what the player had is
|
|
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
|
|
563
|
+
save. A game that reads
|
|
564
|
+
at boot WITHOUT awaiting ready() may still see an empty map on the first frame.
|
|
565
|
+
|
|
566
|
+
save()/load() keep working alongside the shim: the player has one save slot, so
|
|
567
|
+
with the shim active both halves share it inside a tagged envelope. load()
|
|
568
|
+
unwraps it and returns only what save() wrote. A raw blob written before the
|
|
569
|
+
shim existed is read back untouched, so turning the shim on never orphans a
|
|
570
|
+
save.
|
|
571
|
+
|
|
572
|
+
## Safe rewarded ad recipe
|
|
573
|
+
|
|
574
|
+
Prepare at the natural break. Keep the game owned button disabled until ready.
|
|
455
575
|
Call show() as the first operation in the direct click/tap handler: no await,
|
|
456
576
|
timer, microtask, animation, state transition, or network call before it.
|
|
457
577
|
|
|
@@ -484,7 +604,7 @@ is normal even though test mode is enabled automatically.
|
|
|
484
604
|
|
|
485
605
|
try {
|
|
486
606
|
const room = await joinRoom({
|
|
487
|
-
create: true, // or code: 'ABCD'
|
|
607
|
+
create: true, // or code: 'ABCD', or match: true for public quick match
|
|
488
608
|
// Only a relay founder uses roomSize; every joinData field is untrusted.
|
|
489
609
|
joinData: { roomSize: 4, avatar: 'golem' },
|
|
490
610
|
});
|
|
@@ -522,79 +642,79 @@ Bounty referee module, or registered external authority. The signed ticket
|
|
|
522
642
|
binds that tier. joinData cannot select or downgrade it; a ticket/registry
|
|
523
643
|
mismatch fails closed, and a registered external slug never falls back to
|
|
524
644
|
relay. All tiers use the same joinRoom transport, room codes, and client
|
|
525
|
-
connection lifecycle; tier/game
|
|
645
|
+
connection lifecycle; tier/game specific state and event schemas still differ.
|
|
526
646
|
|
|
527
647
|
create/code/match select a room. Relay supplies the host contract below, but
|
|
528
648
|
games build any ready/team/start/kick/rematch protocol on top and those rules
|
|
529
|
-
remain client
|
|
649
|
+
remain client trusted. Referee modules and external authorities define their
|
|
530
650
|
own state, input, event, spectator, lobby, and rematch schemas. A module's
|
|
531
651
|
minPlayers does not automatically gate simulation or start a match. Quick match
|
|
532
|
-
may place a player into a joinable room already in progress; late
|
|
533
|
-
is tier/game
|
|
652
|
+
may place a player into a joinable room already in progress; late join behavior
|
|
653
|
+
is tier/game defined.
|
|
534
654
|
|
|
535
655
|
joinData is always untrusted JSON. The SDK only validates its shape and 1 KiB
|
|
536
656
|
cap. A relay reads roomSize only from the founding seat. Referee modules and
|
|
537
|
-
external authorities validate any game
|
|
657
|
+
external authorities validate any game specific admission fields themselves.
|
|
538
658
|
|
|
539
659
|
### Built-in Bounty relay tier
|
|
540
660
|
|
|
541
|
-
The relay is the default Bounty shared
|
|
661
|
+
The relay is the default Bounty shared service tier for a multiplayer approved
|
|
542
662
|
slug that has no bespoke referee module and is not routed to an external
|
|
543
|
-
authority. It is a casual
|
|
663
|
+
authority. It is a casual lobby with no server code, not an authoritative game
|
|
544
664
|
simulation or anti-cheat boundary.
|
|
545
665
|
|
|
546
666
|
- The first admitted seat fixes capacity from joinData.roomSize. It must be an
|
|
547
|
-
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
|
|
548
668
|
may operate with one current occupant. Later joiners cannot change size. Once
|
|
549
669
|
the room is completely empty, the next founder may choose it again. Ship the
|
|
550
|
-
same roomSize from every client so quick
|
|
670
|
+
same roomSize from every client so quick matched rooms found consistently.
|
|
551
671
|
- Welcome and later 1 Hz metadata snapshots are identical for every viewer and
|
|
552
672
|
expose exactly
|
|
553
673
|
{ mode: 'relay', hostId: string | null, size: number, dropped: number }.
|
|
554
674
|
- The oldest retained seat is host. A transient disconnect preserves its seat
|
|
555
675
|
and hostId through the 15-second grace. After the host actually leaves or its
|
|
556
|
-
seat expires, the next
|
|
676
|
+
seat expires, the next oldest retained seat becomes host and live clients get
|
|
557
677
|
{ type: 'relay_host', hostId } through room.on('event'). The founding welcome
|
|
558
678
|
already carries hostId, so no initial relay_host event is required.
|
|
559
679
|
- room.send(data) and room.trySend(data) publicly fan the JSON payload to EVERY
|
|
560
680
|
player, including the sender. There are no private messages, hidden state, or
|
|
561
681
|
viewer filtering. Batches arrive at 20 Hz through room.on('event') as
|
|
562
|
-
{ type: 'relay', messages: [{ from, data }] }, where from is a room
|
|
682
|
+
{ type: 'relay', messages: [{ from, data }] }, where from is a room scoped
|
|
563
683
|
player id. Tick batching adds at most about 50 ms before network latency.
|
|
564
684
|
- Each relay payload's UTF-8 JSON encoding is capped at 1024 bytes. The relay
|
|
565
685
|
accepts at most 15 messages per player per second and 120 per room per second.
|
|
566
686
|
Byte/rate excess is silently dropped and increments cumulative state.dropped,
|
|
567
687
|
visible on a later metadata snapshot. state.dropped does not count malformed,
|
|
568
|
-
stale
|
|
688
|
+
stale sequence, or outer envelope drops.
|
|
569
689
|
- The common outer room guard still caps a client frame at 4096 characters and
|
|
570
690
|
90 accepted envelopes per player per second; server frames are capped at
|
|
571
691
|
256 KiB.
|
|
572
692
|
- Relay rooms are endless. They never emit match_end/room.on('end'), never close
|
|
573
693
|
because a game outcome was claimed, and never report results to Bounty Board.
|
|
574
|
-
All game rules and outcomes are client
|
|
694
|
+
All game rules and outcomes are client trusted; do not use relay claims for
|
|
575
695
|
trusted rewards, standings, or anti-cheat decisions. Implement round boundaries
|
|
576
696
|
in game messages and call room.leave() when the player exits.
|
|
577
697
|
|
|
578
698
|
### Refereed Bounty modules and external authorities
|
|
579
699
|
|
|
580
700
|
A referee validates every input, runs the only game simulation, clears held
|
|
581
|
-
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.
|
|
582
702
|
Clients never report authoritative results. A finite referee may emit one
|
|
583
|
-
server
|
|
703
|
+
server authored match_end; the SDK treats it as terminal and does not reconnect.
|
|
584
704
|
There is no SDK reset/rematch method. Endless authorities do not fabricate
|
|
585
705
|
match_end. Registered external authorities define their own socket shutdown and
|
|
586
|
-
subsequent
|
|
706
|
+
subsequent join behavior.
|
|
587
707
|
|
|
588
708
|
Bounty referee modules declare integer min/max players with
|
|
589
709
|
1 <= min <= max <= 64. tickHz is 1-60. snapshotHz may be lower, from 1 through
|
|
590
|
-
tickHz; not every simulation tick emits a snapshot. Simulation
|
|
710
|
+
tickHz; not every simulation tick emits a snapshot. Simulation backed sync has
|
|
591
711
|
a full network round trip, and the SDK provides no client-side prediction,
|
|
592
|
-
interpolation, or rollback. Expect roughly 50-150 ms input
|
|
593
|
-
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
|
|
594
714
|
snapshot. Relay game payloads instead arrive in the public 20 Hz event batches
|
|
595
715
|
described above; its 1 Hz snapshot contains metadata only.
|
|
596
716
|
|
|
597
|
-
### Common Bounty shared
|
|
717
|
+
### Common Bounty shared service room behavior
|
|
598
718
|
|
|
599
719
|
- Generated rooms use four-character invite codes. match: true works for relay
|
|
600
720
|
and referee rooms: it uses the current public room while live occupancy plus
|
|
@@ -602,13 +722,12 @@ described above; its 1 Hz snapshot contains metadata only.
|
|
|
602
722
|
failed occupancy probes, or reservations roll a new room, so live occupancy
|
|
603
723
|
can roll before reaching the room's capacity. A matched room.code is also an
|
|
604
724
|
invite code.
|
|
605
|
-
- A lost last
|
|
606
|
-
(3 total attempts).
|
|
607
|
-
|
|
608
|
-
last attempt returned.
|
|
725
|
+
- A lost last seat or ended room race triggers at most 2 re-matchmaking retries
|
|
726
|
+
(3 total attempts). Final rejection has code rejected and detail room_full or
|
|
727
|
+
room_over, whichever the last attempt returned.
|
|
609
728
|
- When a finite Bounty referee module ends, it broadcasts match_end once, closes
|
|
610
729
|
its sockets, and subsequent joins to that room reject with room_over. This
|
|
611
|
-
|
|
730
|
+
closing rule does not apply to the endless relay tier.
|
|
612
731
|
- Disconnected seats are reserved for 15 seconds. The SDK obtains fresh tickets
|
|
613
732
|
and makes up to 3 reconnect attempts. During grace, the player remains in
|
|
614
733
|
room.players and playerLeave is delayed until the seat expires. room.leave()
|
|
@@ -618,11 +737,10 @@ described above; its 1 Hz snapshot contains metadata only.
|
|
|
618
737
|
terminal generic admission rejection; the public SDK does not expose the
|
|
619
738
|
WebSocket close code/reason, so do not label every rejected error as this case.
|
|
620
739
|
|
|
621
|
-
Public quick match is
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
the Bounty shared-service defaults, and an external authority may be endless.
|
|
740
|
+
Public quick match is unavailable on registered external authorities, which
|
|
741
|
+
own their capacity, lobby lifecycle, and any matchmaking. Their reviewed
|
|
742
|
+
limits can differ from the Bounty shared service defaults, and an external
|
|
743
|
+
authority may be endless.
|
|
626
744
|
|
|
627
745
|
joinRoom rejection map:
|
|
628
746
|
|
|
@@ -634,20 +752,19 @@ joinRoom rejection map:
|
|
|
634
752
|
- error: malformed host grant, postMessage/socket transport failure, server
|
|
635
753
|
failure, or the 10-second welcome timeout.
|
|
636
754
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
retry or show “already playing elsewhere” without a distinct detail.
|
|
755
|
+
Use error.detail only when it is a specific documented value such as room_full
|
|
756
|
+
or room_over. Generic rejected has multiple causes; do not blind retry or show
|
|
757
|
+
“already playing elsewhere” without a distinct detail.
|
|
641
758
|
|
|
642
759
|
Multiplayer is curated per game slug. After approval, Bounty Board assigns one
|
|
643
760
|
of three rails: (a) the built-in casual relay, (b) a bespoke Bounty referee
|
|
644
761
|
module, or (c) a registered external authority with a dedicated ticket key.
|
|
645
|
-
Relay is the shared
|
|
762
|
+
Relay is the shared service default when no bespoke module or external route is
|
|
646
763
|
registered; clients cannot select the tier. External servers keep their existing
|
|
647
|
-
simulation and add only an isolated signed
|
|
648
|
-
envelope
|
|
764
|
+
simulation and add only an isolated signed ticket adapter plus the SDK room
|
|
765
|
+
envelope. Never create a second simulation beside one.
|
|
649
766
|
|
|
650
|
-
Local Bounty shared
|
|
767
|
+
Local Bounty shared service development bypasses the host ticket handshake only
|
|
651
768
|
with paired roomUrl and ticket values (for example wrangler dev with
|
|
652
769
|
DEV_ALLOW_UNSIGNED=1). A registered slug resolves to its referee module; another
|
|
653
770
|
well-formed slug resolves to relay:
|
|
@@ -675,21 +792,34 @@ https://www.bountyboard.gg/arcade/sdk/external-authoritative-servers.txt
|
|
|
675
792
|
- Mock rewarded ready, unavailable, dismissed, error, and viewed; only viewed grants.
|
|
676
793
|
- Assert prepared.show() is called directly from the player gesture.
|
|
677
794
|
- Test initial room.state/players rendering before the first later event.
|
|
678
|
-
- Test multiplayer reconnect/grace, trySend false, tier
|
|
795
|
+
- Test multiplayer reconnect/grace, trySend false, tier appropriate events/state,
|
|
679
796
|
and leave.
|
|
680
|
-
- Test create/code, and match
|
|
681
|
-
- 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
|
|
682
799
|
succession after grace, byte/rate drops, state.dropped, and absence of end.
|
|
683
|
-
- For referee/external rooms, test viewer
|
|
684
|
-
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.
|
|
685
802
|
- Test the identical artifact in its standalone location and inside Bounty Board.
|
|
686
803
|
|
|
687
804
|
## Troubleshooting map
|
|
688
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.
|
|
689
811
|
- Boot waits forever -> game startup depends on an SDK promise -> start init
|
|
690
812
|
fire-and-forget and give every promise feature a standalone outcome.
|
|
691
|
-
- save/load unsupported -> not a Bounty
|
|
692
|
-
- 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.
|
|
816
|
+
- SecurityError touching localStorage/sessionStorage/document.cookie -> hosted
|
|
817
|
+
builds run on an opaque origin -> use save()/load(), or storage.install() for
|
|
818
|
+
an engine export that cannot be rewired.
|
|
819
|
+
- Shimmed storage reads empty at boot -> the cloud read had not landed yet ->
|
|
820
|
+
await BBArcade.storage.ready() before restoring progress.
|
|
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.
|
|
693
823
|
- direct_user_action_required -> show() lost browser activation -> make show()
|
|
694
824
|
the first line of the click/tap handler.
|
|
695
825
|
- host_disabled/unavailable -> host or inventory is not ready -> keep the normal
|
|
@@ -701,15 +831,15 @@ https://www.bountyboard.gg/arcade/sdk/external-authoritative-servers.txt
|
|
|
701
831
|
- trySend false -> disconnected/raced socket or local serialization/send failure
|
|
702
832
|
-> stop sending, show reconnecting when disconnected, and resume only after
|
|
703
833
|
connection.connected is true. A true result still does not prove acceptance.
|
|
704
|
-
- match rejected on an external authority -> external matchmaking is authority
|
|
834
|
+
- match rejected on an external authority -> external matchmaking is authority
|
|
705
835
|
owned -> use code/create or that authority's separately documented flow.
|
|
706
836
|
|
|
707
837
|
## More
|
|
708
838
|
|
|
709
839
|
- Human guide: https://www.bountyboard.gg/arcade/sdk
|
|
840
|
+
- npm package: https://www.npmjs.com/package/@bountyboard/arcade-sdk
|
|
710
841
|
- Relay room guide: https://www.bountyboard.gg/arcade/sdk/relay-rooms.txt
|
|
711
842
|
- Standalone types: https://www.bountyboard.gg/arcade-sdk.d.ts
|
|
712
843
|
- Submit a game: https://www.bountyboard.gg/arcade/submit
|
|
713
|
-
- Package README, changelog, AGENTS.md,
|
|
714
|
-
|
|
715
|
-
in 1.2.0.
|
|
844
|
+
- Package README, changelog, AGENTS.md, design playbook, relay room guide, and
|
|
845
|
+
external authority guide all ship in the npm package.
|