Referensi API
Dihasilkan dari tipe @ralyoo/sdk 1.1.0. Penjelasan tiap entri berasal dari komentar di kode sumber dan berbahasa Inggris.
| Impor | Isi |
|---|---|
@ralyoo/sdk |
Yang dipakai setiap game: defineGame, ctx, dan tipe-tipenya. |
@ralyoo/sdk/widgets |
Layout controller deklaratif dan validasinya. |
@ralyoo/sdk/testing |
Menjalankan game dalam tes, tanpa browser. |
@ralyoo/sdk/governor |
Resolusi render dinamis untuk menjaga fps. |
@ralyoo/sdk/vite |
Preset build game. |
@ralyoo/sdk/manifest |
Tipe dan validasi ralyoo.json. |
@ralyoo/sdk/bundle |
Aturan bundel yang dipakai ralystudioo dan registry. |
@ralyoo/sdk/vendor |
Pustaka bersama yang disediakan platform. |
@ralyoo/sdk/semver |
Rentang versi untuk field sdk. |
@ralyoo/sdk
Yang dipakai setiap game: defineGame, ctx, dan tipe-tipenya.
defineGame
fungsi
Entry point of every game bundle. Runs inside the platform's sandboxed iframe: says hello to the parent, receives a MessagePort, and from then on talks only over that port.
export default defineGame({
onStart(ctx) { ctx.setController('all', layout) },
onInput(ctx, player, input) { … },
});
function defineGame(def: GameDefinition): GameDefinition;
BUTTON_BITS
konstanta
Bit of each button in PadState.buttons (the same bits as the INPUT frame on the wire).
const BUTTON_BITS: Readonly<Record<ButtonName, number>>;
isDown
fungsi
True while button is held in state: isDown(ctx.input(1), 'A').
function isDown(state: PadState, button: ButtonName): boolean;
SDK_MAJOR
konstanta
const SDK_MAJOR = 1;
SDK_VERSION
konstanta
SDK version, the same number as the @ralyoo/sdk package (checked by a test). The platform and
every game bundle carry one; ralyoo.json declares the range a game runs on ("sdk").
Compatibility (semver, see COMPATIBILITY.md):
- Same major = same message contract. Both sides ignore message types they do not know, so a game built with 1.0 runs on a 1.x platform, and a 1.x game degrades gracefully on 1.0.
- A minor adds optional API. A game that needs it says so in its range (
">=1.1 <2"). - A major changes or removes API; the platform keeps serving the previous major's games.
const SDK_VERSION = "1.1.0";
mulberry32
fungsi
Small, fast, seedable PRNG (mulberry32). Same seed → same sequence on every device.
function mulberry32(seed: number): () => number;
AudioBus
tipe
type AudioBus = 'music' | 'sfx' | 'voice';
ButtonName
tipe
type ButtonName = 'A' | 'B' | 'X' | 'Y' | 'up' | 'down' | 'left' | 'right' | 'start';
EndOptions
interface
Optional extras for ctx.end. team marks a co-op game that everyone wins or loses
together: the results screen shows the team outcome instead of a ranking. screen: 'game'
keeps the game on screen after the end: the platform records the scores but shows no results
screen, and the game later calls ctx.exit('again' | 'lobby').
interface EndOptions {
team?: 'win' | 'lose';
screen?: 'platform' | 'game';
}
ExitTarget
tipe
Where ctx.exit goes after a game-owned results screen.
type ExitTarget = 'again' | 'lobby';
InputEvent
tipe
What a game receives in onInput: { widget: "A", type: "press" } and friends.
type InputEvent = {
widget: ButtonName;
type: 'press' | 'release';
} | {
widget: string;
type: 'press' | 'release' | 'page' | 'vote';
value?: string | number;
};
MusicMix
interface
The game's own music stage, between its music sounds and the platform music bus: a level
(dB, −40..0) and an optional low-pass (Hz, 100..22050; null = open), reached over rampMs.
The platform resets it when the game ends.
interface MusicMix {
db: number;
lowpassHz: number | null;
rampMs: number;
}
PadState
interface
Latest controller state; sticks normalised to −1..1.
interface PadState {
buttons: number;
lx: number;
ly: number;
rx: number;
ry: number;
}
PerfStats
interface
Frame statistics of the game document over the last second, measured by the SDK inside the
iframe while the platform asks for them (the studio HUD, ?debug on the host).
interface PerfStats {
fps: number;
/** Frame time in ms: mean, 95th percentile, and worst. */
avgMs: number;
p95Ms: number;
maxMs: number;
/** Frames longer than 50 ms. */
long: number;
}
PlayerId
tipe
type PlayerId = 1 | 2 | 3 | 4;
PlayerInfo
interface
interface PlayerInfo {
id: PlayerId;
name: string;
/** Player colour (P1 ultraviolet, P2 plasma, P3 LED, P4 aurora). */
color: string;
connected: boolean;
}
PlayOptions
interface
Per-play options for a game sound.
interface PlayOptions {
/** Defaults to `sfx`. */
bus?: AudioBus;
loop?: boolean;
/** Playback rate (pitch and speed), clamped to 0.5..2. Default 1. */
rate?: number;
/** Linear gain on top of the bus, clamped to 0..2. Default 1. */
gain?: number;
}
GameContext
interface
interface GameContext {
/** The game's id from `ralyoo.json`. */
readonly gameId: string;
/** Language of the room (`id` for now); `t` already uses it. */
readonly locale: string;
/** SDK version of the platform running the game, e.g. `1.1.0`. */
readonly platformSdk: string;
/** Everyone in the room, in slot order. Updated before `onPlayers` runs. */
readonly players: readonly PlayerInfo[];
/** Shows a declarative layout on one phone or on all of them. */
setController(target: PlayerId | 'all', layout: ControllerLayout): void;
/**
* Finishes the game; the platform shows the results and returns to the lobby. Co-op games
* pass `{ team: 'win' | 'lose' }` (everyone usually gets the same team score). With
* `{ screen: 'game' }` the game keeps the screen and the phones for its own results, then
* calls `exit`; without `exit` within 60 s (or after an error) the platform shows its results.
*/
end(scores: Partial<Record<PlayerId, number>>, options?: EndOptions): void;
/**
* After `end(scores, { screen: 'game' })`: `'again'` restarts this game, `'lobby'` goes back
* to the lobby. Ignored before such an `end`.
*/
exit(to: ExitTarget): void;
/** Seeded PRNG. Without a seed it derives one from the session seed and a counter. */
random(seed?: number): () => number;
/** Game text by key from `locales`, with `{name}` placeholders. */
t(key: string, vars?: Record<string, string | number>): string;
audio: {
/** A platform UI sound (`tick`, `select`, `join`, …) or a path inside the game folder. */
play(name: string, options?: PlayOptions): void;
stop(name: string): void;
/** Fetches and decodes a game sound now so its first `play` starts without delay. */
preload(name: string): void;
/** Sets the game's music stage (level and low-pass), e.g. to duck or muffle the music. */
mix(mix: MusicMix): void;
};
assets: {
/** Absolute URL for a file inside the game folder. */
url(path: string): string;
};
/** Per-game key/value storage. Prototype: kept by the platform on this host device only. */
storage: {
get(key: string): Promise<unknown>;
set(key: string, value: unknown): Promise<void>;
};
/** Latest pad state of a player (zeros before the first input). */
input(player: PlayerId): PadState;
readonly reducedMotion: boolean;
/** True while the host menu pauses the game (only for games with `onPause`). */
readonly paused: boolean;
}
GameDefinition
interface
interface GameDefinition {
locales?: Record<string, Strings>;
/**
* Runs as soon as the game is mounted, while the host shows "3·2·1·GAS!": load assets,
* compile shaders, build the scene. The game is not visible yet.
*/
preload?(ctx: GameContext): void | Promise<void>;
/** Runs at "GAS!" (after `preload` finished). */
onStart(ctx: GameContext): void | Promise<void>;
onInput?(ctx: GameContext, player: PlayerId, input: InputEvent): void;
/** Called every animation frame with `dt` in seconds (capped at 0.1). */
onTick?(ctx: GameContext, dt: number): void;
onPlayers?(ctx: GameContext, players: readonly PlayerInfo[]): void;
/**
* The host menu opened over the game. Defining it opts the game into pausing: from now until
* `onResume`, `onTick` is not called (the first frame after has `dt` 0). Freeze timers and
* your own render loop here; the music may be muffled with `ctx.audio.mix`. Input is already
* released. Without `onPause` the game keeps running under the menu, as before.
*/
onPause?(ctx: GameContext): void;
/** The menu closed and the game continues. */
onResume?(ctx: GameContext): void;
onStop?(ctx: GameContext): void;
}
Strings
tipe
type Strings = Record<string, string>;
ControllerLayout
interface
interface ControllerLayout {
/**
* `pad`: controls in thumb zones (d-pad/stick left, buttons right; info on top in portrait,
* in the middle in landscape). `stack`: one centred column (buttons side by side in landscape).
*/
layout: 'pad' | 'stack';
/** Defaults to `any`. */
orientation?: Orientation;
widgets: Widget[];
}
Orientation
tipe
How the phone should be held. any (the default) follows the phone. portrait/landscape
show a "Putar HP-mu" prompt when the phone is held the other way, and on Android the
controller tries to lock the screen after the first tap. Input keeps working either way.
type Orientation = 'any' | 'portrait' | 'landscape';
Tone
tipe
type Tone = 'pearl' | 'led' | 'plasma' | 'ultraviolet' | 'aurora' | 'champagne' | 'arcade';
Widget
tipe
type Widget = {
type: 'text';
id?: string;
value: string;
size?: 'm' | 'l';
}
/** `id` A/B/X/Y uses the fast path; any other id sends a reliable `press` event. */
| {
type: 'button';
id: string;
label?: string;
tone?: Tone;
hold?: boolean;
} | {
type: 'dpad';
}
/** `stick` → lx/ly, `stick2` → rx/ry in the INPUT frame. */
| {
type: 'joystick';
id: 'stick' | 'stick2';
}
/** Swipeable pages, e.g. the secret manual in Jinakkan Bom. Sends `page` events. */
| {
type: 'cards';
id: string;
pages: {
title?: string;
body: string;
}[];
}
/** One choice per player; sends a `vote` event with the option id. */
| {
type: 'vote';
id: string;
question?: string;
options: {
id: string;
label: string;
}[];
};
@ralyoo/sdk/widgets
Layout controller deklaratif dan validasinya.
FACE_BUTTONS
konstanta
Face buttons that map onto the INPUT bitmask (same bits as @ralyoo/protocol Button).
const FACE_BUTTONS: {
readonly A: number;
readonly B: number;
readonly X: number;
readonly Y: number;
};
LIMITS
konstanta
const LIMITS: {
readonly widgets: 12;
readonly text: 280;
readonly label: 24;
readonly pages: 20;
readonly pageBody: 600;
readonly options: 8;
readonly id: 32;
};
ORIENTATIONS
konstanta
const ORIENTATIONS: readonly Orientation[];
validateLayout
fungsi
Validates a layout coming from a game. Unknown widgets or oversized content reject the whole
layout (returns null) so a buggy game cannot half-render a phone. An unknown orientation
is dropped (the phone falls back to any), like other optional fields.
function validateLayout(raw: unknown): ControllerLayout | null;
orientationOf
fungsi
The orientation a layout asks for; a missing field means any.
function orientationOf(layout: ControllerLayout | null | undefined): Orientation;
isFaceButton
fungsi
function isFaceButton(id: string): id is FaceButton;
validateWidgetEvent
fungsi
function validateWidgetEvent(raw: unknown): WidgetEvent | null;
FaceButton
tipe
type FaceButton = keyof typeof FACE_BUTTONS;
Tone
tipe
type Tone = 'pearl' | 'led' | 'plasma' | 'ultraviolet' | 'aurora' | 'champagne' | 'arcade';
Widget
tipe
type Widget = {
type: 'text';
id?: string;
value: string;
size?: 'm' | 'l';
}
/** `id` A/B/X/Y uses the fast path; any other id sends a reliable `press` event. */
| {
type: 'button';
id: string;
label?: string;
tone?: Tone;
hold?: boolean;
} | {
type: 'dpad';
}
/** `stick` → lx/ly, `stick2` → rx/ry in the INPUT frame. */
| {
type: 'joystick';
id: 'stick' | 'stick2';
}
/** Swipeable pages, e.g. the secret manual in Jinakkan Bom. Sends `page` events. */
| {
type: 'cards';
id: string;
pages: {
title?: string;
body: string;
}[];
}
/** One choice per player; sends a `vote` event with the option id. */
| {
type: 'vote';
id: string;
question?: string;
options: {
id: string;
label: string;
}[];
};
Orientation
tipe
How the phone should be held. any (the default) follows the phone. portrait/landscape
show a "Putar HP-mu" prompt when the phone is held the other way, and on Android the
controller tries to lock the screen after the first tap. Input keeps working either way.
type Orientation = 'any' | 'portrait' | 'landscape';
ControllerLayout
interface
interface ControllerLayout {
/**
* `pad`: controls in thumb zones (d-pad/stick left, buttons right; info on top in portrait,
* in the middle in landscape). `stack`: one centred column (buttons side by side in landscape).
*/
layout: 'pad' | 'stack';
/** Defaults to `any`. */
orientation?: Orientation;
widgets: Widget[];
}
WidgetEvent
interface
Reliable widget events (custom buttons, card pages, votes) as sent over DATA.
interface WidgetEvent {
id: string;
type: 'press' | 'release' | 'page' | 'vote';
value?: string | number;
}
@ralyoo/sdk/testing
Menjalankan game dalam tes, tanpa browser.
createTestHost
fungsi
function createTestHost(def: GameDefinition, options?: TestHostOptions): TestHost;
TestHostOptions
interface
interface TestHostOptions {
/** Player count (2 by default), or the players themselves. */
players?: number | PlayerInfo[];
gameId?: string;
locale?: string;
/** Session seed behind `ctx.random()`. Default 1, so runs repeat. */
seed?: number;
reducedMotion?: boolean;
/** What `ctx.storage` already holds. */
storage?: Record<string, unknown>;
/** Base URL behind `ctx.assets.url()`. */
base?: string;
}
TestHost
interface
interface TestHost {
/** The platform's side of the game, for anything the helpers below do not cover. */
readonly link: GameLink;
readonly players: readonly PlayerInfo[];
/** Mounts the game, runs `preload`, sends "GAS!", and waits until `onStart` has run. */
start(): Promise<void>;
/** Lets pending promises (storage answers, async handlers) settle. */
flush(): Promise<void>;
/** Runs one animation frame, `ms` after the previous one (default 1000/60). */
frame(ms?: number): void;
/** Runs `count` animation frames. */
frames(count: number, ms?: number): void;
press(player: PlayerId, button: ButtonName): void;
release(player: PlayerId, button: ButtonName): void;
/** Press and release. */
tap(player: PlayerId, button: ButtonName): void;
/** Moves a stick: `stick(1, { lx: 1 })`. Axes are −1..1, missing ones keep their value. */
stick(player: PlayerId, axes: Partial<Omit<PadState, 'buttons'>>): void;
/** A reliable widget event: `event(2, { widget: 'tersangka', type: 'vote', value: 'p1' })`. */
event(player: PlayerId, input: InputEvent): void;
/** The phone lost its connection (the slot stays), or came back. */
disconnect(player: PlayerId): void;
reconnect(player: PlayerId): void;
/** The host menu opened over the game / closed again. */
pause(): void;
resume(): void;
/** The layout a phone shows now, or null before the game set one. */
layout(player: PlayerId): ControllerLayout | null;
/** `ctx.end(…)`, once the game called it. */
readonly ended: {
scores: Partial<Record<PlayerId, number>>;
options: EndOptions;
} | null;
/** `ctx.exit(…)`, once the game called it. */
readonly exited: ExitTarget | null;
/** Every `ctx.audio` call so far, oldest first. */
readonly audio: readonly AudioCommand[];
readonly mixes: readonly MusicMix[];
/** Errors the game reported or threw. A passing test usually wants this empty. */
readonly errors: readonly string[];
readonly storage: Map<string, unknown>;
/** Frame statistics the SDK reported after `link.perf(true)`. */
readonly perf: readonly PerfStats[];
stop(): void;
}
@ralyoo/sdk/governor
Resolusi render dinamis untuk menjaga fps.
ResolutionGovernor
kelas
class ResolutionGovernor {
readonly levels: readonly number[];
private level;
/** Lowest index (highest resolution) still allowed. */
private ceiling;
/** Level entered by the last step up, until it proves itself. */
private retry;
private frames;
private elapsed;
private slow;
private fast;
private readonly lowFps;
private readonly highFps;
private readonly windowMs;
private readonly upAfter;
private readonly stallMs;
/** fps of the last complete window. */
lastFps: number;
constructor(options: GovernorOptions);
get resolution(): number;
/** Feeds one frame duration (ms). Returns the new resolution when it changes, else null. */
frame(ms: number): number | null;
}
GovernorOptions
interface
Render-resolution governor for canvas games (@ralyoo/sdk/governor, bundled into the game):
keeps the host at 60 fps on integrated GPUs by lowering the render resolution when frames
get slow, and carefully trying one level up again later. Used by Arena Dorong and Balap Liar.
Frame times are averaged per window (1 s). Two slow windows in a row (< lowFps) step down
one level. upAfter fast windows in a row (≥ highFps) step up one level; a level that
fails again right after such a retry becomes the ceiling, so the picture never oscillates.
A single frame over stallMs (tab hidden, GC, alt-tab) discards the window: it says
nothing about the GPU.
interface GovernorOptions {
/** Highest resolution, e.g. min(devicePixelRatio, 1.5). */
max: number;
min?: number;
lowFps?: number;
highFps?: number;
windowMs?: number;
upAfter?: number;
stallMs?: number;
}
@ralyoo/sdk/vite
Preset build game.
gameViteConfig
fungsi
Build config for a game: src/main.ts → dist/main.js (one ES module), the shared libraries
left external for the platform's import map, everything in assets/ copied next to it, and
ralyoo.json beside them.
// vite.config.ts
import { gameViteConfig } from '@ralyoo/sdk/vite';
import { defineConfig } from 'vite';
export default defineConfig(gameViteConfig());
function gameViteConfig(): UserConfig;
@ralyoo/sdk/manifest
Tipe dan validasi ralyoo.json.
PILLARS
konstanta
const PILLARS: readonly ['konsol', 'rahasia'];
INPUT_KINDS
konstanta
const INPUT_KINDS: readonly ['buttons', 'dpad', 'joystick', 'cards', 'vote'];
PERMISSIONS
konstanta
const PERMISSIONS: readonly ['vibrate'];
MANIFEST_SCHEMA
konstanta
JSON Schema (draft 2020-12) of ralyoo.json.
const MANIFEST_SCHEMA: {
readonly $schema: 'https://json-schema.org/draft/2020-12/schema';
readonly $id: 'https://docs.ralyoo.com/schema/ralyoo.json';
readonly title: 'Ralyoo game manifest';
readonly type: 'object';
readonly required: readonly ['id', 'version', 'sdk', 'title', 'description', 'author', 'players', 'pillars', 'locales', 'media'];
readonly properties: {
readonly $schema: {
readonly type: 'string';
readonly description: 'Schema URL, for editor completion.';
};
readonly id: {
readonly type: 'string';
readonly pattern: '^[a-z][a-z0-9]*(-[a-z0-9]+)*$';
readonly minLength: 3;
readonly maxLength: 40;
readonly description: 'Permanent identifier: lowercase letters, digits, and dashes. Part of the bundle URL and of the storage key, so it never changes after the first upload.';
};
readonly version: {
readonly type: 'string';
readonly pattern: '^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$';
readonly description: 'Version of the game (semver). Every upload needs a new one; a published version is immutable.';
};
readonly sdk: {
readonly type: 'string';
readonly minLength: 1;
readonly maxLength: 40;
readonly description: 'SDK versions the game runs on, as a semver range, e.g. ">=1.1 <2". Checked against the platform at upload and before the game is mounted.';
};
readonly title: {
description: string;
anyOf: ({
type: string;
minLength: number;
maxLength: number;
minProperties?: never;
propertyNames?: never;
additionalProperties?: never;
} | {
minLength?: never;
maxLength?: never;
type: string;
minProperties: number;
propertyNames: {
pattern: string;
};
additionalProperties: {
type: string;
minLength: number;
maxLength: number;
};
})[];
};
readonly description: {
description: string;
anyOf: ({
type: string;
minLength: number;
maxLength: number;
minProperties?: never;
propertyNames?: never;
additionalProperties?: never;
} | {
minLength?: never;
maxLength?: never;
type: string;
minProperties: number;
propertyNames: {
pattern: string;
};
additionalProperties: {
type: string;
minLength: number;
maxLength: number;
};
})[];
};
readonly controls: {
description: string;
anyOf: ({
type: string;
minLength: number;
maxLength: number;
minProperties?: never;
propertyNames?: never;
additionalProperties?: never;
} | {
minLength?: never;
maxLength?: never;
type: string;
minProperties: number;
propertyNames: {
pattern: string;
};
additionalProperties: {
type: string;
minLength: number;
maxLength: number;
};
})[];
};
readonly badge: {
description: string;
anyOf: ({
type: string;
minLength: number;
maxLength: number;
minProperties?: never;
propertyNames?: never;
additionalProperties?: never;
} | {
minLength?: never;
maxLength?: never;
type: string;
minProperties: number;
propertyNames: {
pattern: string;
};
additionalProperties: {
type: string;
minLength: number;
maxLength: number;
};
})[];
};
readonly author: {
readonly type: 'object';
readonly required: readonly ['name'];
readonly description: 'Who made the game.';
readonly properties: {
readonly name: {
readonly type: 'string';
readonly minLength: 1;
readonly maxLength: 60;
};
readonly url: {
readonly type: 'string';
readonly pattern: '^https://';
readonly maxLength: 200;
};
};
readonly additionalProperties: false;
};
readonly players: {
readonly type: 'object';
readonly required: readonly ['min', 'max'];
readonly description: 'How many players the game supports (the platform seats 1–4).';
readonly properties: {
readonly min: {
readonly type: 'integer';
readonly minimum: 1;
readonly maximum: 4;
};
readonly max: {
readonly type: 'integer';
readonly minimum: 1;
readonly maximum: 4;
};
};
readonly additionalProperties: false;
};
readonly pillars: {
readonly type: 'array';
readonly minItems: 1;
readonly uniqueItems: true;
readonly items: {
readonly enum: readonly ["konsol", "rahasia"];
};
readonly description: 'Which Ralyoo pillar the game belongs to: "konsol" (the phone is a gamepad) or "rahasia" (the phone shows secret information).';
};
readonly locales: {
readonly type: 'array';
readonly minItems: 1;
readonly uniqueItems: true;
readonly items: {
readonly type: 'string';
readonly pattern: '^[a-z]{2}(-[A-Z]{2})?$';
};
readonly description: 'Languages the game ships, default first. Localized manifest texts must cover the default.';
};
readonly media: {
readonly type: 'object';
readonly required: readonly ['icon', 'cover', 'color'];
readonly description: 'Catalog artwork: paths inside the bundle, and the game colour.';
readonly properties: {
readonly icon: {
readonly type: 'string';
readonly pattern: '^(?!.*\\.\\.)[A-Za-z0-9_-][A-Za-z0-9_./-]*\\.(svg|png)$';
readonly maxLength: 120;
readonly description: 'Square icon used as a mask (one colour, transparent background). SVG preferred.';
};
readonly cover: {
readonly type: 'string';
readonly pattern: "^(?!.*\\.\\.)[A-Za-z0-9_-][A-Za-z0-9_./-]*\\.(png|webp|svg|jpg|jpeg|avif)$";
readonly maxLength: 120;
readonly description: 'Cover image, 16:9, at least 1280×720.';
};
readonly color: {
readonly type: 'string';
readonly pattern: "^#[0-9A-Fa-f]{6}$";
readonly description: 'Game colour as #rrggbb; must read on the dark host background (≥ 3:1).';
};
};
readonly additionalProperties: false;
};
readonly tier: {
readonly enum: readonly ['free'];
readonly description: 'Access tier. Only "free" for now.';
};
readonly entry: {
readonly type: 'object';
readonly required: readonly ['host', 'controller'];
readonly description: 'Entry points. The host entry is an ES module; controllers are declarative.';
readonly properties: {
readonly host: {
readonly const: 'main.js';
};
readonly controller: {
readonly const: 'declarative';
};
};
readonly additionalProperties: false;
};
readonly inputs: {
readonly type: 'array';
readonly uniqueItems: true;
readonly items: {
readonly enum: readonly ["buttons", "dpad", "joystick", "cards", "vote"];
};
readonly description: 'Controller widgets the game uses (shown to players before they pick it).';
};
readonly permissions: {
readonly type: 'array';
readonly uniqueItems: true;
readonly items: {
readonly enum: readonly ["vibrate"];
};
readonly description: 'Device capabilities the game asks for.';
};
readonly offline: {
readonly type: 'boolean';
readonly description: 'True when the game needs nothing but its own bundle (always, for now).';
};
readonly opening: {
readonly enum: readonly ['platform', 'game'];
readonly description: '"platform" (default): the platform shows "3·2·1·GAS!". "game": the game runs its own opening.';
};
readonly theme: {
readonly type: 'object';
readonly description: 'Colours, frame, and labels of the host menu while the game is on screen. Colours below the contrast floor are ignored as a set.';
readonly properties: {
readonly panel: {
readonly type: 'string';
readonly pattern: "^#[0-9A-Fa-f]{6}$";
};
readonly accent: {
readonly type: 'string';
readonly pattern: "^#[0-9A-Fa-f]{6}$";
};
readonly text: {
readonly type: 'string';
readonly pattern: "^#[0-9A-Fa-f]{6}$";
};
readonly frame: {
readonly type: 'string';
readonly pattern: '^(?!.*\\.\\.)[A-Za-z0-9_-][A-Za-z0-9_./-]*\\.(png|webp|svg)$';
readonly maxLength: 120;
};
readonly labels: {
readonly type: 'object';
readonly additionalProperties: {
readonly type: 'object';
readonly additionalProperties: {
readonly type: 'string';
readonly maxLength: 80;
};
};
};
};
readonly additionalProperties: false;
};
};
readonly additionalProperties: true;
};
localize
fungsi
A localized text for locale, falling back to the first one given.
function localize(text: Localized | undefined, locale: string): string;
validateManifest
fungsi
Validates a parsed ralyoo.json. Errors make the bundle unusable; warnings do not. Besides the
schema this checks what a schema cannot: the player range, the SDK range, colour contrast, and
that localized texts cover the default locale.
function validateManifest(raw: unknown): ManifestReport;
Localized
tipe
A text, or one text per locale ({ "id": "…", "en": "…" }).
type Localized = string | Record<string, string>;
Pillar
tipe
type Pillar = (typeof PILLARS)[number];
InputKind
tipe
type InputKind = (typeof INPUT_KINDS)[number];
Permission
tipe
type Permission = (typeof PERMISSIONS)[number];
GameManifest
interface
interface GameManifest {
$schema?: string;
id: string;
version: string;
sdk: string;
title: Localized;
description: Localized;
controls?: Localized;
badge?: Localized;
author: {
name: string;
url?: string;
};
players: {
min: number;
max: number;
};
pillars: Pillar[];
locales: string[];
media: {
icon: string;
cover: string;
color: string;
};
tier?: 'free';
entry?: {
host: string;
controller: 'declarative';
};
inputs?: InputKind[];
permissions?: Permission[];
offline?: boolean;
opening?: 'platform' | 'game';
theme?: {
panel?: string;
accent?: string;
text?: string;
frame?: string;
labels?: Record<string, Record<string, string>>;
};
}
ManifestIssue
interface
interface ManifestIssue {
/** JSON pointer of the offending value, e.g. `/media/color`; empty for the manifest itself. */
path: string;
message: string;
}
ManifestReport
interface
interface ManifestReport {
ok: boolean;
errors: ManifestIssue[];
/** Things that work but deserve a look: unknown fields, weak contrast, missing translations. */
warnings: ManifestIssue[];
/** The manifest, typed, when `ok`. */
manifest: GameManifest | null;
}
@ralyoo/sdk/bundle
Aturan bundel yang dipakai ralystudioo dan registry.
BUNDLE_EXTENSION
konstanta
const BUNDLE_EXTENSION = ".ralyoo";
BUNDLE_MANIFEST
konstanta
const BUNDLE_MANIFEST = "ralyoo.json";
BUNDLE_ENTRY
konstanta
const BUNDLE_ENTRY = "main.js";
BUNDLE_LIMITS
konstanta
const BUNDLE_LIMITS: {
/** Everything in the bundle, unpacked. */
readonly totalBytes: number;
/** One file, unpacked. */
readonly fileBytes: number;
readonly files: 1500;
readonly manifestBytes: number;
/** All JavaScript in the bundle. Shared libraries come from the platform, not the bundle. */
readonly scriptBytes: number;
readonly coverBytes: number;
readonly iconBytes: number;
readonly pathLength: 120;
};
BUNDLE_BUDGETS
konstanta
Above these a bundle still uploads, with a warning: they cost every player loading time.
const BUNDLE_BUDGETS: {
readonly totalBytes: number;
readonly scriptBytes: number;
readonly fileBytes: number;
};
BUNDLE_FILE_TYPES
konstanta
File types a bundle may contain, and how the platform serves them. No HTML, no source maps.
const BUNDLE_FILE_TYPES: Readonly<Record<string, string>>;
extensionOf
fungsi
function extensionOf(path: string): string;
bundleContentType
fungsi
The content type the platform serves a bundle file with, or null when the type is not allowed.
function bundleContentType(path: string): string | null;
bundlePathProblem
fungsi
Why a path cannot be in a bundle, or null when it can.
function bundlePathProblem(path: string): string | null;
checkBundle
fungsi
Checks the file list of a bundle against its (already valid) manifest: required files, allowed types and paths, hard limits (errors) and loading budgets (warnings).
function checkBundle(manifest: GameManifest, files: readonly BundleFile[]): BundleReport;
checkScript
fungsi
Looks at a built script for what the sandbox would refuse at run time: imports the platform's
import map does not provide, eval, and addresses outside the bundle. Heuristic: it reads the
text, so a string that merely looks like one of these is reported too (as a warning).
function checkScript(path: string, source: string): Pick<BundleReport, 'errors' | 'warnings'>;
BundleFile
interface
interface BundleFile {
/** Path inside the bundle, `/`-separated, no leading slash. */
path: string;
/** Unpacked size in bytes. */
size: number;
}
BundleStats
interface
interface BundleStats {
files: number;
totalBytes: number;
scriptBytes: number;
largest: BundleFile | null;
}
BundleReport
interface
interface BundleReport {
ok: boolean;
errors: ManifestIssue[];
warnings: ManifestIssue[];
stats: BundleStats;
}
@ralyoo/sdk/vendor
Pustaka bersama yang disediakan platform.
VENDOR_LIBS
konstanta
Shared game libraries. The platform serves each one once under /vendor/
const VENDOR_LIBS: {
readonly three: '0.187.0';
readonly 'pixi.js': '8.22.0';
readonly '@dimforge/rapier2d-compat': '0.21.0';
readonly '@dimforge/rapier3d-compat': '0.21.0';
};
VENDOR_BASE
konstanta
Served with Cache-Control: immutable, so a directory name is a cache key: when the files
under it change for the same library version, the name must change too (rapier became
<lib>@<version>-wasm when its wasm moved out of the JS).
const VENDOR_BASE = "/vendor/";
VENDOR_IMPORTS
konstanta
Bare specifier → path under VENDOR_BASE.
const VENDOR_IMPORTS: Record<string, string>;
VENDOR_WASM
konstanta
Rapier's wasm, served as its own file next to rapier.mjs instead of the base64 the -compat
build inlines (2D: 926 KB gz instead of 1,252 KB). RAPIER.init() fetches it.
const VENDOR_WASM: readonly ["[email protected]/rapier_wasm2d_bg.wasm", "[email protected]/rapier_wasm3d_bg.wasm"];
VENDOR_EXTERNALS
konstanta
Specifiers games must leave external in their builds.
const VENDOR_EXTERNALS: string[];
importMap
fungsi
Import map for a game document; origin makes it work from srcdoc and opaque origins.
function importMap(origin: string): {
imports: Record<string, string>;
};
@ralyoo/sdk/semver
Rentang versi untuk field sdk.
parseVersion
fungsi
Parses a full version (1.2.3, prerelease and build tags allowed and ignored).
function parseVersion(text: string): Version | null;
compareVersions
fungsi
function compareVersions(a: Version, b: Version): number;
validRange
fungsi
True when range is a range this module understands.
function validRange(range: string): boolean;
satisfies
fungsi
True when version lies inside every comparator of range. Unparseable input is false.
function satisfies(version: string, range: string): boolean;
Version
tipe
The small part of semver the manifest needs: x.y.z versions and ranges made of comparators
(>=1.1 <2), carets (^1.1), tildes (~1.1.0), and x-ranges (1.x, 1, *). No ||.
type Version = [major: number, minor: number, patch: number];