# 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`](#ralyoo-sdk) | Yang dipakai setiap game: `defineGame`, `ctx`, dan tipe-tipenya. |
| [`@ralyoo/sdk/widgets`](#ralyoo-sdk-widgets) | Layout controller deklaratif dan validasinya. |
| [`@ralyoo/sdk/testing`](#ralyoo-sdk-testing) | Menjalankan game dalam tes, tanpa browser. |
| [`@ralyoo/sdk/governor`](#ralyoo-sdk-governor) | Resolusi render dinamis untuk menjaga fps. |
| [`@ralyoo/sdk/vite`](#ralyoo-sdk-vite) | Preset build game. |
| [`@ralyoo/sdk/manifest`](#ralyoo-sdk-manifest) | Tipe dan validasi `ralyoo.json`. |
| [`@ralyoo/sdk/bundle`](#ralyoo-sdk-bundle) | Aturan bundel yang dipakai `ralystudioo` dan registry. |
| [`@ralyoo/sdk/vendor`](#ralyoo-sdk-vendor) | Pustaka bersama yang disediakan platform. |
| [`@ralyoo/sdk/semver`](#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.

```ts
export default defineGame({
  onStart(ctx) { ctx.setController('all', layout) },
  onInput(ctx, player, input) { … },
});
```

```ts
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).

```ts
const BUTTON_BITS: Readonly<Record<ButtonName, number>>;
```

### isDown

*fungsi*

True while `button` is held in `state`: `isDown(ctx.input(1), 'A')`.

```ts
function isDown(state: PadState, button: ButtonName): boolean;
```

### SDK_MAJOR

*konstanta*

```ts
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.

```ts
const SDK_VERSION = "1.1.0";
```

### mulberry32

*fungsi*

Small, fast, seedable PRNG (mulberry32). Same seed → same sequence on every device.

```ts
function mulberry32(seed: number): () => number;
```

### AudioBus

*tipe*

```ts
type AudioBus = 'music' | 'sfx' | 'voice';
```

### ButtonName

*tipe*

```ts
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')`.

```ts
interface EndOptions {
    team?: 'win' | 'lose';
    screen?: 'platform' | 'game';
}
```

### ExitTarget

*tipe*

Where `ctx.exit` goes after a game-owned results screen.

```ts
type ExitTarget = 'again' | 'lobby';
```

### InputEvent

*tipe*

What a game receives in `onInput`: `{ widget: "A", type: "press" }` and friends.

```ts
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.

```ts
interface MusicMix {
    db: number;
    lowpassHz: number | null;
    rampMs: number;
}
```

### PadState

*interface*

Latest controller state; sticks normalised to −1..1.

```ts
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).

```ts
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*

```ts
type PlayerId = 1 | 2 | 3 | 4;
```

### PlayerInfo

*interface*

```ts
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.

```ts
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*

```ts
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*

```ts
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*

```ts
type Strings = Record<string, string>;
```

### ControllerLayout

*interface*

```ts
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.

```ts
type Orientation = 'any' | 'portrait' | 'landscape';
```

### Tone

*tipe*

```ts
type Tone = 'pearl' | 'led' | 'plasma' | 'ultraviolet' | 'aurora' | 'champagne' | 'arcade';
```

### Widget

*tipe*

```ts
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).

```ts
const FACE_BUTTONS: {
    readonly A: number;
    readonly B: number;
    readonly X: number;
    readonly Y: number;
};
```

### LIMITS

*konstanta*

```ts
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*

```ts
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.

```ts
function validateLayout(raw: unknown): ControllerLayout | null;
```

### orientationOf

*fungsi*

The orientation a layout asks for; a missing field means `any`.

```ts
function orientationOf(layout: ControllerLayout | null | undefined): Orientation;
```

### isFaceButton

*fungsi*

```ts
function isFaceButton(id: string): id is FaceButton;
```

### validateWidgetEvent

*fungsi*

```ts
function validateWidgetEvent(raw: unknown): WidgetEvent | null;
```

### FaceButton

*tipe*

```ts
type FaceButton = keyof typeof FACE_BUTTONS;
```

### Tone

*tipe*

```ts
type Tone = 'pearl' | 'led' | 'plasma' | 'ultraviolet' | 'aurora' | 'champagne' | 'arcade';
```

### Widget

*tipe*

```ts
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.

```ts
type Orientation = 'any' | 'portrait' | 'landscape';
```

### ControllerLayout

*interface*

```ts
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.

```ts
interface WidgetEvent {
    id: string;
    type: 'press' | 'release' | 'page' | 'vote';
    value?: string | number;
}
```

## @ralyoo/sdk/testing

Menjalankan game dalam tes, tanpa browser.

### createTestHost

*fungsi*

```ts
function createTestHost(def: GameDefinition, options?: TestHostOptions): TestHost;
```

### TestHostOptions

*interface*

```ts
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*

```ts
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*

```ts
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.

```ts
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.

```ts
// vite.config.ts
import { gameViteConfig } from '@ralyoo/sdk/vite';
import { defineConfig } from 'vite';
export default defineConfig(gameViteConfig());
```

```ts
function gameViteConfig(): UserConfig;
```

## @ralyoo/sdk/manifest

Tipe dan validasi `ralyoo.json`.

### PILLARS

*konstanta*

```ts
const PILLARS: readonly ['konsol', 'rahasia'];
```

### INPUT_KINDS

*konstanta*

```ts
const INPUT_KINDS: readonly ['buttons', 'dpad', 'joystick', 'cards', 'vote'];
```

### PERMISSIONS

*konstanta*

```ts
const PERMISSIONS: readonly ['vibrate'];
```

### MANIFEST_SCHEMA

*konstanta*

JSON Schema (draft 2020-12) of `ralyoo.json`.

```ts
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.

```ts
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.

```ts
function validateManifest(raw: unknown): ManifestReport;
```

### Localized

*tipe*

A text, or one text per locale (`{ "id": "…", "en": "…" }`).

```ts
type Localized = string | Record<string, string>;
```

### Pillar

*tipe*

```ts
type Pillar = (typeof PILLARS)[number];
```

### InputKind

*tipe*

```ts
type InputKind = (typeof INPUT_KINDS)[number];
```

### Permission

*tipe*

```ts
type Permission = (typeof PERMISSIONS)[number];
```

### GameManifest

*interface*

```ts
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*

```ts
interface ManifestIssue {
    /** JSON pointer of the offending value, e.g. `/media/color`; empty for the manifest itself. */
    path: string;
    message: string;
}
```

### ManifestReport

*interface*

```ts
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*

```ts
const BUNDLE_EXTENSION = ".ralyoo";
```

### BUNDLE_MANIFEST

*konstanta*

```ts
const BUNDLE_MANIFEST = "ralyoo.json";
```

### BUNDLE_ENTRY

*konstanta*

```ts
const BUNDLE_ENTRY = "main.js";
```

### BUNDLE_LIMITS

*konstanta*

```ts
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.

```ts
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.

```ts
const BUNDLE_FILE_TYPES: Readonly<Record<string, string>>;
```

### extensionOf

*fungsi*

```ts
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.

```ts
function bundleContentType(path: string): string | null;
```

### bundlePathProblem

*fungsi*

Why a path cannot be in a bundle, or null when it can.

```ts
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).

```ts
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).

```ts
function checkScript(path: string, source: string): Pick<BundleReport, 'errors' | 'warnings'>;
```

### BundleFile

*interface*

```ts
interface BundleFile {
    /** Path inside the bundle, `/`-separated, no leading slash. */
    path: string;
    /** Unpacked size in bytes. */
    size: number;
}
```

### BundleStats

*interface*

```ts
interface BundleStats {
    files: number;
    totalBytes: number;
    scriptBytes: number;
    largest: BundleFile | null;
}
```

### BundleReport

*interface*

```ts
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/<dir>/ 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.

```ts
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).

```ts
const VENDOR_BASE = "/vendor/";
```

### VENDOR_IMPORTS

*konstanta*

Bare specifier → path under VENDOR_BASE.

```ts
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.

```ts
const VENDOR_WASM: readonly ["rapier2d@0.21.0-wasm/rapier_wasm2d_bg.wasm", "rapier3d@0.21.0-wasm/rapier_wasm3d_bg.wasm"];
```

### VENDOR_EXTERNALS

*konstanta*

Specifiers games must leave external in their builds.

```ts
const VENDOR_EXTERNALS: string[];
```

### importMap

*fungsi*

Import map for a game document; `origin` makes it work from srcdoc and opaque origins.

```ts
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).

```ts
function parseVersion(text: string): Version | null;
```

### compareVersions

*fungsi*

```ts
function compareVersions(a: Version, b: Version): number;
```

### validRange

*fungsi*

True when `range` is a range this module understands.

```ts
function validRange(range: string): boolean;
```

### satisfies

*fungsi*

True when `version` lies inside every comparator of `range`. Unparseable input is false.

```ts
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 `||`.

```ts
type Version = [major: number, minor: number, patch: number];
```
