@@ -0,0 +1,133 @@
|
||||
// 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/fdestefano/RocketLeagueBot-Renderer/internal/hub"
|
||||
"git.destefano.cloud/fdestefano/RocketLeagueBot-Renderer/internal/recording"
|
||||
"git.destefano.cloud/fdestefano/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")
|
||||
}
|
||||
Reference in New Issue
Block a user