Private
Public Access
1
0

Add platform-specific implementations for assets, audio, WebSocket, and rendering on Desktop and WebAssembly platforms. Introduce embedded assets for WebAssembly and native file handling for Desktop. Add platform-specific chunk loading and game state synchronization.

This commit is contained in:
Sebastian Unterschütz
2026-01-04 01:25:04 +01:00
parent 85d697df19
commit 3232ee7c2f
86 changed files with 4931 additions and 486 deletions

View File

@@ -113,15 +113,33 @@ func main() {
}
})
// 6. HANDLER: LEADERBOARD REQUEST
// 6. HANDLER: LEADERBOARD REQUEST (alt, für Kompatibilität)
_, _ = ec.Subscribe("leaderboard.get", func(subject, reply string, _ *struct{}) {
top10 := server.GlobalLeaderboard.GetTop10()
log.Printf("📊 Leaderboard-Request beantwortet: %d Einträge", len(top10))
ec.Publish(reply, top10)
})
// 7. HANDLER: LEADERBOARD REQUEST (neu, für WebSocket-Gateway)
_, _ = ec.Subscribe("leaderboard.request", func(req *game.LeaderboardRequest) {
top10 := server.GlobalLeaderboard.GetTop10()
log.Printf("📊 Leaderboard-Request (Mode=%s): %d Einträge", req.Mode, len(top10))
// Response an den angegebenen Channel senden
if req.ResponseChannel != "" {
resp := game.LeaderboardResponse{
Entries: top10,
}
ec.Publish(req.ResponseChannel, &resp)
log.Printf("📤 Leaderboard-Response gesendet an %s", req.ResponseChannel)
}
})
log.Println("✅ Server bereit. Warte auf Spieler...")
// 5. WEBSOCKET-GATEWAY STARTEN (für Browser-Clients)
go StartWebSocketGateway("8080", ec)
// Block forever
select {}
}

View File

@@ -0,0 +1,228 @@
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
roomID string
send chan []byte
mutex sync.Mutex
subUpdates *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()
}
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)
c.natsConn.Publish("score.submit", &submit)
default:
log.Printf("⚠️ Unbekannter Nachrichtentyp: %s", msg.Type)
}
}
// StartWebSocketGateway startet den WebSocket-Server
func StartWebSocketGateway(port string, ec *nats.EncodedConn) {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
handleWebSocket(w, r, ec)
})
log.Printf("🌐 WebSocket-Gateway läuft auf http://localhost:%s/ws", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal("❌ WebSocket-Server Fehler: ", err)
}
}