Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion vendor/plot-client/handler-client.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Player, RoomDefinition } from './handler';
import { Player, RoomDefinition } from '@plot/handler';

type RunHandlerCtx = {
state: unknown;
Expand Down Expand Up @@ -73,6 +73,9 @@ declare class CorrectionTrack {
private hasRecord;
constructor(opts: CorrectionOpts);
record(drift: Value, now: number): void;
/** Drop any in-flight correction immediately (used when a drift exceeds the
* snap threshold — the caller snaps instead of easing). */
reset(): void;
read(now: number): Value;
}

Expand Down
95 changes: 93 additions & 2 deletions vendor/plot-client/handler.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Channel, Profile, PeriodOverride, LeaderboardEntry } from './protocol';
import { Channel, Profile, PeriodOverride, LeaderboardEntry } from '@plot/protocol';

/**
* Read-only view of the world at a past timestamp.
Expand Down Expand Up @@ -40,6 +40,89 @@ interface ReplayApi {
data: unknown;
}): void;
}
interface AssetMeta {
path: string;
hash: string;
size: number;
contentType: string;
visibility: 'public' | 'private';
}
interface AssetsApi {
/** Public, immutable content-hash CDN URL. Synchronous, offline (built from
* the seeded manifest). Throws if the path is unknown or private (use
* `signedUrl`). */
url(path: string): string;
/** Short-lived signed URL for a private asset (ttl seconds, clamped to an app
* ceiling). Resolves via local HMAC for managed/proxy apps. */
signedUrl(path: string, opts?: {
ttl?: number;
}): Promise<string>;
/** Manifest read from the seeded map — no I/O. */
list(prefix?: string): Promise<AssetMeta[]>;
}
/** The completion of an NPC line requested via `ctx.ai.npc(id).say()`. Delivered
* later via the room's `onAiResult(result, ctx)` — never returned inline. */
interface AiResult {
requestId: string;
npcId: string;
from: string;
/** The generated line, or '' when `error` is set. */
text: string;
usage?: {
inputTokens: number;
outputTokens: number;
cached: boolean;
};
/** `timeout` = the request was in flight when the room was evicted and could
* not complete; the reaper delivered this so your handler never hangs.
* `blocked` = the generated line failed the (opt-in) moderation filter. */
error?: 'rate_limited' | 'quota_exceeded' | 'disabled' | 'model_error' | 'timeout' | 'blocked';
/** Tool calls the NPC requested (when `say({ tools })` was used). Present
* instead of `text` when the model chose to call a tool. */
toolCalls?: AiToolCall[];
}
/** A function/tool the NPC decided to invoke. Your `onAiResult` executes it and
* may continue the exchange with another `say()`. */
interface AiToolCall {
/** The tool name (matches one you passed in `say({ tools })`). */
name: string;
/** Arguments the model produced, parsed from its JSON. */
arguments: Record<string, unknown>;
}
/** A tool the NPC may call, declared per `say()`. The model is told the name,
* description, and JSON-schema parameters; if it calls the tool, the call
* arrives on `onAiResult.toolCalls` for your handler to execute. */
interface AiToolDef {
name: string;
description: string;
/** JSON Schema for the arguments object (as Workers AI expects). */
parameters: Record<string, unknown>;
}
interface NpcHandle {
/** Request one in-character NPC line. Records an intent and resolves with a
* requestId — it does NOT return the line; the completion arrives later via
* `onAiResult` (matching requestId). maxTokens is clamped server-side.
*
* - `stream: true` streams the line to the room's players as it generates
* (an `ai-chunk` message per delta); the final `onAiResult` still fires.
* - `tools` lets the NPC call your game functions — a chosen call arrives on
* `onAiResult.toolCalls` instead of `text`. */
say(input: {
from: string;
text: string;
conversationKey?: string;
maxTokens?: number;
stream?: boolean;
tools?: AiToolDef[];
}): Promise<{
requestId: string;
}>;
/** Reset short-term memory for a conversation (default key = the player id). */
forget(conversationKey?: string): Promise<void>;
}
interface AiApi {
npc(id: string): NpcHandle;
}
interface HandlerContext<S> {
state: S;
roomCode: string;
Expand Down Expand Up @@ -68,6 +151,8 @@ interface HandlerContext<S> {
leaderboard: (name: string) => LeaderboardHandle;
save: SaveApi;
replay: ReplayApi;
assets: AssetsApi;
ai: AiApi;
/**
* Run `cb` against the world state at `targetTs` (clamped to the
* server's 500ms rewind horizon). Use this to validate hits or other
Expand Down Expand Up @@ -116,7 +201,13 @@ interface RoomDefinition<S, M = unknown> {
* Workers-for-Platforms isolate boundary).
*/
onTimer?: (payload: unknown, ctx: HandlerContext<S>) => void | Promise<void>;
/**
* Fired when an NPC completion requested via `ctx.ai.npc(id).say()` is ready.
* Delivered as a fresh RPC (like onTimer) — the originating say() call returned
* a requestId long before the model finished. `result.requestId` matches.
*/
onAiResult?: (result: AiResult, ctx: HandlerContext<S>) => void | Promise<void>;
}
declare function defineRoom<S, M = unknown>(def: RoomDefinition<S, M>): RoomDefinition<S, M>;

export { type ChannelConfig, type ChannelsConfig, type HandlerContext, HandlerReject, type LeaderboardHandle, PersistenceError, type Player, type ProfileApi, type ReplayApi, type RewoundContext, type RoomDefinition, type SaveApi, defineRoom };
export { type AiApi, type AiResult, type AssetMeta, type AssetsApi, type ChannelConfig, type ChannelsConfig, type HandlerContext, HandlerReject, type LeaderboardHandle, type NpcHandle, PersistenceError, type Player, type ProfileApi, type ReplayApi, type RewoundContext, type RoomDefinition, type SaveApi, defineRoom };
147 changes: 143 additions & 4 deletions vendor/plot-client/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CorrectionType } from '@plot/handler-client';
import { QueueEntry, CorrectionType } from '@plot/handler-client';
export { CorrectionType } from '@plot/handler-client';
import { RoomDefinition } from '@plot/handler';
import { ClientEnvelope, ServerEnvelope, Channel, Profile, LeaderboardEntry, MatchMode } from '@plot/protocol';
Expand All @@ -22,13 +22,21 @@ declare class SnapshotBuffer {
get size(): number;
get oldest(): Snapshot | null;
get newest(): Snapshot | null;
/** The snapshot just before `newest` — the other end of the last segment,
* used to estimate velocity when extrapolating past the newest snapshot. */
get secondNewest(): Snapshot | null;
}

type InterpType = 'number' | 'vec2' | 'vec3' | 'quat';
type InterpolateOpts = {
path: string;
type: InterpType;
renderDelay?: number;
/** When the render target runs *past* the newest snapshot (a late/dropped
* packet), project forward using the last segment's velocity instead of
* freezing — clamped to this many ms so a long gap can't overshoot. Default
* 0 (disabled → hold at the last value). Ignored for `quat`. */
extrapolateMs?: number;
};

/**
Expand Down Expand Up @@ -62,8 +70,50 @@ interface Transport {
}) => void): void;
}

interface LocalStore {
get(key: string): string | null;
set(key: string, value: string): void;
remove(key: string): void;
}
declare class MemoryStore implements LocalStore {
private map;
get(key: string): string | null;
set(key: string, value: string): void;
remove(key: string): void;
}
declare class WebStorageStore implements LocalStore {
private storage;
constructor(storage: Storage);
get(key: string): string | null;
set(key: string, value: string): void;
remove(key: string): void;
}
declare function createLocalStore(preferred?: LocalStore): LocalStore;

interface SessionState {
reconnectToken?: string;
tokenExpiresAt?: number;
snapshot?: {
ts: number;
state: unknown;
};
outbox: QueueEntry[];
lastAckedSeq: number;
nextSeq: number;
}
declare function sessionKey(appId: string, roomCode: string, playerId: string): string;
declare class SessionStore {
private store;
private key;
constructor(store: LocalStore, key: string);
load(): SessionState | null;
save(patch: Partial<SessionState>): void;
clear(): void;
}

type RoomMessageEvent = {
from: string;
channel: Channel;
data: unknown;
};
type PresenceEvent = {
Expand All @@ -83,6 +133,12 @@ type PredictOpts = {
path: string;
type: CorrectionType;
correctionMs?: number;
/** Drift magnitude above which the correction snaps instead of easing —
* large mispredictions (teleports, server resets) jump straight to the
* authoritative value rather than sliding across the world. In the same
* units as the predicted value (Euclidean norm for vectors). Default:
* Infinity (always ease). */
snapThreshold?: number;
};
interface RoomEvents {
message: (e: RoomMessageEvent) => void;
Expand All @@ -97,6 +153,13 @@ interface RoomEvents {
}) => void;
/** A reconnect succeeded; the server will re-send a snapshot to resync. */
reconnect: () => void;
/** A streamed NPC line delta (from a handler `say({ stream: true })`). Append
* `delta` to the line identified by `requestId` to show the NPC "typing". */
npcChunk: (e: {
requestId: string;
npcId: string;
delta: string;
}) => void;
}
interface SendOptions {
channel?: Channel;
Expand All @@ -108,6 +171,9 @@ declare class Room {
/** Re-establishes a transport for the same room (fresh token, optional
* resume token). Null when the Room was created without reconnect support. */
private reconnectFn;
/** Durable session cache (outbox + last snapshot + resume token). Null
* disables persistence; the server snapshot stays authoritative either way. */
private session;
private listeners;
/** Latest server-issued resume token (refreshed each (re)connect); presented
* on reconnect to resume the same slot within the grace window. */
Expand All @@ -128,12 +194,22 @@ declare class Room {
private lastDrift;
private predictedTracks;
correctedState: Record<string, unknown>;
/** Unacked predicted inputs, replayed after a reconnect and persisted via
* `session` so a page reload can resume without losing them. */
private outbox;
constructor(_playerId: string, transport: Transport,
/** Re-establishes a transport for the same room (fresh token, optional
* resume token). Null when the Room was created without reconnect support. */
reconnectFn?: ((resumeToken: string | null) => Promise<Transport>) | null);
reconnectFn?: ((resumeToken: string | null) => Promise<Transport>) | null,
/** Durable session cache (outbox + last snapshot + resume token). Null
* disables persistence; the server snapshot stays authoritative either way. */
session?: SessionStore | null);
/** Internal: register a sink that receives every applied (ts, state). */
_onSnapshot(sink: (ts: number, state: unknown) => void): void;
/** Prune the outbox up to the server's ack and persist the authoritative
* snapshot + outbox. Called from both state-snapshot and state-patch. */
private onAck;
private persistSession;
/** (Re)bind message + close handlers to the current transport. Called on
* construction and after each successful reconnect (the transport is swapped). */
private wireTransport;
Expand Down Expand Up @@ -222,6 +298,42 @@ declare class SaveClient {
get<T = unknown>(slot: string): Promise<T | null>;
}

interface AssetMeta {
path: string;
hash: string;
size: number;
contentType: string;
visibility: 'public' | 'private';
}
/**
* Browser SDK asset client (`plot.assets`), mirroring the handler `ctx.assets`.
* `list()` fetches the manifest once and caches it; `url()` then builds public
* immutable URLs offline (no per-call network). `signedUrl()` round-trips to the
* server so private URLs work regardless of backend/sign mode.
*/
declare class AssetClient {
private apiUrl;
private tokenGetter;
/** CDN/proxy base for this app, delivered by /v1/connect. */
private baseUrlGetter;
private cache;
constructor(apiUrl: string, tokenGetter: () => string,
/** CDN/proxy base for this app, delivered by /v1/connect. */
baseUrlGetter: () => string);
private auth;
/** Fetch + cache the manifest. Called lazily by list()/ready(). */
refresh(): Promise<void>;
/** Ensure the manifest is cached (so url() can resolve offline). */
ready(): Promise<void>;
list(prefix?: string): Promise<AssetMeta[]>;
/** Public immutable URL, built offline from the cached manifest. Throws if the
* manifest hasn't been loaded, or the path is unknown/private. */
url(path: string): string;
signedUrl(path: string, opts?: {
ttl?: number;
}): Promise<string>;
}

/**
* Managed netcode layer.
*
Expand All @@ -242,8 +354,14 @@ interface PathDecl {
type: InterpType;
/** Local paths only: error-correction window (ms). Default 100. */
correctionMs?: number;
/** Local paths only: drift magnitude above which the correction snaps to
* the authoritative value instead of easing. Default: always ease. */
snapThreshold?: number;
/** Remote paths only: snapshot render delay (ms). Default 100. */
renderDelay?: number;
/** Remote paths only: when a snapshot is late, project forward with the last
* velocity up to this many ms instead of freezing. Default 0 (hold). */
extrapolateMs?: number;
}
/** Resolved per-frame view handed to `onFrame`, assembled from the Room. */
interface FrameView<S> {
Expand Down Expand Up @@ -305,27 +423,48 @@ interface Game<S, I> {

interface PlotOptions {
appKey: string;
playerId: string;
/** Stable anonymous player id. Optional when `playerToken` is supplied — the
* server then trusts the token's `sub` claim as the player id. */
playerId?: string;
/** A developer-issued player JWT (HS256, signed with the app's JWT secret,
* `sub` = your player id). When present, the server verifies it and uses its
* `sub` as the authoritative player id — the way to tie Plot identity to
* your own accounts so players can't impersonate one another. */
playerToken?: string;
apiUrl?: string;
/** App id for multi-tenant routing — sent as the `X-Plot-App` header so the
* server resolves THIS app (not its default APP_ID) and verifies `appKey`
* against it. Required when an account hosts more than one app. */
appId?: string;
/** Backing store for durable session resume (outbox + last snapshot).
* Defaults to localStorage when available, in-memory otherwise. */
store?: LocalStore;
}
interface JoinOptions {
mode?: MatchMode;
roomCode?: string;
maxPlayers?: number;
attrs?: Record<string, string | number>;
rank?: number;
/** Restore a cached session (last snapshot + unacked outbox) for this
* app/room/player and keep persisting it. Default true. */
resume?: boolean;
}
declare class Plot {
private options;
private currentToken;
private assetBaseUrl;
profile: ProfileClient;
save: SaveClient;
leaderboard: (name: string) => LeaderboardClient;
assets: AssetClient;
constructor(options: PlotOptions);
/** The /v1/connect request body. Includes the developer JWT as `token` when a
* `playerToken` is configured (the server derives the player id from it). */
private connectBody;
/** Player id for keying local session storage. Falls back to the token when
* no explicit playerId is set (keeps a stable-per-token key). */
private get playerKey();
private getToken;
/** Headers for /v1/connect: JSON + the multi-tenant X-Plot-App routing header
* when an appId is configured. */
Expand Down Expand Up @@ -361,4 +500,4 @@ type Quat = {
w: number;
};

export { type FrameEvent, type FrameView, type Game, type InterpType, type InterpolateOpts, type PathDecl, type PlayOptions, Plot, type PredictOpts, type PredictedEvent, type Quat, Room, type SendOptions, type Vec2, type Vec3 };
export { AssetClient, type AssetMeta, type FrameEvent, type FrameView, type Game, type InterpType, type InterpolateOpts, type LocalStore, MemoryStore, type PathDecl, type PlayOptions, Plot, type PredictOpts, type PredictedEvent, type Quat, Room, type SendOptions, type SessionState, SessionStore, type Vec2, type Vec3, WebStorageStore, createLocalStore, sessionKey };
Loading
Loading