@bountyboard/arcade-sdk 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/README.md +8 -8
- package/dist/global.js +4 -1
- package/dist/multiplayer.cjs +4 -1
- package/dist/multiplayer.cjs.map +1 -1
- package/dist/multiplayer.js +4 -1
- package/dist/multiplayer.js.map +1 -1
- 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/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.1
|
|
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.1, 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.
|