import type { SaveConfig } from "./save.js"; import type { CommandAccess, CommandDef, CommandScope } from "./commandRunner.js"; import { runCommand } from "./commandRunner.js"; import { type GameRuntimeSnapshot, type RuntimeChunkRow, type RuntimePlayerRow, type RuntimeProfileRow, type RuntimeServerRow } from "./snapshot.js"; export type ServerLoopHooks = { onInit?: (ctx: RuntimeInitContext) => void; onNewPlayer?: (ctx: RuntimeLoopContext) => void; onTick?: (ctx: RuntimeWorldContext, dtSeconds: number) => void; /** * What `onNewPlayer` touches, so a join hydrates that instead of the whole world. Only consulted * when `onNewPlayer` exists — without the hook a join provably needs the joining member and nothing * else. Omit it and a join keeps hydrating everything. */ joinScope?: (userId: string, isNew: boolean) => CommandScope; }; export type RuntimeInitContext = { snapshot: GameRuntimeSnapshot; setSnapshot: (snapshot: GameRuntimeSnapshot) => void; /** * Host wall clock at the start of this call, in ms. The host already knows it, so anything keyed to * real time — a UTC date rollover, a `lastTickAt` anchor other code paths read, a seed or a * `createdAt` stamp — reads it here instead of re-deriving one by accumulating `dtSeconds` against a * persisted epoch. It is authoritative: unlike a `now` carried in command input, no client supplied it. */ nowMs: number; }; export type RuntimeLoopContext = RuntimeInitContext & { player: { userId: string; isNew: boolean; }; }; export type RuntimeWorldContext = RuntimeInitContext & { playerIds: readonly string[]; }; export type GameRuntimeDefinition = { gameId: string; topology?: "shared" | "rooms"; save: SaveConfig; commands: Record; loop?: ServerLoopHooks; }; export type HydrateInput = { gameId: string; serverId: string; serverRow: RuntimeServerRow; playersByUserId: Record; chunksByKey: Record; revision?: number; /** Host wall clock for `onInit`, in ms. Defaults to `Date.now()`. */ nowMs?: number; }; export type GameRuntime = { gameId: string; topology?: "shared" | "rooms"; save: SaveConfig; /** * Whether this runtime declares `loop.onTick`. A host's tick cron reads it to skip hydrating and * persisting a server whose `tick` is a no-op by construction — the difference between a world that * costs a full read/write every second while idle and one that costs nothing. */ hasTick: boolean; hydrate: (input: HydrateInput) => GameRuntimeSnapshot; runCommand: (snapshot: GameRuntimeSnapshot, actorUserId: string, commandName: string, input: unknown, /** Host wall clock handed to `validate`/`apply`, in ms. Defaults to `Date.now()`. */ nowMs?: number) => ReturnType; /** * What a command declares it will touch, evaluated before hydration so a host can load that slice * and nothing else. Missing scopes load only the actor and no chunks; `{}` explicitly requests all state. */ commandScope: (commandName: string, input: unknown, actorUserId: string) => CommandScope | undefined; /** Who may send a command, or `undefined` when the runtime has no command by that name. */ commandAccess: (commandName: string) => CommandAccess | undefined; /** What a join has to hydrate. `undefined` means the whole world, as when `onNewPlayer` is unscoped. */ joinScope: (userId: string, isNew: boolean) => CommandScope | undefined; /** `nowMs` is the host wall clock at the start of the tick, in ms; defaults to `Date.now()`. */ tick: (snapshot: GameRuntimeSnapshot, dtSeconds: number, nowMs?: number) => GameRuntimeSnapshot; joinPlayer: (snapshot: GameRuntimeSnapshot, userId: string, isNew: boolean, nowMs?: number) => GameRuntimeSnapshot; toProfileRow: (snapshot: GameRuntimeSnapshot, userId: string) => RuntimeProfileRow | null; }; /** @internal */ export declare function createGameRuntime(definition: GameRuntimeDefinition): GameRuntime; /** Seed a fresh player through the same onNewPlayer hook as joining, without hydrating or modifying a world. */ export declare function initialPlayerState(runtime: GameRuntime, userId: string, nowMs?: number): RuntimePlayerRow;