- Rust 67%
- Lua 27%
- Jinja-like 2%
- HTML 2%
- CSS 2%
- 4 more languages <1%
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, scheduledctx.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 clientview,synced(new, old)andframe. The engine holds the world natively (the tree, routing, the queue,ctx.atschedules, 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#worldpatches.ctx.effect{...}results come back to the entity that asked. The host stamps every message withviewer,serverTimeandorigin. - server module:
{ kinds, root }. The root is spawned withconfig:date,now_ms,room, the pack'sdatafiles, andlegacy(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);helpis{ 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" }) orui.menu_button{...}opens it. It shows who is playing (the server's#welcomeframe), sign in / sign out, How to play and All games, in the manifest's page palette. Games fire$menu,$help,$signin,$signoutfrom 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.