openvibers/openvibe.games AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Games |
| 2 | |
| 3 | ## Project Overview |
| 4 | |
| 5 | OpenVibe.Games hosts the browser games at `openvibe.games` and `play.openvibe.games`: **Scraplandia** (a persistent |
| 6 | multiplayer physics sandbox with the scored "Haul" mode), **Salvage Rush** (on-foot timed collection), |
| 7 | **creator worlds** (map editor + `/worlds`, sandboxed), the casual line (plan T22, starting with **Vibe Heroes**), |
| 8 | server-run AI draft matches/Bench scoring, and the moved **OpenVibe.Play player pages** (`/achievements`, |
| 9 | `/leaderboards`, `/developers`) which read Play's public API server-side. |
| 10 | |
| 11 | - **Domain:** `openvibe.games`, `play.openvibe.games` · **Port:** 8000 (server; Vite dev client 5173) · |
| 12 | **Service id:** `games` · **Env prefix:** `GAMES`/`OV_` |
| 13 | - **Stack:** TypeScript everywhere, pnpm 10 monorepo, Babylon.js (WebGPU/WebGL) client, Rapier |
| 14 | `@dimforge/rapier3d-deterministic-compat` (one pinned build headless on server and in client prediction), WS |
| 15 | protocol, PostgreSQL 18 + Valkey through `openvibe-sdk`, QuickJS for script mods. |
| 16 | - **Read first:** [README.md](README.md), [ARCHITECTURE.md](ARCHITECTURE.md), [ROADMAP.md](ROADMAP.md), |
| 17 | [docs/games.md](docs/games.md), [docs/casual.md](docs/casual.md), [docs/mods.md](docs/mods.md), |
| 18 | [docs/instances.md](docs/instances.md), [docs/ai-matches.md](docs/ai-matches.md), [ADR-0007](docs/adr/), [ADR-0013](docs/adr/). |
| 19 | |
| 20 | **Network context:** players are keyed by Network `subject_id` (`usr_…`/`gst_…`); SSO/JWKS/identity/mod grants are |
| 21 | OpenVibe.Network. Events are OpenVibe.Events (transactional outbox). Achievements/scores go to OpenVibe.Play's API; |
| 22 | six built-in achievements grant Live items through OpenVibe.Inventory. Map-editor assets are also copied to |
| 23 | OpenVibe.Media objects. Mod grants' authority is Network (`mods.grant.manage`); Games runs its own bounded |
| 24 | `games-quickjs@1` scripts. The platform boundary is `apps/server/src/platform` and `apps/server/src/mods`; the |
| 25 | simulation packages do not know the platform exists. |
| 26 | |
| 27 | ## Commands |
| 28 | |
| 29 | ```bash |
| 30 | corepack enable && pnpm install # Node ≥22, pnpm 10.6.5 |
| 31 | pnpm dev # server + client dev in parallel |
| 32 | pnpm dev:server # authoritative server on :8000 (tsx watch) |
| 33 | pnpm dev:client # client hot reload on :5173 (proxies /ws to :8000) |
| 34 | pnpm test # all vitest files through test/run.js |
| 35 | pnpm typecheck # strict TypeScript across every package |
| 36 | pnpm lint # ESLint (no-explicit-any) |
| 37 | pnpm format # Prettier write (format:check in CI) |
| 38 | pnpm build # all packages + client bundle |
| 39 | node test/run.js --index # regenerate the flat test/*.test.js pointers after adding/moving a test |
| 40 | tsx apps/server/scripts/sliceTest.ts # end-to-end vertical slice: real server, protocol clients, restart |
| 41 | pnpm test:editor | test:collab | test:console # e2e Playwright checks (editor, collaboration, console) |
| 42 | pnpm audit:editor # editor architecture audit (vitest config) |
| 43 | ``` |
| 44 | |
| 45 | Production: `pnpm build`, then `DATABASE_URL=… DATABASE_DIRECT_URL=… STATIC_DIR=apps/client/dist PORT=8000 pnpm |
| 46 | --filter @openvibe/server start`. `DATABASE_URL` serves through PgBouncer; migrations run at boot on |
| 47 | `DATABASE_DIRECT_URL` (owner, direct); without both and outside production, PGlite under `DATABASE_DIR`. |
| 48 | |
| 49 | ## Architecture at a Glance |
| 50 | |
| 51 | - **Server entry:** `apps/server/src/main.ts`; authoritative simulation, networking, persistence, platform boundary |
| 52 | ([apps/server/src/platform](apps/server/src/platform)), mods ([apps/server/src/mods](apps/server/src/mods)), |
| 53 | maps, worlds, casual, AI matches, Steam lists, observability. |
| 54 | - **Client:** [apps/client/](apps/client/) — rendering, prediction, input, HUD, the map editor (`src/editor/`), e2e |
| 55 | scripts; Babylon is imported **only** here. |
| 56 | - **Packages:** [packages/shared/](packages/shared/) (math, ids, events, logging, fixed timestep), |
| 57 | [packages/protocol/](packages/protocol/) (versioned wire messages, zod-validated inbound), |
| 58 | [packages/content/](packages/content/) (data-driven items/recipes/worlds + validation registry), |
| 59 | [packages/gameplay/](packages/gameplay/) (pure inventory/crafting/movement/zones/entities), |
| 60 | [packages/physics/](packages/physics/) (abstraction + Rapier adapter), |
| 61 | [packages/persistence/](packages/persistence/) (DTOs, repositories, PostgreSQL, migrations), |
| 62 | [packages/casual/](packages/casual/) (casual rules/auto-battle/Vibe Heroes, engine-free). |
| 63 | - **Dependency graph (ARCHITECTURE.md):** shared → protocol/content/physics → gameplay → persistence → apps; only |
| 64 | `apps/server` and `packages/persistence` know PostgreSQL; `packages/*` never import browser-only APIs. |
| 65 | - **Simulation:** 30 Hz fixed tick, 15 Hz snapshots; kinematic Source/Quake controller, client prediction + rewind + |
| 66 | interpolation; physics sleep is first-class (settled props cost nothing on the wire). |
| 67 | - **Data:** [packages/persistence/migrations/](packages/persistence/migrations/) (23 migrations: characters, world, |
| 68 | props, instances/leases, progress, hauls, inventory rewards, casual, Play sync, telemetry, creator worlds/scripts/ |
| 69 | teams/assets, surf, AI matches, sharing, outbox). A write-behind flusher means the tick never awaits I/O; SIGTERM |
| 70 | drains it. |
| 71 | - **Pages:** `/` (portal; on play.openvibe.games `/` is the game), `/play`, `/editor`, `/worlds`, |
| 72 | `/achievements`, `/leaderboards`, `/developers`, `/play/vibe-heroes`, `/healthz`, `/api/ready`, `/metrics`. |
| 73 | - **Deploy:** [deploy/openvibe-games.service](deploy/openvibe-games.service) (PORT 8000, HOST 127.0.0.1, |
| 74 | `/etc/openvibe/games.env`, `GAMES_MIGRATIONS_DIR`, `MAP_PATH`, `STATIC_DIR`, `TRUST_PROXY=127.0.0.1`, |
| 75 | `TimeoutStopSec=30`), [deploy/nginx-openvibe-games.conf](deploy/nginx-openvibe-games.conf), |
| 76 | [deploy/scripts/deploy.sh](deploy/scripts/deploy.sh) wrapping `ovhost deploy games` (strategy `pnpm-build`: |
| 77 | `pnpm install --frozen-lockfile`, `pnpm build`, restore tracked `dist-types/`, restart). `--wait-idle` holds the |
| 78 | restart until nobody plays; `DRY_RUN=1` prints `ovhost plan games`; rollback `sudo deploy/scripts/deploy.sh |
| 79 | --rollback` (rebuilds the previous commit). |
| 80 | |
| 81 | ## Ownership and Boundaries |
| 82 | |
| 83 | Games owns the authoritative simulation (world, characters, inventories, skills, blueprints, props), the |
| 84 | write-behind persistence schema, the map editor and its assets, the mod registry/content/script sandbox and its audit, |
| 85 | the `games.*` events, the WS protocol and the portal/play/editor pages, the casual kit and its server economy, creator |
| 86 | worlds, AI draft matches, and the moved Play player pages. It does **not** own identity (Network subjects), files |
| 87 | beyond the local copy (Media mirrors), event delivery (Events), Play's service/API (Play), Inventory items (Inventory), |
| 88 | or general-purpose executable mods (Host Stage C; Games' QuickJS scripts are bounded and approved per install). |
| 89 | |
| 90 | ## Platform Integration (the boundary) |
| 91 | |
| 92 | - **Identity:** a signed-in player is the token's `subject_id`; a token without one cannot name an account, and a |
| 93 | guest token that looks like `usr_…` is refused. Never key a row on a display name. |
| 94 | - **Service principal:** calls to Events/Media/Network identity/Inventory/Play carry a client-credentials token for |
| 95 | the `games` client, one per audience. No shared keys. SSO calls for a player use the player's own token. |
| 96 | - **Events:** `games.player.joined|left`, `games.skill.leveled`, `games.blueprint.unlocked`, `games.world.saved` |
| 97 | (≤1 per `WORLD_SAVED_EVENT_MINUTES`, plus shutdown), `games.mod.installed|enabled|disabled|grants_changed|revoked`. |
| 98 | Achievements are **not** events: an unlock is stored in Games and sent to Play; the six inventory-bearing |
| 99 | achievements grant through Inventory and retry on failure. |
| 100 | - **Play:** `GAMES_PLAY_URL` (unset = scores/unlocks stay local); Play is the network-wide read surface. |
| 101 | - **Media:** only map-editor assets (textures, paint masks, glb) are uploaded; a restart-safe `media_mirrors` queue |
| 102 | copies them to the `games` tenant. There is no screenshot/blueprint upload. |
| 103 | - **Mods:** manifests follow `[email protected]` validated against the installed Contracts (pin drift is a |
| 104 | defect); `games-content@2` packs merge definitions at boot, the client sends its definition digest in `hello` and the |
| 105 | server rejects a mismatch; each install stores the approved subset of requested capabilities, every runtime binding |
| 106 | checks it, revocation stops affecting the world on the next tick, and install/grant/use/deny/revoke are audited. |
| 107 | `games-quickjs@1` runs only with `GAMES_SCRIPT_MODS=1` in a per-mod QuickJS sandbox (defaults `cpuMs` 2/tick, |
| 108 | `memoryMb` 8, `stackKb` 128; over budget disables the mod for the instance). |
| 109 | - **Frame:** the portal/play/editor load the OpenVibe Frame from this server's own `/shared/` copy, so it keeps its |
| 110 | frame while Network is down. `GET /api/v1/platform` reports events/Media-mirror state and backlog without secrets. |
| 111 | |
| 112 | ## Capabilities and Grants |
| 113 | |
| 114 | Implemented: `games.mod.manage`, `games.mod.read` (mods API), `games.world.announce`, `games.prop.place`, |
| 115 | `games.def.define` (declarative packs), and for script packs `games.script.run`, `games.players.read`, |
| 116 | `games.player.message`, `games.world.emit`, `games.storage.kv`. Script bindings are absent unless approved for that |
| 117 | install. The `games` principal needs `inventory.item.grant`, `events.event.publish|subscription.manage`, |
| 118 | `media.object.upload` (namespace `games`), `identity.subject.resolve`, `network.modules.write` on |
| 119 | `games.progress.summary`, and Network's `mods.grant.manage`. |
| 120 | |
| 121 | ## Conventions and Pitfalls |
| 122 | |
| 123 | - **The server is the game.** Every gameplay-relevant decision is server-simulated/validated; no client field is |
| 124 | trusted; inbound messages are zod-validated and flood-controlled. |
| 125 | - **Do not import Babylon into `apps/server`** (the architecture audit fails) or platform modules into `packages/*`; |
| 126 | keep gameplay engine-free and deterministic (same Rapier build on both sides). |
| 127 | - **Data-driven content:** add definitions in `@openvibe/content`, validate at startup; stable ids, display names are |
| 128 | cosmetic. A new built-in pack must not bypass the capability check. |
| 129 | - **Migrations** are `packages/persistence/migrations/NNNN_*.sql`, applied at boot with the owner URL; never edit one |
| 130 | that has run. The tick never awaits I/O — write-behind, then drain on SIGTERM (30 s stop timeout). |
| 131 | - **Instances/leases:** PostgreSQL records each room and a renewable fenced lease; `GAMES_INSTANCES_PER_PLACE` only |
| 132 | within one process, `GAMES_CREATOR_WORLDS_MAX` caps creator worlds, empty ones stop after the idle timeout. A lease |
| 133 | that is lost must stop the place (old failure: five places lost leases in one night). |
| 134 | - **`TRUST_PROXY`:** unset means the TCP peer is the client and all forwarded headers are ignored (otherwise a caller |
| 135 | mints fresh rate-limit buckets); behind nginx it is `127.0.0.1`, and nginx overwrites the headers from |
| 136 | `$remote_addr`. Never read `X-Forwarded-For` directly. |
| 137 | - **Static serving:** an unknown path is a 404 (HTML page, `{"error":"not_found"}` under `/api`, plain text for a |
| 138 | missing asset), never the portal with 200. `/api/ready` is 200 only while the database answers a real round trip. |
| 139 | - **Test pointers:** after adding/moving a `*.test.ts`, run `node test/run.js --index`; a missing/stale flat pointer |
| 140 | fails the suite. Vitest reads the real colocated file. |
| 141 | - **Build:** pnpm's `onlyBuiltDependencies` is `esbuild`; Corepack downloads pnpm with Node's fetch in CI (the image |
| 142 | bakes it). `pnpm install --frozen-lockfile` and a clean `pnpm build` are the deploy gate. |
| 143 | - **nginx** keeps `/api/ready` off the public vhosts; `/metrics` answers loopback only. Secrets |
| 144 | (`OV_OAUTH_CLIENT_SECRET`, `GAMES_EVENTS_SECRET`, `OV_NETWORK_AUTH_URL`) live in `/etc/openvibe/games.env` by name |
| 145 | only. |
| 146 | - **Play's old pages 301 here:** do not re-add `apps/server` player pages under play.openvibe.network; the service and |
| 147 | API stay there. |
| 148 | - Phone first for casual/portal pages; no Font Awesome; "Calm reduces, only Off removes". |
| 149 | |
| 150 | ## Testing |
| 151 | |
| 152 | `pnpm test` runs every vitest file through [test/run.js](test/run.js) (package and server units incl. |
| 153 | `apps/server/src/game/platformIntegration.test.ts`, which boots the real server twice on one database, and the |
| 154 | architecture audit). `pnpm typecheck`/`pnpm lint` keep strict TS and no `any`; `tsx apps/server/scripts/sliceTest.ts` |
| 155 | drives protocol clients across a restart; `pnpm test:editor|test:collab|test:console` are Playwright e2e checks. Run |
| 156 | the narrowest file first, then the suite. |
| 157 | |
| 158 | ## Documentation |
| 159 | |
| 160 | - [README.md](README.md) — purpose, owns, does-not-own, depends on, layout, development, commands, production, |
| 161 | platform integration, capabilities, tests, security. |
| 162 | - [ARCHITECTURE.md](ARCHITECTURE.md) — principles, package graph, simulation, networking, server systems, persistence, |
| 163 | platform boundary, zones, entities, observability, security posture. |
| 164 | - [ROADMAP.md](ROADMAP.md), [docs/games.md](docs/games.md), [docs/casual.md](docs/casual.md), |
| 165 | [docs/MAP_EDITOR.md](docs/MAP_EDITOR.md), [docs/EDITOR_COMPLETION_STATUS.md](docs/EDITOR_COMPLETION_STATUS.md), |
| 166 | [docs/instances.md](docs/instances.md), [docs/mods.md](docs/mods.md), [docs/ai-matches.md](docs/ai-matches.md), |
| 167 | [docs/platform-direction.md](docs/platform-direction.md), [docs/adr/](docs/adr/), |
| 168 | [docs/capabilities-proposal/](docs/capabilities-proposal/), [docs/service-manifest-proposal.json](docs/service-manifest-proposal.json). |
| 169 | - [SECURITY.md](SECURITY.md), [.env.example](.env.example), [CHANGELOG.md](CHANGELOG.md). |
| 170 |