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/statsendpoint 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
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
git clone <repo>
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, specifically:
{
"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):
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.sinceis RFC3339;limitis optional row cap. -
DELETE /api/recordings/:id— Delete a recording (cascades frames). -
GET /ws/playback?id=<n>— 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":<state>}{"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
-domainfor free Let's Encrypt certs (ports 80 + 443 must be reachable). - Behind a reverse proxy: ensure it forwards
Authorizationheaders and supports WebSocket upgrades on/wsand/ws/playback. Disable any buffering on those paths.
Dependencies
- Go 1.22+ (uses
log/slog,signal.NotifyContext) - Fiber, websocket
- klauspost/compress (zstd)
- ncruces/go-sqlite3 (pure-Go SQLite)
- Three.js (loaded from CDN by the viewer)
License
MIT.
Note: Not affiliated with Psyonix or Rocket League. For educational and personal use only.
