Files
RocketLeagueBot-Renderer/main.go
T
fdestefano 58b5a5bd9a
release / goreleaser (push) Successful in 27s
Release v1.0.0 #major
2026-06-03 00:52:41 -05:00

134 lines
3.8 KiB
Go

// RocketLeagueBot-Renderer
//
// Live UDP ingest + WebSocket fan-out + SQLite recording/playback for
// RocketSimVis-compatible state telemetry.
//
// Architecture:
//
// bot ──UDP──▶ server.ListenUDP ──▶ hub.Broadcast ──WS──▶ live viewers
// │
// └──▶ recording.Recorder (batched, zstd) ──▶ SQLite
// │
// └──▶ recording.Player ──WS──▶ playback viewers
package main
import (
"context"
"flag"
"log/slog"
"os"
"os/signal"
"strings"
"syscall"
"time"
_ "github.com/ncruces/go-sqlite3/driver"
"git.destefano.cloud/texasmade/RocketLeagueBot-Renderer/internal/hub"
"git.destefano.cloud/texasmade/RocketLeagueBot-Renderer/internal/recording"
"git.destefano.cloud/texasmade/RocketLeagueBot-Renderer/internal/server"
)
var (
httpAddr = flag.String("http", ":80", "HTTP listen address (must be :80 for autocert)")
udpAddr = flag.String("udp", ":9273", "UDP listen address (RocketSimVis port)")
tlsAddr = flag.String("tls", ":443", "HTTPS listen address with autocert")
domain = flag.String("domain", "", "Domain for autocert (e.g., example.com)")
password = flag.String("password", "", "Password to protect the viewer (required)")
certDir = flag.String("certdir", "./certs", "Directory for storing certificates")
dbPath = flag.String("db", "./recordings.db", "Path to SQLite database for recordings")
retention = flag.Duration("retention", 7*24*time.Hour, "How long to keep recordings (e.g., 72h)")
compress = flag.Bool("compress", true, "Compress recorded frames with zstd")
verbose = flag.Bool("verbose", false, "Verbose HTTP request logging (including WS upgrades)")
logLevel = flag.String("log-level", "info", "Log level: debug|info|warn|error")
)
func setupLogger() {
var level slog.Level
switch strings.ToLower(*logLevel) {
case "debug":
level = slog.LevelDebug
case "warn":
level = slog.LevelWarn
case "error":
level = slog.LevelError
default:
level = slog.LevelInfo
}
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})))
}
func main() {
flag.Parse()
setupLogger()
if *password == "" {
slog.Error("password flag is required (use -password=yourpassword)")
os.Exit(1)
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
h := hub.New()
db, err := recording.OpenDB(*dbPath)
if err != nil {
slog.Error("db open failed", "err", err)
os.Exit(1)
}
defer db.Close()
rec, err := recording.NewRecorder(db, h, *compress)
if err != nil {
slog.Error("recorder init failed", "err", err)
os.Exit(1)
}
rec.Start()
defer rec.Stop()
// Retention cleanup: once at boot, then hourly.
recording.CleanOld(db, h, *retention)
go func() {
t := time.NewTicker(time.Hour)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
recording.CleanOld(db, h, *retention)
}
}
}()
go server.ListenUDP(ctx, *udpAddr, h, rec)
app := server.New(server.Options{
Password: *password,
DBPath: *dbPath,
Verbose: *verbose,
Compress: *compress,
Retention: *retention,
DB: db,
Hub: h,
Recorder: rec,
})
serverErrCh := server.Listen(app, *httpAddr, *tlsAddr, *domain, *certDir)
select {
case <-ctx.Done():
slog.Info("shutdown signal received")
case err := <-serverErrCh:
slog.Error("server error", "err", err)
}
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
if err := app.ShutdownWithContext(shutdownCtx); err != nil {
slog.Warn("app shutdown error", "err", err)
}
slog.Info("bye")
}