diff --git a/examples/next-js/next.config.ts b/examples/next-js/next.config.ts index 7921f35d74..64626507f1 100644 --- a/examples/next-js/next.config.ts +++ b/examples/next-js/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + serverExternalPackages: ["@rivetkit/sqlite", "@rivetkit/sqlite-vfs"], }; export default nextConfig; diff --git a/rivetkit-typescript/packages/rivetkit/package.json b/rivetkit-typescript/packages/rivetkit/package.json index fe0c527440..556d7c15f0 100644 --- a/rivetkit-typescript/packages/rivetkit/package.json +++ b/rivetkit-typescript/packages/rivetkit/package.json @@ -239,7 +239,7 @@ "tar": "^7.5.0", "uuid": "^12.0.0", "vbare": "^0.0.4", - "wa-sqlite": "^1.0.0", + "@rivetkit/sqlite": "^0.1.0", "zod": "^4.1.0" }, "devDependencies": { diff --git a/rivetkit-typescript/packages/sqlite-vfs/package.json b/rivetkit-typescript/packages/sqlite-vfs/package.json index 58edbf192d..5fb868d7d7 100644 --- a/rivetkit-typescript/packages/sqlite-vfs/package.json +++ b/rivetkit-typescript/packages/sqlite-vfs/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@rivetkit/bare-ts": "^0.6.2", - "wa-sqlite": "^1.0.0", + "@rivetkit/sqlite": "^0.1.0", "vbare": "^0.0.4" }, "devDependencies": { diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts b/rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts index ddca21c907..1308adab50 100644 --- a/rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts +++ b/rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts @@ -8,10 +8,10 @@ // Note: wa-sqlite VFS.Base type definitions have incorrect types for xRead/xWrite // The actual runtime uses Uint8Array, not the {size, value} object shown in types -import * as VFS from "wa-sqlite/src/VFS.js"; +import * as VFS from "@rivetkit/sqlite/src/VFS.js"; -import SQLiteESMFactory from "wa-sqlite/dist/wa-sqlite-async.mjs"; -import { Factory } from "wa-sqlite"; +import SQLiteESMFactory from "@rivetkit/sqlite/dist/wa-sqlite-async.mjs"; +import { Factory } from "@rivetkit/sqlite"; import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import { CHUNK_SIZE, getMetaKey, getChunkKey } from "./kv"; @@ -235,7 +235,7 @@ export class SqliteVfs { this.#initPromise = (async () => { // Load WASM binary (Node.js environment) const require = createRequire(import.meta.url); - const wasmPath = require.resolve("wa-sqlite/dist/wa-sqlite-async.wasm"); + const wasmPath = require.resolve("@rivetkit/sqlite/dist/wa-sqlite-async.wasm"); const wasmBinary = readFileSync(wasmPath); // Initialize wa-sqlite module - each instance gets its own module diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/wa-sqlite.d.ts b/rivetkit-typescript/packages/sqlite-vfs/src/wa-sqlite.d.ts index 9492d96a6f..eeb81a13b9 100644 --- a/rivetkit-typescript/packages/sqlite-vfs/src/wa-sqlite.d.ts +++ b/rivetkit-typescript/packages/sqlite-vfs/src/wa-sqlite.d.ts @@ -1,8 +1,8 @@ -declare module "wa-sqlite" { +declare module "@rivetkit/sqlite" { export function Factory(module: any): any; } -declare module "wa-sqlite/src/VFS.js" { +declare module "@rivetkit/sqlite/src/VFS.js" { export class Base { handleAsync(fn: () => Promise): number; } @@ -20,7 +20,7 @@ declare module "wa-sqlite/src/VFS.js" { export const SQLITE_OPEN_READWRITE: number; } -declare module "wa-sqlite/dist/wa-sqlite-async.mjs" { +declare module "@rivetkit/sqlite/dist/wa-sqlite-async.mjs" { const factory: (config?: { wasmBinary?: ArrayBuffer }) => Promise; export default factory; } diff --git a/website/src/content/cookbook/multiplayer-game.mdx b/website/src/content/cookbook/multiplayer-game.mdx index 7bd99216e5..c82ca5dcae 100644 --- a/website/src/content/cookbook/multiplayer-game.mdx +++ b/website/src/content/cookbook/multiplayer-game.mdx @@ -23,12 +23,13 @@ Start with one of the working examples on [GitHub](https://github.com/rivet-dev/ | Turn-Based | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/turn-based/) | Chess correspondence, Words With Friends, async board games | | Idle | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/idle/) | Cookie Clicker, Idle Miner Tycoon, Adventure Capitalist | +## Server Simulation -## Game Loop And Tick Rates +### Game Loop And Tick Rates Per-pattern tick rate recommendations are listed in the summary bullets at the start of each architecture pattern. -### Implementing Tick Loops +#### Implementing Tick Loops | Pattern | Use When | Implementation Guidance | | --- | --- | --- | @@ -36,49 +37,7 @@ Per-pattern tick rate recommendations are listed in the summary bullets at the s | Action-driven updates | Party, Turn-Based | Mutate and broadcast only on actions/events rather than scheduled ticks. | | Coarse offline progression | Any mode with idle progression | Use `c.schedule.after(...)` with coarse windows (for example 5 to 15 minutes) and apply catch-up from elapsed wall clock time. | -## Realtime Data Model - -- Publish game state from the server as events, using snapshots and/or diffs. In hybrid modes, this covers combat, entities, and other players. In server-authoritative modes, this covers everything including movement. - - Example pattern: send a full snapshot on join/resync, then send per-tick diffs for regular updates. -- Keep events small and typed. For high-frequency updates, batch per tick. -- Avoid using UI-framework state/hooks for high-frequency game updates. Keep simulation and render loops in engine code, and use framework state for basic UI only (menus, HUD, forms), especially with Canvas or Three.js renderers. -- When all players should receive the same update, use `c.broadcast(...)`. When players should receive different/private data, send per-connection payloads with `conn.send(...)`. -- Frontend bundling: place shared types, constants, and pure helpers in `src/shared/` (for example `src/shared/sim/*`). Avoid importing backend actor modules into browser bundles, since backend modules often include Node-only dependencies (`fs`, SQLite bindings, etc). - -## Interest Management - -### Per-Player Replication Filters - -- Send each client only the state relevant to that player (for example by proximity, line-of-sight, team, or game phase). -- For shooters and action games, limit replication by proximity and optional field-of-view checks. -- Keep filtering server-side so clients never receive data they should not see. - -### Sharded Worlds - -- Partition very large worlds across data actors (for example chunk actors keyed by `worldId:chunkX:chunkY`). -- Clients subscribe only to nearby partitions (for example a 3x3 chunk window around the player). -- Use this when you are confident the world is large and state-heavy (for example sandbox builders or MMOs), not as a default for small matches. - -## Netcode - -Per-pattern netcode recommendations live under each architecture pattern in `#### Netcode`. - -| Model | When To Use | How To Implement (Basic) | -| --- | --- | --- | -| Hybrid (client movement, server combat) | Latency-sensitive competitive games where movement responsiveness is critical but combat integrity matters (shooters, action sports, ranked duels). | Client owns movement locally and sends position updates at a capped rate. Server validates speed, bounds, and collision for anti-cheat. Projectiles, hits, damage, and other game entities are fully server-authoritative. Server simulates combat from its own state and broadcasts results. | -| Server-authoritative with interpolation | Realtime games where fairness and simplicity outweigh instant local feedback (IO Style, persistent worlds). | Client sends input commands. Server simulates on fixed ticks, resolves movement and collision, and publishes authoritative snapshots. Client interpolates between snapshots and extrapolates briefly on missed updates. | -| Server-authoritative (basic logic) | Event-driven or turn-based games with no realtime simulation. | Server validates and applies discrete actions (turns, phase transitions, votes). No continuous tick loop needed. Client displays confirmed server state. | - -## Shared Simulation Logic - -Use this for logic shared between client and server. - -- Keep shared logic basic and pure (movement integration, input transforms, constants). -- Put shared code in `src/shared/`, and keep deterministic simulation helpers in `src/shared/sim/*` with no side effects. -- Do not share actor runtime code (DB access, network calls, `c.*` context, timers). -- In hybrid modes, the client runs shared movement logic as the primary authority and the server runs it for anti-cheat validation. In server-authoritative modes, the client uses shared logic for interpolation and prediction only. - -## Physics And Spatial Indexing +### Physics And Spatial Indexing Per-pattern physics recommendations and optional spatial indexing notes are listed in the summary tables at the start of each architecture pattern. @@ -105,40 +64,53 @@ Per-pattern physics recommendations and optional spatial indexing notes are list | Backend/runtime boundary | Keep frontend-only libs out of backend simulation paths; treat server state as authoritative. | | Client mesh raycasts | `three-mesh-bvh` is optional and mostly for client-side fast raycasts on dense static meshes. | -## Security And Anti-Cheat +## Networking & State Sync -Start with this baseline, then harden further for competitive or high-risk environments. +### Netcode -### Baseline Checklist +Per-pattern netcode recommendations live under each architecture pattern in `#### Netcode`. -Apply this checklist to all implementations. +| Model | When To Use | How To Implement (Basic) | +| --- | --- | --- | +| Hybrid (client movement, server combat) | Latency-sensitive competitive games where movement responsiveness is critical but combat integrity matters (shooters, action sports, ranked duels). | Client owns movement locally and sends position updates at a capped rate. Server validates speed, bounds, and collision for anti-cheat. Projectiles, hits, damage, and other game entities are fully server-authoritative. Server simulates combat from its own state and broadcasts results. | +| Server-authoritative with interpolation | Realtime games where fairness and simplicity outweigh instant local feedback (IO Style, persistent worlds). | Client sends input commands. Server simulates on fixed ticks, resolves movement and collision, and publishes authoritative snapshots. Client interpolates between snapshots and extrapolates briefly on missed updates. | +| Server-authoritative (basic logic) | Event-driven or turn-based games with no realtime simulation. | Server validates and applies discrete actions (turns, phase transitions, votes). No continuous tick loop needed. Client displays confirmed server state. | -- Identity: - - Use `c.conn.id` as the authoritative transport identity of the caller. - - If you accept `playerId`/`username` in params, treat it as untrusted input and bind it through server-issued assignment/join tickets before attaching player state. -- Authorization: - - Validate that the caller is allowed to mutate the target entity (room membership, turn ownership, host-only actions). -- Input validation: - - Clamp sizes/lengths and validate enums. - - Validate usernames (length, allowed chars, avoid unbounded Unicode). -- Rate limiting: - - Per-connection rate limits for spammy actions (chat, join/leave, fire, movement updates). -- State integrity: - - Server recomputes derived state (scores, win conditions, placements). - - Avoid client-authoritative changes to inventory/currency/leaderboard totals. +### Realtime Data Model -### Movement Validation +- Publish game state from the server as events, using snapshots and/or diffs. In hybrid modes, this covers combat, entities, and other players. In server-authoritative modes, this covers everything including movement. + - Example pattern: send a full snapshot on join/resync, then send per-tick diffs for regular updates. +- Keep events small and typed. For high-frequency updates, batch per tick. +- Avoid using UI-framework state/hooks for high-frequency game updates. Keep simulation and render loops in engine code, and use framework state for basic UI only (menus, HUD, forms), especially with Canvas or Three.js renderers. +- When all players should receive the same update, use `c.broadcast(...)`. When players should receive different/private data, send per-connection payloads with `conn.send(...)`. +- Frontend bundling: place shared types, constants, and pure helpers in `src/shared/` (for example `src/shared/sim/*`). Avoid importing backend actor modules into browser bundles, since backend modules often include Node-only dependencies (`fs`, SQLite bindings, etc). -Use this for any mode with client-authoritative movement (hybrid and client-authoritative flows). +### Shared Simulation Logic -Clients may send position/rotation updates for smoothness, but the server must: +Use this for logic shared between client and server. -- Enforce max delta per update (speed cap) based on elapsed time. -- Reject or clamp teleports. -- Enforce world bounds (and basic collision if applicable). -- Rate limit update frequency (for example 20Hz max). +- Keep shared logic basic and pure (movement integration, input transforms, constants). +- Put shared code in `src/shared/`, and keep deterministic simulation helpers in `src/shared/sim/*` with no side effects. +- Do not share actor runtime code (DB access, network calls, `c.*` context, timers). +- In hybrid modes, the client runs shared movement logic as the primary authority and the server runs it for anti-cheat validation. In server-authoritative modes, the client uses shared logic for interpolation and prediction only. -## Persistence +### Interest Management + +#### Per-Player Replication Filters + +- Send each client only the state relevant to that player (for example by proximity, line-of-sight, team, or game phase). +- For shooters and action games, limit replication by proximity and optional field-of-view checks. +- Keep filtering server-side so clients never receive data they should not see. + +#### Sharded Worlds + +- Partition very large worlds across data actors (for example chunk actors keyed by `worldId:chunkX:chunkY`). +- Clients subscribe only to nearby partitions (for example a 3x3 chunk window around the player). +- Use this when you are confident the world is large and state-heavy (for example sandbox builders or MMOs), not as a default for small matches. + +## Backend Infrastructure + +### Persistence Use this section to decide when SQLite is a better fit than in-memory actor state. @@ -150,11 +122,11 @@ Use this section to decide when SQLite is a better fit than in-memory actor stat - Matchmakers/coordinators may also use SQLite when their indexing state can grow large (room registries, matchmaking pools, large queues). - When using `rivetkit/db`, assume multiple actions can hit the same actor DB concurrently. Serialize DB work through a queue. -## Matchmaking Orchestration Primitives +### Matchmaking Orchestration Primitives Use these cross-cutting primitives as a baseline before picking a specific architecture pattern. -### Actor Topology Primitives +#### Actor Topology Primitives | Primitive | Use When | Typical Ownership | | --- | --- | --- | @@ -164,7 +136,7 @@ Use these cross-cutting primitives as a baseline before picking a specific archi | `player[username]` | Canonical profile/rating reused across matches | Durable player stats (for example rating and win/loss). | | `leaderboard["main"]` | Shared rankings across many matches/players | Global ordered score rows and top lists. | -### Matchmaking Building Blocks +#### Matchmaking Building Blocks - **Queue row**: A pending intent to match (mode/rating/capacity metadata). - **Assignment row**: Binds a connection/player to a specific `matchId`. @@ -173,13 +145,46 @@ Use these cross-cutting primitives as a baseline before picking a specific archi - **Occupancy heartbeat**: Match reports connected and pending counts back to matchmaker. - **Close/cleanup flow**: Remove queue, assignment, and reservation rows when a match ends. -### Queueing Strategy +#### Queueing Strategy - Prefer actor queues to serialize DB mutations on shared matchmaking tables. - Queue matchmaker actions that can race: find/create, queue/unqueue, verify/pending-connected, update/close. - Keep queueing on the actor that owns the shared index tables (usually the matchmaker). - Let match-local actions run without queueing unless they mutate shared matchmaking indexes. +## Security And Anti-Cheat + +Start with this baseline, then harden further for competitive or high-risk environments. + +### Baseline Checklist + +Apply this checklist to all implementations. + +- Identity: + - Use `c.conn.id` as the authoritative transport identity of the caller. + - If you accept `playerId`/`username` in params, treat it as untrusted input and bind it through server-issued assignment/join tickets before attaching player state. +- Authorization: + - Validate that the caller is allowed to mutate the target entity (room membership, turn ownership, host-only actions). +- Input validation: + - Clamp sizes/lengths and validate enums. + - Validate usernames (length, allowed chars, avoid unbounded Unicode). +- Rate limiting: + - Per-connection rate limits for spammy actions (chat, join/leave, fire, movement updates). +- State integrity: + - Server recomputes derived state (scores, win conditions, placements). + - Avoid client-authoritative changes to inventory/currency/leaderboard totals. + +### Movement Validation + +Use this for any mode with client-authoritative movement (hybrid and client-authoritative flows). + +Clients may send position/rotation updates for smoothness, but the server must: + +- Enforce max delta per update (speed cap) based on elapsed time. +- Reject or clamp teleports. +- Enforce world bounds (and basic collision if applicable). +- Rate limit update frequency (for example 20Hz max). + ## Architecture Patterns Each game type below starts with a quick summary table, then details actors and lifecycle.