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 worldConfig *WorldConfig mutex sync.RWMutex nextID uint32 timeOfDay float32 startTime time.Time } // 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) } // Create world configuration from heightmap // Default to 1 unit per sample (1 meter per sample) worldConfig := NewWorldConfig(heightmap, 1.0) log.Printf("World configured: %dx%d samples, %.1fx%.1f units, bounds: (%.1f,%.1f) to (%.1f,%.1f)", worldConfig.SamplesPerSide, worldConfig.SamplesPerSide, worldConfig.WorldWidth, worldConfig.WorldHeight, worldConfig.MinBounds.X, worldConfig.MinBounds.Z, worldConfig.MaxBounds.X, worldConfig.MaxBounds.Z) server := &Server{ conn: conn, players: make(map[uint32]*Player), usersByName: make(map[string]*Player), userData: make(map[string]*UserData), heightmap: heightmap, worldConfig: worldConfig, nextID: 0, timeOfDay: 0.0, startTime: time.Now(), } 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(10 * time.Second) defer ticker.Stop() for range ticker.C { s.checkTimeouts() } }() // Start time of day updater and broadcaster go func() { ticker := time.NewTicker(100 * time.Millisecond) // Update time 10 times per second defer ticker.Stop() for range ticker.C { s.updateAndBroadcastTime() } }() 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) case MSG_HEARTBEAT: s.handleHeartbeat(buffer[:n], addr) } } } // updateAndBroadcastTime updates the server time and broadcasts it to all clients func (s *Server) updateAndBroadcastTime() { // Calculate elapsed time since server start elapsed := time.Since(s.startTime).Seconds() // Day cycle duration in seconds (e.g., 10 minutes = 600 seconds) dayDuration := 600.0 // Calculate time of day (0.0 to 1.0, where 0.5 is noon) s.timeOfDay = float32(math.Mod(elapsed/dayDuration, 1.0)) // Broadcast to all connected players msg := EncodeTimeSyncPacket(s.timeOfDay) s.mutex.RLock() for _, p := range s.players { if p.Address != nil { s.conn.WriteToUDP(msg, p.Address) } } s.mutex.RUnlock() } 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] // Spawn within world bounds x := rand.Float32()*s.worldConfig.WorldWidth - s.worldConfig.WorldWidth/2 z := rand.Float32()*s.worldConfig.WorldHeight - s.worldConfig.WorldHeight/2 y := s.worldConfig.GetHeightAt(s.heightmap, 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) } // Send current time of day to new player timeMsg := EncodeTimeSyncPacket(s.timeOfDay) s.conn.WriteToUDP(timeMsg, 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, addr *net.UDPAddr) { playerID, delta, ok := DecodeMovePacket(data) if !ok { return } s.mutex.Lock() player, exists := s.players[playerID] if !exists { s.mutex.Unlock() return } // Update the player's address in case it changed (NAT, port change, etc.) player.Address = addr // Server-authoritative movement deltaTime := float32(0.016) // 60fps newPos := Vec3{ X: player.Position.X + delta.X*15.0*deltaTime, Y: player.Position.Y, Z: player.Position.Z + delta.Z*15.0*deltaTime, } // Clamp to world bounds using WorldConfig newPos = s.worldConfig.ClampPosition(newPos) // Set Y to terrain height with smoothing targetY := s.worldConfig.GetHeightAt(s.heightmap, newPos.X, newPos.Z) + 1.0 smoothFactor := float32(0.15) newPos.Y = player.Position.Y + (targetY-player.Position.Y)*smoothFactor player.Position = newPos 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, addr *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 } // Update the player's address and last seen time player.Address = addr player.LastSeen = time.Now() 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) } // getHeightAt is deprecated - use WorldConfig.GetHeightAt instead 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) handleHeartbeat(data []byte, addr *net.UDPAddr) { playerID, ok := DecodeHeartbeatPacket(data) if !ok { return } s.mutex.Lock() player, exists := s.players[playerID] if !exists { s.mutex.Unlock() return } // Update the player's address and last seen time player.Address = addr player.LastSeen = time.Now() s.mutex.Unlock() } 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) } }