package net import ( "slices" "encoding/json" "fmt" "log" "math" "math/rand" "net" "os" "sync" "time" ) // Player represents a connected player type Player struct { ID uint32 Username string Position Vec3 Velocity Vec3 Color string Address *net.UDPAddr LastSeen time.Time } // UserData represents persistent user data type UserData struct { Username string `json:"username"` Color string `json:"color"` Position Vec3 `json:"position"` } // Server manages the game state and networking type Server struct { conn *net.UDPConn players map[uint32]*Player usersByName map[string]*Player // Track by username for preventing duplicates userData map[string]*UserData // Persistent user data heightmap [][]float32 mutex sync.RWMutex nextID uint32 } // NewServer creates a new game server func NewServer(port string, heightmap [][]float32) (*Server, error) { addr, err := net.ResolveUDPAddr("udp", ":"+port) if err != nil { return nil, fmt.Errorf("failed to resolve UDP address: %w", err) } conn, err := net.ListenUDP("udp", addr) if err != nil { return nil, fmt.Errorf("failed to listen on UDP: %w", err) } server := &Server{ conn: conn, players: make(map[uint32]*Player), usersByName: make(map[string]*Player), userData: make(map[string]*UserData), heightmap: heightmap, nextID: 0, } server.loadUserData() return server, nil } // Run starts the server main loop func (s *Server) Run() error { defer s.conn.Close() // Start periodic save go func() { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for range ticker.C { s.saveUserData() } }() // Start player timeout checker go func() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for range ticker.C { s.checkTimeouts() } }() buffer := make([]byte, 1024) log.Println("Server running...") for { n, addr, err := s.conn.ReadFromUDP(buffer) if err != nil { log.Printf("Error reading UDP: %v", err) continue } if n < 1 { continue } msgType := buffer[0] switch msgType { case MSG_LOGIN: s.handleLogin(buffer[:n], addr) case MSG_MOVE: s.handleMove(buffer[:n], addr) case MSG_CHANGE_COLOR: s.handleColorChange(buffer[:n], addr) case MSG_LOGOUT: s.handleLogout(buffer[:n], addr) } } } func (s *Server) handleLogin(data []byte, addr *net.UDPAddr) { username, ok := DecodeLoginPacket(data) if !ok || username == "" { // Invalid login packet log.Printf("Received invalid login packet from %s", addr) responseMsg := EncodeLoginResponsePacket(false, "Invalid username") s.conn.WriteToUDP(responseMsg, addr) return } log.Printf("Login attempt for username: %s from %s", username, addr) s.mutex.Lock() // Check if username is already logged in if existingPlayer, exists := s.usersByName[username]; exists { s.mutex.Unlock() responseMsg := EncodeLoginResponsePacket(false, "User already logged in") s.conn.WriteToUDP(responseMsg, addr) log.Printf("Login rejected for %s: already logged in (ID: %d)", username, existingPlayer.ID) return } s.nextID++ playerID := s.nextID // Load saved user data or create new var userData *UserData var exists bool if userData, exists = s.userData[username]; !exists { // New user - assign default color and random position colors := []string{"red", "green", "orange", "purple", "white"} colorIndex := (playerID - 1) % uint32(len(colors)) color := colors[colorIndex] x := rand.Float32()*100 - 50 z := rand.Float32()*100 - 50 y := s.getHeightAt(x, z) + 1.0 userData = &UserData{ Username: username, Color: color, Position: Vec3{x, y, z}, } s.userData[username] = userData } player := &Player{ ID: playerID, Username: username, Position: userData.Position, Color: userData.Color, Address: addr, LastSeen: time.Now(), } // Send existing players to new player before adding them existingPlayers := make([]*Player, 0) for _, p := range s.players { if p.ID != playerID { existingPlayers = append(existingPlayers, p) } } s.players[playerID] = player s.usersByName[username] = player s.mutex.Unlock() // Send login success response responseMsg := EncodeLoginResponsePacket(true, "Login successful") s.conn.WriteToUDP(responseMsg, addr) // Send spawn message with saved position and color spawnMsg := EncodeSpawnPacket(playerID, userData.Position, userData.Color) s.conn.WriteToUDP(spawnMsg, addr) // Send player list to new player if len(existingPlayers) > 0 { listMsg := EncodePlayerListPacket(existingPlayers) s.conn.WriteToUDP(listMsg, addr) } // Notify other players about new player s.broadcastPlayerJoined(player) log.Printf("Player %s (ID %d) logged in at (%.2f, %.2f, %.2f) with color %s", username, playerID, userData.Position.X, userData.Position.Y, userData.Position.Z, userData.Color) s.saveUserData() } func (s *Server) handleMove(data []byte, _ *net.UDPAddr) { playerID, delta, ok := DecodeMovePacket(data) if !ok { return } s.mutex.Lock() player, exists := s.players[playerID] if !exists { s.mutex.Unlock() return } // Server-authoritative movement deltaTime := float32(0.016) // 60fps newX := player.Position.X + delta.X*15.0*deltaTime newZ := player.Position.Z + delta.Z*15.0*deltaTime // Clamp to world bounds newX = float32(math.Max(-50, math.Min(50, float64(newX)))) newZ = float32(math.Max(-50, math.Min(50, float64(newZ)))) // Set Y to terrain height with smoothing targetY := s.getHeightAt(newX, newZ) + 1.0 smoothFactor := float32(0.15) newY := player.Position.Y + (targetY-player.Position.Y)*smoothFactor player.Position.X = newX player.Position.Y = newY player.Position.Z = newZ player.LastSeen = time.Now() // Update persistent user data if userData, exists := s.userData[player.Username]; exists { userData.Position = player.Position } s.mutex.Unlock() // Broadcast position update to all players s.broadcastUpdate(player) } func (s *Server) handleLogout(data []byte, _ *net.UDPAddr) { playerID, ok := DecodeLogoutPacket(data) if !ok { log.Printf("Failed to decode logout packet") return } log.Printf("Received logout request for player ID %d", playerID) s.mutex.Lock() player, exists := s.players[playerID] if !exists { s.mutex.Unlock() log.Printf("Player ID %d not found in active players", playerID) return } // Save final position if userData, exists := s.userData[player.Username]; exists { userData.Position = player.Position userData.Color = player.Color } // Remove from active players username := player.Username delete(s.players, playerID) delete(s.usersByName, username) s.mutex.Unlock() // Notify other players s.broadcastPlayerLeft(playerID) log.Printf("Player %s (ID %d) successfully logged out", username, playerID) s.saveUserData() } func (s *Server) handleColorChange(data []byte, _ *net.UDPAddr) { playerID, newColor, ok := DecodeColorChangePacket(data) if !ok { return } // Validate color validColors := []string{"red", "green", "orange", "purple", "white"} isValid := slices.Contains(validColors, newColor) if !isValid { return } s.mutex.Lock() player, exists := s.players[playerID] if !exists { s.mutex.Unlock() return } player.Color = newColor // Update persistent user data if userData, exists := s.userData[player.Username]; exists { userData.Color = newColor } s.mutex.Unlock() // Broadcast color change to all players s.broadcastColorChanged(playerID, newColor) } func (s *Server) getHeightAt(x, z float32) float32 { // Convert world coords to heightmap coords with bilinear interpolation size := float32(len(s.heightmap)) fx := (x/100 + 0.5) * (size - 1) fz := (z/100 + 0.5) * (size - 1) // Get integer coordinates x0 := int(math.Floor(float64(fx))) z0 := int(math.Floor(float64(fz))) x1 := x0 + 1 z1 := z0 + 1 // Clamp to bounds if x0 < 0 || x1 >= len(s.heightmap) || z0 < 0 || z1 >= len(s.heightmap) { return 0 } // Get fractional parts tx := fx - float32(x0) tz := fz - float32(z0) // Bilinear interpolation h00 := s.heightmap[z0][x0] h10 := s.heightmap[z0][x1] h01 := s.heightmap[z1][x0] h11 := s.heightmap[z1][x1] h0 := h00*(1-tx) + h10*tx h1 := h01*(1-tx) + h11*tx return h0*(1-tz) + h1*tz } func (s *Server) broadcastUpdate(player *Player) { msg := EncodeUpdatePacket(player.ID, player.Position) s.mutex.RLock() for _, p := range s.players { if p.Address != nil { s.conn.WriteToUDP(msg, p.Address) } } s.mutex.RUnlock() } func (s *Server) broadcastPlayerJoined(newPlayer *Player) { msg := EncodePlayerJoinedPacket(newPlayer.ID, newPlayer.Position, newPlayer.Color) s.mutex.RLock() for _, p := range s.players { if p.ID != newPlayer.ID && p.Address != nil { s.conn.WriteToUDP(msg, p.Address) } } s.mutex.RUnlock() } func (s *Server) broadcastPlayerLeft(playerID uint32) { msg := EncodePlayerLeftPacket(playerID) s.mutex.RLock() for _, p := range s.players { if p.ID != playerID && p.Address != nil { s.conn.WriteToUDP(msg, p.Address) } } s.mutex.RUnlock() } func (s *Server) broadcastColorChanged(playerID uint32, color string) { msg := EncodeColorChangedPacket(playerID, color) s.mutex.RLock() for _, p := range s.players { if p.Address != nil { s.conn.WriteToUDP(msg, p.Address) } } s.mutex.RUnlock() } func (s *Server) checkTimeouts() { s.mutex.Lock() defer s.mutex.Unlock() now := time.Now() for id, player := range s.players { if now.Sub(player.LastSeen) > 30*time.Second { // Save final position before removing if userData, exists := s.userData[player.Username]; exists { userData.Position = player.Position userData.Color = player.Color } delete(s.players, id) delete(s.usersByName, player.Username) go s.broadcastPlayerLeft(id) log.Printf("Player %s (ID %d) timed out", player.Username, id) go s.saveUserData() } } } func (s *Server) loadUserData() { data, err := os.ReadFile("players.json") if err != nil { log.Printf("No existing user data found: %v", err) return } var savedUsers map[string]*UserData if err := json.Unmarshal(data, &savedUsers); err != nil { log.Printf("Failed to parse user data: %v", err) return } s.userData = savedUsers log.Printf("Loaded data for %d users", len(savedUsers)) } func (s *Server) saveUserData() { s.mutex.RLock() // Deep copy userData to avoid holding lock during file I/O savedUsers := make(map[string]*UserData) for username, data := range s.userData { savedUsers[username] = &UserData{ Username: data.Username, Color: data.Color, Position: data.Position, } } s.mutex.RUnlock() data, err := json.MarshalIndent(savedUsers, "", " ") if err != nil { log.Printf("Failed to marshal user data: %v", err) return } if err := os.WriteFile("players.json", data, 0644); err != nil { log.Printf("Failed to save user data: %v", err) } }