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 Position Vec3 Velocity Vec3 Color string Address *net.UDPAddr LastSeen time.Time } // Server manages the game state and networking type Server struct { conn *net.UDPConn players map[uint32]*Player 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), heightmap: heightmap, nextID: 0, } server.loadPlayerPositions() 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.savePlayerPositions() } }() // 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(addr) case MSG_MOVE: s.handleMove(buffer[:n], addr) case MSG_CHANGE_COLOR: s.handleColorChange(buffer[:n], addr) } } } func (s *Server) handleLogin(addr *net.UDPAddr) { s.mutex.Lock() s.nextID++ playerID := s.nextID // Assign color based on player ID colors := []string{"red", "green", "orange", "purple", "white"} colorIndex := (playerID - 1) % uint32(len(colors)) color := colors[colorIndex] // Spawn at random position on heightmap x := rand.Float32()*100 - 50 z := rand.Float32()*100 - 50 y := s.getHeightAt(x, z) + 1.0 player := &Player{ ID: playerID, Position: Vec3{x, y, z}, Color: 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.mutex.Unlock() // Send spawn message with color spawnMsg := EncodeSpawnPacket(playerID, player.Position, 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 %d logged in at (%.2f, %.2f, %.2f) with color %s", playerID, x, y, z, color) s.savePlayerPositions() } 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() s.mutex.Unlock() // Broadcast position update to all players s.broadcastUpdate(player) } 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 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 { delete(s.players, id) go s.broadcastPlayerLeft(id) log.Printf("Player %d timed out", id) } } } func (s *Server) loadPlayerPositions() { data, err := os.ReadFile("players.json") if err != nil { return } var savedPlayers map[uint32]Vec3 json.Unmarshal(data, &savedPlayers) for id, pos := range savedPlayers { if id > s.nextID { s.nextID = id } s.players[id] = &Player{ ID: id, Position: pos, LastSeen: time.Now(), } } } func (s *Server) savePlayerPositions() { s.mutex.RLock() savedPlayers := make(map[uint32]Vec3) for id, player := range s.players { savedPlayers[id] = player.Position } s.mutex.RUnlock() data, _ := json.Marshal(savedPlayers) os.WriteFile("players.json", data, 0644) }