package main import ( "encoding/json" "log" "net/http" "sync" "github.com/gorilla/websocket" "github.com/nats-io/nats.go" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" ) var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true // Erlaube alle Origins für Development }, } // WebSocketMessage ist das allgemeine Format für WebSocket-Nachrichten type WebSocketMessage struct { Type string `json:"type"` // "join", "input", "start", "leaderboard_request" Payload json.RawMessage `json:"payload"` // Beliebige JSON-Daten } // WebSocketClient repräsentiert einen verbundenen WebSocket-Client type WebSocketClient struct { conn *websocket.Conn natsConn *nats.EncodedConn playerID string playerCode string roomID string send chan []byte mutex sync.Mutex subUpdates *nats.Subscription subScoreResp *nats.Subscription } // handleWebSocket verwaltet eine WebSocket-Verbindung func handleWebSocket(w http.ResponseWriter, r *http.Request, ec *nats.EncodedConn) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Printf("❌ WebSocket Upgrade Fehler: %v", err) return } client := &WebSocketClient{ conn: conn, natsConn: ec, send: make(chan []byte, 256), } log.Printf("🔌 Neuer WebSocket-Client verbunden: %s", conn.RemoteAddr()) // Goroutinen für Lesen und Schreiben starten go client.writePump() go client.readPump() } // readPump liest Nachrichten vom WebSocket-Client func (c *WebSocketClient) readPump() { defer func() { if c.subUpdates != nil { c.subUpdates.Unsubscribe() } if c.subScoreResp != nil { c.subScoreResp.Unsubscribe() } c.conn.Close() log.Printf("🔌 WebSocket-Client getrennt: %s", c.conn.RemoteAddr()) }() for { var msg WebSocketMessage err := c.conn.ReadJSON(&msg) if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Printf("⚠️ WebSocket Fehler: %v", err) } break } // Nachricht basierend auf Typ verarbeiten c.handleMessage(msg) } } // writePump sendet Nachrichten zum WebSocket-Client func (c *WebSocketClient) writePump() { defer c.conn.Close() for { message, ok := <-c.send if !ok { c.conn.WriteMessage(websocket.CloseMessage, []byte{}) return } err := c.conn.WriteMessage(websocket.TextMessage, message) if err != nil { log.Printf("⚠️ Fehler beim Senden: %v", err) return } } } // handleMessage verarbeitet eingehende Nachrichten vom Client func (c *WebSocketClient) handleMessage(msg WebSocketMessage) { switch msg.Type { case "join": var req game.JoinRequest if err := json.Unmarshal(msg.Payload, &req); err != nil { log.Printf("❌ Join-Payload ungültig: %v", err) return } c.playerID = req.Name c.roomID = req.RoomID if c.roomID == "" { c.roomID = "lobby" } log.Printf("📥 WebSocket JOIN: Name=%s, RoomID=%s", req.Name, req.RoomID) // An NATS weiterleiten c.natsConn.Publish("game.join", &req) // Auf Game-Updates für diesen Raum subscriben roomChannel := "game.update." + c.roomID sub, err := c.natsConn.Subscribe(roomChannel, func(state *game.GameState) { // GameState an WebSocket-Client senden data, _ := json.Marshal(map[string]interface{}{ "type": "game_update", "payload": state, }) select { case c.send <- data: default: log.Printf("⚠️ Send channel voll, Nachricht verworfen") } }) if err != nil { log.Printf("❌ Fehler beim Subscribe auf %s: %v", roomChannel, err) } else { c.subUpdates = sub log.Printf("👂 WebSocket-Client lauscht auf %s", roomChannel) } case "input": var input game.ClientInput if err := json.Unmarshal(msg.Payload, &input); err != nil { log.Printf("❌ Input-Payload ungültig: %v", err) return } // PlayerID setzen falls nicht vorhanden if input.PlayerID == "" { input.PlayerID = c.playerID } // An NATS weiterleiten c.natsConn.Publish("game.input", &input) case "start": var req game.StartRequest if err := json.Unmarshal(msg.Payload, &req); err != nil { log.Printf("❌ Start-Payload ungültig: %v", err) return } log.Printf("▶️ WebSocket START: RoomID=%s", req.RoomID) c.natsConn.Publish("game.start", &req) case "leaderboard_request": var req game.LeaderboardRequest if err := json.Unmarshal(msg.Payload, &req); err != nil { log.Printf("❌ Leaderboard-Request ungültig: %v", err) return } log.Printf("🏆 WebSocket Leaderboard-Request: Mode=%s", req.Mode) // Auf Leaderboard-Response subscriben (einmalig) responseChannel := "leaderboard.response." + c.playerID sub, _ := c.natsConn.Subscribe(responseChannel, func(resp *game.LeaderboardResponse) { data, _ := json.Marshal(map[string]interface{}{ "type": "leaderboard_response", "payload": resp, }) select { case c.send <- data: default: } }) // Nach 5 Sekunden unsubscriben go func() { <-make(chan struct{}) sub.Unsubscribe() }() // Request mit ResponseChannel an NATS senden req.ResponseChannel = responseChannel c.natsConn.Publish("leaderboard.request", &req) case "score_submit": var submit game.ScoreSubmission if err := json.Unmarshal(msg.Payload, &submit); err != nil { log.Printf("❌ Score-Submit ungültig: %v", err) return } log.Printf("📊 WebSocket Score-Submit: Player=%s, Score=%d", submit.PlayerCode, submit.Score) // Speichere PlayerCode und subscribe auf Response-Channel if c.playerCode == "" && submit.PlayerCode != "" { c.playerCode = submit.PlayerCode // Subscribe auf Score-Response für diesen Spieler responseChannel := "score.response." + submit.PlayerCode sub, err := c.natsConn.Subscribe(responseChannel, func(resp *game.ScoreSubmissionResponse) { // ScoreSubmissionResponse an WebSocket-Client senden data, _ := json.Marshal(map[string]interface{}{ "type": "score_response", "payload": resp, }) select { case c.send <- data: log.Printf("📤 Proof-Code an Client gesendet: %s", resp.ProofCode) default: log.Printf("⚠️ Send channel voll, Proof-Code verworfen") } }) if err != nil { log.Printf("❌ Fehler beim Subscribe auf %s: %v", responseChannel, err) } else { c.subScoreResp = sub log.Printf("👂 WebSocket-Client lauscht auf Score-Responses: %s", responseChannel) } } c.natsConn.Publish("score.submit", &submit) default: log.Printf("⚠️ Unbekannter Nachrichtentyp: %s", msg.Type) } }