README.md

Cavu Games #

Small games that play nicely with your Atmosphere account. Ludos liberi.

A Lua game runtime run isomorphically on the server and in a Rust→wasm browser client, the play.cavu.games game fleet, the cavu.games host (catalog, sign-in, launch, delegated record writes) and first-party games in games/. The plan and design notes are in PLAN.md; work is tracked with beads (bd ready).

./build.sh                      # wasm client -> web/pkg, server binary
./target/release/cavu-server    # http://127.0.0.1:8788/?game=haiku-day   (&debug for stats)
crate role
cavu-world the entity engine: worlds of entities held natively, routing, transactions, schedules, projection, the client's mirror and prediction; lua/glue.lua calls the classes.
cavu-lua luars VM + deterministic math (pure-Rust libm), pack-scoped require, Lua <-> IPLD. lua/shared.lua (both sides), lua/server.lua (server bridge), lua/client.lua (UI stdlib + client bridge).
cavu-server axum: /packs/{id}/client.json (client modules only), websocket games.cavu.subscribeSession, one game thread owning every room's VM.
cavu-client wasm: UI kit (taffy, behaviors, skin cache, WebGL2, hidden DOM) + session socket.

Game contract: an entity world (see games/haiku-day, games/rpg-actor-in-space, games/entity-demo; the engine is crates/cavu-world, native, and runs only the classes' Lua).

  • Entities: the world is a tree of entities, each plain data (id, kind, parent, children, fields) whose kind is a Lua table: init, on.<msg> (trusted messages: ctx.send, scheduled ctx.at, effect results), intent.<msg> (player intents, to = an entity id), predict (intents the client re-runs to predict), project(self, viewer) (fields starting with _ stay on the server), and on the client view, synced(new, old) and frame. The engine holds the world natively (the tree, routing, the queue, ctx.at schedules, persistence, projection, the client's mirror and prediction); a handler gets its entity as a plain Lua table and mutates it, and the engine reads the tables back at the end of the action (all or nothing), sending viewers per-entity #world patches. ctx.effect{...} results come back to the entity that asked. The host stamps every message with viewer, serverTime and origin.
  • server module: { kinds, root }. The root is spawned with config: date, now_ms, room, the pack's data files, and legacy (the old state of a room saved before its game was an entity world).
  • client module: { kinds, screen, skins?, loading?, menu?(rd), help?, music?(rd) }: the screen and its children are client-only entities (the UI); help is { title, lines, first_visit }.
  • Shared classes: a manifest's classes ({ name: "classes/x.lua" | "at://…/games.cavu.entityClass/…" }) are loaded, sandboxed, before the modules on both sides.
  • The menu is the engine's: Esc, a corner button (manifest "menu": { "corner": "top-right" | "top-left" | "none" }) or ui.menu_button{...} opens it. It shows who is playing (the server's #welcome frame), sign in / sign out, How to play and All games, in the manifest's page palette. Games fire $menu, $help, $signin, $signout from any behavior instead of building host URLs.
  • manifest.json lists which modules each side gets; server-only modules are never served.

Wire: DAG-CBOR event-stream frames (crates/wire), lexicons in lexicons/games/cavu/.

Local dev #

Everything at once: ./dev.sh starts vivarium (a local atmosphere), the cavu.games host, the play.cavu.games gateway and a dev node for games/ (hot reload), creates test accounts and prints the links. Ctrl-C stops it all; state lives in .dev.

./build.sh
./target/release/cavu-server                     # real OAuth via the loopback client
# open http://127.0.0.1:8788/?game=haiku-day     (127.0.0.1, not localhost, or the cookie won't stick)

./target/release/cavu-server --dev-auth --data /tmp/cavu-dev
# open http://127.0.0.1:8788/?game=haiku-day&dev_did=did:plc:alice&dev_handle=alice.test
# --dev-auth trusts the DID in #join and stores records in a local table (signed for real). Never in prod.

Server flags: --host --port --games --web --data --public-url --did --dev-auth --secrets. Everything durable is in <data>/cavu.db: rooms (Lua state as DAG-CBOR), OAuth sessions + client key (auth_*), and the record-signing key ring (records_signing_keys). Losing it signs everyone out and rotates the signing identity's keys (old records still verify only while their keys stay published).

Outside services (the http effect). A game lists the hosts it calls in game.json ("network": [{ "host": "openrouter.ai", "name": "OpenRouter" }]; players see the names before launch) and its keys go in a secrets file, never in the pack:

./target/release/cavu-server --secrets ~/cavu-secrets.json
# { "<game>": { "openrouter": { "value": "sk-or-...", "hosts": ["openrouter.ai"] } } }
# ("env": "OPENROUTER_API_KEY" instead of "value" reads it from the environment)

Requests go only to declared hosts, over HTTPS, never to private addresses, and a key is only ever sent to the hosts listed with it. See crates/cavu-server/src/http.rs.

Local: host + fleet together (the launch flow) #

cargo build --release -p cavu-host && ./build.sh
./target/release/cavu-server --did 'did:web:127.0.0.1%3A8788' --public-url http://127.0.0.1:8788 \
    --host-url http://127.0.0.1:8787 --data /tmp/cavu-play
./target/release/cavu-host --dev-launch-did 'did:web:127.0.0.1%3A8788' --data /tmp/cavu-host
# open http://127.0.0.1:8787/  (catalog) -> Play -> http://127.0.0.1:8788/?game=haiku-day#launch=...

Deploy: cavu.games (host) + play.cavu.games (fleet) on Fly #

One image (both binaries), two apps, from the repo root (.dockerignore allowlists what the image needs):

fly apps create cavu-host && fly volumes create cavu_host_data --app cavu-host --region iad --size 1
fly apps create cavu-play && fly volumes create cavu_data      --app cavu-play --region iad --size 1
fly deploy -c fly.host.toml --ha=false
fly deploy -c fly.toml      --ha=false
fly certs add cavu.games --app cavu-host        # apex: A/AAAA to the app's IPs
fly certs add play.cavu.games --app cavu-play   # CNAME play -> cavu-play.fly.dev

Check after deploy: https://play.cavu.games/.well-known/did.json (signing keys + #cavu_launch), https://cavu.games/oauth/client-metadata.json and /oauth/jwks.json, then sign in and play. Back up both volumes: the host's holds players' OAuth sessions, the fleet's holds the signing keys.