Mirror of github.com/OpenVibers/OpenVibe.Games
  • TypeScript 94.8%
  • CSS 2.1%
  • HTML 2%
  • JavaScript 1%
  • Shell 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
OpenVibers 04c711338e
openvibe-contracts v0.217.0 (was v0.216.0) (#173)
Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v0.217.0 release.
2026-10-27 05:18:02 -07:00
.github/workflows Games M6: Babylon's core as a chain of layers (largest chunk 350 KB, was 2.09 MB); WebGPU and particles on demand (#66) 2026-10-14 15:41:48 -07:00
apps openvibe-contracts v0.217.0 (was v0.216.0) (#173) 2026-10-27 05:18:02 -07:00
deploy nginx: the repo vhost is production's again (www redirect, security.txt, /tos) plus /internal/ and /metrics closed at the edge and HSTS, nosniff and a referrer policy (#98) 2026-10-17 03:35:47 -07:00
docs New creator worlds start on ground; top-down worlds draw a ring under each player (#167) 2026-10-26 23:54:45 -07:00
packages New creator worlds start on ground; top-down worlds draw a ring under each player (#167) 2026-10-26 23:54:45 -07:00
test Top-down worlds: a press on the game canvas acts without pointer lock (tools, gathering, the hotbar wheel); the HUD and menus keep their own clicks (#170) 2026-10-27 02:31:21 -07:00
.env.example The asset store: share builds and scripts for anyone's worlds (free first) (#163) 2026-10-26 23:09:17 -07:00
.gitignore Untrack committed nodemodules symlinks and ignore them in git 2026-10-03 03:28:24 -07:00
.gitleaks.toml gitleaks: the made-up session token's allowlist also covers its first-commit path (sqlite/sqliteStore.test.ts), so main's secrets scan is green again (#99) 2026-10-17 03:34:52 -07:00
.npmrc M1 persistence: Games runs on PostgreSQL 18 + Valkey through openvibe-sdk (ADR-0007 decision 5). SQLite is gone (no data is restored; a new schema from scratch, no schema-version history). 2026-09-28 21:40:50 -07:00
.prettierrc.json Initial OpenVibe release 2026-08-17 12:56:35 -07:00
ARCHITECTURE.md Games M4: instances per place, fenced leases, room transfer, region interest (one process) (#45) 2026-10-11 17:26:22 -07:00
CHANGELOG.md Creator worlds page: Open Graph and Twitter tags, so a shared /worlds link has a preview (#129) 2026-10-20 11:41:09 -07:00
eslint.config.js Initial OpenVibe release 2026-08-17 12:56:35 -07:00
package.json Games: vitest 4 (no tinypool), clearing the two critical tinypool advisories. (#14) 2026-10-07 08:52:11 -07:00
pnpm-lock.yaml openvibe-contracts v0.217.0 (was v0.216.0) (#173) 2026-10-27 05:18:02 -07:00
pnpm-workspace.yaml M1 persistence: Games runs on PostgreSQL 18 + Valkey through openvibe-sdk (ADR-0007 decision 5). SQLite is gone (no data is restored; a new schema from scratch, no schema-version history). 2026-09-28 21:40:50 -07:00
README.md openvibe-contracts v0.217.0 (was v0.216.0) (#173) 2026-10-27 05:18:02 -07:00
ROADMAP.md Games M4: instances per place, fenced leases, room transfer, region interest (one process) (#45) 2026-10-11 17:26:22 -07:00
SECURITY.md SECURITY.md: how to report a vulnerability ([email protected], 7-day reply, scope, supported versions) 2026-09-23 19:27:36 -07:00
STATUS.json openvibe-contracts v0.217.0 (was v0.216.0) (#173) 2026-10-27 05:18:02 -07:00
tsconfig.base.json Initial OpenVibe release 2026-08-17 12:56:35 -07:00
tsconfig.solution.json Games: the casual line's kit and Vibe Heroes, an idle hero collector at /play/vibe-heroes (plan T22) (#58) 2026-10-14 06:47:48 -07:00
vitest.audit.config.ts Initial OpenVibe release 2026-08-17 12:56:35 -07:00
vitest.config.ts M1 persistence: Games runs on PostgreSQL 18 + Valkey through openvibe-sdk (ADR-0007 decision 5). SQLite is gone (no data is restored; a new schema from scratch, no schema-version history). 2026-09-28 21:40:50 -07:00

OpenVibe.Games

Purpose

OpenVibe.Games hosts Scraplandia at https://openvibe.games and https://play.openvibe.games. Scraplandia's extraction events double as its scored "Haul" mode: securing at-risk valuables at a recovery bird earns haul points, with its own all-time/this-week leaderboard and achievements. The same Games process also hosts Salvage Rush, an on-foot timed collection game with its own place, world, rooms and round system. Play it at /play?game=salvage-rush on the same host. See docs/games.md for both games and local progression. Players build their own creator worlds in the map editor and publish them at /worlds for anyone to join (sandboxed: characters are not saved there); see docs/games.md. It also hosts the casual line (plan T22): small phone-first browser games built from one kit, starting with Vibe Heroes, an idle hero collector at /play/vibe-heroes. See docs/casual.md. A persistent multiplayer physics sandbox for the browser: Source-style movement, physgun-driven building from crafted physical objects, gathering, crafting, and extraction risk — running against an authoritative dedicated server.

Stack: TypeScript everywhere · Babylon.js (WebGPU with WebGL fallback) · Rapier physics (@dimforge/rapier3d-deterministic-compat, one pinned build headless on the server and in client prediction) · WebSocket protocol · PostgreSQL 18 + Valkey persistence (openvibe-sdk) · pnpm monorepo.

Owns

  • the game: the authoritative simulation, the world, characters, inventories, skills, blueprints and props, in PostgreSQL (ADR-0007 decision 5: schema from scratch, migrations under packages/persistence/migrations, a write-behind flusher — the tick never awaits I/O)
  • the map editor and its assets, the mod registry (games-content@2 packs, grants, audit), the games-quickjs@1 server script mods (docs/mods.md) and the games.* events
  • the game WebSocket protocol and the portal, /play and /editor pages
  • the casual kit (packages/casual), its server economy (saves, ledger, idempotent actions) and the /play/<game> pages

Does not own

  • identity (OpenVibe.Network: players are keyed by Network subjects), file storage beyond the local copy (OpenVibe.Media mirrors map-editor assets), events delivery (OpenVibe.Events)
  • mod grants' authority (Network keeps them, mods.grant.manage) and general-purpose executable mods (OpenVibe.Host Stage C); Games runs its own bounded games-quickjs@1 scripts

Depends on

  • OpenVibe.Network (SSO, JWKS, client-credentials tokens, identity resolve, the games.progress.summary user module, mod grants), OpenVibe.Events (outbox relay, subscriptions), OpenVibe.Media (asset mirror)
  • openvibe-contracts v0.77.0, openvibe-sdk v0.22.0 and openvibe-shared v4.15.2 (pinned by release tarball in apps/server/package.json), Babylon.js, Rapier, PostgreSQL (pg, or embedded PGlite in development), Valkey (iovalkey, optional)

Repository layout

apps/
  client/        Browser client: rendering, prediction, input, HUD
  server/        Dedicated authoritative server: simulation, networking, persistence
packages/
  shared/        Math, ids, events, logging facade, fixed-timestep primitives
  protocol/      Versioned wire messages + codec (zod-validated inbound)
  content/       Data-driven item/recipe/world definitions + validation registry
  gameplay/      Pure domain logic: inventory, crafting, movement sim, zones, entities
  physics/       PhysicsWorld abstraction + Rapier adapter (@openvibe/physics/rapier)
  persistence/   DTOs, repository interfaces, PostgreSQL implementation, migrations
  casual/        The casual kit: rules, auto-battle and the Vibe Heroes pack (engine-free)
docs/adr/        Architecture decision records

See ARCHITECTURE.md for boundaries and data flow, ROADMAP.md for what exists and what is planned.

Prerequisites

  • Node.js >= 22
  • pnpm 10 (corepack enable)

Development

pnpm install

# Terminal 1 — authoritative server on :8000 (tsx watch)
pnpm dev:server

# Terminal 2 — client with hot reload on :5173 (proxies /ws to :8000)
pnpm dev:client

Open http://localhost:5173 in two browser windows to see multiplayer.

Controls: WASD move · Space jump (hold to bunnyhop) · Shift sprint · 1-6 hotbar · Tab menu (inventory / crafting / skills / players) · G drop held item · E gather / pick up props. Physgun (GMod-style): hold LMB grab (grabbing a frozen prop unfreezes it) · release to let go · RMB freeze · wheel push/pull · hold E rotate like a globe (Shift+E snaps) · hold Shift for grid-lock. Building: craft pieces → drop them (G or drag out of the inventory) → position with the physgun → freeze. Dropping IS placement; E picks any prop back up into your inventory.

The loop: spawn in Scrap City (safe city — no PvP, no building, no physgun), gather branches/stones/scrap outside the gates, craft tools, harvest the forest (axe/woodcutting) and quarry (pickaxe/mining), craft building pieces, place them in the wilds, position with the physgun and freeze. Props are protected: only you and players you trust can move them.

Commands

Command Purpose
pnpm dev Server + client dev processes in parallel
pnpm test All unit tests (vitest, through test/run.js)
pnpm typecheck Strict TypeScript across every package
pnpm lint ESLint (typescript-eslint, no-explicit-any)
pnpm format Prettier write
pnpm build Production build of all packages + client bundle
tsx apps/server/scripts/sliceTest.ts End-to-end vertical-slice test (boots a real server, drives protocol clients, restarts, asserts persistence)

Production

pnpm build
DATABASE_URL=postgres://… DATABASE_DIRECT_URL=postgres://… STATIC_DIR=apps/client/dist PORT=8000 pnpm --filter @openvibe/server start

DATABASE_URL (pooled, through PgBouncer) serves; migrations run at boot on DATABASE_DIRECT_URL (the owner role, a direct session). With neither set and NODE_ENV not production, the server uses an embedded PGlite database under DATABASE_DIR (default data/) — development only, one process. GAMES_PLACE_ID (default scraplandia) selects the default place for older clients and the edited map. Both built-in places have separate world rows and instance leases. VALKEY_URL (optional) makes the per-actor limits and the WebSocket upgrade tickets shared across instances (issue is SET … PX … NX, consume is GETDEL, so a ticket is single-use cluster-wide). GAMES_INSTANCES_PER_PLACE (default 1) enables several room simulations for a place in this process; MAX_PLAYERS is per-room capacity. PostgreSQL records each room and its renewable fenced lease. GAMES_INTEREST_CELL (default 16) and GAMES_INTEREST_RADIUS (default 80) tune spatial replication. See docs/instances.md for transfer, lease expiry and the one-process limit. TRUST_PROXY (optional) says which proxies may set the client-address headers: unset means the TCP peer is the client and X-Forwarded-For/X-Real-IP/CF-Connecting-IP are ignored (they are client-supplied and would otherwise let a caller mint fresh rate-limit buckets); true means the server is only reachable through the proxy; otherwise a comma-separated list of trusted proxy IPs/CIDRs (behind nginx, which sets X-Real-IP from $remote_addr). See .env.example.

The server serves the built client, /healthz (liveness), /api/ready, /metrics, and the game WebSocket on one port. Pages are / (portal), /play and /editor; on play.openvibe.games / is the game and /play redirects there. Any other path that is neither a file of the build nor a route answers 404 (an HTML page with links to those pages; {"error":"not_found"} under /api; plain text for a missing asset), never the portal with a 200. /api/ready is 200 only while the database answers a real round trip (db.ready()) and the simulation has ticked within the last 5 s, otherwise 503 naming the failed check (status ready/not_ready, checks.db, checks.tick); nginx keeps it off the public vhosts. Environment: PORT, HOST, STATIC_DIR, MAX_PLAYERS, LOG_LEVEL, GAMES_PLACE_ID, the database variables above (platform variables below).

Deployed at https://openvibe.games (nginx TLS termination → server on 127.0.0.1:8000, systemd unit openvibe-games.service from deploy/openvibe-games.service, env file /etc/openvibe/games.env (which holds DATABASE_URL, DATABASE_DIRECT_URL and VALKEY_URL); play.openvibe.games Host-routes to the same process).

Deploy on the host with sudo /opt/openvibe.games/deploy/scripts/deploy.sh, which runs ovhost deploy games (OpenVibe.Host, strategy pnpm-build; roadmap WS-N task 11): a fast-forward pull as the checkout owner, pnpm install --frozen-lockfile, every workspace package's dependencies checked, pnpm build, the restart (the SDK migrator applies any new migration as the owner before serving), /api/ready, and on failure the checkout restored, reinstalled and rebuilt and the server restarted again. Players online are reported and reconnect; --wait-idle holds the restart until nobody plays, --rollback runs ovhost rollback games, DRY_RUN=1 prints ovhost plan games. Do not pull by hand first (ovhost would find nothing new; pass --restart if you did). The wrapper exits with an error if ovhost is missing.

Rollback: sudo deploy/scripts/deploy.sh --rollback (ovhost rollback games), which rebuilds the previous commit. Migrations are additive and applied at boot, so an older build starts on the newer schema; the SDK migrator refuses a migration file that was edited after it ran (write a new one instead).

Platform integration (OpenVibe network)

Games integrates with the rest of OpenVibe at its service boundary only (apps/server/src/platform, apps/server/src/mods); the simulation packages do not know the platform exists. See ADR-0006.

  • Identity. Signed-in players are keyed by their canonical openvibe.network subject (usr_…/gst_…, the token's subject_id); a token without one cannot name an account. Guest tokens that look like account keys (usr_…) are refused.
  • Service principal. Calls to Events, Media and Network identity carry a client-credentials token of the games OAuth client (same client id and secret as SSO), one per audience. There are no shared keys. The SSO calls made for a player (/api/auth/me, code and FedCM exchanges) keep using the player's own token.
  • Events (OpenVibe.Events, through a transactional outbox in PostgreSQL, table event_outbox): games.player.joined|left, games.skill.leveled, games.blueprint.unlocked, games.world.saved (at most one checkpoint per WORLD_SAVED_EVENT_MINUTES, plus shutdown) and games.mod.installed|enabled|disabled|grants_changed|revoked. Achievements are not published as events: an unlock is stored in Games (its achievement rules read those tables) and sent to OpenVibe.Play over Play's API (GAMES_PLAY_URL), which is the network-wide read surface for titles, scores and unlocks. Progression events come from what a save actually wrote, in the same transaction.
  • 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.
  • Media. Map-editor assets (textures, paint masks, glb models; the only uploaded files Games has) are still served from local disk and are also copied into Media objects (kind: asset, namespace MEDIA_NAMESPACE), with a restart-safe queue (media_mirrors). There are no screenshots or blueprint files to upload: blueprints are per-player recipe unlocks.
  • Mods (ADR-013 in OpenVibe.Contracts). Manifests follow [email protected] (Games validates against the schema in the installed openvibe-contracts package — pin drift is a defect, ADR-0007 decision 12). The content runtime is games-content@2: declarative data packs checked against @openvibe/content (announcements; inert, mod-owned props; definitions — items, recipes, crops, NPC archetypes — merged into the content registry at boot, so they take effect on the next 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 requested capabilities; every runtime binding checks it at call time, a revoked install or capability stops affecting the world on the next tick, and install/grant/use/deny/revoke are audited (mod_audit). Trust tiers are metadata only. Staff can install games-quickjs@1 script packs through the mods API; built-in packs can carry scripts too. Scripts are pinned by the def-set digest and run in a per-mod QuickJS sandbox only where GAMES_SCRIPT_MODS=1 (budgets cpuMs 2 per tick, memoryMb 8, stackKb 128 by default; over budget disables the mod for the instance; host API and limits in docs/mods.md). API: GET /api/v1/mods, GET /api/v1/mods/:id (public); POST /api/v1/mods, POST /api/v1/mods/:id/enable|disable|revoke|grants|reload, DELETE /api/v1/mods/:id/grants/:capability, GET /api/v1/mods/:id/audit (owner/admin session, or a principal token with games.mod.manage). Fresh clients fetch GET /api/v1/content before the digest handshake so live script changes can be joined.
  • Status: GET /api/v1/platform reports whether events and the Media mirror are on, the outbox backlog and mirror counts (no secrets).
  • In production (2026-09-23, release f11e21c): events and the Media mirror are on. games.world.saved events reach OpenVibe.Events; no mod is installed, no grant exists and no asset has been mirrored to Media yet.
  • OpenVibe Frame. The portal, /play and /editor load the OpenVibe Frame (navbar, footer, theme loader) from this server's own pinned copy at /shared/ (apps/server/src/net/sharedAssets.ts), so they keep their frame while Network is down.
  • Persistence proof. apps/server/src/game/platformIntegration.test.ts boots the real server twice on one database and checks characters, world props, the world clock and the mod registry survive the restart; sliceTest.ts covers the gameplay state end to end.

Platform environment (all optional; service calls need OV_OAUTH_CLIENT_SECRET):

Variable Purpose
OV_OAUTH_CLIENT_ID OAuth client / principal id (default games)
OV_OAUTH_CLIENT_SECRET Its secret: enables SSO and every service call
OV_NETWORK_INTERNAL_URL Network base for /oauth/token, identity and the JWKS (e.g. http://127.0.0.1:4000)
EVENTS_URL OpenVibe.Events base; enables the outbox (EVENTS_PUBLISH=off disables)
MEDIA_URL OpenVibe.Media base; enables the asset mirror (MEDIA_MIRROR=off disables)
GAMES_INVENTORY_URL Inventory base (default https://inventory.openvibe.network; production can use loopback)
GAMES_ITEMS on (default) or off; off also when the OAuth client secret is unset
GAMES_PLAY_URL OpenVibe.Play base (e.g. http://127.0.0.1:5040); unset = scores and unlocks stay local
MEDIA_NAMESPACE Media tenant for the copies (default games)
WORLD_SAVED_EVENT_MINUTES Checkpoint event interval (default 15)
GAMES_CREATOR_WORLDS_MAX Creator worlds running at once (default 2; 0 stops new ones starting)
GAMES_CREATOR_WORLDS_PER_PERSON Worlds one account keeps (default 5)
GAMES_CREATOR_WORLD_IDLE_SECONDS An empty creator world stops after this long (default 180)
GAMES_CREATOR_WORLD_PLAYERS Players per creator world (default 16)
GAMES_CREATOR_SCRIPTS 1 runs creator worlds' own scripts in their rooms (QuickJS sandbox; off by default)

Capabilities

Implemented here (the service manifest's capabilities): games.mod.manage and games.mod.read (the mods API, for a principal token or an owner/admin session); games.world.announce, games.prop.place, and games.def.define for declarative packs; and games.script.run, games.players.read, games.player.message, games.world.emit, and games.storage.kv for script packs. Script bindings are absent unless approved for that install.

Service grants the games principal needs: inventory.item.grant (openvibe.inventory), events.event.publish and events.subscription.manage (openvibe.events: the outbox, and the account, revocation and mod-grant subscriptions), media.object.upload (openvibe.media, namespace games), identity.subject.resolve (openvibe.network), network.modules.write on games.progress.summary, and mods.grant.manage (Network's mod grants, roadmap WS-M task 3).

Tests

pnpm test runs every vitest file (unit tests of the packages and the server, including apps/server/src/game/platformIntegration.test.ts, which boots the real server twice on one database); it goes through test/run.js, which maps the flat test/*.test.js pointers back to the colocated TypeScript tests so ov test can select and shard them (vitest reads the real files, so their @vitest-environment docblocks and imports are untouched). After adding or moving a *.test.ts, run node test/run.js --index to regenerate the pointers — a missing or stale pointer fails the run. pnpm typecheck and pnpm lint keep strict TypeScript and no any; tsx apps/server/scripts/sliceTest.ts drives protocol clients against a real server across a restart.

Security

Reporting a vulnerability: SECURITY.md. The server is authoritative: inbound protocol messages are validated (zod), the WebSocket has per-socket flood control, and HTTP writes that cross a capability boundary (editor saves and uploads, the mods API) have per-actor limits (openvibe-sdk/limits). Players are keyed by Network subjects; guest tokens that look like account keys are refused. Script mods run in isolated QuickJS sandboxes with only their approved bindings. nginx keeps /api/ready off the public vhosts and /metrics answers direct loopback callers only. OV_OAUTH_CLIENT_SECRET, GAMES_EVENTS_SECRET (signed Events deliveries) and OV_NETWORK_AUTH_URL live in /etc/openvibe/games.env, by name only; there are no shared keys between services.

  • openvibe-contracts: v0.217.0
  • openvibe-sdk: v0.65.0
  • openvibe-shared: v4.15.2
  • openvibe-publishing: v1.4.0