package main import ( "context" "errors" "log" "os" "os/signal" "syscall" "time" companion "browser.local/plugins/scum-server-plugin/companion" ) func main() { log.SetFlags(log.Ldate | log.Ltime | log.LUTC) config, err := loadConfig() if err != nil { log.Printf("SCUM companion configuration failed: %v", err) os.Exit(1) } client, err := companion.NewClient(config, companion.Options{}) if err != nil { log.Printf("SCUM companion client failed: %v", err) os.Exit(1) } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() trajectoryStatus := &companion.TrajectoryCollectionStatus{} collector, cleanup := buildTrajectoryCollector(config, trajectoryStatus) defer cleanup() registry := companion.NewHandlerRegistry(defaultHandlerAvailability(config), companion.RuntimeAdapter{ BoundServerID: config.Component.ServerInstanceID, DiagnosticsState: map[string]string{ "trajectory": trajectoryDiagnostic(config, collector), "coordinates": "raw-world", }, }) runtime := companion.Runtime{ Client: client, Dispatcher: companion.Dispatcher{ Client: client, Registry: registry, PollLimit: 10, Backoff: 2 * time.Second, }, HeartbeatEvery: time.Duration(config.Timing.HeartbeatIntervalSeconds) * time.Second, PollEvery: time.Duration(config.Timing.CommandPollIntervalSeconds) * time.Second, Backoff: 2 * time.Second, Health: trajectoryStatus.HealthReport, } errorsCh := make(chan error, 2) go func() { errorsCh <- runtime.Run(ctx) }() if collector != nil { go func() { errorsCh <- collector.Run(ctx, trajectoryStatus) }() } err = <-errorsCh stop() if err != nil && !errors.Is(err, context.Canceled) { log.Printf("SCUM companion stopped: %v", err) os.Exit(1) } } func loadConfig() (companion.Config, error) { file, err := os.Open("config.yaml") if err != nil { return companion.Config{}, err } defer file.Close() return companion.LoadConfig(file) } func buildTrajectoryCollector(config companion.Config, status *companion.TrajectoryCollectionStatus) (*companion.TrajectoryCollector, func()) { cleanup := func() {} if !config.Trajectory.Enabled { status.Record(companion.TrajectoryCollectionReport{Status: "healthy", Reason: "trajectory collection disabled"}, nil) return nil, cleanup } source, err := companion.OpenSCUMSQLiteSourceFromEnv(config.Trajectory.FileEnv) if err != nil { status.Record(companion.TrajectoryCollectionReport{Status: "degraded", Reason: "trajectory source unavailable"}, err) return nil, cleanup } store, err := companion.OpenSCUMSQLStoreFromEnv(companion.PlatformMySQLDSNEnvironment) if err != nil { _ = source.Close() status.Record(companion.TrajectoryCollectionReport{Status: "degraded", Reason: "trajectory store unavailable"}, err) return nil, cleanup } cleanup = func() { _ = source.Close() _ = store.Close() } return companion.NewTrajectoryCollector(config, source, store), cleanup } func defaultHandlerAvailability(config companion.Config) companion.HandlerAvailability { capabilities := map[string]bool{"companion.diagnostics": true} for _, capability := range config.Capabilities { if capability == "handler.vehicle.spawn" { capabilities["vehicle.spawn"] = true } } return companion.HandlerAvailability{BoundServerID: config.Component.ServerInstanceID, Approved: true, Capabilities: capabilities} } func trajectoryDiagnostic(config companion.Config, collector *companion.TrajectoryCollector) string { if !config.Trajectory.Enabled { return "disabled" } if collector == nil { return "waiting" } return "enabled" }