package main import ( "context" "log/slog" "os" "os/signal" "syscall" "time" sdk "agones.dev/agones/sdks/go" q3c "bp99.eu/ut-agones/internal/quake3/client" ) const ( checkInterval = 5 * time.Second playersListName = "players" ) type sidecarState struct { q3 *q3c.RobustQ3Client sdk *sdk.SDK players map[string]q3c.Player } type agonesObserver struct { id string state *sidecarState ctx context.Context firstPing bool } func (o *agonesObserver) Update(event string) { slog.Debug("Got notification", "event", event) switch event { case q3c.PongEvent: if o.firstPing { slog.Debug("This is the first time gameserver was reached; reporting ready to Agones") if err := o.state.sdk.Ready(); err != nil { slog.Error("Failed to send ready signal to Agones", "error", err) } o.firstPing = false } else { slog.Debug("Sending health ping to Agones") if err := o.state.sdk.Health(); err != nil { slog.Error("Failed to send health ping to Agones", "error", err) } } } } func (o *agonesObserver) GetID() string { return o.id } func StartAgonesSidecar() error { slog.Info("Starting Urban Terror Agones sidecar") // Create context ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Create simple game client client, err := q3c.New("localhost", 27960) if err != nil { slog.Error("Failed to create new Quake3 client", "error", err) return err } // Create robust game client robustClient := q3c.NewRobust(ctx, client) // Initialize the Agones SDK sdk, err := sdk.NewSDK() if err != nil { slog.Error("Failed to initialize Agones SDK", "error", err) return err } // Set up SIGINT/SIGTERM handling sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan slog.Info("Received shutdown signal") cancel() }() // Keep state state := &sidecarState{ q3: robustClient, sdk: sdk, players: make(map[string]q3c.Player, 0), } // Create and subscribe game client event observer o := &agonesObserver{ id: "agones", state: state, ctx: ctx, firstPing: true, } robustClient.Subscribe(o) // Start status check loop go statusLoop(ctx, state) // Keep alive select { case <-ctx.Done(): slog.Info("Shutting down Urban Terror Agones sidecar") } // Graceful shutdown if err := state.q3.Stop(); err != nil { slog.Error("Failed to gracefully Quake3 client", "error", err) return err } return nil } func statusLoop(ctx context.Context, state *sidecarState) { slog.Info("Starting status check loop", "interval", checkInterval) ticker := time.NewTicker(checkInterval) defer ticker.Stop() for { select { case <-ctx.Done(): slog.Info("Exiting status check loop", "reason", "context cancelled") return case <-ticker.C: slog.Debug("Tick; Retrieving gameserver status") reportStatus(state) } } } func reportStatus(state *sidecarState) { status, err := state.q3.GetStatus() if err != nil { slog.Error("Failed to get gameserer status from Quake3 client", "error", err) } if err := updatePlayersList(state, status.Players); err != nil { slog.Error("Error while updating player list", "error", err) } } func updatePlayersList(state *sidecarState, players []q3c.Player) error { curPlayerMap := make(map[string]q3c.Player) for _, p := range players { curPlayerMap[p.Name] = p } // New and still connected players for _, p := range players { if _, exists := state.players[p.Name]; exists { slog.Debug("Player already known to be online", "player", p.Name) } else { slog.Debug("Player joined", "player", p.Name) if err := tryAppendToPlayerList(state.sdk, p.Name); err != nil { return err } state.players[p.Name] = p } } // Check for disconnected players for name := range state.players { if _, exists := curPlayerMap[name]; !exists { slog.Debug("Player disconnected", "player", name) if err := tryDeleteFromPlayerList(state.sdk, name); err != nil { return err } delete(state.players, name) } } slog.Debug("Updated players") return nil } func tryAppendToPlayerList(sdk *sdk.SDK, player string) error { exists, err := sdk.Beta().ListContains(playersListName, player) if err != nil { return err } if exists { return nil } if err := sdk.Beta().AppendListValue(playersListName, player); err != nil { return err } return nil } func tryDeleteFromPlayerList(sdk *sdk.SDK, player string) error { exists, err := sdk.Beta().ListContains(playersListName, player) if err != nil { return err } if !exists { return nil } if err := sdk.Beta().DeleteListValue(playersListName, player); err != nil { return err } return nil }