diff --git a/game/game.go b/game/game.go index 5185866..d496bc2 100644 --- a/game/game.go +++ b/game/game.go @@ -410,7 +410,13 @@ func (g *Game) Cleanup() { rl.UnloadMusicStream(g.AssetManager.Music) } - close(g.quitChan) + // Only close the channel if it hasn't been closed yet + select { + case <-g.quitChan: + // Channel already closed, do nothing + default: + close(g.quitChan) + } }) } @@ -491,7 +497,8 @@ func (g *Game) DrawMenu() { } func (g *Game) Shutdown() { - close(g.quitChan) + // Use the cleanup method which has channel-closing safety + g.Cleanup() } func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) { diff --git a/network/network.go b/network/network.go index 013f444..3527962 100644 --- a/network/network.go +++ b/network/network.go @@ -248,23 +248,43 @@ func ConnectToServer(username, password string, isRegistering bool) (net.Conn, i func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) { msgHandler := NewMessageHandler(conn) + // Create channels for coordinating goroutines + errChan := make(chan error, 1) + done := make(chan struct{}) + + // Create a WaitGroup to track both sender and receiver goroutines + var wg sync.WaitGroup + wg.Add(2) // One for sender, one for receiver + + // Set up a deferred cleanup function defer func() { if r := recover(); r != nil { log.Printf("Recovered from panic in HandleServerCommunication: %v", r) } + + // Close the done channel to signal both goroutines to exit + close(done) + + // Wait for both goroutines to finish + wg.Wait() + + // Close the connection conn.Close() + + // Close the player's QuitDone channel if it exists if player.QuitDone != nil { - close(player.QuitDone) + select { + case <-player.QuitDone: // Check if it's already closed + // Already closed, do nothing + default: + close(player.QuitDone) + } } }() actionTicker := time.NewTicker(types.ClientTickRate) defer actionTicker.Stop() - // Create error channel for goroutine communication - errChan := make(chan error, 1) - done := make(chan struct{}) - // Add a heartbeat ticker to detect connection issues heartbeatTicker := time.NewTicker(5 * time.Second) defer heartbeatTicker.Stop() @@ -276,8 +296,14 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play defer func() { if r := recover(); r != nil { log.Printf("Recovered from panic in message sender: %v", r) - errChan <- fmt.Errorf("message sender panic: %v", r) + select { + case errChan <- fmt.Errorf("message sender panic: %v", r): + default: + // Channel already closed or full, just log + log.Printf("Unable to send error: %v", r) + } } + wg.Done() // Mark this goroutine as done }() for { @@ -291,8 +317,11 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play PlayerId: playerID, }}, } - msgHandler.WriteMessage(disconnectMsg) - done <- struct{}{} + + // Try to send disconnect message, ignoring errors + _ = msgHandler.WriteMessage(disconnectMsg) + + // No need to signal done channel here, the main goroutine handles this return case <-done: return @@ -307,8 +336,11 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play } if err := msgHandler.WriteMessage(emptyBatch); err != nil { log.Printf("Failed to send heartbeat: %v", err) - errChan <- err - return + select { + case errChan <- err: + case <-done: + return + } } lastMessageTime = time.Now() } @@ -327,8 +359,11 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play player.Unlock() if err := msgHandler.WriteMessage(batch); err != nil { - errChan <- err - return + select { + case errChan <- err: + case <-done: + return + } } lastMessageTime = time.Now() } else { @@ -343,14 +378,22 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play defer func() { if r := recover(); r != nil { log.Printf("Recovered from panic in message receiver: %v", r) - errChan <- fmt.Errorf("message receiver panic: %v", r) + select { + case errChan <- fmt.Errorf("message receiver panic: %v", r): + default: + // Channel already closed or full, just log + log.Printf("Unable to send error: %v", r) + } } + wg.Done() // Mark this goroutine as done }() for { select { case <-quitChan: return + case <-done: + return default: serverMessage, err := msgHandler.ReadMessage() if err != nil { @@ -358,7 +401,11 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play log.Printf("Network timeout: %v", err) } else if err != io.EOF { log.Printf("Network read error: %v", err) - errChan <- err + select { + case errChan <- err: + case <-done: + return + } } else { log.Printf("Connection closed by server") } @@ -375,19 +422,12 @@ func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Play select { case <-quitChan: log.Printf("Received quit signal, sending disconnect message") - // Send disconnect message - disconnectMsg := &pb.ActionBatch{ - PlayerId: playerID, - Actions: []*pb.Action{{ - Type: pb.Action_DISCONNECT, - PlayerId: playerID, - }}, - } - msgHandler.WriteMessage(disconnectMsg) - close(done) + // The cleanup will happen in the deferred function + return case err := <-errChan: log.Printf("Network error: %v", err) - close(done) + // The cleanup will happen in the deferred function + return } } @@ -420,7 +460,13 @@ func NewConnection(username, password string, isRegistering bool) (*Connection, func (c *Connection) Close() { c.closeOnce.Do(func() { - close(c.quitChan) + select { + case <-c.quitChan: // Check if it's already closed + // Already closed, do nothing + default: + close(c.quitChan) + } + // Wait with timeout for network cleanup select { case <-c.quitDone: @@ -428,6 +474,8 @@ func (c *Connection) Close() { case <-time.After(500 * time.Millisecond): log.Println("Network cleanup timed out") } + + // Make sure the connection is closed c.conn.Close() }) }