BeamNG.Earth
A 1:1 scale streamed Earth for BeamNG.drive: real elevation and satellite imagery, floating origin, LOD streaming from a companion server
Play retro games online with friends — no install needed
Translation pending
GamesJavaScript · PythonGitHub @kwilson21OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Play N64 games online with friends in your browser — no install needed. WebRTC netplay powered by EmulatorJS.
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Play retro games online with friends — no downloads, no emulator setup. Visit the URL, drop your ROM, and play.
kaillera-next is a browser-based netplay platform built on EmulatorJS (mupen64plus-next WASM core). Players connect through WebRTC for low-latency peer-to-peer gameplay, with the server handling only room management and signaling.Read the full README ↗Play retro games online with friends — no downloads, no emulator setup. Visit the URL, drop your ROM, and play.
kaillera-next is a browser-based netplay platform built on EmulatorJS (mupen64plus-next WASM core). Players connect through WebRTC for low-latency peer-to-peer gameplay, with the server handling only room management and signaling.
Rollback — All players run the emulator in sync. Inputs are exchanged every frame over WebRTC DataChannels in a full mesh (up to 4 players, 6 connections). Uses a patched mupen64plus-next WASM core with deterministic timing (_kn_set_deterministic, _kn_set_frame_time) and a C-level GGPO-style rollback engine (_kn_pre_tick / _kn_post_tick) that predicts remote inputs and replays on misprediction, keeping responsive play under ~150 ms RTT. Strict lockstep is the silent stall fallback inside the same engine (boot convergence, menu phase-lock gate, and stock cores without kn_pre_tick). Delay is symmetrically negotiated via host-authoritative broadcast at game start and works mobile↔mobile with bit-identical state across iOS Safari and desktop Chrome.
Streaming — Host runs the only emulator and streams the canvas as video to guests via WebRTC MediaStream. Guests send controller input back over a DataChannel. Zero desync by design — only one emulator instance exists. SDP is optimized for low-latency gaming (H264/VP9 preference, high bitrate floor, minimal jitter buffer).
Both modes support:
Gamepads work alongside keyboard controls; phones and tablets also get on-screen touch controls.
If a controller's buttons land in the wrong place, open Configure Controller in the room, before the game starts, and press Quick Setup to remap them one at a time. Mappings are saved in your browser, and the same panel has stick deadzone and sensitivity settings.
To use a Switch 2 Pro Controller:
You only do this once. After that first permission grant the controller is re-enabled automatically each time it is plugged in, and the Connect button hides itself while a granted controller is plugged in; press any button to wake it, as with any controller.
Limits: this needs a browser with WebUSB (Chrome; Edge should work), so Safari and Firefox are out. Bluetooth is not supported (the controller uses a custom BLE protocol). On Windows the browser may need the WinUSB driver to open the controller (for example installed with Zadig), and on Linux it may need a udev rule; neither is tested.
Requires Python 3.11+, uv, and just.
# Install dependencies
just setup
# Start the server (HTTP, no Redis)
just serve
# → http://localhost:27888/
This is the fastest path to a running server. Over plain HTTP you can explore the lobby, create rooms, and use streaming mode. Rollback mode requires HTTPS — see below.
| Feature | HTTP | HTTPS |
|---|---|---|
| Lobby, rooms, Socket.IO | works | works |
| Streaming mode | works | works |
| Rollback mode | broken (needs SharedArrayBuffer) | works |
| High-res frame timing | degraded (~1ms vs ~5μs) | works |
Browsers gate SharedArrayBuffer and performance.now() precision behind
cross-origin isolation,
which requires a secure context (HTTPS). The server sends the required
COOP/COEP headers automatically — you just need HTTPS.
# One-time: generate HTTPS certs (see HTTPS setup below)
just certs
# Start dev server (Redis + HTTPS)
just dev
# → https://<your-domain>:27888/
HTTPS is required — browsers need crossOriginIsolated (SharedArrayBuffer, high-res timers) which only works over secure contexts. just certs issues a real Let's Encrypt certificate via lego using the Cloudflare DNS-01 challenge, so the cert is trusted by every device (including mobile) without installing a local CA. The subdomain never needs to resolve publicly — point it at your LAN IP via local DNS (AdGuard, Pi-hole, /etc/hosts, etc.).
brew install lego # macOS
# or download a release: https://github.com/go-acme/lego/releases
In the Cloudflare dashboard, create a token with Zone:DNS:Edit scoped to the zone you'll use.
Add to server/.env:
[email protected]
LEGO_DOMAIN=kn-test.example.com
CLOUDFLARE_DNS_API_TOKEN=your-scoped-token-here
Then:
just certs
Certs land in certs/. Re-run just certs to renew (Let's Encrypt certs expire every ~90 days).
mkcert works for localhost but requires installing its CA on every mobile device. Prefer the lego flow for cross-device testing.
docker build -t kaillera-next .
docker run -p 27888:27888 -e ALLOWED_ORIGIN="https://yourdomain.com" kaillera-next
The Docker image runs as a non-root user with a health check on /health.
docker stack deploy -c docker-compose.prod.yml kaillera-next
Includes Redis for session persistence (blue-green deploys, reconnect survival) and persistent log volumes.
┌──────────────────┐ ┌──────────────────┐
│ Browser A │ │ Browser B │
│ EmulatorJS + │◄──P2P──►│ EmulatorJS + │
│ WebRTC mesh │ WebRTC │ WebRTC mesh │
└────────┬─────────┘ └────────┬─────────┘
│ Socket.IO │
└────────────┬───────────────┘
▼
┌─────────────────────┐
│ kaillera-next │
│ FastAPI + Socket.IO │
│ + Redis │
│ :27888 │
└─────────────────────┘
The server handles room creation, player coordination, and WebRTC signaling. Once peers are connected, game data flows directly between browsers — the server is idle during rollback gameplay. Redis persists room state across deploys and reconnects.
All frontend assets are self-hosted (EmulatorJS, Socket.IO) — zero CDN dependencies. The server sends Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers to enable crossOriginIsolated on all browsers, unlocking SharedArrayBuffer and high-resolution performance.now().
server/ Python signaling server (FastAPI + Socket.IO + uvloop)
src/
main.py Entry point — serves API, Socket.IO, and static frontend
state.py Redis-backed room persistence
ratelimit.py Per-IP rolling-window rate limiting
db.py Database: SQLite or Cloudflare D1 backend, SQL migrations in server/migrations/
api/
app.py REST endpoints, security headers (COOP/COEP/CSP), admin API
signaling.py Socket.IO events — rooms, WebRTC relay, ROM sharing, game data
payloads.py Pydantic v2 payload models + @validated decorator
og.py Open Graph meta-tag injection + build-time-rendered OG cards (no runtime browser)
web/ Static frontend (HTML + JS, served by FastAPI)
index.html Lobby — create/join rooms, invite links
join.html Invite page (/join?room=CODE) — who invited you, Join vs Watch, waking
play.html Game page — overlay, EmulatorJS embed, toolbar
admin.html Admin dashboard (session logs, events, feedback, screenshots)
privacy.html Privacy notice — what's collected (diagnostics, screenshots), opt-out
demo.html Interactive rollback demo (user-supplied ROM required)
lag-test.html Standalone dev harness for lagviz.js
error.html Error/fallback page
static/
play.js Play page orchestrator (Socket.IO, overlay, ROM handling, engine dispatch)
landing.js Front-page board controller (room creation, open-rooms board)
landing.css Front-page styles (direction A tokens)
join.js Invite-page controller (room lookup, join/watch states)
lagviz.js Waking-state lag visualizer (front page + invite page)
admin.js Admin dashboard logic (sessions, events, feedback, screenshots)
admin.css Admin dashboard styles
play.css Play page styles (room overlay tokens, direction A)
netplay-rollback.js Primary engine: GGPO-style C-level rollback (4P mesh WebRTC, prediction + replay); strict lockstep is the silent stall fallback inside this file
rollback-shadow-worker.js Shadow worker: presentation overlay, plus a Mode 2 replay coprocessor whose corrected state main adopts
netplay-lockstep.js Deprecated compat shim (re-fetches netplay-rollback.js for cached play.html pages that still request the old filename)
netplay-streaming.js Streaming engine (host video → guests via WebRTC MediaStream)
gamepad-manager.js True analog gamepad input (3-stage pipeline: deadzone → scale → N64 quantize)
switch2-usb.js WebUSB handshake for the Switch 2 Pro Controller
controller-settings.js In-game controller settings panel
virtual-gamepad.js On-screen touch controls for mobile
shared.js Input encoding/decoding, wire format, input application to WASM
kn-audio.js AudioWorklet pump + rollback audio capture
kn-capture.js Diagnostic gameplay-canvas capture opt-out policy (localStorage flag)
kn-desync-detector.js Field-granular cross-peer desync detection (RDRAM probes)
kn-diagnostics.js Rollback diagnostics ring buffer (RB-CHECK, audit log)
kn-vision-client.js Claude vision client for screenshot-diff desync analysis
audio-worklet-processor.js AudioWorklet ring buffer for rollback audio
core-redirector.js Redirect EJS core download to patched WASM, IDB cache management
api-sandbox.js Browser API interception (rAF, performance.now, getGamepads)
storage.js Safe localStorage/sessionStorage wrapper
kn-state.js Shared cross-module state
feedback.js In-app feedback collection
version.js Version display + changelog modal
version-guard.js Cross-tab version mismatch guard (forces reload after deploys)
ejs-loader.js EmulatorJS loader shim (boot + script setup hooks)
demo.js Demo page orchestrator (fake peer + menu autopilot)
fake-peer.js Synthetic P2 for the demo page (simulated input/jitter)
ssb64-menu-autopilot.js Closed-loop menu driver for the demo (reads RDRAM menu state)
rollback-timeline.js Demo page's live rollback-metrics HUD
socket.io.min.js Self-hosted Socket.IO client (v4.8.4)
vendor/fflate.min.js Self-hosted compression lib used to extract ROM ZIP archives
ejs/ Self-hosted EmulatorJS runtime, compression libs, localization
ejs/cores/ Patched mupen64plus-next WASM core
build/ WASM core build system (Docker + C patches)
tests/ E2E tests (pytest + Playwright) + VGP visual regression
docs/ Roadmap, MVP plan, design specs
certs/ TLS certificates for HTTPS dev (gitignored)
The pre-built patched core is included at web/static/ejs/cores/. You only need to rebuild if you modify the C patches.
The build compiles a patched mupen64plus-next core with:
_kn_set_deterministic, _kn_set_frame_time)_kn_sync_hash, _kn_sync_read, _kn_sync_write, _kn_sync_hash_regions)_kn_get_audio_ptr, _kn_get_audio_samples, _kn_reset_audio, _kn_get_audio_rate)-fno-fast-math, -ffp-contract=off)wasm-opt --denan (injected before --asyncify)srand(0)) and RTC# 1. Build the Docker image with Emscripten SDK (one-time, ~10 min)
docker build -t emulatorjs-builder build/
# 2. Compile the patched core (~5-15 min depending on CPU)
docker run --rm -v $(pwd)/build:/build emulatorjs-builder bash /build/build.sh
# 3. Deploy to web/static/ejs/cores/ (all three must come from the same build)
cp build/output/mupen64plus_next-wasm.data web/static/ejs/cores/
cp build/output/mupen64plus_next_libretro.js web/static/ejs/cores/
cp build/output/mupen64plus_next_libretro.wasm web/static/ejs/cores/
The build clones EmulatorJS's forks of mupen64plus-libretro-nx and RetroArch, applies patches from build/patches/, compiles to LLVM bitcode, links through RetroArch with asyncify, and packages into a 7z .data archive that EmulatorJS loads at runtime.
| Patch | Description |
|---|---|
mupen64plus-kn-all.patch | Core exports: deterministic timing, resync hash/read/write, audio capture |
mupen64plus-deterministic-timing.patch | features_cpu.c and profile.c timing fixes |
mupen64plus-wasm-determinism.patch | Strict FP compile flags, FPU NaN canonicalization, srand(0), deterministic RTC |
mupen64plus-ai-determinism.patch | AI DMA deterministic audio interface timing |
mupen64plus-fpu-trace.patch | FPU operation tracing for cross-platform determinism verification |
mupen64plus-softfloat.patch | SoftFloat FPU for bit-exact cross-platform floating point |
retroarch-deterministic-timing.patch | RetroArch _emscripten_get_now() override + AUDIO_FLAG_SUSPENDED bypass |
| Variable | Default | Description |
|---|---|---|
ALLOWED_ORIGIN | * | CORS origin — set to your domain in production |
PORT | 27888 | Server listen port |
MAX_ROOMS | 100 | Maximum concurrent rooms |
MAX_SPECTATORS | 20 | Maximum spectators per room |
REDIS_URL | — | Redis connection URL (required for session persistence) |
REDIS_HOST | redis | Redis host (used if REDIS_URL not set) |
REDIS_PORT | 6379 | Redis port |
REDIS_PASSWORD | — | Redis password |
ADMIN_KEY | — | Admin API key for management endpoints |
LOG_RETENTION_DAYS | 7 | Auto-delete old session data after N days |
FLAGGED_STALE_DAYS | 180 | Auto-delete unresolved flagged matches after N days |
DB_PATH | data/kn.db | SQLite database file path |
D1_DATABASE_ID, D1_API_TOKEN, CF_ACCOUNT_ID | — | Switch to the Cloudflare D1 backend instead of SQLite |
IP_HASH_SALT | random | Salt for IP address hashing (random per restart if unset) |
KN_PROXY_SECRET | — | Shared secret trusting the landing Worker's forwarded client IP |
PUBLIC_ORIGINS | — | Extra origins allowed to read /health, /list, /room/*, /api/stats/public |
TRUSTED_PROXY_IPS | — | Comma-separated trusted proxy IPs for X-Forwarded-For |
Public:
| Endpoint | Description |
|---|---|
GET /health | Health check |
GET /list?game_id=... | Room listing (EmulatorJS-Netplay compatible) |
GET /room/{room_id} | Room info (rate-limited) |
GET /ice-servers | ICE/TURN server configuration |
GET /api/cached-state/{rom_hash} | Retrieve cached save state |
POST /api/cache-state/{rom_hash} | Upload save state to cache |
POST /api/session-log | HTTP fallback for session log flush |
POST /api/client-event | Client event logging |
POST /api/feedback | Feedback submission |
GET /api/rom-hashes | Known ROM hashes |
GET /api/core-info | Content-hashed WASM core build info |
GET /api/version | Static asset version |
GET /api/landing-build | Content-hash build id shared with the landing Worker |
GET /api/stats/public | Matches-started-this-week counter for the front page |
GET /room/{room_id}/card.jpg | Invite card image (room name + live frame) for social sharing |
GET /room/{room_id}/frame.jpg | Latest board frame for a listed, in-game room |
GET /privacy, GET /privacy.html | Privacy notice page |
Admin (requires ADMIN_KEY):
| Endpoint | Description |
|---|---|
GET /admin/api/stats | Server statistics |
GET /admin/api/session-logs | List session logs |
GET /admin/api/session-logs/{log_id} | Download a session log |
GET /admin/api/client-events | List client events |
GET /admin/api/client-events/{event_id} | Client event detail |
GET /admin/api/feedback | List feedback |
GET /admin/api/feedback/{feedback_id} | Feedback detail |
GET /admin/api/screenshots/{match_id} | List screenshots for a match |
GET /admin/api/screenshots/img/{screenshot_id} | Serve screenshot image |
GET /admin/api/session-logs/{log_id}/export | Stream a session log's entries as newline-delimited JSON |
GET /admin/api/matches | List recently-rotated matches with precomputed metrics |
GET /admin/api/matches/{match_id} | One match's precomputed metrics row |
GET /admin/api/input-audit/{match_id} | Per-slot input audit for a match |
GET /admin/api/session-timeline | Session timeline; requires match_id or room query parameter |
GET /admin/api/desync-events | List vision-validated desync events; requires match_id query parameter |
V1 is feature-complete and deployment-ready:
GPL-3.0
TURN_SECRET| — |
| HMAC secret for time-limited TURN credentials |
TURN_SERVERS | — | Comma-separated TURN server URLs |
ICE_SERVERS | — | Legacy static ICE server config (JSON) |
CF_TURN_KEY_ID, CF_TURN_API_TOKEN | — | Cloudflare Realtime TURN credentials for /ice-servers |
PER_IP_SPECTATOR_CAP | 3 | Max spectator slots one IP can hold in a room |
ROM_SHARING_ENABLED | true | Enable host ROM P2P sharing |
GAME_IMAGES_ENABLED | true | Read by og.py but not currently wired to anything — OG-card game images are always on |
ANTHROPIC_API_KEY, DESYNC_VISION_ENABLED | — | Enable the Claude desync-vision endpoint (DESYNC_VISION_ENABLED=1) |
DISABLE_HTTPS | — | Set to disable HTTPS even when certs are present |
DISABLE_RATE_LIMIT | — | Set to 1 to disable per-IP rate limiting (dev only) |
DEBUG_MODE | — | Enables debug-sync and local log files; debug-logs writes to stdout even when unset |
GIT_VERSION | — | Override version string (set automatically in Docker) |
KEEPALIVE_SECONDS | 0 (off) | How often play pages ping /health |
RENDER_EXTERNAL_URL, RENDER | — | Render.com-provided external URL / platform flag (CORS origin, trusted-proxy behavior) |
BUDGET_AUTO_FLAGGED_BYTES | 200 MiB | Byte budget for auto-flagged match blob retention |
PARQUET_DIR | data/parquet | Output dir for rolled-up match-log Parquet files |