469 lines
11 KiB
Go
469 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(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)
|
|
}
|
|
}
|