354 lines
7.3 KiB
Go
354 lines
7.3 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
|
|
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)
|
|
}
|