openvibers/openvibe.games AGENTS.md

AGENTS.md

main 13.3 KB · 170 lines · markdown

1# AGENTS.md — OpenVibe.Games
2
3## Project Overview
4
5OpenVibe.Games hosts the browser games at `openvibe.games` and `play.openvibe.games`: **Scraplandia** (a persistent
6multiplayer 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**),
8server-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
21OpenVibe.Network. Events are OpenVibe.Events (transactional outbox). Achievements/scores go to OpenVibe.Play's API;
22six built-in achievements grant Live items through OpenVibe.Inventory. Map-editor assets are also copied to
23OpenVibe.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
25simulation packages do not know the platform exists.
26
27## Commands
28
29```bash
30corepack enable && pnpm install # Node ≥22, pnpm 10.6.5
31pnpm dev # server + client dev in parallel
32pnpm dev:server # authoritative server on :8000 (tsx watch)
33pnpm dev:client # client hot reload on :5173 (proxies /ws to :8000)
34pnpm test # all vitest files through test/run.js
35pnpm typecheck # strict TypeScript across every package
36pnpm lint # ESLint (no-explicit-any)
37pnpm format # Prettier write (format:check in CI)
38pnpm build # all packages + client bundle
39node test/run.js --index # regenerate the flat test/*.test.js pointers after adding/moving a test
40tsx apps/server/scripts/sliceTest.ts # end-to-end vertical slice: real server, protocol clients, restart
41pnpm test:editor | test:collab | test:console # e2e Playwright checks (editor, collaboration, console)
42pnpm audit:editor # editor architecture audit (vitest config)
43```
44
45Production: `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
83Games owns the authoritative simulation (world, characters, inventories, skills, blueprints, props), the
84write-behind persistence schema, the map editor and its assets, the mod registry/content/script sandbox and its audit,
85the `games.*` events, the WS protocol and the portal/play/editor pages, the casual kit and its server economy, creator
86worlds, AI draft matches, and the moved Play player pages. It does **not** own identity (Network subjects), files
87beyond the local copy (Media mirrors), event delivery (Events), Play's service/API (Play), Inventory items (Inventory),
88or 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
114Implemented: `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
117install. 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
154architecture audit). `pnpm typecheck`/`pnpm lint` keep strict TS and no `any`; `tsx apps/server/scripts/sliceTest.ts`
155drives protocol clients across a restart; `pnpm test:editor|test:collab|test:console` are Playwright e2e checks. Run
156the 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