diff --git a/cmd/client/web/game.js b/cmd/client/web/game.js index 4f522d3..4906a33 100644 --- a/cmd/client/web/game.js +++ b/cmd/client/web/game.js @@ -234,7 +234,7 @@ window.onWasmReady = function() { }; // Cache Management - Version wird bei jedem Build aktualisiert -const CACHE_VERSION = 1767553871926; // Wird durch Build-Prozess ersetzt +const CACHE_VERSION = 1767555402485; // Wird durch Build-Prozess ersetzt // Fetch mit Cache-Busting async function fetchWithCache(url) { diff --git a/cmd/client/web/index.html b/cmd/client/web/index.html index 3320ff4..076750c 100644 --- a/cmd/client/web/index.html +++ b/cmd/client/web/index.html @@ -291,7 +291,7 @@ diff --git a/cmd/server/main.go b/cmd/server/main.go index 0bbd070..9c17aaa 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -21,7 +21,8 @@ var ( playerSessions = make(map[string]*server.Room) mu sync.RWMutex globalWorld *game.World - serverID string // Eindeutige Server-ID für diesen Pod + serverID string // Eindeutige Server-ID für diesen Pod + roomRegistry *RoomRegistry // Redis-basierte Room-Registry ) func main() { @@ -46,7 +47,14 @@ func main() { if err := server.InitLeaderboard(redisAddr); err != nil { log.Fatal("❌ Konnte nicht zu Redis verbinden: ", err) } - log.Printf("✅ Verbunden mit Redis: %s", redisAddr) + log.Printf("✅ Verbunden mit Redis (Leaderboard): %s", redisAddr) + + // 1c. Room-Registry initialisieren + var err error + roomRegistry, err = NewRoomRegistry(redisAddr, serverID) + if err != nil { + log.Fatal("❌ Konnte Room-Registry nicht initialisieren: ", err) + } // 2. NATS VERBINDUNG mit Reconnect-Logik natsURL := getEnv("NATS_URL", "nats://localhost:4222") @@ -125,10 +133,25 @@ func main() { log.Printf("📥 JOIN empfangen: Name=%s, RoomID=%s (Pod: %s)", req.Name, roomID, serverID) + // Prüfe Room-Registry: Welcher Pod soll diesen Raum hosten? + claimed, assignedPod, err := roomRegistry.ClaimRoom(roomID) + if err != nil { + log.Printf("❌ Room-Registry Fehler: %v", err) + return + } + + // Wenn Raum einem anderen Pod zugewiesen ist, ignoriere den JOIN + if !claimed && assignedPod != serverID { + log.Printf("⏩ Raum '%s' gehört zu Pod %s, ignoriere JOIN auf Pod %s", roomID, assignedPod, serverID) + // Republish JOIN an den richtigen Pod (spezifischer Channel) + ec.Publish("game.join."+assignedPod, req) + return + } + mu.Lock() defer mu.Unlock() - // Raum finden oder erstellen + // Raum finden oder erstellen (nur wenn wir der Owner sind) room, exists := rooms[roomID] if !exists { log.Printf("🆕 Erstelle neuen Raum: '%s' auf Pod %s", roomID, serverID) @@ -158,7 +181,45 @@ func main() { if err != nil { log.Fatal("❌ Fehler beim Subscribe auf game.join:", err) } - log.Printf("👂 Lausche auf 'game.join' in Queue Group 'game-servers'... (Sub Valid: %v)", sub.IsValid()) + log.Printf("👂 Lausche auf 'game.join' in Queue Group 'room-handlers'... (Sub Valid: %v)", sub.IsValid()) + + // 3b. HANDLER: Pod-spezifische JOINs (für Weiterleitung) + podJoinChannel := "game.join." + serverID + _, err = ec.Subscribe(podJoinChannel, func(req *game.JoinRequest) { + playerID := req.Name + if playerID == "" { + playerID = "Unknown" + } + + roomID := req.RoomID + if roomID == "" { + roomID = "lobby" + } + + log.Printf("📥 Pod-spezifischer JOIN empfangen: Name=%s, RoomID=%s (Pod: %s)", req.Name, roomID, serverID) + + mu.Lock() + defer mu.Unlock() + + // Raum muss bereits existieren + room, exists := rooms[roomID] + if !exists { + log.Printf("⚠️ Raum '%s' existiert nicht auf Pod %s", roomID, serverID) + return + } + + // Spieler hinzufügen + room.AddPlayer(playerID, req.Name) + + // Session speichern + playerSessions[playerID] = room + log.Printf("✅ Spieler '%s' ist Raum '%s' beigetreten (weitergeleitet zu Pod: %s).", playerID, roomID, serverID) + }) + + if err != nil { + log.Fatal("❌ Fehler beim Subscribe auf "+podJoinChannel+":", err) + } + log.Printf("👂 Lausche auf pod-spezifische JOINs: %s", podJoinChannel) // 4. HANDLER: GAME START (broadcast - alle Pods empfangen, nur der mit dem Raum reagiert) _, _ = ec.Subscribe("game.start", func(req *game.StartRequest) { diff --git a/cmd/server/room_registry.go b/cmd/server/room_registry.go new file mode 100644 index 0000000..02fb080 --- /dev/null +++ b/cmd/server/room_registry.go @@ -0,0 +1,105 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/redis/go-redis/v9" +) + +// RoomRegistry verwaltet die Zuordnung von Räumen zu Pods via Redis +type RoomRegistry struct { + client *redis.Client + ctx context.Context + podID string // Eindeutige Pod-ID + ttl time.Duration +} + +// NewRoomRegistry erstellt eine neue Room-Registry +func NewRoomRegistry(redisAddr string, podID string) (*RoomRegistry, error) { + client := redis.NewClient(&redis.Options{ + Addr: redisAddr, + Password: "", // kein Password + DB: 0, // default DB + }) + + ctx := context.Background() + + // Test connection + _, err := client.Ping(ctx).Result() + if err != nil { + return nil, fmt.Errorf("Redis connection failed: %w", err) + } + + log.Printf("✅ Redis verbunden: %s", redisAddr) + + return &RoomRegistry{ + client: client, + ctx: ctx, + podID: podID, + ttl: 5 * time.Minute, // Räume verfallen nach 5 Minuten Inaktivität + }, nil +} + +// ClaimRoom versucht, einen Raum für diesen Pod zu claimen +// Gibt zurück: (claimed bool, assignedPodID string, error) +func (rr *RoomRegistry) ClaimRoom(roomID string) (bool, string, error) { + key := fmt.Sprintf("room:%s:pod", roomID) + + // Versuche den Raum zu claimen (nur wenn noch nicht existiert) + success, err := rr.client.SetNX(rr.ctx, key, rr.podID, rr.ttl).Result() + if err != nil { + return false, "", fmt.Errorf("failed to claim room: %w", err) + } + + if success { + // Erfolgreich geclaimed! + log.Printf("🏠 Raum '%s' für Pod %s geclaimed", roomID, rr.podID) + return true, rr.podID, nil + } + + // Raum existiert bereits - hole den zugewiesenen Pod + assignedPod, err := rr.client.Get(rr.ctx, key).Result() + if err == redis.Nil { + // Key existiert nicht mehr - retry + return rr.ClaimRoom(roomID) + } else if err != nil { + return false, "", fmt.Errorf("failed to get room pod: %w", err) + } + + // Raum gehört einem anderen Pod + return false, assignedPod, nil +} + +// RenewRoom erneuert die TTL eines Raums +func (rr *RoomRegistry) RenewRoom(roomID string) error { + key := fmt.Sprintf("room:%s:pod", roomID) + return rr.client.Expire(rr.ctx, key, rr.ttl).Err() +} + +// GetRoomPod gibt den Pod zurück, der einen Raum hostet +func (rr *RoomRegistry) GetRoomPod(roomID string) (string, error) { + key := fmt.Sprintf("room:%s:pod", roomID) + podID, err := rr.client.Get(rr.ctx, key).Result() + if err == redis.Nil { + return "", fmt.Errorf("room not found") + } + return podID, err +} + +// ReleaseRoom gibt einen Raum frei +func (rr *RoomRegistry) ReleaseRoom(roomID string) error { + key := fmt.Sprintf("room:%s:pod", roomID) + return rr.client.Del(rr.ctx, key).Err() +} + +// IsMyRoom prüft, ob dieser Pod der Owner eines Raums ist +func (rr *RoomRegistry) IsMyRoom(roomID string) bool { + podID, err := rr.GetRoomPod(roomID) + if err != nil { + return false + } + return podID == rr.podID +}