split networking on server
This commit is contained in:
parent
ebeae76cb0
commit
c05a27a621
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
511
server/main.go
511
server/main.go
@ -2,59 +2,19 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/png"
|
"image/png"
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"server/net"
|
||||||
"time"
|
|
||||||
"slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Vec3 struct {
|
const WorldSize = 100
|
||||||
X, Y, Z float32
|
|
||||||
}
|
|
||||||
|
|
||||||
type Player struct {
|
|
||||||
ID uint32
|
|
||||||
Position Vec3
|
|
||||||
Velocity Vec3
|
|
||||||
Color string
|
|
||||||
Address *net.UDPAddr
|
|
||||||
LastSeen time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type GameServer struct {
|
|
||||||
conn *net.UDPConn
|
|
||||||
players map[uint32]*Player
|
|
||||||
heightmap [][]float32
|
|
||||||
mutex sync.RWMutex
|
|
||||||
nextID uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
MSG_LOGIN = 0x01
|
|
||||||
MSG_POSITION = 0x02
|
|
||||||
MSG_SPAWN = 0x03
|
|
||||||
MSG_MOVE = 0x04
|
|
||||||
MSG_UPDATE = 0x05
|
|
||||||
MSG_PLAYER_JOINED = 0x06
|
|
||||||
MSG_PLAYER_LEFT = 0x07
|
|
||||||
MSG_PLAYER_LIST = 0x08
|
|
||||||
MSG_CHANGE_COLOR = 0x09
|
|
||||||
MSG_COLOR_CHANGED = 0x0A
|
|
||||||
|
|
||||||
WORLD_SIZE = 100
|
|
||||||
WORLD_SCALE = 10.0
|
|
||||||
MOVE_SPEED = 15.0
|
|
||||||
GRAVITY = -9.8
|
|
||||||
PLAYER_HEIGHT = 1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
func generateHeightmap(size int) [][]float32 {
|
func generateHeightmap(size int) [][]float32 {
|
||||||
heightmap := make([][]float32, size)
|
heightmap := make([][]float32, size)
|
||||||
@ -63,19 +23,19 @@ func generateHeightmap(size int) [][]float32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Simple perlin-like noise
|
// Simple perlin-like noise
|
||||||
for y := 0; y < size; y++ {
|
for y := range heightmap {
|
||||||
for x := 0; x < size; x++ {
|
for x := range heightmap[y] {
|
||||||
nx := float64(x) / float64(size) * 4
|
nx := float64(x) / float64(size) * 4
|
||||||
ny := float64(y) / float64(size) * 4
|
ny := float64(y) / float64(size) * 4
|
||||||
heightmap[y][x] = float32(
|
heightmap[y][x] = float32(
|
||||||
math.Sin(nx*2+rand.Float64()) * 0.5 +
|
math.Sin(nx*2+rand.Float64())*0.5+
|
||||||
math.Cos(ny*3+rand.Float64()) * 0.3 +
|
math.Cos(ny*3+rand.Float64())*0.3+
|
||||||
rand.Float64() * 0.2) * 10.0
|
rand.Float64()*0.2) * 10.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smooth the heightmap
|
// Smooth the heightmap
|
||||||
for i := 0; i < 3; i++ {
|
for range 3 {
|
||||||
newHeightmap := make([][]float32, size)
|
newHeightmap := make([][]float32, size)
|
||||||
for y := range newHeightmap {
|
for y := range newHeightmap {
|
||||||
newHeightmap[y] = make([]float32, size)
|
newHeightmap[y] = make([]float32, size)
|
||||||
@ -100,7 +60,7 @@ func generateHeightmap(size int) [][]float32 {
|
|||||||
return heightmap
|
return heightmap
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveHeightmapPNG(heightmap [][]float32, filename string) {
|
func saveHeightmapPNG(heightmap [][]float32, filename string) error {
|
||||||
size := len(heightmap)
|
size := len(heightmap)
|
||||||
img := image.NewGray(image.Rect(0, 0, size, size))
|
img := image.NewGray(image.Rect(0, 0, size, size))
|
||||||
|
|
||||||
@ -117,393 +77,124 @@ func saveHeightmapPNG(heightmap [][]float32, filename string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply normalized values to image
|
||||||
for y := range heightmap {
|
for y := range heightmap {
|
||||||
for x := range heightmap[y] {
|
for x := range heightmap[y] {
|
||||||
normalized := (heightmap[y][x] - minH) / (maxH - minH)
|
normalized := (heightmap[y][x] - minH) / (maxH - minH)
|
||||||
img.SetGray(x, y, color.Gray{uint8(normalized * 255)})
|
img.SetGray(x, y, color.Gray{Y: uint8(normalized * 255)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file, _ := os.Create(filename)
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create file %s: %w", filename, err)
|
||||||
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
png.Encode(file, img)
|
|
||||||
|
if err := png.Encode(file, img); err != nil {
|
||||||
|
return fmt.Errorf("failed to encode PNG: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveHeightmapBinary(heightmap [][]float32, filename string) {
|
func saveHeightmapBinary(heightmap [][]float32, filename string) error {
|
||||||
size := len(heightmap)
|
size := len(heightmap)
|
||||||
file, _ := os.Create(filename)
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create file %s: %w", filename, err)
|
||||||
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
binary.Write(file, binary.LittleEndian, int32(size))
|
if err := binary.Write(file, binary.LittleEndian, int32(size)); err != nil {
|
||||||
|
return fmt.Errorf("failed to write size: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
for y := range heightmap {
|
for y := range heightmap {
|
||||||
for x := range heightmap[y] {
|
for x := range heightmap[y] {
|
||||||
binary.Write(file, binary.LittleEndian, heightmap[y][x])
|
if err := binary.Write(file, binary.LittleEndian, heightmap[y][x]); err != nil {
|
||||||
|
return fmt.Errorf("failed to write heightmap data: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GameServer) getHeightAt(x, z float32) float32 {
|
// loadHeightmapBinary loads a heightmap from a binary file
|
||||||
// Convert world coords to heightmap coords with bilinear interpolation
|
func loadHeightmapBinary(filename string) ([][]float32, error) {
|
||||||
size := float32(len(s.heightmap))
|
file, err := os.Open(filename)
|
||||||
fx := (x/WORLD_SIZE + 0.5) * (size - 1)
|
|
||||||
fz := (z/WORLD_SIZE + 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 *GameServer) loadPlayerPositions() {
|
|
||||||
data, err := os.ReadFile("players.json")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, fmt.Errorf("failed to open file %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var size int32
|
||||||
|
if err := binary.Read(file, binary.LittleEndian, &size); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read size: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var savedPlayers map[uint32]Vec3
|
heightmap := make([][]float32, size)
|
||||||
json.Unmarshal(data, &savedPlayers)
|
for y := range heightmap {
|
||||||
|
heightmap[y] = make([]float32, size)
|
||||||
for id, pos := range savedPlayers {
|
for x := range heightmap[y] {
|
||||||
if id > s.nextID {
|
if err := binary.Read(file, binary.LittleEndian, &heightmap[y][x]); err != nil {
|
||||||
s.nextID = id
|
return nil, fmt.Errorf("failed to read heightmap data: %w", err)
|
||||||
}
|
}
|
||||||
s.players[id] = &Player{
|
|
||||||
ID: id,
|
|
||||||
Position: pos,
|
|
||||||
LastSeen: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameServer) sendPlayerList(addr *net.UDPAddr, players []*Player) {
|
|
||||||
if len(players) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := make([]byte, 1024)
|
|
||||||
msg[0] = MSG_PLAYER_LIST
|
|
||||||
msg[1] = uint8(len(players))
|
|
||||||
|
|
||||||
offset := 2
|
|
||||||
for _, p := range players {
|
|
||||||
binary.LittleEndian.PutUint32(msg[offset:], p.ID)
|
|
||||||
binary.LittleEndian.PutUint32(msg[offset+4:], math.Float32bits(p.Position.X))
|
|
||||||
binary.LittleEndian.PutUint32(msg[offset+8:], math.Float32bits(p.Position.Y))
|
|
||||||
binary.LittleEndian.PutUint32(msg[offset+12:], math.Float32bits(p.Position.Z))
|
|
||||||
|
|
||||||
colorBytes := []byte(p.Color)
|
|
||||||
msg[offset+16] = uint8(len(colorBytes))
|
|
||||||
copy(msg[offset+17:], colorBytes)
|
|
||||||
|
|
||||||
offset += 17 + len(colorBytes)
|
|
||||||
if offset > 1000 {
|
|
||||||
break // Prevent overflow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.conn.WriteToUDP(msg[:offset], addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameServer) broadcastPlayerJoined(newPlayer *Player) {
|
|
||||||
colorBytes := []byte(newPlayer.Color)
|
|
||||||
msg := make([]byte, 18+len(colorBytes))
|
|
||||||
msg[0] = MSG_PLAYER_JOINED
|
|
||||||
binary.LittleEndian.PutUint32(msg[1:5], newPlayer.ID)
|
|
||||||
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(newPlayer.Position.X))
|
|
||||||
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(newPlayer.Position.Y))
|
|
||||||
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(newPlayer.Position.Z))
|
|
||||||
msg[17] = uint8(len(colorBytes))
|
|
||||||
copy(msg[18:], colorBytes)
|
|
||||||
|
|
||||||
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 *GameServer) broadcastPlayerLeft(playerID uint32) {
|
|
||||||
msg := make([]byte, 5)
|
|
||||||
msg[0] = MSG_PLAYER_LEFT
|
|
||||||
binary.LittleEndian.PutUint32(msg[1:5], 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 *GameServer) broadcastUpdate(player *Player) {
|
|
||||||
msg := make([]byte, 17)
|
|
||||||
msg[0] = MSG_UPDATE
|
|
||||||
binary.LittleEndian.PutUint32(msg[1:5], player.ID)
|
|
||||||
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(player.Position.X))
|
|
||||||
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(player.Position.Y))
|
|
||||||
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(player.Position.Z))
|
|
||||||
|
|
||||||
s.mutex.RLock()
|
|
||||||
for _, p := range s.players {
|
|
||||||
if p.Address != nil {
|
|
||||||
s.conn.WriteToUDP(msg, p.Address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mutex.RUnlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameServer) handleColorChange(data []byte, addr *net.UDPAddr) {
|
|
||||||
if len(data) < 6 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
playerID := binary.LittleEndian.Uint32(data[1:5])
|
|
||||||
colorLen := data[5]
|
|
||||||
|
|
||||||
if len(data) < 6+int(colorLen) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newColor := string(data[6 : 6+colorLen])
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
fmt.Printf("Player %d changed color to %s\n", playerID, newColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameServer) broadcastColorChanged(playerID uint32, color string) {
|
|
||||||
colorBytes := []byte(color)
|
|
||||||
msg := make([]byte, 6+len(colorBytes))
|
|
||||||
msg[0] = MSG_COLOR_CHANGED
|
|
||||||
binary.LittleEndian.PutUint32(msg[1:5], playerID)
|
|
||||||
msg[5] = uint8(len(colorBytes))
|
|
||||||
copy(msg[6:], colorBytes)
|
|
||||||
|
|
||||||
s.mutex.RLock()
|
|
||||||
for _, p := range s.players {
|
|
||||||
if p.Address != nil {
|
|
||||||
s.conn.WriteToUDP(msg, p.Address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mutex.RUnlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameServer) 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameServer) handleLogin(addr *net.UDPAddr) {
|
|
||||||
s.mutex.Lock()
|
|
||||||
s.nextID++
|
|
||||||
playerID := s.nextID
|
|
||||||
|
|
||||||
// Assign color based on player ID to ensure variety
|
|
||||||
colors := []string{"red", "green", "orange", "purple", "white"}
|
|
||||||
// Cycle through colors based on player ID
|
|
||||||
colorIndex := (playerID - 1) % uint32(len(colors))
|
|
||||||
color := colors[colorIndex]
|
|
||||||
|
|
||||||
// Spawn at random position on heightmap
|
|
||||||
x := rand.Float32() * WORLD_SIZE - WORLD_SIZE/2
|
|
||||||
z := rand.Float32() * WORLD_SIZE - WORLD_SIZE/2
|
|
||||||
y := s.getHeightAt(x, z) + PLAYER_HEIGHT
|
|
||||||
|
|
||||||
player := &Player{
|
|
||||||
ID: playerID,
|
|
||||||
Position: Vec3{x, y, z},
|
|
||||||
Color: color,
|
|
||||||
Address: addr,
|
|
||||||
LastSeen: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send existing players to new player
|
|
||||||
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
|
|
||||||
colorBytes := []byte(color)
|
|
||||||
msg := make([]byte, 18+len(colorBytes))
|
|
||||||
msg[0] = MSG_SPAWN
|
|
||||||
binary.LittleEndian.PutUint32(msg[1:5], playerID)
|
|
||||||
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(player.Position.X))
|
|
||||||
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(player.Position.Y))
|
|
||||||
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(player.Position.Z))
|
|
||||||
msg[17] = uint8(len(colorBytes))
|
|
||||||
copy(msg[18:], colorBytes)
|
|
||||||
|
|
||||||
s.conn.WriteToUDP(msg, addr)
|
|
||||||
|
|
||||||
// Send player list to new player
|
|
||||||
s.sendPlayerList(addr, existingPlayers)
|
|
||||||
|
|
||||||
// Notify other players about new player
|
|
||||||
s.broadcastPlayerJoined(player)
|
|
||||||
|
|
||||||
fmt.Printf("Player %d logged in at (%.2f, %.2f, %.2f) with color %s\n", playerID, x, y, z, color)
|
|
||||||
|
|
||||||
s.savePlayerPositions()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameServer) handleMove(data []byte, addr *net.UDPAddr) {
|
|
||||||
if len(data) < 17 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
playerID := binary.LittleEndian.Uint32(data[1:5])
|
|
||||||
dx := math.Float32frombits(binary.LittleEndian.Uint32(data[5:9]))
|
|
||||||
// dy := math.Float32frombits(binary.LittleEndian.Uint32(data[9:13])) // Not used - Y position is determined by terrain height
|
|
||||||
dz := math.Float32frombits(binary.LittleEndian.Uint32(data[13:17]))
|
|
||||||
|
|
||||||
s.mutex.Lock()
|
|
||||||
player, exists := s.players[playerID]
|
|
||||||
if !exists {
|
|
||||||
s.mutex.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server-authoritative movement - server decides the actual speed
|
|
||||||
// dx/dz from client are just normalized direction vectors
|
|
||||||
deltaTime := float32(0.016) // Assume 60fps for now
|
|
||||||
newX := player.Position.X + dx * MOVE_SPEED * deltaTime
|
|
||||||
newZ := player.Position.Z + dz * MOVE_SPEED * deltaTime
|
|
||||||
|
|
||||||
// Clamp to world bounds
|
|
||||||
newX = float32(math.Max(float64(-WORLD_SIZE/2), math.Min(float64(WORLD_SIZE/2), float64(newX))))
|
|
||||||
newZ = float32(math.Max(float64(-WORLD_SIZE/2), math.Min(float64(WORLD_SIZE/2), float64(newZ))))
|
|
||||||
|
|
||||||
// Set Y to terrain height with some smoothing
|
|
||||||
targetY := s.getHeightAt(newX, newZ) + PLAYER_HEIGHT
|
|
||||||
// Smooth the Y transition
|
|
||||||
smoothFactor := float32(0.15) // How quickly to adapt to new height
|
|
||||||
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 *GameServer) run() {
|
|
||||||
buffer := make([]byte, 1024)
|
|
||||||
|
|
||||||
// Periodic save
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(10 * time.Second)
|
|
||||||
for range ticker.C {
|
|
||||||
s.savePlayerPositions()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
n, addr, err := s.conn.ReadFromUDP(buffer)
|
|
||||||
if err != nil {
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return heightmap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Generate and save heightmap
|
// Parse command-line flags
|
||||||
fmt.Println("Generating heightmap...")
|
var (
|
||||||
heightmap := generateHeightmap(WORLD_SIZE)
|
port = flag.String("port", "9999", "UDP port to listen on")
|
||||||
saveHeightmapPNG(heightmap, "../assets/heightmap.png")
|
worldSize = flag.Int("size", WorldSize, "World size for heightmap generation")
|
||||||
saveHeightmapBinary(heightmap, "../assets/heightmap.bin")
|
skipGen = flag.Bool("skip-gen", false, "Skip heightmap generation")
|
||||||
|
assetsPath = flag.String("assets", "../assets", "Path to assets directory")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
// Start UDP server
|
// Setup logging
|
||||||
addr, _ := net.ResolveUDPAddr("udp", ":9999")
|
log.SetPrefix("[GameServer] ")
|
||||||
conn, err := net.ListenUDP("udp", addr)
|
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
|
||||||
|
|
||||||
|
var heightmap [][]float32
|
||||||
|
|
||||||
|
if !*skipGen {
|
||||||
|
// Generate and save heightmap
|
||||||
|
log.Printf("Generating %dx%d heightmap...", *worldSize, *worldSize)
|
||||||
|
heightmap = generateHeightmap(*worldSize)
|
||||||
|
|
||||||
|
pngPath := fmt.Sprintf("%s/heightmap.png", *assetsPath)
|
||||||
|
if err := saveHeightmapPNG(heightmap, pngPath); err != nil {
|
||||||
|
log.Printf("Warning: Failed to save PNG heightmap: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Saved heightmap PNG to %s", pngPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
binPath := fmt.Sprintf("%s/heightmap.bin", *assetsPath)
|
||||||
|
if err := saveHeightmapBinary(heightmap, binPath); err != nil {
|
||||||
|
log.Fatalf("Failed to save binary heightmap: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Saved heightmap binary to %s", binPath)
|
||||||
|
} else {
|
||||||
|
// Load existing heightmap
|
||||||
|
binPath := fmt.Sprintf("%s/heightmap.bin", *assetsPath)
|
||||||
|
log.Printf("Loading existing heightmap from %s", binPath)
|
||||||
|
var err error
|
||||||
|
heightmap, err = loadHeightmapBinary(binPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load heightmap: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := net.NewServer(*port, heightmap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatalf("Failed to create server: %v", err)
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
server := &GameServer{
|
|
||||||
conn: conn,
|
|
||||||
players: make(map[uint32]*Player),
|
|
||||||
heightmap: heightmap,
|
|
||||||
nextID: 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.loadPlayerPositions()
|
log.Printf("Starting game server on port %s", *port)
|
||||||
|
if err := server.Run(); err != nil {
|
||||||
fmt.Println("Server running on :9999")
|
log.Fatalf("Server failed: %v", err)
|
||||||
server.run()
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
144
server/net/packets.go
Normal file
144
server/net/packets.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message type constants
|
||||||
|
const (
|
||||||
|
MSG_LOGIN = 0x01
|
||||||
|
MSG_POSITION = 0x02
|
||||||
|
MSG_SPAWN = 0x03
|
||||||
|
MSG_MOVE = 0x04
|
||||||
|
MSG_UPDATE = 0x05
|
||||||
|
MSG_PLAYER_JOINED = 0x06
|
||||||
|
MSG_PLAYER_LEFT = 0x07
|
||||||
|
MSG_PLAYER_LIST = 0x08
|
||||||
|
MSG_CHANGE_COLOR = 0x09
|
||||||
|
MSG_COLOR_CHANGED = 0x0A
|
||||||
|
)
|
||||||
|
|
||||||
|
// Vec3 represents a 3D vector
|
||||||
|
type Vec3 struct {
|
||||||
|
X, Y, Z float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeSpawnPacket creates a spawn packet
|
||||||
|
func EncodeSpawnPacket(playerID uint32, position Vec3, color string) []byte {
|
||||||
|
colorBytes := []byte(color)
|
||||||
|
msg := make([]byte, 18+len(colorBytes))
|
||||||
|
msg[0] = MSG_SPAWN
|
||||||
|
binary.LittleEndian.PutUint32(msg[1:5], playerID)
|
||||||
|
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(position.X))
|
||||||
|
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(position.Y))
|
||||||
|
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(position.Z))
|
||||||
|
msg[17] = uint8(len(colorBytes))
|
||||||
|
copy(msg[18:], colorBytes)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeUpdatePacket creates an update packet
|
||||||
|
func EncodeUpdatePacket(playerID uint32, position Vec3) []byte {
|
||||||
|
msg := make([]byte, 17)
|
||||||
|
msg[0] = MSG_UPDATE
|
||||||
|
binary.LittleEndian.PutUint32(msg[1:5], playerID)
|
||||||
|
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(position.X))
|
||||||
|
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(position.Y))
|
||||||
|
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(position.Z))
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodePlayerJoinedPacket creates a player joined packet
|
||||||
|
func EncodePlayerJoinedPacket(playerID uint32, position Vec3, color string) []byte {
|
||||||
|
colorBytes := []byte(color)
|
||||||
|
msg := make([]byte, 18+len(colorBytes))
|
||||||
|
msg[0] = MSG_PLAYER_JOINED
|
||||||
|
binary.LittleEndian.PutUint32(msg[1:5], playerID)
|
||||||
|
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(position.X))
|
||||||
|
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(position.Y))
|
||||||
|
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(position.Z))
|
||||||
|
msg[17] = uint8(len(colorBytes))
|
||||||
|
copy(msg[18:], colorBytes)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodePlayerLeftPacket creates a player left packet
|
||||||
|
func EncodePlayerLeftPacket(playerID uint32) []byte {
|
||||||
|
msg := make([]byte, 5)
|
||||||
|
msg[0] = MSG_PLAYER_LEFT
|
||||||
|
binary.LittleEndian.PutUint32(msg[1:5], playerID)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeColorChangedPacket creates a color changed packet
|
||||||
|
func EncodeColorChangedPacket(playerID uint32, color string) []byte {
|
||||||
|
colorBytes := []byte(color)
|
||||||
|
msg := make([]byte, 6+len(colorBytes))
|
||||||
|
msg[0] = MSG_COLOR_CHANGED
|
||||||
|
binary.LittleEndian.PutUint32(msg[1:5], playerID)
|
||||||
|
msg[5] = uint8(len(colorBytes))
|
||||||
|
copy(msg[6:], colorBytes)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodePlayerListPacket creates a player list packet
|
||||||
|
func EncodePlayerListPacket(players []*Player) []byte {
|
||||||
|
if len(players) == 0 {
|
||||||
|
return []byte{MSG_PLAYER_LIST, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := make([]byte, 1024)
|
||||||
|
msg[0] = MSG_PLAYER_LIST
|
||||||
|
msg[1] = uint8(len(players))
|
||||||
|
|
||||||
|
offset := 2
|
||||||
|
for _, p := range players {
|
||||||
|
binary.LittleEndian.PutUint32(msg[offset:], p.ID)
|
||||||
|
binary.LittleEndian.PutUint32(msg[offset+4:], math.Float32bits(p.Position.X))
|
||||||
|
binary.LittleEndian.PutUint32(msg[offset+8:], math.Float32bits(p.Position.Y))
|
||||||
|
binary.LittleEndian.PutUint32(msg[offset+12:], math.Float32bits(p.Position.Z))
|
||||||
|
|
||||||
|
colorBytes := []byte(p.Color)
|
||||||
|
msg[offset+16] = uint8(len(colorBytes))
|
||||||
|
copy(msg[offset+17:], colorBytes)
|
||||||
|
|
||||||
|
offset += 17 + len(colorBytes)
|
||||||
|
if offset > 1000 {
|
||||||
|
break // Prevent overflow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg[:offset]
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeMovePacket decodes a move packet
|
||||||
|
func DecodeMovePacket(data []byte) (playerID uint32, delta Vec3, ok bool) {
|
||||||
|
if len(data) < 17 {
|
||||||
|
return 0, Vec3{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
playerID = binary.LittleEndian.Uint32(data[1:5])
|
||||||
|
delta.X = math.Float32frombits(binary.LittleEndian.Uint32(data[5:9]))
|
||||||
|
delta.Y = math.Float32frombits(binary.LittleEndian.Uint32(data[9:13]))
|
||||||
|
delta.Z = math.Float32frombits(binary.LittleEndian.Uint32(data[13:17]))
|
||||||
|
|
||||||
|
return playerID, delta, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeColorChangePacket decodes a color change packet
|
||||||
|
func DecodeColorChangePacket(data []byte) (playerID uint32, color string, ok bool) {
|
||||||
|
if len(data) < 6 {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
playerID = binary.LittleEndian.Uint32(data[1:5])
|
||||||
|
colorLen := data[5]
|
||||||
|
|
||||||
|
if len(data) < 6+int(colorLen) {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
color = string(data[6 : 6+colorLen])
|
||||||
|
return playerID, color, true
|
||||||
|
}
|
||||||
353
server/net/server.go
Normal file
353
server/net/server.go
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
{"1":{"X":-2.5774379,"Y":0.3485479,"Z":3.2305741},"2":{"X":-1.6390398,"Y":0.5682664,"Z":1.0276936}}
|
{"1":{"X":-13.046684,"Y":-0.008753866,"Z":15.9760895}}
|
||||||
Loading…
x
Reference in New Issue
Block a user