517 lines
12 KiB
Go
517 lines
12 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
|
|
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)
|
|
}
|
|
}
|