Ralyoo dokumentasi developerSDK 1.1.0ralyoo.com

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/

/ with immutable caching, and every game iframe gets an import map pointing bare specifiers at them, so games build with these as externals and never ship their own copy.

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];

Halaman ini sebagai Markdown: referensi-api.md · semua halaman: llms.txt