Imported from Zendevve/OpenCade (
AGENTS.md). Install upstream withnpx skills add Zendevve/OpenCade. Copyright stays with the author.
Repository Guidelines
Community: https://discord.gg/Y4rDyTScPe — where we discuss OpenCade and everything around it (not only OpenCade).
Project Overview
OpenCade — open-source arcade netplay platform, clean-room alternative to proprietary Fightcade. Monorepo at D:/OpenCade (Apache-2.0) with Tauri + React + TypeScript client and Rust + Axum + PostgreSQL server. D:/Fightcade (v2.1.45) is read-only reference — never copied into this repo (see docs/ARCHITECTURE.md §2 and docs/reference-fightcade-install.md). Goal: lobby → challenge → versioned signaling → P2P (or WS relay) → safe emulator launch.
Architecture & Data Flow
- System:
apps/client(Tauri) ↔apps/server(Axum) over HTTPS/WS/ws→ P2P/relay between peers. Server never sees game inputs except when relaying as fallback. - Client: Tauri 2.x hosts React routes
Games | Lobbies | Friends | Servers | Settings. Rust core ownsfs(ROM scan underemulator/<core>/ROMs/),process(safe direct process spawning with arg escaping, no shell),diag(Network Test). State: TanStack Query + Zustand, WS client with typedpackages/protocoldiscriminated unions and reconnect backoff. - Server: Single Axum monolith +
postgres:16-alpine(no Redis in MVP). RoutesPOST /api/v1/auth/*,GET /api/v1/games,GET /api/v1/lobbies/:game,POST /api/v1/rooms+.../:id/{accept,decline,cancel}, WS/wswith versioned envelope{type,version,request_id,timestamp,payload}(presence.update,chat.message,challenge.*,session.offer/answer/candidate,room.*). In-memory presence Hub, Postgres for durable state. - Networking: Signaling relayed verbatim through server; client-driven UDP hole-punch → STUN hint (
GET /serversreturnsstun:host:portwhen configured) → WS relay fallback (in-processrelaymodule, futureservices/relaybinary). Room statesWAITING → CHALLENGING → CONNECTING → PLAYING → FINISHED|CANCELLED. Latencyrtt_ms/loss/jitterviapresence.update. - Reference only:
docs/reference-fightcade-install.mddescribes the opaque PyInstaller launcher (emulator/fcade.exe/frm.exe→fightcade/launcher.py) andfbneo-training-modeLua surface — not used at runtime.
Key Directories
apps/client/— Tauri app (replacesfc2-electron).src/routes/{Games,Lobbies,Friends,Servers,Settings}.tsx,src/components/*,src/lib/{api,ws,store}.ts,src-tauri/src/{main.rs,commands/{fs,process,diag}.rs,adapters/fbneo.rs},tauri.conf.json(least-privilegefs/processallowlist).apps/server/— Axum monolith (replacesfightcade.com/replay).src/{main.rs,routes/*,ws.rs,state.rs,auth.rs,models/*},migrations/001_users.sql,Dockerfile.packages/protocol/— shared wire types (Envelope,RoomState,PresenceState) — single source of truth,serde+ts-rsgeneration.packages/emulator-sdk/—pub trait EmulatorAdapter { detect/validate/get_version/launch/stop/configure }+LaunchCtx,ChildHandle. No shell, path canonicalization + prefix check.packages/game-definitions/— declarativegames/*.toml(schema_version=1,id,name,emulator="fbneo",[launch] args=["{rom}"],[validation] required_files=["neogeo.zip"]) +src/loader.rs+ legacyemulator/*.json→ TOML importer (build-time only).adapters/fbneo/— only required adapter in MVP (fcadefbneo.exedetection,fcadefbneo.default.iniversion check, safe arg building). Futureflycast/snes9xbehind feature flags.services/relay/— placeholder crateopencade-relay(future STUN/TURN); not required for MVP (in-process WS relay suffices).research/— not shipped —observations/,protocol/,binaries/(gitignored),network/,behavior/,notes/+GUARDRAILS.md. Keepresearch/binaries/.gitkeep.docs/—ARCHITECTURE.md(authoritative),reference-fightcade-install.md(read-only install notes).tests/,docker/,.github/workflows/— integration tests, compose overlay, CI.- Reference (read-only, outside repo):
D:/Fightcade/emulator/fbneo/fbneo-training-mode/— vendored Luagames/<rom>/<rom>.lua+hitboxes/*.lua+Run()hook; pattern only.
Development Commands
# prerequisites: Rust 1.98+, Node 24+, pnpm 11+, Docker, Postgres 16
pnpm install # workspace install (apps/*, packages/*)
pnpm -C apps/client tauri dev # Vite + Tauri dev (Windows)
pnpm -C apps/client build # TS check + Vite build
pnpm format && pnpm lint # prettier + eslint
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo run -p opencade-server -- --migrate # sqlx migrate run
cargo run -p game-defs-import -- D:/Fightcade/emulator/fbneo_roms.json --out packages/game-definitions/games
# infra
docker compose -f docker/docker-compose.yml up --build -d # or docker-compose.yml at root
curl http://localhost:8080/health && curl http://localhost:8080/ready
psql $DATABASE_URL -f apps/server/migrations/001_users.sql
# diagnostics
pnpm tauri dev -- --log opencade # client logs to %APPDATA%/OpenCade/logs/opencade.log
Code Conventions & Common Patterns
- Formatting:
cargo fmt(rustfmt edition 2021,max_width=100) andpnpm format(Prettier) — CI blocks onfmt --check.clippy -D warningsrequired. - Naming: crates
opencade-*, TS packages@opencade/*, adaptersfbneo(kebab), TOML idssfiii3,kof98(snake, lowercase). DB tablessnake_case; API/WS JSON alsosnake_casevia#[serde(rename_all = "snake_case")]for payload keys and#[serde(rename = "type")]for thetypefield (seeEnvelopeinapps/server/src/main.rs:17-24andpackages/protocol). - Error handling:
thiserror+anyhowin Rust, neverunwrap()in server paths; TSResult<T,E>-style returns fromsrc/lib/api.ts. Structuredtracinglogs (JSON in prod, pretty in dev) — never log tokens, passwords, or ROM paths with PII. - Async: Tokio everywhere server-side; Tauri commands
asyncwith#[tauri::command(async)]; WS client usestokio-tungstenite+ backoff. No blocking in async context. - Protocol: every WS message
Envelope {type:"signaling.offer", version:"1.0", request_id, timestamp, payload:{room_id,candidate}}withversionas string"1.0"canonical (compat"1"accepted) matchingpub const PROTOCOL_VERSION: &str = "1.0"andis_supported_versioninpackages/protocol/src/lib.rs:14-21/pub version: Stringinapps/server/src/main.rs:20. Server validatesversionthentype, returns{code:"unknown_type"}for unknowntype,{code:"version_unsupported"}for unsupported version; forward-compatible bump via"2.0"handler. - Process launch:
Command::new(exe).args(escaped_os_strings).current_dir(exe.parent()).spawn()— canonicalize exe, verify under allowlist root, reject..traversal, nocmd /C,extra_envallowlisted only.ChildHandletracks pid, streams stdout/stderr tologs/emulator.log. - Adapter contract:
detect()checksemulator/fbneo/fcadefbneo.exe+fcadefbneo.default.ini;validate()checksrequired_filesexistence and warns on version mismatch vsVERSION.txt 2.1.45;launch(ctx)buildsLaunchCtx{exe, rom:PathBuf, args:Vec<OsString>};stop(handle)gracefulCTRL+C→killafter timeout. - Game defs:
schema_versionmandatory (MVP1), loader rejects unknownschema_version,idmust be^[a-z0-9_]{3,20}$,launch.argsisVec<String>with{rom}substitution viaOsStringpositional replacement — no string concat. - Clean-room:
research/is observation-only, never compiled.cargo deny+license = "Apache-2.0"allowlist; no GPL emulator cores linked. Citations forfbneo-training-modeinspiration inNOTICE.
Important Files
docs/ARCHITECTURE.md— authoritative system boundaries, diagram, M0-M7 phases, guardrails (read before coding)docs/reference-fightcade-install.md— read-only notes onD:/Fightcade(Electron wrapper, PyInstaller launcher, training-mode Lua, JSON catalogs)research/GUARDRAILS.md— forbidden/allowed lists, Observation→Documentation→Design→Implementation processapps/client/src-tauri/tauri.conf.json— Tauri permissions (fs:allow-read-dir ROMs,process:allow-spawnonly known binaries,store:allow)apps/client/src/routes/Games.tsx— game list (derived fromgame-defs+ local scan) + challenge flowapps/server/src/main.rs+apps/server/src/ws.rs— Axum router + WS versioned envelope relayapps/server/migrations/001_users.sql—users(id,username,password_hash),sessions,games,rooms,matches,chat_messagespackages/protocol/src/lib.rs—Envelope,RoomState,PresenceState(source of truth)packages/emulator-sdk/src/lib.rs—EmulatorAdaptertraitpackages/game-definitions/games/sfiii3.toml— example declarative game (template for new games)docker-compose.yml+apps/server/Dockerfile+.env.example(DATABASE_URL,SESSION_SECRET)pnpm-workspace.yaml,rustfmt.toml,.clippy.toml,.github/workflows/ci.yml
Runtime/Tooling Preferences
- Runtime: Rust 1.98+ (MSRV), Tauri 2.x (WebView2 on Windows), Node 24+ with pnpm 11 (not npm/yarn), Postgres 16-alpine (sqlx compile-time checked). No Bun. No Electron.
- Package manager:
pnpmat root (pnpm-workspace.yamlcoversapps/*,packages/*,adapters/*,services/*) —pnpm installonly, commitpnpm-lock.yaml. - Build:
cargo build --workspace,pnpm -C apps/client build,tauri build(MSI/NSIS on Windows). Docker multi-stagerust:1.98-bookworm → debian:bookworm-slim. - Env:
DATABASE_URL=postgres://opencade:opencade@db:5432/opencade,SESSION_SECRET(32B CSPRNG),RUST_LOG=info,opencade_server=debug. Never commit.env(see.env.example). - OS: Windows 10/11 primary (Tauri), Linux/macOS viable via same stack — no
.lnkshortcuts, usetauri::path. - Tooling constraints: keep
disableDevToolsoff in dev, on in prod via Tauritauri.conf.json > build > devPath. Noshellpermission intauri.conf.json; useprocessallowlist.
Testing & QA
- No ROMs/binaries in tests: use fixtures under
tests/fixtures/(tiny TOML, mock adapter).research/binaries/is gitignored. - Unit:
cargo test -p opencade-protocol -- envelope serde,-p emulator-sdk -- arg escaping,-p game-definitions -- loader/scan,pnpm testforpackages/shared(Vitest). - Integration:
cargo test --workspace -- --ignored(spinsAxum+postgresviadocker compose up -d db, registers two users, WS presence →challenge.send/accept→signaling.offer/answer/candidate→room PLAYING→ disconnect). - ** networking:** LAN, same NAT, different NAT, symmetric NAT (expect relay fallback), packet loss/latency injection via
tc;diagnostics:network_testcommand assertsnat:cone|symmetric,rtt_ms,relay_reachable. - Manual QA loop (MVP):
docker compose up -d→curl /health→pnpm tauri dev→ login →Gamesshows ownedsfiii3(needssfiii3.zip+neogeo.zipunderemulator/fbneo/ROMs/scanned locally) → challenge peer inLobbies/:gameId→ accept →CONNECTING(P2P or relay) → emulator spawnsfcadefbneo.exewith escapedC:\path with spaces\sfiii3.zip→ play →FINISHED→ export logsSettings → Export Logs. - CI gate:
.github/workflows/ci.yml(cargo fmt --check,clippy -D warnings,cargo test,pnpm format:check,pnpm build,docker compose configlint). Workflow push requiresworkflowscope — seedocs/ARCHITECTURE.md §16.