188 lines
8.1 KiB
Markdown
188 lines
8.1 KiB
Markdown
# 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
|
||
|
||

|
||
|
||
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 <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](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=<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 `-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.
|