@castarcade/game-runtime
v0.3.1
Published
The browser runtime Cast Arcade uses to mount and drive games.
Readme
@castarcade/game-runtime
Standalone browser ESM host for the Cast Arcade beta bridge. Its shipped JavaScript uses only relative module imports and browser APIs. Type declarations need no private repository packages. Install with npm install @castarcade/game-runtime, or serve all three JavaScript files together.
import { mountGame } from '@castarcade/game-runtime';
const game = mountGame(document.querySelector('#game'), {
url: 'https://your-platform.example/api/community/v1/assets/BUILD/index.html',
manifest, // Use a server-validated GameManifest, policy/SDK version 1.
mode: 'shared-screen',
playerCount: 2,
onResult(result) { results.textContent = JSON.stringify(result); },
onError(error) { status.textContent = error.message; },
});
game.pause();
game.resume();
game.setMuted(true);
game.dispose(); // Idempotent; always call when leaving the player.For a Cast Arcade room, pass the fixed room roster and use controls: 'managed':
const game = mountGame(container, {
url,
manifest,
mode: 'shared-screen',
players: roomPlayers,
controls: 'managed',
});
game.applyInput({ playerId, actionId, value, pressed });Managed controls accept authorized phone actions through applyInput and local keyboard, touch, or browser gamepad input. Bluetooth and wired controllers both appear through the browser Gamepad API after the operating system connects them. Cast Arcade handles the room and phone controls; partner games handle the declared semantic actions.
mode defaults to solo when supported; otherwise shared-screen. playerCount defaults to one for solo, or the minimum supported shared-screen count (at least two). Counts must fit the manifest and are capped at four. The returned players (p1–p4, names Player 1–Player 4) and mode describe the exact started session. Changing configuration requires disposing and mounting again. loadTimeoutMs defaults to 15000; pointerLock:true only adds allow-pointer-lock.
The iframe always uses sandbox="allow-scripts" without same-origin access. Ready messages must come from that exact frame, have opaque origin null, and match protocol version 1. A dedicated MessageChannel carries lifecycle, viewport, semantic inputs and bounded results. Score or elapsed-time rows must refer to started players and match the manifest's declared record kind. Optional metadata uses the same account-safe scalar rules described by the SDK. Browser results may feed private personal history, but they are not verified public records or trusted economic events. Dispose closes ports, removes listeners/frame, disconnects ResizeObserver, and cancels timers and gamepad polling. Page visibility pauses and conditionally resumes; blur/disconnect neutralizes held input.
The integrating host must serve games with createArtifactHeaders from the public game contract and restrict its own page CSP frame-src to the exact approved artifact directory. The library cannot set its embedding document's HTTP CSP. Keep preview capability paths out of logs. Do not expose credentials or account identity to frames.
Keyboard maps the first two axis actions then first two button actions in manifest order:
| Player | Axis 1 | Axis 2 | Buttons | |---|---|---|---| | 1 | Left/Right | Up/Down | Space, Enter | | 2 | A/D | W/S | F, G | | 3 | J/L | I/K | O, P | | 4 | Numpad 4/6 | Numpad 8/2 | Numpad 0, Decimal |
Touch buttons support every declared action and are rendered as text in a separate host panel. Axis controls have minus/plus buttons. Gamepad connection slots 1–4 map to players 1–4; axis/button actions map in manifest order with a 0.15 axis dead zone. Press a physical controller button to activate browser gamepad access. Standard controllers may lack actions beyond four axes/sixteen buttons; guidance points to touch or game-specific controls. Missing gamepad APIs are reported visibly.
Iframe focus: keyboard events inside an opaque frame do not bubble to its host. Games must implement native keyboard handling using these same assignments in addition to bridge inputs; clear native input on blur/pause and prevent duplicate default button activation. Both provided CLI starters do this. Imported games need explicit adaptation. Touch and gamepad bridge inputs continue to work with the frame focused.
Unit tests exercise a real SDK instance through paired test ports plus input, shape and cleanup behavior. Browser verification is a separate manual check; these tests do not claim actual device coverage.
For manifests declaring play.privateViews: true, onControllerView(view) receives a validated { playerId, title?, text?, actions?: [{ id, label? }] }. Route it only to that authorized seat and render its strings as text. A view with only playerId clears existing content. Runtime and SDK both enforce declared actions, fixed roster, 4 KiB serialized size and bounded text lengths.
For play.checkpoint: 'versioned', onCheckpoint(checkpoint) receives validated { version, state } JSON up to 32 KiB. Persist it under the immutable release and session, then supply checkpoint when mounting that same session. Runtime snapshots and validates it before creating an iframe. The SDK invokes onRestore before start and buffers input while an asynchronous restore completes. Restore failure reaches onError and disposes the frame. Checkpoints remain advisory client state and must never be treated as verified score evidence.
Managed local and phone input share one source-aware state reducer. Releasing or disconnecting one device preserves another device's hold; pause and disposal clear every held source.
For a validated schema v2 server: { entry, tickHz } manifest, mounted.updateState(display) accepts at most 32 KiB JSON, snapshots the data, and returns whether it was accepted. Before handshake/start it keeps only the newest valid update. After start it delivers SDK state(display) under the authoritative-session flag; undeclared or disposed hosts reject updates. The runtime never executes the server entry in the browser.
