openvibers/openvibe.games README.md

README.md

main 21.3 KB · 312 lines · markdown

1![OpenVibe.Games](https://openvibe.games/shared/social/games.jpg)
2
3# OpenVibe.Games
4
5**Browser games and creator worlds** — [openvibe.games](https://openvibe.games). Play in the browser, with achievements on [OpenVibe.Play](https://play.openvibe.network).
6
7**Live site:** https://openvibe.games · **API docs:** https://openvibe.services · **For AI:** https://openvibe.games/llms.txt
8**Run it:** `pnpm install && pnpm dev` (Node 22 or newer) · **Tests:** `npm test`
9
10Part of the [OpenVibe network](https://openvibe.network); [help](https://openvibe.help) and the [developer API](https://openvibe.services).
11
12## Purpose
13
14OpenVibe.Games hosts Scraplandia at https://openvibe.games and https://play.openvibe.games.
15Scraplandia's extraction events double as its scored "Haul" mode: securing at-risk valuables at a
16recovery bird earns haul points, with its own all-time/this-week leaderboard and achievements.
17The same Games process also hosts Salvage Rush, an on-foot timed collection game with its own
18place, world, rooms and round system. Play it at `/play?game=salvage-rush` on the same host.
19See [docs/games.md](docs/games.md) for both games and local progression.
20Server-run Vibe Heroes AI draft matches and Bench scoring are described in [docs/ai-matches.md](docs/ai-matches.md).
21Players build their own **creator worlds** in the map editor and publish them at `/worlds` for anyone to join
22(sandboxed: characters are not saved there); see [docs/games.md](docs/games.md#creator-worlds).
23It also hosts the casual line (plan T22): small phone-first browser games built from one kit, starting with
24**Vibe Heroes**, an idle hero collector at `/play/vibe-heroes`. See [docs/casual.md](docs/casual.md).
25The player pages of OpenVibe.Play (the game services behind Games) live here too: `/achievements`, `/leaderboards`
26and the API guide for game makers at `/developers`. They read Play's public API server-side; the service and its API
27stay at play.openvibe.network, whose old pages 301 here.
28
29A persistent multiplayer physics sandbox for the browser: Source-style movement,
30physgun-driven building from crafted physical objects, gathering, crafting, and
31extraction risk — running against an authoritative dedicated server.
32
33**Stack:** TypeScript everywhere · Babylon.js (WebGPU with WebGL fallback) ·
34Rapier physics (`@dimforge/rapier3d-deterministic-compat`, one pinned build
35headless on the server and in client prediction) ·
36WebSocket protocol · PostgreSQL 18 + Valkey persistence (openvibe-sdk) · pnpm monorepo.
37
38## Owns
39
40- the game: the authoritative simulation, the world, characters, inventories, skills, blueprints and
41 props, in PostgreSQL (ADR-0007 decision 5: schema from scratch, migrations under
42 `packages/persistence/migrations`, a write-behind flusher — the tick never awaits I/O)
43- the map editor and its assets, the mod registry (`games-content@2` packs, grants, audit), the
44 `games-quickjs@1` server script mods (docs/mods.md) and the `games.*` events
45- the game WebSocket protocol and the portal, `/play` and `/editor` pages
46- the casual kit (`packages/casual`), its server economy (saves, ledger, idempotent actions) and the `/play/<game>`
47 pages
48
49## Does not own
50
51- identity (OpenVibe.Network: players are keyed by Network subjects), file storage beyond the local
52 copy (OpenVibe.Media mirrors map-editor assets), events delivery (OpenVibe.Events)
53- mod grants' authority (Network keeps them, `mods.grant.manage`) and general-purpose executable
54 mods (OpenVibe.Host Stage C); Games runs its own bounded `games-quickjs@1` scripts
55
56## Depends on
57
58- OpenVibe.Network (SSO, JWKS, client-credentials tokens, identity resolve, the `games.progress.summary`
59 user module, mod grants), OpenVibe.Events (outbox relay, subscriptions), OpenVibe.Media (asset mirror)
60- `openvibe-contracts` v0.77.0, `openvibe-sdk` v0.22.0 and `openvibe-shared` v4.33.0 (pinned by release
61 tarball in `apps/server/package.json`), Babylon.js, Rapier, PostgreSQL (`pg`, or embedded PGlite in
62 development), Valkey (`iovalkey`, optional)
63
64## Repository layout
65
66```
67apps/
68 client/ Browser client: rendering, prediction, input, HUD
69 server/ Dedicated authoritative server: simulation, networking, persistence
70packages/
71 shared/ Math, ids, events, logging facade, fixed-timestep primitives
72 protocol/ Versioned wire messages + codec (zod-validated inbound)
73 content/ Data-driven item/recipe/world definitions + validation registry
74 gameplay/ Pure domain logic: inventory, crafting, movement sim, zones, entities
75 physics/ PhysicsWorld abstraction + Rapier adapter (@openvibe/physics/rapier)
76 persistence/ DTOs, repository interfaces, PostgreSQL implementation, migrations
77 casual/ The casual kit: rules, auto-battle and the Vibe Heroes pack (engine-free)
78docs/adr/ Architecture decision records
79```
80
81See [ARCHITECTURE.md](ARCHITECTURE.md) for boundaries and data flow,
82[ROADMAP.md](ROADMAP.md) for what exists and what is planned.
83
84## Prerequisites
85
86- Node.js >= 22
87- pnpm 10 (`corepack enable`)
88
89## Development
90
91```bash
92pnpm install
93
94# Terminal 1 — authoritative server on :8000 (tsx watch)
95pnpm dev:server
96
97# Terminal 2 — client with hot reload on :5173 (proxies /ws to :8000)
98pnpm dev:client
99```
100
101Open http://localhost:5173 in two browser windows to see multiplayer.
102
103**Controls:** WASD move · Space jump (hold to bunnyhop) · Shift sprint ·
1041-6 hotbar · Tab menu (inventory / crafting / skills / players) · G drop
105held item · E gather / pick up props.
106**Physgun (GMod-style):** hold LMB grab (grabbing a frozen prop unfreezes
107it) · release to let go · RMB freeze · wheel push/pull · hold E rotate
108like a globe (Shift+E snaps) · hold Shift for grid-lock.
109**Building:** craft pieces → drop them (G or drag out of the inventory) →
110position with the physgun → freeze. Dropping IS placement; E picks any
111prop back up into your inventory.
112
113**The loop:** spawn in Scrap City (safe city — no PvP, no building, no
114physgun), gather branches/stones/scrap outside the gates, craft tools,
115harvest the forest (axe/woodcutting) and quarry (pickaxe/mining), craft
116building pieces, place them in the wilds, position with the physgun and
117freeze. Props are protected: only you and players you trust can move them.
118
119## Commands
120
121| Command | Purpose |
122| -------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
123| `pnpm dev` | Server + client dev processes in parallel |
124| `pnpm test` | All unit tests (vitest, through `test/run.js`) |
125| `pnpm typecheck` | Strict TypeScript across every package |
126| `pnpm lint` | ESLint (typescript-eslint, no-explicit-any) |
127| `pnpm format` | Prettier write |
128| `pnpm build` | Production build of all packages + client bundle |
129| `tsx apps/server/scripts/sliceTest.ts` | End-to-end vertical-slice test (boots a real server, drives protocol clients, restarts, asserts persistence) |
130
131## Production
132
133```bash
134pnpm build
135DATABASE_URL=postgres://… DATABASE_DIRECT_URL=postgres://… STATIC_DIR=apps/client/dist PORT=8000 pnpm --filter @openvibe/server start
136```
137
138`DATABASE_URL` (pooled, through PgBouncer) serves; migrations run at boot on `DATABASE_DIRECT_URL`
139(the owner role, a direct session). With neither set and `NODE_ENV` not `production`, the server uses
140an embedded PGlite database under `DATABASE_DIR` (default `data/`) — development only, one process.
141`GAMES_PLACE_ID` (default `scraplandia`) selects the default place for older clients and the
142edited map. Both built-in places have separate world rows and instance leases. `VALKEY_URL`
143(optional) makes the per-actor limits and the WebSocket upgrade tickets shared across instances
144(issue is `SET … PX … NX`, consume is `GETDEL`, so a ticket is single-use cluster-wide).
145`GAMES_INSTANCES_PER_PLACE` (default `1`) enables several room simulations for a place in this process;
146`MAX_PLAYERS` is per-room capacity. PostgreSQL records each room and its renewable fenced lease.
147`GAMES_INTEREST_CELL` (default `16`) and `GAMES_INTEREST_RADIUS` (default `80`) tune spatial replication.
148See [docs/instances.md](docs/instances.md) for transfer, lease expiry and the one-process limit.
149`TRUST_PROXY` (optional) says which proxies may set the client-address headers: unset means the TCP
150peer is the client and `X-Forwarded-For`/`X-Real-IP`/`CF-Connecting-IP` are ignored (they are
151client-supplied and would otherwise let a caller mint fresh rate-limit buckets); `true` means the
152server is only reachable through the proxy; otherwise a comma-separated list of trusted proxy
153IPs/CIDRs (behind nginx, which sets `X-Real-IP` from `$remote_addr`). See [.env.example](.env.example).
154
155The server serves the built client, `/healthz` (liveness), `/api/ready`, `/metrics`, and the game
156WebSocket on one port. Pages are `/` (portal), `/play` and `/editor`; on play.openvibe.games `/` is
157the game and `/play` redirects there. Any other path that is neither a file of the build nor a route
158answers 404 (an HTML page with links to those pages; `{"error":"not_found"}` under `/api`; plain text
159for a missing asset), never the portal with a 200. `/api/ready` is 200 only while the database answers a real round trip
160(`db.ready()`) and the simulation
161has ticked within the last 5 s, otherwise 503 naming the failed check (`status`
162`ready`/`not_ready`, `checks.db`, `checks.tick`); nginx keeps it off the public vhosts. Environment: `PORT`, `HOST`, `STATIC_DIR`,
163`MAX_PLAYERS`, `LOG_LEVEL`, `GAMES_PLACE_ID`, the database variables above (platform variables below).
164
165Deployed at https://openvibe.games (nginx TLS termination → server on 127.0.0.1:8000,
166systemd unit `openvibe-games.service` from [deploy/openvibe-games.service](deploy/openvibe-games.service),
167env file `/etc/openvibe/games.env` (which holds `DATABASE_URL`, `DATABASE_DIRECT_URL` and `VALKEY_URL`);
168play.openvibe.games Host-routes to the same process).
169
170Deploy on the host with `sudo /opt/openvibe.games/deploy/scripts/deploy.sh`, which runs
171`ovhost deploy games` (OpenVibe.Host, strategy `pnpm-build`; roadmap WS-N task 11): a fast-forward pull as
172the checkout owner, `pnpm install --frozen-lockfile`, every workspace package's dependencies checked, `pnpm build`,
173the restart (the SDK migrator applies any new migration as the owner before serving), `/api/ready`, and on failure the checkout
174restored, reinstalled and rebuilt and the server restarted again. Players online are reported and
175reconnect; `--wait-idle` holds the restart until nobody plays, `--rollback` runs `ovhost rollback games`,
176`DRY_RUN=1` prints `ovhost plan games`. Do not pull by hand first (ovhost would find nothing new; pass
177`--restart` if you did). The wrapper exits with an error if ovhost is missing.
178
179Rollback: `sudo deploy/scripts/deploy.sh --rollback` (`ovhost rollback games`), which rebuilds the previous
180commit. Migrations are additive and applied at boot, so an older build starts on the newer schema; the SDK
181migrator refuses a migration file that was edited after it ran (write a new one instead).
182
183## Platform integration (OpenVibe network)
184
185Games integrates with the rest of OpenVibe at its service boundary only
186(`apps/server/src/platform`, `apps/server/src/mods`); the simulation packages
187do not know the platform exists. See [ADR-0006](docs/adr/0006-canonical-subjects-and-platform-boundary.md).
188
189- **Identity.** Signed-in players are keyed by their canonical
190 openvibe.network subject (`usr_…`/`gst_…`, the token's `subject_id`); a
191 token without one cannot name an account. Guest tokens that look like
192 account keys (`usr_…`) are refused.
193- **Service principal.** Calls to Events, Media and Network identity carry a
194 client-credentials token of the `games` OAuth client (same client id and
195 secret as SSO), one per audience. There are no shared keys. The SSO calls
196 made for a player (`/api/auth/me`, code and FedCM exchanges) keep using the
197 player's own token.
198- **Events** (OpenVibe.Events, through a transactional outbox in
199 PostgreSQL, table `event_outbox`): `games.player.joined|left`,
200 `games.skill.leveled`, `games.blueprint.unlocked`, `games.world.saved`
201 (at most one checkpoint per `WORLD_SAVED_EVENT_MINUTES`, plus shutdown) and
202 `games.mod.installed|enabled|disabled|grants_changed|revoked`. Achievements are
203 not published as events: an unlock is stored in Games (its achievement rules read
204 those tables) and sent to OpenVibe.Play over Play's API (`GAMES_PLAY_URL`), which
205 is the network-wide read surface for titles, scores and unlocks.
206 Progression events come from what a save actually wrote, in the same
207 transaction.
208- **Inventory.** Six built-in achievements give Live items: Scraplandia First Haul (Toxic Name), Big Haul (Golden Name), Haul Week (Cowboy Hat); Salvage Rush First Haul (Neon Name), Big Haul (Flames), Arena Winner (Stars). Games grants them after unlock or guest adoption using its own OAuth client; failed grants retry. Inventory items are free, never sold or traded.
209- **Media.** Map-editor assets (textures, paint masks, glb models; the only
210 uploaded files Games has) are still served from local disk and are also
211 copied into Media objects (`kind: asset`, namespace `MEDIA_NAMESPACE`), with
212 a restart-safe queue (`media_mirrors`). There are no screenshots or blueprint
213 files to upload: blueprints are per-player recipe unlocks.
214- **Mods** (ADR-013 in OpenVibe.Contracts). Manifests follow
215 `[email protected]` (Games validates against the schema in the
216 installed `openvibe-contracts` package — pin drift is a defect, ADR-0007
217 decision 12). The content runtime is `games-content@2`:
218 declarative data packs checked against `@openvibe/content` (announcements;
219 inert, mod-owned props; definitions — items, recipes, crops, NPC archetypes —
220 merged into the content registry at boot, so they take effect on the next
221 restart). The client sends its definition digest in `hello`; the server checks it before joining and echoes its digest in `welcome`. A mismatch is rejected so installed definitions cannot silently desync clients. Scraplandia's default blank-slate map is supplied by the built-in pack. Each install stores the approved subset of its
222 requested capabilities; every runtime binding checks it at call time, a
223 revoked install or capability stops affecting the world on the next tick,
224 and install/grant/use/deny/revoke are audited (`mod_audit`). Trust tiers are
225 metadata only. Staff can install `games-quickjs@1` script packs through the mods API;
226 built-in packs can carry scripts too. Scripts are pinned by the def-set digest and run
227 in a per-mod QuickJS sandbox only where `GAMES_SCRIPT_MODS=1` (budgets
228 `cpuMs` 2 per tick, `memoryMb` 8, `stackKb` 128 by default; over budget
229 disables the mod for the instance; host API and limits in docs/mods.md). API: `GET /api/v1/mods`,
230 `GET /api/v1/mods/:id` (public); `POST /api/v1/mods`,
231 `POST /api/v1/mods/:id/enable|disable|revoke|grants|reload`,
232 `DELETE /api/v1/mods/:id/grants/:capability`, `GET /api/v1/mods/:id/audit`
233 (owner/admin session, or a principal token with `games.mod.manage`). Fresh clients fetch
234 `GET /api/v1/content` before the digest handshake so live script changes can be joined.
235- **Status:** `GET /api/v1/platform` reports whether events and the Media
236 mirror are on, the outbox backlog and mirror counts (no secrets).
237- **In production (2026-09-23, release `f11e21c`):** events and the Media
238 mirror are on. `games.world.saved` events reach OpenVibe.Events; no mod is
239 installed, no grant exists and no asset has been mirrored to Media yet.
240- **OpenVibe Frame.** The portal, `/play` and `/editor` load the OpenVibe Frame
241 (navbar, footer, theme loader) from this server's own pinned copy at `/shared/`
242 (`apps/server/src/net/sharedAssets.ts`), so they keep their frame while Network is down.
243- **Persistence proof.** `apps/server/src/game/platformIntegration.test.ts`
244 boots the real server twice on one database and checks characters, world
245 props, the world clock and the mod registry survive the restart;
246 `sliceTest.ts` covers the gameplay state end to end.
247
248Platform environment (all optional; service calls need `OV_OAUTH_CLIENT_SECRET`):
249
250| Variable | Purpose |
251| ---------------------------------- | ------------------------------------------------------------------------------------------ |
252| `OV_OAUTH_CLIENT_ID` | OAuth client / principal id (default `games`) |
253| `OV_OAUTH_CLIENT_SECRET` | Its secret: enables SSO and every service call |
254| `OV_NETWORK_INTERNAL_URL` | Network base for `/oauth/token`, identity and the JWKS (e.g. `http://127.0.0.1:4000`) |
255| `EVENTS_URL` | OpenVibe.Events base; enables the outbox (`EVENTS_PUBLISH=off` disables) |
256| `MEDIA_URL` | OpenVibe.Media base; enables the asset mirror (`MEDIA_MIRROR=off` disables) |
257| `GAMES_INVENTORY_URL` | Inventory base (default `https://inventory.openvibe.network`; production can use loopback) |
258| `GAMES_ITEMS` | `on` (default) or `off`; off also when the OAuth client secret is unset |
259| `GAMES_PLAY_URL` | OpenVibe.Play base (e.g. `http://127.0.0.1:5040`); unset = scores and unlocks stay local |
260| `MEDIA_NAMESPACE` | Media tenant for the copies (default `games`) |
261| `WORLD_SAVED_EVENT_MINUTES` | Checkpoint event interval (default 15) |
262| `GAMES_CREATOR_WORLDS_MAX` | Creator worlds running at once (default 2; 0 stops new ones starting) |
263| `GAMES_CREATOR_WORLDS_PER_PERSON` | Worlds one account keeps (default 5) |
264| `GAMES_CREATOR_WORLD_IDLE_SECONDS` | An empty creator world stops after this long (default 180) |
265| `GAMES_CREATOR_WORLD_PLAYERS` | Players per creator world (default 16) |
266| `GAMES_CREATOR_SCRIPTS` | `1` runs creator worlds' own scripts in their rooms (QuickJS sandbox; off by default) |
267
268## Capabilities
269
270Implemented here (the service manifest's `capabilities`): `games.mod.manage` and `games.mod.read` (the
271mods API, for a principal token or an owner/admin session); `games.world.announce`,
272`games.prop.place`, and `games.def.define` for declarative packs; and
273`games.script.run`, `games.players.read`, `games.player.message`,
274`games.world.emit`, and `games.storage.kv` for script packs. Script bindings
275are absent unless approved for that install.
276
277Service grants the `games` principal needs:
278`inventory.item.grant` (openvibe.inventory), `events.event.publish` and `events.subscription.manage` (openvibe.events: the outbox, and the account,
279revocation and mod-grant subscriptions), `media.object.upload`
280(openvibe.media, namespace `games`), `identity.subject.resolve`
281(openvibe.network), `network.modules.write` on `games.progress.summary`, and `mods.grant.manage`
282(Network's mod grants, roadmap WS-M task 3).
283
284## Tests
285
286`pnpm test` runs every vitest file (unit tests of the packages and the server, including
287`apps/server/src/game/platformIntegration.test.ts`, which boots the real server twice on one database);
288it goes through `test/run.js`, which maps the flat `test/*.test.js` pointers back to the colocated
289TypeScript tests so `ov test` can select and shard them (vitest reads the real files, so their
290`@vitest-environment` docblocks and imports are untouched). After adding or moving a `*.test.ts`,
291run `node test/run.js --index` to regenerate the pointers — a missing or stale pointer fails the run.
292`pnpm typecheck` and `pnpm lint` keep strict TypeScript and no `any`;
293`tsx apps/server/scripts/sliceTest.ts` drives protocol clients against a real server across a restart.
294
295## Security
296
297Reporting a vulnerability: [SECURITY.md](SECURITY.md). The server is authoritative: inbound protocol
298messages are validated (zod), the WebSocket has per-socket flood control, and HTTP writes that cross a
299capability boundary (editor saves and uploads, the mods API) have per-actor limits (openvibe-sdk/limits).
300Players are keyed by Network subjects; guest tokens that look like account keys are refused.
301Script mods run in isolated QuickJS sandboxes with only their approved bindings.
302nginx keeps `/api/ready` off the public vhosts and `/metrics` answers direct loopback callers only. `OV_OAUTH_CLIENT_SECRET`, `GAMES_EVENTS_SECRET` (signed
303Events deliveries) and `OV_NETWORK_AUTH_URL` live in `/etc/openvibe/games.env`, by name only; there are no shared keys
304between services.
305
306<!-- versions:start -->
307- openvibe-contracts: v0.422.0
308- openvibe-sdk: v0.166.0
309- openvibe-shared: v4.241.0
310- openvibe-publishing: v1.7.0
311<!-- versions:end -->
312