package main import ( "encoding/hex" "flag" "fmt" "log" "math" "net" "server/internal/db" "server/internal/net/packets" "sync" "time" ) type WorldServer struct { conn *net.UDPConn database *db.Database serverID string clients map[uint32]*Client clientLock sync.RWMutex world *packets.WorldConfig heightmap [][]float32 timeOfDay float32 nextID uint32 } type Client struct { ID uint32 PlayerID int64 Username string Addr *net.UDPAddr Position packets.Vec3 Velocity packets.Vec3 LastUpdate time.Time Authed bool } func main() { var ( port = flag.String("port", "9999", "World server port") dbDSN = flag.String("db", "user:password@tcp(localhost:3306)/game", "Database DSN") ) flag.Parse() database, err := db.New(*dbDSN) if err != nil { log.Fatalf("Failed to connect to database: %v", err) } defer database.Close() heightmap := generateDefaultHeightmap(257) worldConfig := packets.NewWorldConfig(heightmap, 1.0) server := &WorldServer{ database: database, serverID: generateServerID(), clients: make(map[uint32]*Client), world: worldConfig, heightmap: heightmap, timeOfDay: 12.0, nextID: 1, } addr, err := net.ResolveUDPAddr("udp", ":"+*port) if err != nil { log.Fatalf("Failed to resolve address: %v", err) } conn, err := net.ListenUDP("udp", addr) if err != nil { log.Fatalf("Failed to start world server: %v", err) } server.conn = conn log.Printf("World server started on UDP port %s (ID: %s)", *port, server.serverID) log.Printf("World bounds: Min(%v), Max(%v)", server.world.MinBounds, server.world.MaxBounds) go server.gameLoop() go server.updateTimeOfDay() // Main packet processing loop buffer := make([]byte, 1024) for { n, clientAddr, err := conn.ReadFromUDP(buffer) if err != nil { log.Printf("Read error: %v", err) continue } go server.handlePacket(buffer[:n], clientAddr) } } func (s *WorldServer) handlePacket(data []byte, addr *net.UDPAddr) { if len(data) == 0 { return } msgType := data[0] switch msgType { case packets.MSG_AUTH: s.handleAuth(data, addr) case packets.MSG_MOVE: s.handleMove(data, addr) case packets.MSG_HEARTBEAT: s.handleHeartbeat(data, addr) case packets.MSG_LOGOUT: s.handleLogout(data, addr) } } func (s *WorldServer) handleAuth(data []byte, addr *net.UDPAddr) { tokenBytes, ok := packets.DecodeAuth(data) if !ok { response := packets.EncodeAuthResponse(false, "Invalid auth packet") s.conn.WriteToUDP(response, addr) return } tokenHex := hex.EncodeToString(tokenBytes) session, err := s.database.ValidateSession(tokenHex) if err != nil { response := packets.EncodeAuthResponse(false, "Invalid or expired session") s.conn.WriteToUDP(response, addr) return } player, err := s.database.GetPlayerByID(session.PlayerID) if err != nil { response := packets.EncodeAuthResponse(false, "Player not found") s.conn.WriteToUDP(response, addr) return } // Send auth success response := packets.EncodeAuthResponse(true, "Authenticated") s.conn.WriteToUDP(response, addr) // Get saved position or default position, _ := s.database.GetPlayerPosition(player.ID) // Create client s.clientLock.Lock() clientID := s.nextID s.nextID++ client := &Client{ ID: clientID, PlayerID: player.ID, Username: player.Username, Addr: addr, Position: packets.Vec3{X: position.X, Y: position.Y, Z: position.Z}, LastUpdate: time.Now(), Authed: true, } s.clients[clientID] = client s.clientLock.Unlock() // Send spawn packet spawnMsg := packets.EncodeSpawnPacket(clientID, client.Position) s.conn.WriteToUDP(spawnMsg, addr) // Send time sync timeMsg := packets.EncodeTimeSyncPacket(s.timeOfDay) s.conn.WriteToUDP(timeMsg, addr) // Send existing players to new client s.sendPlayerList(client) // Broadcast new player to others s.broadcastPlayerJoined(client) log.Printf("Player %s (ID: %d) joined the world", player.Username, clientID) } func (s *WorldServer) handleMove(data []byte, addr *net.UDPAddr) { playerID, delta, ok := packets.DecodeMovePacket(data) if !ok { return } s.clientLock.Lock() client, exists := s.clients[playerID] if !exists || !client.Authed { s.clientLock.Unlock() return } // Update position with delta (server authoritative) moveSpeed := float32(10.0) deltaTime := float32(0.016) // Assume 60 FPS client.Position.X += delta.X * moveSpeed * deltaTime client.Position.Z += delta.Z * moveSpeed * deltaTime // Clamp to world bounds client.Position = s.world.ClampPosition(client.Position) // Check terrain height terrainHeight := s.world.GetHeightAt(s.heightmap, client.Position.X, client.Position.Z) if client.Position.Y < terrainHeight { client.Position.Y = terrainHeight } client.LastUpdate = time.Now() s.clientLock.Unlock() // Send position update back to client updateMsg := packets.EncodeUpdatePacket(playerID, client.Position) s.conn.WriteToUDP(updateMsg, addr) // Broadcast to other players s.broadcastMovement(client) } func (s *WorldServer) handleHeartbeat(data []byte, addr *net.UDPAddr) { playerID, ok := packets.DecodeHeartbeatPacket(data) if !ok { return } s.clientLock.Lock() if client, exists := s.clients[playerID]; exists { client.LastUpdate = time.Now() } s.clientLock.Unlock() } func (s *WorldServer) handleLogout(data []byte, addr *net.UDPAddr) { playerID, ok := packets.DecodeLogoutPacket(data) if !ok { return } s.clientLock.Lock() client, exists := s.clients[playerID] if !exists { s.clientLock.Unlock() return } // Save position before removing s.database.SavePlayerPosition(client.PlayerID, &db.Position{ X: client.Position.X, Y: client.Position.Y, Z: client.Position.Z, World: "main", }) delete(s.clients, playerID) s.clientLock.Unlock() // Broadcast player left leftMsg := packets.EncodePlayerLeftPacket(playerID) s.broadcast(leftMsg, playerID) log.Printf("Player %s (ID: %d) logged out", client.Username, playerID) } func (s *WorldServer) sendPlayerList(newClient *Client) { s.clientLock.RLock() defer s.clientLock.RUnlock() for _, client := range s.clients { if client.ID != newClient.ID { msg := packets.EncodePlayerJoinedPacket(client.ID, client.Position, client.Username) s.conn.WriteToUDP(msg, newClient.Addr) } } } func (s *WorldServer) broadcastPlayerJoined(newClient *Client) { msg := packets.EncodePlayerJoinedPacket(newClient.ID, newClient.Position, newClient.Username) s.broadcast(msg, newClient.ID) } func (s *WorldServer) broadcastMovement(movedClient *Client) { msg := packets.EncodeUpdatePacket(movedClient.ID, movedClient.Position) s.broadcast(msg, movedClient.ID) } func (s *WorldServer) broadcast(msg []byte, excludeID uint32) { s.clientLock.RLock() defer s.clientLock.RUnlock() for _, client := range s.clients { if client.ID != excludeID && client.Authed { s.conn.WriteToUDP(msg, client.Addr) } } } func (s *WorldServer) gameLoop() { ticker := time.NewTicker(50 * time.Millisecond) defer ticker.Stop() for range ticker.C { now := time.Now() s.clientLock.Lock() for id, client := range s.clients { if now.Sub(client.LastUpdate) > 30*time.Second { // Save position before timeout s.database.SavePlayerPosition(client.PlayerID, &db.Position{ X: client.Position.X, Y: client.Position.Y, Z: client.Position.Z, World: "main", }) delete(s.clients, id) // Broadcast player left leftMsg := packets.EncodePlayerLeftPacket(id) s.broadcast(leftMsg, id) log.Printf("Player %s (ID: %d) timed out", client.Username, id) } } s.clientLock.Unlock() } } func (s *WorldServer) updateTimeOfDay() { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for range ticker.C { s.timeOfDay += 0.1 if s.timeOfDay >= 24.0 { s.timeOfDay -= 24.0 } // Broadcast time update msg := packets.EncodeTimeSyncPacket(s.timeOfDay) s.clientLock.RLock() for _, client := range s.clients { if client.Authed { s.conn.WriteToUDP(msg, client.Addr) } } s.clientLock.RUnlock() } } func generateDefaultHeightmap(size int) [][]float32 { heightmap := make([][]float32, size) for i := range heightmap { heightmap[i] = make([]float32, size) for j := range heightmap[i] { x := float64(j-size/2) / float64(size) * 10 z := float64(i-size/2) / float64(size) * 10 heightmap[i][j] = float32(10 * (math.Sin(x) + math.Cos(z))) } } return heightmap } func generateServerID() string { return fmt.Sprintf("world-%d", time.Now().Unix()) }