1
0
game/server/net/server.go

496 lines
11 KiB
Go

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(10 * 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)
case MSG_HEARTBEAT:
s.handleHeartbeat(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, 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
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, 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)
}
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) 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)
}
}