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" func SetServerAddr(addr string) { serverAddr = addr } func ConnectToServer(username, password string, isRegistering bool) (net.Conn, int32, error) { conn, err := net.Dial("tcp", serverAddr) if err != nil { log.Printf("Failed to dial server: %v", err) return nil, 0, err } log.Println("Connected to server. Authenticating...") // 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 := writeMessage(conn, authBatch); err != nil { conn.Close() return nil, 0, fmt.Errorf("failed to send auth: %v", err) } // Read server response reader := bufio.NewReader(conn) lengthBuf := make([]byte, 4) if _, err := io.ReadFull(reader, lengthBuf); err != nil { conn.Close() return nil, 0, fmt.Errorf("failed to read auth response: %v", err) } messageLength := binary.BigEndian.Uint32(lengthBuf) messageBuf := make([]byte, messageLength) if _, err := io.ReadFull(reader, messageBuf); err != nil { conn.Close() return nil, 0, fmt.Errorf("failed to read auth response body: %v", err) } var response pb.ServerMessage if err := proto.Unmarshal(messageBuf, &response); err != nil { conn.Close() return nil, 0, fmt.Errorf("failed to unmarshal auth response: %v", err) } 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) return conn, playerID, nil } func HandleServerCommunication(conn net.Conn, playerID int32, player *types.Player, otherPlayers map[int32]*types.Player, quitChan <-chan struct{}) { reader := bufio.NewReader(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{}) // 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, }}, } writeMessage(conn, disconnectMsg) done <- struct{}{} return case <-done: return 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, } player.ActionQueue = player.ActionQueue[:0] player.Unlock() if err := writeMessage(conn, batch); err != nil { errChan <- err return } } 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: lengthBuf := make([]byte, 4) if _, err := io.ReadFull(reader, lengthBuf); err != nil { if err != io.EOF { errChan <- fmt.Errorf("failed to read message length: %v", err) } return } messageLength := binary.BigEndian.Uint32(lengthBuf) messageBuf := make([]byte, messageLength) if _, err := io.ReadFull(reader, messageBuf); err != nil { log.Printf("Failed to read message body: %v", err) return } var serverMessage pb.ServerMessage if err := proto.Unmarshal(messageBuf, &serverMessage); err != nil { log.Printf("Failed to unmarshal server message: %v", err) continue } 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() for _, state := range serverMessage.Players { 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 } if otherPlayer, exists := otherPlayers[state.PlayerId]; exists { otherPlayer.UpdatePosition(state, types.ServerTickRate) } else { otherPlayers[state.PlayerId] = types.NewPlayer(state) } } // Remove players that are no longer in the server state for id := range otherPlayers { if id != playerID { delete(otherPlayers, id) } } if handler, ok := player.UserData.(types.ChatMessageHandler); ok && len(serverMessage.ChatMessages) > 0 { handler.HandleServerMessages(serverMessage.ChatMessages) } } } }() // Wait for error or quit signal select { case <-quitChan: // Send disconnect message disconnectMsg := &pb.ActionBatch{ PlayerId: playerID, Actions: []*pb.Action{{ Type: pb.Action_DISCONNECT, PlayerId: playerID, }}, } writeMessage(conn, disconnectMsg) case err := <-errChan: log.Printf("Network error: %v", err) } } // Helper function to write length-prefixed messages func writeMessage(conn net.Conn, 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 := conn.Write(lengthBuf); err != nil { return err } // Write message body _, err = conn.Write(data) return err } 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 }