import type { LeaderboardScope } from "../game/leaderboard.js"; import type { NormalizedScenarioReset } from "./persistenceScope.js"; import type { SaveConfig } from "./save.js"; import type { GameRuntimeSnapshot, RuntimeChunkRow, RuntimePlayerRow, RuntimeServerRow } from "./snapshot.js"; export type GameServerStatus = "open" | "running" | "closed"; export type SessionVisibility = "public" | "private"; export type SessionAttributes = { label?: string; mode?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; }; export type GameServerRecord = { serverId: string; gameId: string; status: GameServerStatus; mode?: string; modeConfig?: unknown; label?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; memberUserIds: string[]; slotsPerServer: number; save: SaveConfig; serverState: RuntimeServerRow; sessionPlayers: Record; revision: number; tickAnchorMs: number; lastSavedAt?: number; dirtyAt?: number; createdAt: number; updatedAt: number; }; export type PlayerProfileRecord = { userId: string; gameId: string; playerState: RuntimePlayerRow; revision: number; updatedAt: number; }; export type WorldChunkRecord = { serverId: string; chunkKey: string; snapshot: RuntimeChunkRow; updatedAt: number; }; export type LeaderboardIncrement = { userId: string; stat: string; scope: LeaderboardScope; serverId?: string; by: number; }; export type LeaderboardEntry = { userId: string; value: number; }; export declare const LEADERBOARD_PENDING_KEY = "leaderboardPending"; /** @internal */ export declare function drainPendingLeaderboardIncrements(session: Record): { increments: LeaderboardIncrement[]; session: Record; }; export declare const LEADERBOARD_TOP_LIMIT = 100; /** @internal */ export declare function clampLimit(value: number | undefined, fallback: number, max: number): number; export type LeaderboardRow = { gameId: string; stat: string; scope: LeaderboardScope; serverId?: string; userId: string; value: number; updatedAt: number; }; /** @internal */ export declare function leaderboardRowKey(row: Omit): string; /** @internal */ export declare function applyLeaderboardRows(rows: Map, gameId: string, entries: LeaderboardIncrement[], now: number): void; /** @internal */ export declare function topLeaderboardRows(rows: Iterable, args: { gameId: string; stat: string; scope: LeaderboardScope; serverId?: string; limit?: number; }): LeaderboardEntry[]; /** @internal */ export declare function profileLeaderboardStats(rows: Iterable, gameId: string, userId: string): Record; export declare const FEED_RING_LIMIT = 20; /** @internal */ export declare function trimFeedEntries(entries: T[], limit?: number): T[]; /** @internal */ export declare function shouldAutoSave(save: SaveConfig, dirtyAt: number | undefined, lastSavedAt: number | undefined, now: number): boolean; export type ServerListing = { serverId: string; status: GameServerStatus; memberCount: number; slotsPerServer: number; mode?: string; label?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; updatedAt: number; }; export type ToServerListingOptions = { includeJoinCode?: boolean; }; /** @internal */ export declare function toServerListing(record: GameServerRecord, options?: ToServerListingOptions): ServerListing; export declare const OPEN_SERVER_LISTING_LIMIT = 20; export declare const OPEN_SERVER_LISTING_MAX = 100; /** @internal */ export declare function toOpenServerListings(listings: Iterable, limit?: number): ServerListing[]; export type HostPersistence = { savePlan?: (plan: ServerPersistPlan) => Promise; resetScenario?: (reset: NormalizedScenarioReset) => Promise; loadServer: (serverId: string) => Promise; saveServer: (record: GameServerRecord) => Promise; listServers: (gameId: string) => Promise; loadProfile: (args: { userId: string; gameId: string; }) => Promise; saveProfile: (record: PlayerProfileRecord) => Promise; loadChunks: (serverId: string) => Promise; saveChunks: (serverId: string, chunks: WorldChunkRecord[]) => Promise; deleteChunks: (serverId: string, chunkKeys: string[]) => Promise; loadFeed: (args: { serverId: string; action: string; }) => Promise; appendFeed: (args: { serverId: string; action: string; entry: unknown; }) => Promise; applyLeaderboardIncrements: (gameId: string, entries: LeaderboardIncrement[]) => Promise; getLeaderboardTop: (args: { gameId: string; stat: string; scope: LeaderboardScope; serverId?: string; limit?: number; }) => Promise; getLeaderboardProfile: (args: { gameId: string; userId: string; }) => Promise>; }; /** * Hydrate the runtime's player map from the server row plus whatever profiles were loaded. * * `userIds` narrows the work to a subset of the roster (partial hydration); omit it for the whole * roster. A member with a loaded profile takes it as authoritative and keeps its session blob from * the server row. * * A member with no profile row resolves by save scope, not by falling back to the blob: when the game * persists players, a missing profile means the profile was deleted, so the member hydrates empty and * the deletion sticks. Only a game that never writes profiles (`save: "none"`, or a scope without * `player`) treats the server row's session copy as the player's state. * @internal */ export declare function buildHydratePlayers(server: GameServerRecord, profiles: Record, userIds?: readonly string[]): Record; /** True when a snapshot carries no unwritten mutation — nothing for a persist to do. */ export declare function isSnapshotClean(snapshot: GameRuntimeSnapshot): boolean; export type ServerPersistPlan = { server: GameServerRecord; profiles: PlayerProfileRecord[]; chunks: WorldChunkRecord[]; deletedChunks: string[]; leaderboard: LeaderboardIncrement[]; /** * Which fields of the server row the plan actually moved. A store that pays per written field — * or whose subscribers re-push a whole document on any change — writes only what changed and skips * the row entirely when `changed.any` is false. */ changed: { serverState: boolean; sessionPlayers: boolean; revision: boolean; any: boolean; }; }; /** @internal */ export declare function planServerPersist(server: GameServerRecord, snapshot: GameRuntimeSnapshot, save: SaveConfig, now: number): ServerPersistPlan;