# RocketLeagueBot-Renderer A web-based 3D Rocket League **live viewer and replay system** for telemetry streamed from a RocketSimVis-compatible UDP source (e.g., a training bot). Built with Go (Fiber + SQLite + zstd) and Three.js. ``` bot ──UDP──▶ ┌──────────────────────┐ │ udpListener │ │ │ │ │ ├──▶ Hub ───WS──▶│──▶ live viewers (read-only) │ │ │ │ └──▶ Recorder ──▶│──▶ SQLite (zstd frames) └──────────┬───────────┘ │ ▼ Player ──WS──▶ playback viewer (seek/pause/speed) ``` ## Features - **Live UDP ingest** at any rate; non-blocking hub keeps a slow client from stalling the whole pipeline. - **3D Three.js viewer**: orbit, zoom, pan, mobile-friendly, low-quality mode for weak GPUs. - **Rich HUD**: ball position/height/speed/distance to each goal, per-car boost, speed, distance to ball, flags (DEMO / AIR / SONIC / TOUCH), packets-per-sec and last-packet age. - **Automatic recording** to SQLite with optional **zstd compression** (default on). Typical compression ratio of 5–10× on JSON telemetry. - **Frame-accurate playback** with **seek bar, pause, variable speed** (0.25× – 8×), keyboard shortcuts, and autoplay-to-newer. - **Server-pushed events**: new/stopped/deleted recordings reflect in the UI without polling. - **Live `/api/stats`** endpoint and an in-panel stats box (uptime, packets, drops, db size, compression ratio). - **HTTPS via Let's Encrypt autocert** with HTTP→HTTPS redirect. - **Basic Auth** on every endpoint (constant-time comparison). - **Graceful shutdown** flushes the recorder before exiting. ## Example ![Live viewer with ball trail and predicted path](imgs/Screenshot_2026-05-16_00-32-01.png) The viewer shows a detailed arena, car models (blue/orange), ball with ring indicator, boost pads with pulsing animation when active, and optional ball-trail/prediction lines. The HUD displays real-time metrics, and the side panel lists recordings and server stats. ## Quickstart ```sh git clone cd RocketLeagueBot-Renderer go build -o RocketLeagueBot-Renderer . # Local HTTP only ./RocketLeagueBot-Renderer -password=secret -http=:8080 # Production with HTTPS (ports 80+443 must be reachable, DNS pointed at host) sudo ./RocketLeagueBot-Renderer -password=secret -domain=example.com ``` Open the viewer at `http://localhost:8080` (or `https://example.com`). Log in with any username and the password you set. ## Flags | Flag | Default | Purpose | |----------------|--------------------|--------------------------------------------------------| | `-password` | *(required)* | Password for HTTP Basic Auth. | | `-domain` | *(empty)* | Domain for Let's Encrypt autocert. Empty → HTTP only. | | `-http` | `:80` | HTTP listen address (must be `:80` for ACME). | | `-tls` | `:443` | HTTPS listen address. | | `-udp` | `:9273` | UDP ingest address. | | `-certdir` | `./certs` | Directory for autocert cache. | | `-db` | `./recordings.db` | SQLite database file. | | `-retention` | `168h` (7 days) | How long to keep recordings. | | `-compress` | `true` | zstd-compress recorded frames. | | `-verbose` | `false` | Log every HTTP request (including WS upgrades). | | `-log-level` | `info` | `debug` / `info` / `warn` / `error`. | ## UDP payload Bytes received on the UDP port are treated as **opaque** — they are recorded verbatim (optionally compressed) and forwarded verbatim to live viewers. The viewer expects each packet to be a JSON object compatible with [RocketSimVis](https://github.com/ZealanL/RocketSimVis), specifically: ```jsonc { "ball_phys": { "pos": [x,y,z], "vel": [x,y,z], "ang_vel": [...] }, "cars": [ { "car_id": 0, "team_num": 0, "phys": { "pos": [x,y,z], "vel": [...], "forward": [...], "up": [...], "right": [...] }, "boost_amount": 0.0..1.0, "is_demoed": false, "on_ground": true, "ball_touched": false } ], "boost_pad_states": [true, false, ...] // length 34, same order as PAD_DEFS in viewer.html } ``` Coordinates are Unreal Units (1 uu = 1 cm). Field dimensions used by the viewer are the official RL values (4096 × 5120 × 2044 uu). Quick UDP smoke test (requires `nc`): ```sh echo '{"ball_phys":{"pos":[0,0,200],"vel":[0,0,0]},"cars":[]}' | nc -u -w0 127.0.0.1 9273 ``` ## Playback controls | Action | Mouse/UI | Keyboard | |-----------------|-------------------------|-----------------------| | Play / Pause | ⏸ / ▶ button | `Space` | | Scrub | Drag the seek bar | `←` / `→` (±5 s) | | Step ±1 frame | ⏮ / ⏭ buttons | `Shift+←` / `Shift+→` | | Speed down/up | Speed dropdown | `[` / `]` | | Stop playback | ✕ button or LIVE button | — | | Toggle autoplay | "Auto: ON/OFF" button | — | Speed and autoplay preferences are persisted in `localStorage`. ## API All endpoints require HTTP Basic Auth. - `GET /` — Viewer HTML. - `GET /ws` — Live WebSocket stream (text JSON). Also delivers out-of-band server events: `{"type":"event","kind":"recording_started|stopped|deleted","recording":{...}}`. - `GET /api/recordings?since=&limit=` — Newest-first JSON list of recordings. `since` is RFC3339; `limit` is optional row cap. - `DELETE /api/recordings/:id` — Delete a recording (cascades frames). - `GET /ws/playback?id=` — Playback WebSocket. Client sends: - `{"cmd":"play"}` / `{"cmd":"pause"}` / `{"cmd":"stop"}` - `{"cmd":"seek","ms":12345}` - `{"cmd":"speed","value":2.0}` Server emits: - `{"type":"playback_start","name":...,"frames":N,"duration_ms":D}` - `{"type":"playback_frame","offset_ms":T,"frame":i,"data":}` - `{"type":"playback_state","playing":bool,"speed":s,"position_ms":T}` (every ~250 ms) - `{"type":"playback_end"}` / `{"error":"..."}` - `GET /api/stats` — Process & DB observability JSON. ## Storage & tuning The recorder buffers frames in memory and flushes to SQLite either every **64 frames** or every **100 ms**, whichever comes first, in a single transaction. This collapses ~120 fsyncs/sec down to ~10/sec at typical telemetry rates. With `-compress=true` (default), each frame is zstd-encoded individually. Typical RocketSimVis JSON shrinks 5–10×. Disable with `-compress=false` if you want raw JSON in the DB. Idle detection automatically ends the current recording after **5 seconds** without packets, finalizing the row (frame count + duration). Old recordings are pruned every hour according to `-retention`. ## Security - Always run behind HTTPS in production; use `-domain` for free Let's Encrypt certs (ports 80 + 443 must be reachable). - Behind a reverse proxy: ensure it forwards `Authorization` headers and supports WebSocket upgrades on `/ws` and `/ws/playback`. Disable any buffering on those paths. ## Dependencies - Go 1.22+ (uses `log/slog`, `signal.NotifyContext`) - [Fiber](https://gofiber.io/), [websocket](https://github.com/gofiber/websocket) - [klauspost/compress](https://github.com/klauspost/compress) (zstd) - [ncruces/go-sqlite3](https://github.com/ncruces/go-sqlite3) (pure-Go SQLite) - [Three.js](https://threejs.org/) (loaded from CDN by the viewer) ## License MIT. --- **Note:** Not affiliated with Psyonix or Rocket League. For educational and personal use only.