package network import ( "bufio" "encoding/binary" "fmt" "io" "log" "net" "sync" "time" "gitea.boner.be/bdnugget/goonscape/types" pb "gitea.boner.be/bdnugget/goonserver/actions" rl "github.com/gen2brain/raylib-go/raylib" "google.golang.org/protobuf/proto" ) const protoVersion = 1 var serverAddr = "boner.be:6969" // Default server address var lastSeenMessageTimestamp int64 = 0 // Track the last message timestamp seen by this client func SetServerAddr(addr string) { serverAddr = addr log.Printf("Server address set to: %s", serverAddr) } // MessageHandler handles reading and writing protobuf messages type MessageHandler struct { conn net.Conn reader *bufio.Reader } // NewMessageHandler creates a new message handler func NewMessageHandler(conn net.Conn) *MessageHandler { return &MessageHandler{ conn: conn, reader: bufio.NewReader(conn), } } // ReadMessage reads a single message from the network func (mh *MessageHandler) ReadMessage() (*pb.ServerMessage, error) { // Read message length lengthBuf := make([]byte, 4) if _, err := io.ReadFull(mh.reader, lengthBuf); err != nil { return nil, fmt.Errorf("failed to read message length: %v", err) } messageLength := binary.BigEndian.Uint32(lengthBuf) // Sanity check message size if messageLength > 1024*1024 { // 1MB max message size return nil, fmt.Errorf("message size too large: %d bytes", messageLength) } // Read message body messageBuf := make([]byte, messageLength) if _, err := io.ReadFull(mh.reader, messageBuf); err != nil { return nil, fmt.Errorf("failed to read message body: %v", err) } // Unmarshal the message var message pb.ServerMessage if err := proto.Unmarshal(messageBuf, &message); err != nil { return nil, fmt.Errorf("failed to unmarshal message: %v", err) } return &message, nil } // WriteMessage writes a protobuf message to the network func (mh *MessageHandler) WriteMessage(msg proto.Message) error { data, err := proto.Marshal(msg) if err != nil { return err } // Write length prefix lengthBuf := make([]byte, 4) binary.BigEndian.PutUint32(lengthBuf, uint32(len(data))) if _, err := mh.conn.Write(lengthBuf); err != nil { return err } // Write message body _, err = mh.conn.Write(data) return err } // UpdateGameState processes a server message and updates game state func UpdateGameState(serverMessage *pb.ServerMessage, player *types.Player, otherPlayers map[int32]*types.Player) { playerID := player.ID player.Lock() player.CurrentTick = serverMessage.CurrentTick tickDiff := serverMessage.CurrentTick - player.CurrentTick if tickDiff > types.MaxTickDesync { for _, state := range serverMessage.Players { if state.PlayerId == playerID { player.ForceResync(state) break } } } player.Unlock() // Process player states validPlayerIds := make(map[int32]bool) for _, state := range serverMessage.Players { validPlayerIds[state.PlayerId] = true if state.PlayerId == playerID { player.Lock() // Update initial position if not set if player.PosActual.X == 0 && player.PosActual.Z == 0 { player.PosActual = rl.Vector3{ X: float32(state.X * types.TileSize), Y: 0, Z: float32(state.Y * types.TileSize), } player.PosTile = types.Tile{X: int(state.X), Y: int(state.Y)} } player.Unlock() continue } // Update or create other players if otherPlayer, exists := otherPlayers[state.PlayerId]; exists { otherPlayer.UpdatePosition(state, types.ServerTickRate) } else { log.Printf("Creating new player with ID: %d", state.PlayerId) otherPlayers[state.PlayerId] = types.NewPlayer(state) } } // Remove players no longer in the server state for id := range otherPlayers { if id != playerID && !validPlayerIds[id] { log.Printf("Removing player with ID: %d", id) delete(otherPlayers, id) } } // Handle chat messages if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 { log.Printf("Received %d chat messages from server", len(serverMessage.ChatMessages)) handler.HandleServerMessages(serverMessage.ChatMessages) // Update the last seen message timestamp to the most recent message if len(serverMessage.ChatMessages) > 0 { lastMsg := serverMessage.ChatMessages[len(serverMessage.ChatMessages)-1] lastSeenMessageTimestamp = lastMsg.Timestamp log.Printf("Updated last seen message timestamp to %d", lastSeenMessageTimestamp) } } } func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) { log.Printf("Connecting to server at %s...", serverAddr) var err error var conn net.Conn // Try connecting with a timeout connChan := make(chan net.Conn, 1) errChan := make(chan error, 1) go func() { c, e := net.Dial("tcp", serverAddr) if e != nil { errChan <- e return } connChan <- c }() // Wait for connection with timeout select { case conn = <-connChan: // Connection successful, continue case err = <-errChan: return nil, 0, fmt.Errorf("failed to dial server: %v", err) case <-time.After(5 * time.Second): return nil, 0, fmt.Errorf("connection timeout after 5 seconds") } log.Println("Connected to server. Authenticating...") // Create a message handler msgHandler := NewMessageHandler(conn) // Send auth message authAction := &pb.Action{ Type: pb.Action_LOGIN, Username: username, Password: password, } if isRegistering { authAction.Type = pb.Action_REGISTER } authBatch := &pb.ActionBatch{ Actions: []*pb.Action{authAction}, ProtocolVersion: protoVersion, } if err := msgHandler.WriteMessage(authBatch); err != nil { conn.Close() return nil, 0, fmt.Errorf("failed to send auth: %v", err) } // Set a read deadline for authentication conn.SetReadDeadline(time.Now().Add(10 * time.Second)) // Read server response response, err := msgHandler.ReadMessage() if err != nil { conn.Close() return nil, 0, fmt.Errorf("failed to read auth response: %v", err) } // Clear read deadline after authentication conn.SetReadDeadline(time.Time{}) if response.ProtocolVersion > protoVersion { conn.Close() return nil, 0, fmt.Errorf("server requires newer protocol version (server: %d, client: %d)", response.ProtocolVersion, protoVersion) } if !response.AuthSuccess { conn.Close() return nil, 0, fmt.Errorf(response.ErrorMessage) } playerID := response.GetPlayerId() log.Printf("Successfully authenticated with player ID: %d", playerID) // Reset the lastSeenMessageTimestamp when reconnecting lastSeenMessageTimestamp = 0 return conn, playerID, nil } func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) { msgHandler := NewMessageHandler(conn) defer func() { if r := recover(); r != nil { log.Printf("Recovered from panic in HandleServerCommunication: %v", r) } conn.Close() if player.QuitDone != nil { 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() lastMessageTime := time.Now() // Start message sending goroutine go func() { 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) } }() for { select { case <-quitChan: // Send disconnect message to server disconnectMsg := &pb.ActionBatch{ PlayerId: playerID, Actions: []*pb.Action{{ Type: pb.Action_DISCONNECT, PlayerId: playerID, }}, } msgHandler.WriteMessage(disconnectMsg) done <- struct{}{} return case <-done: return case <-heartbeatTicker.C: // If no message has been sent for a while, send a heartbeat timeSinceLastMessage := time.Since(lastMessageTime) if timeSinceLastMessage > 5*time.Second { // Send an empty batch as a heartbeat emptyBatch := &pb.ActionBatch{ PlayerId: playerID, LastSeenMessageTimestamp: lastSeenMessageTimestamp, } if err := msgHandler.WriteMessage(emptyBatch); err != nil { log.Printf("Failed to send heartbeat: %v", err) errChan <- err return } lastMessageTime = time.Now() } case <-actionTicker.C: player.Lock() if len(player.ActionQueue) > 0 { actions := make([]*pb.Action, len(player.ActionQueue)) copy(actions, player.ActionQueue) batch := &pb.ActionBatch{ PlayerId: playerID, Actions: actions, Tick: player.CurrentTick, LastSeenMessageTimestamp: lastSeenMessageTimestamp, } player.ActionQueue = player.ActionQueue[:0] player.Unlock() if err := msgHandler.WriteMessage(batch); err != nil { errChan <- err return } lastMessageTime = time.Now() } else { player.Unlock() } } } }() // Main message receiving loop go func() { 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) } }() for { select { case <-quitChan: return default: serverMessage, err := msgHandler.ReadMessage() if err != nil { if err, ok := err.(net.Error); ok && err.Timeout() { log.Printf("Network timeout: %v", err) } else if err != io.EOF { log.Printf("Network read error: %v", err) errChan <- err } else { log.Printf("Connection closed by server") } return } // Process the server message UpdateGameState(serverMessage, player, otherPlayers) } } }() // Wait for error or quit signal 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) case err := <-errChan: log.Printf("Network error: %v", err) close(done) } } // Helper function to write length-prefixed messages func writeMessage(conn net.Conn, msg proto.Message) error { msgHandler := NewMessageHandler(conn) return msgHandler.WriteMessage(msg) } type Connection struct { conn net.Conn playerID int32 quitChan chan struct{} quitDone chan struct{} closeOnce sync.Once } func NewConnection(username, password string, isRegistering bool) (*Connection, error) { conn, playerID, err := ConnectToServer(username, password, isRegistering) if err != nil { return nil, err } return &Connection{ conn: conn, playerID: playerID, quitChan: make(chan struct{}), quitDone: make(chan struct{}), }, nil } func (c *Connection) Close() { c.closeOnce.Do(func() { close(c.quitChan) // Wait with timeout for network cleanup select { case <-c.quitDone: // Clean shutdown completed case <-time.After(500 * time.Millisecond): log.Println("Network cleanup timed out") } c.conn.Close() }) } func (c *Connection) PlayerID() int32 { return c.playerID } func (c *Connection) Start(player *types.Player, otherPlayers map[int32]*types.Player) { go HandleServerCommunication(c.conn, c.playerID, player, otherPlayers, c.quitChan) } func (c *Connection) QuitChan() <-chan struct{} { return c.quitChan }