diff --git a/.gitignore b/.gitignore index 0596227..e0aa6f3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ node_modules/ # SSL/TLS Certificates /etc/letsencrypt/ certbot/ + +.DS_Store diff --git a/attributions.md b/attributions.md index 3d9544c..4628927 100644 --- a/attributions.md +++ b/attributions.md @@ -2,13 +2,12 @@ --- -## Chess Pieces - -### Standard Chess Pieces (Files: Chess_pawn_w.svg, Chess_rook_b.svg, etc.) +## Chess Pieces (Modified) * **Author:** Cburnett -* **Source:** Files obtained from Wikimedia Commons[Category:SVG chess pieces by Cburnett](https://commons.wikimedia.org/wiki/Category:SVG_chess_pieces_by_Cburnett). -* **License:** Creative Commons Attribution-Share Alike 3.0 Unported. Full license text available at: [https://creativecommons.org/licenses/by-sa/3.0/deed.en](https://creativecommons.org/licenses/by-sa/3.0/deed.en) -* **Note:** This attribution covers all chess piece SVG files included in this project's `frontend/assets/chess` folder. +* **Source:** [Wikimedia Commons, Category:SVG chess pieces by Cburnett](https://commons.wikimedia.org/wiki/Category:SVG_chess_pieces_by_Cburnett). +* **License:** [Creative Commons Attribution-Share Alike 3.0 Unported](https://creativecommons.org/licenses/by-sa/3.0/deed.en) +* **Coverage:** This attribution covers all chess piece included in the `frontend/assets/chess` folder. +* **Modification:** These files are modified to use CSS variables for color. --- diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 1151de3..8ed47b6 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -36,6 +36,7 @@ type WS struct { RoomBuffer int64 MsgBuffer int64 SendBuffer int64 + RecvBuffer int64 } func (c *DB) ConnectionStrings() (string, string) { @@ -89,6 +90,7 @@ func Load() (*AppConfig, error) { RoomBuffer: 20, MsgBuffer: 256, SendBuffer: 64, + RecvBuffer: 64, } return cfg, nil diff --git a/backend/internal/game/chess.go b/backend/internal/game/chess.go index dc9fa16..11978f2 100644 --- a/backend/internal/game/chess.go +++ b/backend/internal/game/chess.go @@ -1,7 +1,6 @@ package game import ( - "encoding/json" "fmt" "time" @@ -10,62 +9,28 @@ import ( type chessGame struct { baseGame - game *chess.Game + GameName string + game *chess.Game } func newChess() Factory { - return func(creator string, payload json.RawMessage) (Game, error) { + return func(updator func(GameUpdate)) (Game, error) { game := &chessGame{ - baseGame: newBase(2, "chess"), + baseGame: newBase(2, "chess", updator), game: chess.NewGame(), } - game.Players = []string{creator} - game.Turn = 0 + game.self = game return game, nil } } -func (g *chessGame) Move(sender string, payload json.RawMessage) (*GameState, error) { - mv, _, err := g.validateMove(sender, payload) - if err != nil { - return nil, err - } - - moveStr := rowCol2Move(mv.From) + rowCol2Move(mv.To) - if mv.Change != "" { - moveStr += mv.Change - } - notation := chess.UCINotation{} - move, err := notation.Decode(g.game.Position(), moveStr) - if err != nil { - return nil, fmt.Errorf("invalid move format %q: %w", moveStr, err) - } - if g.game.Move(move, nil) != nil { - return nil, fmt.Errorf("invalid move %q", moveStr) - } - if g.game.Outcome() != chess.NoOutcome { - g.Status = StatusFin - g.EndedAt = time.Now() - switch g.game.Outcome() { - case chess.WhiteWon: - g.Winner = g.Players[0] - case chess.BlackWon: - g.Winner = g.Players[1] - } - } - - g.Turn = 1 - g.Turn - - return g.State(), nil -} - -func (g *chessGame) State() *GameState { +func (c *chessGame) getBoardLocked() any { board := make([][]int, 8) for i := range 8 { board[i] = make([]int, 8) for j := range 8 { sq := chess.Square((7-i)*8 + j) - piece := g.game.Position().Board().Piece(sq) + piece := c.game.Position().Board().Piece(sq) if piece != chess.NoPiece { board[i][j] = pieceToCode(piece) } else { @@ -73,44 +38,68 @@ func (g *chessGame) State() *GameState { } } } - - state := g.state(board) - state.ValidMoves = g.validMoves() - return state + return board } -func (g *chessGame) Tick() (*GameState, string) { - if g.handleTimeout() { - return g.State(), TickBroadcast +func (c *chessGame) getValidMovesLocked() []GameMove { + validMoves := []GameMove{} + if c.status == StatusInProgress { + for _, mv := range c.game.ValidMoves() { + from := mv.S1() + to := mv.S2() + validMoves = append(validMoves, GameMove{ + From: Position{ + Row: 7 - int(from.Rank()), + Col: int(from.File()), + }, + To: Position{ + Row: 7 - int(to.Rank()), + Col: int(to.File()), + }, + }) + } } + return validMoves +} - if g.Status == StatusFin && time.Since(g.EndedAt) > CleanupDelay { - return nil, TickFinished - } +func (c *chessGame) Move(sender string, mv *GameMove) error { + c.mu.Lock() + defer c.mu.Unlock() - return nil, TickNoChange -} + _, err := c.checkTurnLocked(sender) + if err != nil { + return err + } -func (g *chessGame) validMoves() []GameMove { - validMoves := []GameMove{} - if g.Status != StatusInProgress { - return validMoves + moveStr := rowCol2Move(mv.From) + rowCol2Move(mv.To) + if mv.Change != "" { + moveStr += mv.Change } - for _, mv := range g.game.ValidMoves() { - from := mv.S1() - to := mv.S2() - validMoves = append(validMoves, GameMove{ - From: Position{ - Row: 7 - int(from.Rank()), - Col: int(from.File()), - }, - To: Position{ - Row: 7 - int(to.Rank()), - Col: int(to.File()), - }, - }) + notation := chess.UCINotation{} + move, err := notation.Decode(c.game.Position(), moveStr) + if err != nil { + return fmt.Errorf("invalid move format %q: %w", moveStr, err) } - return validMoves + if c.game.Move(move, nil) != nil { + return fmt.Errorf("invalid move %q", moveStr) + } + + if c.game.Outcome() != chess.NoOutcome { + c.status = StatusFin + c.endedAt = time.Now() + switch c.game.Outcome() { + case chess.WhiteWon: + c.winner = c.players[0] + case chess.BlackWon: + c.winner = c.players[1] + } + } + c.turn = 1 - c.turn + c.notify(GameUpdate{ + State: c.stateLocked(), + Action: UpdateAction, + }) + return nil } func rowCol2Move(pos Position) string { diff --git a/backend/internal/game/connect4.go b/backend/internal/game/connect4.go index b9f560a..4ccb889 100644 --- a/backend/internal/game/connect4.go +++ b/backend/internal/game/connect4.go @@ -1,39 +1,44 @@ package game import ( - "encoding/json" - "fmt" + "errors" "time" ) type connect4 struct { baseGame - board [6][7]int + GameName string + board [6][7]int } func newConnect4() Factory { - return func(creator string, payload json.RawMessage) (Game, error) { + return func(updator func(GameUpdate)) (Game, error) { game := &connect4{ - baseGame: newBase(2, "connect4"), + baseGame: newBase(2, "connect4", updator), board: [6][7]int{}, } - game.Players = []string{creator} - game.Turn = 0 + game.self = game return game, nil } } -func (c *connect4) Move(sender string, payload json.RawMessage) (*GameState, error) { - mv, idx, err := c.validateMove(sender, payload) +func (c *connect4) getBoardLocked() any { + return c.board +} + +func (c *connect4) Move(sender string, mv *GameMove) error { + c.mu.Lock() + defer c.mu.Unlock() + + idx, err := c.checkTurnLocked(sender) if err != nil { - return nil, err + return err } if mv.To.Col < 0 || mv.To.Col > 6 { - return nil, fmt.Errorf("invalid move") + return errors.New("invalid move") } - // Find the first empty row in the column var droppedRow int = -1 for row := 5; row >= 0; row-- { if c.board[row][mv.To.Col] == 0 { @@ -44,24 +49,23 @@ func (c *connect4) Move(sender string, payload json.RawMessage) (*GameState, err } if droppedRow == -1 { - return nil, fmt.Errorf("invalid move") + return errors.New("invalid move") } if win := c.checkWinner(droppedRow, mv.To.Col); win != 0 { - c.Status = StatusFin - c.Winner = c.Players[win-1] - c.EndedAt = time.Now() + c.status = StatusFin + c.winner = c.players[win-1] + c.endedAt = time.Now() } else if c.checkDraw() { - c.Status = StatusFin - c.EndedAt = time.Now() + c.status = StatusFin + c.endedAt = time.Now() } - - c.Turn = 1 - c.Turn - return c.State(), nil -} - -func (c *connect4) State() *GameState { - return c.state(c.board) + c.turn = 1 - c.turn + c.notify(GameUpdate{ + State: c.stateLocked(), + Action: UpdateAction, + }) + return nil } func (c *connect4) checkWinner(startRow, startCol int) int { @@ -105,13 +109,3 @@ func (c *connect4) checkDraw() bool { } return true } - -func (c *connect4) Tick() (*GameState, string) { - if c.handleTimeout() { - return c.State(), TickFinished - } - if !c.EndedAt.IsZero() && time.Since(c.EndedAt) > CleanupDelay { - return c.State(), TickFinished - } - return nil, TickNoChange -} diff --git a/backend/internal/game/factory.go b/backend/internal/game/factory.go index a2524ca..fc63e5f 100644 --- a/backend/internal/game/factory.go +++ b/backend/internal/game/factory.go @@ -1,13 +1,11 @@ package game import ( - "encoding/json" "fmt" ) -type Factory func(creator string, payload json.RawMessage) (Game, error) +type Factory func(updator func(GameUpdate)) (Game, error) -// just in case we need payload for alternative modes type GameInfo struct { Factory Factory } @@ -32,10 +30,10 @@ func (r *Registry) RegisterAll() { r.register("chess", newChess()) } -func (r *Registry) Create(name, creator string, payload json.RawMessage) (Game, error) { +func (r *Registry) Create(name string, updator func(GameUpdate)) (Game, error) { info, ok := r.games[name] if !ok { return nil, fmt.Errorf("game type not supported: %s", name) } - return info.Factory(creator, payload) + return info.Factory(updator) } diff --git a/backend/internal/game/game.go b/backend/internal/game/game.go index a05bc2e..a44c98e 100644 --- a/backend/internal/game/game.go +++ b/backend/internal/game/game.go @@ -1,32 +1,58 @@ package game import ( - "encoding/json" - "fmt" + "context" + "errors" "slices" + "sync" "time" ) +type GameAction int + const ( StatusWaiting = "waiting" StatusInProgress = "in_progress" StatusFin = "finished" StatusDisconnected = "disconnected" - TickFinished = "finished" - TickNoChange = "no_change" - TickBroadcast = "broadcast" - DisconnectTimeout = 30 * time.Second CleanupDelay = 10 * time.Second + TickInterval = 1 * time.Second + + UpdateAction GameAction = iota + DeleteAction ) +type Position struct { + Row int `json:"row"` + Col int `json:"col"` +} + +type GameMove struct { + From Position `json:"from"` + To Position `json:"to"` + Change string `json:"change,omitempty"` +} + type Game interface { - Join(sender string) error - Move(sender string, payload json.RawMessage) (*GameState, error) - State() *GameState - Leave(player string, intentional bool) bool - Tick() (*GameState, string) + Join(player string) error + Rejoin(player string) + Leave(player string, intentional bool) + Move(player string, mv *GameMove) error + Start() + Stop() + + GetState() *GameState + getBoardLocked() any + getValidMovesLocked() []GameMove + updateLoop() + handleDisconnects() +} + +type GameUpdate struct { + State *GameState + Action GameAction } type GameState struct { @@ -39,140 +65,219 @@ type GameState struct { ValidMoves []GameMove `json:"validMoves"` } -type Position struct { - Row int `json:"row"` - Col int `json:"col"` -} +type baseGame struct { + self Game + mu sync.RWMutex + gameName string + players []string + turn int + numPlayers int + status string + disconnects map[string]time.Time + notify func(GameUpdate) -type GameMove struct { - From Position `json:"from"` - To Position `json:"to"` - Change string `json:"change,omitempty"` -} + winner string + endedAt time.Time -type baseGame struct { - GameName string - Players []string - Turn int - NumPlayers int - Status string - Winner string - Disconnects map[string]time.Time - EndedAt time.Time + ctx context.Context + cancel context.CancelFunc } -func newBase(numPlayers int, gameName string) baseGame { +func newBase(numPlayers int, gameName string, updator func(GameUpdate)) baseGame { + ctx, cancel := context.WithCancel(context.Background()) return baseGame{ - GameName: gameName, - Players: make([]string, 0, numPlayers), - Turn: 0, - NumPlayers: numPlayers, - Status: StatusWaiting, - Disconnects: make(map[string]time.Time), + gameName: gameName, + players: make([]string, 0, numPlayers), + turn: 0, + numPlayers: numPlayers, + status: StatusWaiting, + disconnects: make(map[string]time.Time), + notify: updator, + ctx: ctx, + cancel: cancel, } } -func (g *baseGame) Join(player string) error { - if g.Status == StatusDisconnected { - if _, ok := g.Disconnects[player]; ok { - delete(g.Disconnects, player) - if len(g.Disconnects) == 0 { - g.Status = StatusInProgress - } - return nil - } - return fmt.Errorf("game is paused, waiting for player(s) to reconnect") - } +func (b *baseGame) Start() { + go b.ticker() +} - if len(g.Players) >= g.NumPlayers { - return fmt.Errorf("game is full") +func (b *baseGame) Stop() { + b.cancel() +} + +func (b *baseGame) Join(player string) error { + b.mu.Lock() + defer b.mu.Unlock() + + if len(b.players) >= b.numPlayers { + return errors.New("game is full") } - if slices.Contains(g.Players, player) { - return fmt.Errorf("player already joined") + if slices.Contains(b.players, player) { + return errors.New("already joined") } - - g.Players = append(g.Players, player) - if len(g.Players) == g.NumPlayers { - g.Status = StatusInProgress + b.players = append(b.players, player) + if len(b.players) == b.numPlayers { + b.status = StatusInProgress } + b.notify(GameUpdate{ + State: b.stateLocked(), + Action: UpdateAction, + }) return nil } -func (g *baseGame) Leave(player string, intentional bool) bool { - idx := slices.Index(g.Players, player) - if idx == -1 { - return false - } +func (b *baseGame) Rejoin(player string) { + b.mu.Lock() + defer b.mu.Unlock() - if !intentional && (g.Status == StatusInProgress || g.Status == StatusDisconnected) { - g.Status = StatusDisconnected - g.Disconnects[player] = time.Now() - return false + if _, ok := b.disconnects[player]; ok { + delete(b.disconnects, player) + if len(b.disconnects) == 0 { + b.status = StatusInProgress + } + b.notify(GameUpdate{ + State: b.stateLocked(), + Action: UpdateAction, + }) } - - g.Players = slices.Delete(g.Players, idx, idx+1) - return len(g.Players) == 0 } -func (g *baseGame) handleTimeout() bool { - if g.Status == StatusDisconnected { - var timedOutPlayer string - for player, disconnectedAt := range g.Disconnects { - if time.Since(disconnectedAt) > DisconnectTimeout { - timedOutPlayer = player - break - } - } - if timedOutPlayer != "" { - g.Status = StatusFin - if len(g.Players) == 2 { - winnerIdx := 1 - slices.Index(g.Players, timedOutPlayer) - if winnerIdx >= 0 && winnerIdx < len(g.Players) { - g.Winner = g.Players[winnerIdx] - } - } - g.EndedAt = time.Now() - return true - } +func (b *baseGame) Leave(player string, intentional bool) { + b.mu.Lock() + defer b.mu.Unlock() + + idx := slices.Index(b.players, player) + if idx == -1 { + return + } + if !intentional && (b.status == StatusInProgress || b.status == StatusDisconnected) { + b.status = StatusDisconnected + b.disconnects[player] = time.Now() + } else { + b.players = slices.Delete(b.players, idx, idx+1) } - if (g.Status == StatusFin) && g.EndedAt.IsZero() { - g.EndedAt = time.Now() + if len(b.players) == 0 { + b.status = StatusFin + b.endedAt = time.Now() + b.notify(GameUpdate{ + State: b.stateLocked(), + Action: DeleteAction, + }) + b.Stop() + return } - return false + b.notify(GameUpdate{ + State: b.stateLocked(), + Action: UpdateAction, + }) } -func (g *baseGame) validateMove(sender string, payload json.RawMessage) (*GameMove, int, error) { - if g.Status != StatusInProgress { - return nil, -1, fmt.Errorf("game not in progress") - } - var mv GameMove - if err := json.Unmarshal(payload, &mv); err != nil { - return nil, -1, fmt.Errorf("invalid move payload: %w", err) +func (b *baseGame) checkTurnLocked(sender string) (int, error) { + if b.status != StatusInProgress { + return -1, errors.New("game not in progress") } idx := -1 - for i, p := range g.Players { + for i, p := range b.players { if p == sender { idx = i break } } if idx == -1 { - return nil, -1, fmt.Errorf("player not in game") + return -1, errors.New("player not in game") } - if idx != g.Turn { - return nil, -1, fmt.Errorf("not your turn") + if idx != b.turn { + return -1, errors.New("not your turn") } - return &mv, idx, nil + return idx, nil +} + +func (b *baseGame) getValidMovesLocked() []GameMove { + return nil } -func (g *baseGame) state(board any) *GameState { +func (b *baseGame) GetState() *GameState { + b.mu.RLock() + defer b.mu.RUnlock() + + return b.stateLocked() +} + +func (b *baseGame) stateLocked() *GameState { return &GameState{ - GameName: g.GameName, - Players: g.Players, - Turn: g.Turn, - Board: board, - Status: g.Status, - Winner: g.Winner, + GameName: b.gameName, + Players: b.players, + Turn: b.turn, + Board: b.self.getBoardLocked(), + ValidMoves: b.self.getValidMovesLocked(), + Status: b.status, + Winner: b.winner, + } +} + +func (b *baseGame) ticker() { + ticker := time.NewTicker(TickInterval) + defer ticker.Stop() + + for { + select { + case <-b.ctx.Done(): + return + case <-ticker.C: + b.self.updateLoop() + } + } +} + +func (b *baseGame) updateLoop() { + b.mu.Lock() + defer b.mu.Unlock() + + if b.status == StatusFin { + if time.Since(b.endedAt) > CleanupDelay { + b.notify(GameUpdate{ + State: b.stateLocked(), + Action: DeleteAction, + }) + b.Stop() + } + return + } + + disconnections := []string{} + for player, disconnectTime := range b.disconnects { + if time.Since(disconnectTime) > DisconnectTimeout { + disconnections = append(disconnections, player) + } + } + if len(disconnections) > 0 { + for _, player := range disconnections { + delete(b.disconnects, player) + idx := slices.Index(b.players, player) + if idx != -1 { + b.players = slices.Delete(b.players, idx, idx+1) + } + } + b.self.handleDisconnects() + } +} + +func (b *baseGame) handleDisconnects() { + // just stop game if player doesn't come back. + b.status = StatusFin + b.endedAt = time.Now() + if len(b.players) == 0 { + b.notify(GameUpdate{ + State: b.stateLocked(), + Action: DeleteAction, + }) + b.Stop() + } else { + b.notify(GameUpdate{ + State: b.stateLocked(), + Action: UpdateAction, + }) } } diff --git a/backend/internal/game/tictactoe.go b/backend/internal/game/tictactoe.go index 4534afc..0db5f13 100644 --- a/backend/internal/game/tictactoe.go +++ b/backend/internal/game/tictactoe.go @@ -1,7 +1,6 @@ package game import ( - "encoding/json" "fmt" "time" ) @@ -12,46 +11,52 @@ type ticTacToe struct { } func newTicTacToe() Factory { - return func(creator string, payload json.RawMessage) (Game, error) { + return func(updator func(GameUpdate)) (Game, error) { game := &ticTacToe{ - baseGame: newBase(2, "tictactoe"), + baseGame: newBase(2, "tictactoe", updator), board: [3][3]int{}, } - game.Players = []string{creator} - game.Turn = 0 + game.self = game return game, nil } } -func (t *ticTacToe) Move(sender string, payload json.RawMessage) (*GameState, error) { - mv, idx, err := t.validateMove(sender, payload) +func (t *ticTacToe) getBoardLocked() any { + return t.board +} + +func (t *ticTacToe) Move(sender string, mv *GameMove) error { + t.mu.Lock() + defer t.mu.Unlock() + + idx, err := t.checkTurnLocked(sender) if err != nil { - return nil, err + return err } if mv.To.Row < 0 || mv.To.Row > 2 || mv.To.Col < 0 || mv.To.Col > 2 { - return nil, fmt.Errorf("invalid move") + return fmt.Errorf("invalid move") } if t.board[mv.To.Row][mv.To.Col] != 0 { - return nil, fmt.Errorf("cell already taken") + return fmt.Errorf("cell already taken") } t.board[mv.To.Row][mv.To.Col] = idx + 1 if win := t.checkWin(); win != 0 { - t.Status = StatusFin - t.Winner = t.Players[win-1] - t.EndedAt = time.Now() + t.status = StatusFin + t.winner = t.players[win-1] + t.endedAt = time.Now() } else if t.checkDraw() { - t.Status = StatusFin - t.EndedAt = time.Now() + t.status = StatusFin + t.endedAt = time.Now() } - t.Turn = 1 - t.Turn - return t.State(), nil -} - -func (t *ticTacToe) State() *GameState { - return t.state(t.board) + t.turn = 1 - t.turn + t.notify(GameUpdate{ + State: t.stateLocked(), + Action: UpdateAction, + }) + return nil } func (t *ticTacToe) checkWin() int { @@ -88,13 +93,3 @@ func (t *ticTacToe) checkDraw() bool { } return true } - -func (t *ticTacToe) Tick() (*GameState, string) { - if t.handleTimeout() { - return t.State(), TickFinished - } - if !t.EndedAt.IsZero() && time.Since(t.EndedAt) > CleanupDelay { - return t.State(), TickFinished - } - return nil, TickNoChange -} diff --git a/backend/internal/live/client.go b/backend/internal/live/client.go index 19d2cc8..31879fc 100644 --- a/backend/internal/live/client.go +++ b/backend/internal/live/client.go @@ -1,4 +1,3 @@ -// room/client.go package live import ( @@ -18,6 +17,7 @@ type client struct { hub *hub conn *websocket.Conn send chan []byte + recv chan []byte room *room token *token.UserPayload ctx context.Context @@ -32,6 +32,7 @@ func newClient(h *hub, conn *websocket.Conn, token *token.UserPayload, cfg *conf hub: h, conn: conn, send: make(chan []byte, cfg.SendBuffer), + recv: make(chan []byte, cfg.RecvBuffer), token: token, ctx: ctx, cancel: cancel, @@ -43,12 +44,20 @@ func (c *client) start() { go c.writePump() go c.readPump() go c.pingPump() + + go c.processPump() +} + +func (c *client) stop() { + c.cancel() + c.hub.unregister <- c + close(c.send) + close(c.recv) + c.conn.Close(websocket.StatusNormalClosure, "client leaving") } func (c *client) readPump() { - defer func() { - c.hub.unregister <- c - }() + defer c.stop() for { msgType, msgRaw, err := c.conn.Read(c.ctx) @@ -61,59 +70,22 @@ func (c *client) readPump() { } if msgType != websocket.MessageText { slog.Error("readPump: unhandled message type", "msgType", msgType.String()) - c.send <- createMsg(msgError, "message", "Invalid message type") + c.trySend(sendMessage(msgError, "Invalid message type")) continue } if len(msgRaw) > int(c.cfg.MaxMsgSize) { - c.send <- createMsg(msgError, "message", "Message too large.") + c.trySend(sendMessage(msgError, "Message too large.")) continue } - var msg roomMsg - if err := json.Unmarshal(msgRaw, &msg); err != nil { - c.send <- createMsg(msgError, "message", "Invalid message format: "+err.Error()) - continue - } - - msg.Sender = c.ID - msg.Client = c - - switch msg.Type { - case msgChat, msgVidSignal, msgRawSignal: - msg.Sender = c.ID - jsonMsg, err := json.Marshal(msg) - if err != nil { - c.send <- createMsg(msgError, "message", "Failed to marshal message: "+err.Error()) - continue - } - c.room.broadcast(jsonMsg) - case msgGameState: - msg.Sender = c.ID - if errStr := c.room.handleGameState(&msg); errStr != "" { - c.send <- createMsg(msgError, "message", "Failed to handle game state: "+errStr) - } - - //Hub actions - case msgJoinRoom: - var payloadMap map[string]any - if err := json.Unmarshal(msg.Payload, &payloadMap); err == nil { - if roomID, ok := payloadMap["roomName"].(string); ok && roomID != "" { - c.hub.joinRoom <- &crPair{Client: c, RoomName: roomID} - } else { - c.send <- createMsg(msgError, "message", "invalid format: missing roomName") - } - } else { - c.send <- createMsg(msgError, "message", "invalid format for join room") - } - case msgLeaveRoom: - c.hub.leaveRoom <- c - case msgGetClients: - if c.room != nil { - c.send <- c.room.getClientList() - } + select { + case <-c.ctx.Done(): + slog.Info("readPump: Context cancelled during send to recv channel", "client", c.ID) + return + case c.recv <- msgRaw: default: - slog.Warn("readPump: Unknown message type received", "type", msg.Type, "client", c.ID) - c.send <- createMsg(msgError, "message", "Unknown message type: "+msg.Type) + slog.Error("readPump: Client recv queue full", "client", c.ID) + c.trySend(sendMessage(msgError, "Server busy. Please try again later.")) } } } @@ -125,7 +97,6 @@ func (c *client) writePump() { return case message, ok := <-c.send: if !ok { - c.cancel() return } writeCtx, cancelWrite := context.WithTimeout(c.ctx, c.cfg.WriteTimeout) @@ -160,3 +131,57 @@ func (c *client) pingPump() { } } } + +func (c *client) processPump() { + for { + select { + case <-c.ctx.Done(): + return + case msgRaw := <-c.recv: + var msg roomMsg + if err := json.Unmarshal(msgRaw, &msg); err != nil { + c.trySend(sendMessage(msgError, "Invalid message format: "+err.Error())) + continue + } + + msg.Sender = c.ID + msg.Client = c + switch msg.Type { + case msgChat, msgVidSignal, msgRawSignal: + c.room.handleRelay(&msg) + case msgGameState: + var payload GameMessagePayload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + c.trySend(sendMessage(msgError, "Invalid payload format: "+err.Error())) + continue + } + c.room.handleGameState(c, &payload) + + case msgJoinRoom: + var payload JoinRoomPayload + if err := json.Unmarshal(msg.Payload, &payload); err == nil { + if roomID := payload.RoomName; roomID != "" { + c.hub.joinRoom <- &crPair{Client: c, RoomName: roomID} + } else { + c.trySend(sendMessage(msgError, "invalid format: missing roomName")) + } + } else { + c.trySend(sendMessage(msgError, "invalid format for join room")) + } + case msgLeaveRoom: + c.hub.leaveRoom <- c + default: + slog.Warn("processPump: Unknown message type received", "type", msg.Type, "client", c.ID) + c.trySend(sendMessage(msgError, "Unknown message type: "+msg.Type)) + } + } + } +} + +func (c *client) trySend(msg []byte) { + select { + case c.send <- msg: + default: + slog.Warn("trySend: Client send queue full", "client", c.ID) + } +} diff --git a/backend/internal/live/hub.go b/backend/internal/live/hub.go index 7112a61..eb65210 100644 --- a/backend/internal/live/hub.go +++ b/backend/internal/live/hub.go @@ -4,9 +4,6 @@ import ( "letsgo/internal/config" "letsgo/internal/game" "log/slog" - "sync" - - "github.com/coder/websocket" ) type hub struct { @@ -19,8 +16,6 @@ type hub struct { unregister chan *client joinRoom chan *crPair leaveRoom chan *client - - mu sync.RWMutex } func newhub(registry *game.Registry, cfg *config.WS) *hub { @@ -43,70 +38,52 @@ func (h *hub) run() { for { select { case client := <-h.register: - h.mu.Lock() h.clients[client] = struct{}{} lobby.addClient(client) - h.mu.Unlock() + client.start() slog.Debug("Registered: ", "client", client.ID) case client := <-h.unregister: - h.mu.Lock() - if client.room != nil { - client.room.removeClient(client) - if client.room != lobby && len(client.room.clients) == 0 { - delete(h.rooms, client.room.name) - } + client.room.removeClient(client) + if client.room != lobby && len(client.room.clients) == 0 { + delete(h.rooms, client.room.name) } if _, ok := h.clients[client]; ok { delete(h.clients, client) - close(client.send) - client.cancel() - client.conn.Close(websocket.StatusNormalClosure, "client leaving") slog.Debug("Unregistered: ", "client", client.ID) } - h.mu.Unlock() case pair := <-h.joinRoom: slog.Debug("join room") - h.mu.Lock() client := pair.Client - roomID := pair.RoomName - if client.room != nil && client.room.name == roomID { - h.mu.Unlock() + roomName := pair.RoomName + if client.room.name == roomName { continue } - if client.room != nil { - oldRoom := client.room - oldRoom.removeClient(client) - if oldRoom != lobby && len(oldRoom.clients) == 0 { - delete(h.rooms, oldRoom.name) - } + oldRoom := client.room + oldRoom.removeClient(client) + if oldRoom != lobby && len(oldRoom.clients) == 0 { + delete(h.rooms, oldRoom.name) } - room, ok := h.rooms[roomID] + room, ok := h.rooms[roomName] if !ok { - room = newRoom(roomID, h.registry) + room = newRoom(roomName, h.registry) h.rooms[room.name] = room } room.addClient(client) - h.mu.Unlock() - slog.Debug("Client joined room successfully.", "client", client.ID, "roomID", roomID) + slog.Debug("Client joined room successfully.", "client", client.ID, "roomID", roomName) case client := <-h.leaveRoom: - h.mu.Lock() - if client.room != nil { - room := client.room - if room == lobby { - h.mu.Unlock() - continue - } - room.removeClient(client) - if len(room.clients) == 0 { - delete(h.rooms, room.name) - } - slog.Debug("Client left room.", "client", client.ID, "roomID", room.name) - lobby.addClient(client) + room := client.room + if room == lobby { + continue } - h.mu.Unlock() + room.removeClient(client) + if len(room.clients) == 0 { + delete(h.rooms, room.name) + } + slog.Debug("Client left room.", "client", client.ID, "roomID", room.name) + lobby.addClient(client) } } } diff --git a/backend/internal/live/message.go b/backend/internal/live/message.go index 761f3a9..b16631a 100644 --- a/backend/internal/live/message.go +++ b/backend/internal/live/message.go @@ -2,7 +2,9 @@ package live import ( "encoding/json" + "errors" "letsgo/internal/game" + "log/slog" ) const ( @@ -31,25 +33,48 @@ type GameMessagePayload struct { Move *game.GameMove `json:"move,omitempty"` } +type JoinRoomPayload struct { + RoomName string `json:"roomName"` +} + type crPair struct { Client *client RoomName string } -func createMsg(msgType, key, msg string) []byte { - payload := map[string]string{key: msg} - payloadBytes, err := json.Marshal(payload) +func internalError(err error) []byte { + slog.Error("internalError", "error", err) + return []byte(`{"type":"error","sender":"_server","payload":{"message":"Internal server error"}}`) +} + +func sendMessage(msgType, msg string) []byte { + return sendKeyVal(msgType, "message", msg) +} + +func sendKeyVal(msgType string, keyVal ...any) []byte { + if len(keyVal)%2 != 0 { + return internalError(errors.New("odd number of arguments")) + } + payloadMap := make(map[string]any) + for i := 0; i < len(keyVal); i += 2 { + payloadMap[keyVal[i].(string)] = keyVal[i+1] + } + payloadBytes, err := json.Marshal(payloadMap) if err != nil { - return []byte(`{"type":"error","payload":{"message":"Internal server error"}}`) + return internalError(err) } - statusMsg := &roomMsg{ + return sendBytes(msgType, payloadBytes) +} + +func sendBytes(msgType string, bytes []byte) []byte { + msg := &roomMsg{ Type: msgType, Sender: "_server", - Payload: json.RawMessage(payloadBytes), + Payload: bytes, } - jsonMessage, err := json.Marshal(statusMsg) + jsonMessage, err := json.Marshal(msg) if err != nil { - return []byte(`{"type":"error","payload":{"message":"Internal server error"}}`) + return internalError(err) } return jsonMessage } diff --git a/backend/internal/live/room.go b/backend/internal/live/room.go index 5388a1c..1c346cd 100644 --- a/backend/internal/live/room.go +++ b/backend/internal/live/room.go @@ -1,61 +1,29 @@ -// room/room.go package live import ( "encoding/json" - "log/slog" "sync" - "time" "letsgo/internal/game" ) -type room struct { - registry *game.Registry - name string - clients map[*client]struct{} - mu sync.RWMutex - game game.Game - done chan struct{} -} +const ( + errorMsg = `{"type":"error","sender":"_server","payload":"Game state corrupted, resetting..."}` + cleanStateMsg = `{"type":"game_state","sender":"_server","payload":{"gameName":"","status":"waiting","players":[],"turn":0,"board":[[]],"winner":"","validMoves":[]}}` +) -func newRoom(name string, registry *game.Registry) *room { - r := &room{ - name: name, - clients: make(map[*client]struct{}), - mu: sync.RWMutex{}, - registry: registry, - done: make(chan struct{}), +func (r *room) broadcastLocked(msg []byte) { + for client := range r.clients { + client.trySend(msg) } - go r.run() - return r } -func (r *room) run() { - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - r.mu.Lock() - if r.game != nil { - _, status := r.game.Tick() - switch status { - case game.TickBroadcast: - r.broadcast(r.gameStateMsg()) - case game.TickFinished: - r.game = nil - r.broadcast(r.gameStateMsg()) - case game.TickNoChange: - default: - } - } - r.mu.Unlock() - case <-r.done: - return - } +func (r *room) clientListMsgLocked() [][2]string { + clientList := make([][2]string, 0, len(r.clients)) + for client := range r.clients { + clientList = append(clientList, [2]string{client.ID, client.token.Displayname}) } + return clientList } func (r *room) addClient(client *client) { @@ -64,10 +32,28 @@ func (r *room) addClient(client *client) { r.clients[client] = struct{}{} client.room = r - client.send <- createMsg(msgJoinRoom, "roomName", r.name) + client.trySend(sendKeyVal(msgJoinRoom, "roomName", r.name)) - r.broadcast(createMsg(msgStatus, "message", client.token.Displayname+" has joined "+r.name)) - r.broadcast(r.getClientList()) + r.broadcastLocked(sendMessage(msgStatus, client.token.Displayname+" has joined "+r.name)) + r.broadcastLocked(sendKeyVal(msgGetClients, + "clients", r.clientListMsgLocked())) +} + +type room struct { + registry *game.Registry + name string + clients map[*client]struct{} + mu sync.RWMutex + game game.Game +} + +func newRoom(name string, registry *game.Registry) *room { + return &room{ + name: name, + clients: make(map[*client]struct{}), + mu: sync.RWMutex{}, + registry: registry, + } } func (r *room) removeClient(client *client) { @@ -76,151 +62,99 @@ func (r *room) removeClient(client *client) { if _, ok := r.clients[client]; ok { if r.game != nil { - if r.game.Leave(client.ID, false) { - r.game = nil - } + r.game.Leave(client.ID, false) } delete(r.clients, client) - r.broadcast(createMsg(msgStatus, "message", client.token.Displayname+" has left "+r.name)) - r.broadcast(r.getClientList()) - } -} - -func (r *room) broadcast(msg []byte) { - for client := range r.clients { - select { - case client.send <- msg: - default: - slog.Warn("Invalid client.", "client", client.ID, "roomID", r.name) - } + r.broadcastLocked(sendMessage(msgStatus, client.token.Displayname+" has left "+r.name)) + r.broadcastLocked(sendKeyVal(msgGetClients, + "clients", r.clientListMsgLocked())) } } -func (r *room) getClientList() []byte { - clientNames := make([]string, 0, len(r.clients)) - for client := range r.clients { - clientNames = append(clientNames, client.token.Displayname) - } - payload := map[string]any{ - "roomName": r.name, - "clients": clientNames, - } - payloadBytes, _ := json.Marshal(payload) - msg := &roomMsg{ - Type: msgGetClients, - Payload: json.RawMessage(payloadBytes), - } - jsonMessage, err := json.Marshal(msg) +func (r *room) handleRelay(msg *roomMsg) { + jsonMsg, err := json.Marshal(msg) if err != nil { - slog.Error("Error marshaling client list message.", "error", err) - return nil + msg.Client.trySend(internalError(err)) + return } - return jsonMessage + r.mu.RLock() + r.broadcastLocked(jsonMsg) + r.mu.RUnlock() } -func (r *room) handleGameState(msg *roomMsg) string { - r.mu.Lock() - defer r.mu.Unlock() - - var payload GameMessagePayload - if err := json.Unmarshal(msg.Payload, &payload); err != nil { - return "invalid payload: " + err.Error() +func (r *room) handleGameUpdate(update game.GameUpdate) { + switch update.Action { + case game.UpdateAction: + r.broadcastLocked(r.sendGameState(update.State)) + case game.DeleteAction: + r.broadcastLocked([]byte(cleanStateMsg)) + r.game = nil } +} - sender := msg.Sender - +func (r *room) handleGameState(client *client, payload *GameMessagePayload) { switch payload.Action { case "get": - msg.Client.send <- r.gameStateMsg() - return "" - case "create": - if r.game != nil { - return "Game already exists in this room" - } - newGame, err := r.registry.Create(payload.GameName, sender, nil) - if err != nil { - return err.Error() - } - r.game = newGame - r.broadcast(r.gameStateMsg()) - return "" - case "join": if r.game == nil { - return "No game to join" - } - err := r.game.Join(sender) - if err != nil { - return err.Error() + client.trySend([]byte(cleanStateMsg)) + } else { + client.trySend(r.sendGameState(r.game.GetState())) } - r.broadcast(r.gameStateMsg()) - return "" - case "move": + case "create": + r.mu.Lock() + defer r.mu.Unlock() if r.game == nil { - return "No game in progress" - } - if payload.Move == nil { - return "missing move payload" + newGame, err := r.registry.Create(payload.GameName, r.handleGameUpdate) + if err != nil { + client.trySend(sendMessage(msgError, "Invalid game name: "+payload.GameName)) + return + } + r.game = newGame + r.game.Join(client.ID) + r.game.Start() } - b, err := json.Marshal(payload.Move) - if err != nil { - return "invalid move payload: " + err.Error() + case "join": + r.mu.RLock() + defer r.mu.RUnlock() + if r.game != nil { + if err := r.game.Join(client.ID); err != nil { + client.trySend(sendMessage(msgError, err.Error())) + } } - _, err = r.game.Move(sender, b) - if err != nil { - return err.Error() + case "move": + r.mu.RLock() + defer r.mu.RUnlock() + if r.game != nil { + if err := r.game.Move(client.ID, payload.Move); err != nil { + client.trySend(sendMessage(msgError, err.Error())) + } } - r.broadcast(r.gameStateMsg()) - return "" case "leave": - if r.game == nil { - return "No game in progress" - } - if r.game.Leave(sender, true) { - r.game = nil + r.mu.RLock() + defer r.mu.RUnlock() + if r.game != nil { + r.game.Leave(client.ID, true) } - r.broadcast(r.gameStateMsg()) - return "" default: - return "unknown action: " + payload.Action + client.trySend(sendMessage(msgError, "unknown action: "+payload.Action)) } } -func (r *room) gameStateMsg() []byte { - var state *game.GameState - - if r.game != nil { - state = r.game.State() - } else { - state = &game.GameState{ - Status: game.StatusWaiting, - Players: []string{}, - } - } +func (r *room) sendGameState(state *game.GameState) []byte { stateBytes, err := json.Marshal(state) if err != nil { return r.panicReset() } - - msg := &roomMsg{ - Type: msgGameState, - Sender: "_server", - Payload: json.RawMessage(stateBytes), - } - jsonMsg, err := json.Marshal(msg) - if err != nil { - return r.panicReset() - } - return jsonMsg + return sendBytes(msgGameState, stateBytes) } func (r *room) panicReset() []byte { - r.game = nil - - errorMsg := []byte(`{"type":"error","sender":"_server","payload":"Game state corrupted, resetting..."}`) - cleanStateMsg := []byte(`{"type":"game_state","sender":"_server","payload":"{\"status\":\"waiting\",\"players\":[],\"turn\":0,\"board\":null,\"winner\":\"\"}"}`) + r.mu.Lock() + defer r.mu.Unlock() - r.broadcast(errorMsg) - r.broadcast(cleanStateMsg) - return cleanStateMsg + r.game = nil + r.broadcastLocked([]byte(errorMsg)) + r.broadcastLocked([]byte(cleanStateMsg)) + return []byte(cleanStateMsg) } diff --git a/backend/internal/live/router.go b/backend/internal/live/router.go index 4d0696c..855e66f 100644 --- a/backend/internal/live/router.go +++ b/backend/internal/live/router.go @@ -26,9 +26,7 @@ func Router(authMdw mdw.Middleware, ctxKey mdw.ContextKey, registry *game.Regist } user := r.Context().Value(ctxKey).(*token.UserPayload) - client := newClient(hub, conn, user, hub.cfg) - hub.register <- client - client.start() + hub.register <- newClient(hub, conn, user, hub.cfg) }) return r diff --git a/frontend/src/app/live/boardgame/page.tsx b/frontend/src/app/live/boardgame/page.tsx index 5e60260..e16ece6 100644 --- a/frontend/src/app/live/boardgame/page.tsx +++ b/frontend/src/app/live/boardgame/page.tsx @@ -19,7 +19,6 @@ const GAME_BOARDS: Record = { connect4: Connect4Board, chess: ChessBoard, }; - export default function BoardGamePage() { const { currentRoom } = useWebSocket(); const { gameState, createGame, joinGame, leaveGame, makeMove } = useBoardGame(); @@ -27,7 +26,7 @@ export default function BoardGamePage() { const handleCreateGame = async (gameName: GameName) => { try { - await createGame(gameName); + createGame(gameName); } catch (error) { console.error('Failed to create game:', error); } diff --git a/frontend/src/components/Chat/ChatBox.tsx b/frontend/src/components/Chat/ChatBox.tsx index 182d5f1..e242dd5 100644 --- a/frontend/src/components/Chat/ChatBox.tsx +++ b/frontend/src/components/Chat/ChatBox.tsx @@ -5,7 +5,7 @@ import { useWebSocket } from '@/hooks/webSocket'; import { useUserStore } from '@/hooks/userStore'; import Input from '../UI/Input'; import Button from '../UI/Button'; -import { Error } from '@/types/wsTypes'; +import { msgError } from '@/types/wsTypes'; import { SquarePen, LogOut, DoorOpen } from 'lucide-react'; export default function ChatBox() { @@ -114,7 +114,7 @@ export default function ChatBox() { >
setHoveredCol(-1)} + onPointerLeave={() => setHoveredCol(-1)} > -
- {hoveredCol >= 0 && ( -
- )} - {gameState.board.map((cellRow, row) => ( - cellRow.map((cell, col) => ( + {[...Array(7)].map((_, col) => ( +
setHoveredCol(col)} + onPointerLeave={() => setHoveredCol(-1)} + > + {hoveredCol === col && ( +
+ )} + {gameState.board.map((cellRow, row) => (
setHoveredCol(col)} >
= 0 ? cell === yourIdx + 1 ? 'bg-secondary' : 'bg-accent' - : cell === 1 ? 'bg-secondary' : 'bg-accent' + cellRow[col] ? + yourIdx >= 0 ? cellRow[col] === yourIdx + 1 ? 'bg-secondary' : 'bg-accent' + : cellRow[col] === 1 ? 'bg-secondary' : 'bg-accent' : 'bg-background' )} />
- )) - ))} -
+ ))} +
+ ))}
; } diff --git a/frontend/src/components/games/chess/ChessBoard.tsx b/frontend/src/components/games/chess/ChessBoard.tsx index 8521817..27c48a1 100644 --- a/frontend/src/components/games/chess/ChessBoard.tsx +++ b/frontend/src/components/games/chess/ChessBoard.tsx @@ -22,26 +22,16 @@ export function ChessBoard({ gameState, makeMove }: GameBoardProps) { const idx = gameState.players.indexOf(username); const yourTurn = gameState.status === 'in_progress' && gameState.turn === idx; - const boardRef = useRef(null); - useEffect(() => { - const board = boardRef.current; - if (!board) return; - const handleTouchStart = (e: TouchEvent) => e.preventDefault(); - board.addEventListener('touchstart', handleTouchStart, { passive: false }); - return () => board.removeEventListener('touchstart', handleTouchStart); - }, []); - const handlePointerDown = (e: React.PointerEvent, piece: number, row: number, col: number) => { if (e.pointerType === 'mouse' && e.button !== 0) return; + e.preventDefault(); setDrag({ piece: piece, from: { row, col }, pos: { x: e.clientX, y: e.clientY }, }); - e.preventDefault(); }; - const validSquares = useMemo(() => gameState.validMoves.filter(vMove => vMove.from?.row === drag?.from.row && vMove.from?.col === drag?.from.col) , [gameState.validMoves, drag?.from.row, drag?.from.col]); @@ -65,6 +55,7 @@ export function ChessBoard({ gameState, makeMove }: GameBoardProps) { } setDrag(null); }; + document.addEventListener('pointermove', handlePointerMove); document.addEventListener('pointerup', handlePointerUp, { once: true }); return () => { @@ -73,7 +64,7 @@ export function ChessBoard({ gameState, makeMove }: GameBoardProps) { }; }, [drag, makeMove, validSquares, yourTurn]); - return
+ return

attention: work in progress

{gameState.board.map((cellRow, row) => cellRow.map((cell, col) => { @@ -88,7 +79,7 @@ export function ChessBoard({ gameState, makeMove }: GameBoardProps) { 'hover:bg-accent/30 group', validSquares.some(move => move.to.row === row && move.to.col === col) ? isLight ? 'bg-primary/33' : 'bg-primary/66' - : isLight ? 'bg-secondary/33' : 'bg-secondary/66', + : isLight ? 'bg-secondary/10' : 'bg-secondary/50', )} onPointerEnter={() => hover.current = { row, col }} onPointerLeave={() => hover.current = null} diff --git a/frontend/src/hooks/useBoardGame.ts b/frontend/src/hooks/useBoardGame.ts index c8c160f..335e334 100644 --- a/frontend/src/hooks/useBoardGame.ts +++ b/frontend/src/hooks/useBoardGame.ts @@ -71,7 +71,7 @@ export default function useBoardGame() { useEffect(() => { const handleGameState = (data: BoardGameState) => { - console.log('Received game state:', data); + console.debug('Received game state:', data); setGameState(data); setIsLoading(false); }; diff --git a/frontend/src/hooks/useDraw.ts b/frontend/src/hooks/useDraw.ts index c887d6e..dd59b96 100644 --- a/frontend/src/hooks/useDraw.ts +++ b/frontend/src/hooks/useDraw.ts @@ -3,7 +3,7 @@ import { useRef, useEffect, useState } from 'react'; import { useWebSocket } from './webSocket'; import type { DrawPayload } from '@/types/wsTypes'; -import { RawSignal } from '@/types/wsTypes'; +import { msgRawSignal } from '@/types/wsTypes'; import { DRAW_START_COLOR, DRAW_STROKE_INTERVAL, DRAW_START_WIDTH } from '@/config/consts'; interface Point { @@ -80,7 +80,7 @@ export function useDraw() { }; sendMessage({ - type: RawSignal, + type: msgRawSignal, sender: 'drawer', payload: drawData }); diff --git a/frontend/src/hooks/userStore.ts b/frontend/src/hooks/userStore.ts index c6c734d..1fac20f 100644 --- a/frontend/src/hooks/userStore.ts +++ b/frontend/src/hooks/userStore.ts @@ -31,8 +31,8 @@ interface UserState { logout: () => Promise; refresh: () => Promise; loggedin: () => boolean; - addRefreshDependent: (id: string) => void; - removeRefreshDependent: (id: string) => void; + addRefDependent: (id: string) => void; + remRefDependent: (id: string) => void; } export const useUserStore = create()( @@ -126,11 +126,11 @@ export const useUserStore = create()( } }, loggedin: () => get().accessExp > Date.now(), - addRefreshDependent: (id: string) => { + addRefDependent: (id: string) => { refreshDependents.add(id); scheduleRefresh(get); }, - removeRefreshDependent: (id: string) => { + remRefDependent: (id: string) => { refreshDependents.delete(id); if (refreshDependents.size === 0 && refreshTimeout) { clearTimeout(refreshTimeout); diff --git a/frontend/src/hooks/webSocket.ts b/frontend/src/hooks/webSocket.ts index 799eab4..8d2c2a7 100644 --- a/frontend/src/hooks/webSocket.ts +++ b/frontend/src/hooks/webSocket.ts @@ -76,25 +76,25 @@ export const useWebSocket = create()((set, get) => ({ const msg: t.IncomingMsg = JSON.parse(e.data); switch (msg.type) { - case t.Chat: - case t.Status: - case t.Error: + case t.msgChat: + case t.msgStatus: + case t.msgError: set(state => ({ ...state, msgLog: [...state.msgLog, msg] })); break; - case t.RawSignal: + case t.msgRawSignal: if (drawHandler) { drawHandler(msg.payload); } break; - case t.GameState: + case t.msgGameState: if (gameHandler) { gameHandler(msg.payload); } break; - case t.VidSignal: + case t.msgVidSignal: if (vidSigHandler === null) { console.warn('WS received vid signal, but no handler'); } else { @@ -102,11 +102,11 @@ export const useWebSocket = create()((set, get) => ({ } break; - case t.JoinRoom: + case t.msgJoinRoom: set({ currentRoom: msg.payload.roomName }); break; - case t.GetClients: - set({ clients: msg.payload.clients, currentRoom: msg.payload.roomName }) + case t.msgGetClients: + set({ clients: msg.payload.clients }) break; default: console.warn('Unknown message', msg); @@ -135,6 +135,7 @@ export const useWebSocket = create()((set, get) => ({ ws.close(1000, 'Client wants to leave'); ws = null; } + console.debug('WS disconnect') set({ currentRoom: '', msgLog: [], clients: [], error: '' }) }, sendMessage: (msg: t.OutgoingMsg) => { @@ -152,30 +153,30 @@ export const useWebSocket = create()((set, get) => ({ sendChat: (message: string) => { const { username, displayName } = useUserStore.getState(); const msg: t.ChatMsg = { - type: t.Chat, + type: t.msgChat, sender: username, payload: { message, displayName } } get().sendMessage(msg); }, joinRoom: (roomName: string) => { - const msg: t.JoinRoomMsg = { type: t.JoinRoom, sender: '', payload: { roomName } } + const msg: t.JoinRoomMsg = { type: t.msgJoinRoom, sender: '', payload: { roomName } } get().sendMessage(msg); }, leaveRoom: () => { - const msg: t.LeaveRoomMsg = { type: t.LeaveRoom } + const msg: t.LeaveRoomMsg = { type: t.msgLeaveRoom } get().sendMessage(msg); }, setVidSigHandler: handler => { vidSigHandler = handler }, sendVidSignal: (payload: t.VidSignalMsg['payload']) => { - const msg: t.VidSignalMsg = { type: t.VidSignal, sender: '', payload } + const msg: t.VidSignalMsg = { type: t.msgVidSignal, sender: '', payload } get().sendMessage(msg); }, setDrawHandler: (handler: (data: t.DrawPayload) => void) => { drawHandler = handler }, setGameHandler: handler => { gameHandler = handler }, sendGameMsg: (payload: t.GamePayload) => { const msg: t.OutgoingMsg = { - type: t.GameState, + type: t.msgGameState, sender: '', payload: payload, }; @@ -185,7 +186,7 @@ export const useWebSocket = create()((set, get) => ({ export const useWSConnect = () => { - const { loggedin, refresh, addRefreshDependent: addRefDependent, removeRefreshDependent: remRefDependent } = useUserStore(); + const { loggedin, refresh, addRefDependent, remRefDependent, username } = useUserStore(); const { connect, disconnect, getStatus } = useWebSocket(); useEffect(() => { @@ -219,5 +220,6 @@ export const useWSConnect = () => { remRefDependent('ws'); console.warn('not loggedin disconnect'); }; - }, [loggedin, refresh, connect, disconnect, getStatus, addRefDependent, remRefDependent]); + }, [loggedin, refresh, connect, disconnect, getStatus, + addRefDependent, remRefDependent, username]); }; \ No newline at end of file diff --git a/frontend/src/types/wsTypes.ts b/frontend/src/types/wsTypes.ts index d51e6c2..afc3765 100644 --- a/frontend/src/types/wsTypes.ts +++ b/frontend/src/types/wsTypes.ts @@ -1,19 +1,18 @@ export type WsStatus = 'disconnected' | 'connecting' | 'connected'; -export const Chat = 'chat' as const; -export const VidSignal = 'video_signal' as const; -export const RawSignal = 'raw_signal' as const; -export const GameState = 'game_state' as const; -export const JoinRoom = 'join_room' as const; -export const LeaveRoom = 'leave_room' as const; -export const GetRoom = 'get_rooms' as const; -export const GetClients = 'get_clients' as const; -export const Error = 'error' as const; -export const Status = 'status' as const; -export const Draw = 'draw' as const; +export const msgError = 'error' as const; +export const msgStatus = 'status' as const; +export const msgChat = 'chat' as const; +export const msgVidSignal = 'video_signal' as const; +export const msgRawSignal = 'raw_signal' as const; +export const msgGameState = 'game_state' as const; +export const msgJoinRoom = 'join_room' as const; +export const msgLeaveRoom = 'leave_room' as const; +export const msgGetRooms = 'get_rooms' as const; +export const msgGetClients = 'get_clients' as const; export interface ErrorMsg { - type: typeof Error; + type: typeof msgError; sender: '_server'; payload: { message: string; @@ -21,7 +20,7 @@ export interface ErrorMsg { } export interface StatusMsg { - type: typeof Status; + type: typeof msgStatus; sender: '_server'; payload: { message: string; @@ -29,7 +28,7 @@ export interface StatusMsg { } export interface JoinRoomMsg { - type: typeof JoinRoom; + type: typeof msgJoinRoom; sender: string; payload: { roomName: string; @@ -37,12 +36,12 @@ export interface JoinRoomMsg { } export interface LeaveRoomMsg { - type: typeof LeaveRoom; + type: typeof msgLeaveRoom; // Payload: {}; } export interface ChatMsg { - type: typeof Chat; + type: typeof msgChat; sender: string; payload: { message: string; @@ -51,7 +50,7 @@ export interface ChatMsg { } export interface VidSignalMsg { - type: typeof VidSignal; + type: typeof msgVidSignal; sender: string; payload: { type: 'join' | 'leave' | 'offer' | 'answer' | 'ice'; @@ -63,7 +62,7 @@ export interface VidSignalMsg { } export interface GetClientRes { - type: typeof GetClients; + type: typeof msgGetClients; sender: '_server'; payload: { roomName: string; @@ -71,11 +70,6 @@ export interface GetClientRes { }; } -export interface GetClientsReq { - type: typeof GetClients; - // Payload: {}; -} - export interface DrawPayload { type: 'draw'; points: Array<{ x: number; y: number }>; @@ -84,7 +78,7 @@ export interface DrawPayload { } export interface RawDrawMsg { - type: typeof RawSignal; + type: typeof msgRawSignal; sender: string; payload: DrawPayload; } @@ -122,13 +116,13 @@ export interface GamePayload { } export interface OutgoingGameState { - type: typeof GameState; + type: typeof msgGameState; sender: string; payload: GamePayload; } export interface IncomingGameState { - type: typeof GameState; + type: typeof msgGameState; sender: '_server'; payload: BoardGameState; } @@ -150,7 +144,6 @@ export type OutgoingMsg = | OutgoingGameState | JoinRoomMsg | LeaveRoomMsg - | GetClientsReq | RawDrawMsg export type DisplayableMsg =