361 lines
8.4 KiB
Go
361 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"net"
|
|
"server/internal/db"
|
|
"server/internal/net/packets"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type WorldServer struct {
|
|
conn *net.UDPConn
|
|
database *db.Database
|
|
serverID string
|
|
clients map[uint32]*Client
|
|
clientLock sync.RWMutex
|
|
world *packets.WorldConfig
|
|
heightmap [][]float32
|
|
timeOfDay float32
|
|
nextID uint32
|
|
}
|
|
|
|
type Client struct {
|
|
ID uint32
|
|
PlayerID int64
|
|
Username string
|
|
Addr *net.UDPAddr
|
|
Position packets.Vec3
|
|
Velocity packets.Vec3
|
|
LastUpdate time.Time
|
|
Authed bool
|
|
}
|
|
|
|
func main() {
|
|
var (
|
|
port = flag.String("port", "9999", "World server port")
|
|
dbDSN = flag.String("db", "user:password@tcp(localhost:3306)/game", "Database DSN")
|
|
)
|
|
flag.Parse()
|
|
|
|
database, err := db.New(*dbDSN)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to database: %v", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
heightmap := generateDefaultHeightmap(257)
|
|
worldConfig := packets.NewWorldConfig(heightmap, 1.0)
|
|
|
|
server := &WorldServer{
|
|
database: database,
|
|
serverID: generateServerID(),
|
|
clients: make(map[uint32]*Client),
|
|
world: worldConfig,
|
|
heightmap: heightmap,
|
|
timeOfDay: 12.0,
|
|
nextID: 1,
|
|
}
|
|
|
|
addr, err := net.ResolveUDPAddr("udp", ":"+*port)
|
|
if err != nil {
|
|
log.Fatalf("Failed to resolve address: %v", err)
|
|
}
|
|
|
|
conn, err := net.ListenUDP("udp", addr)
|
|
if err != nil {
|
|
log.Fatalf("Failed to start world server: %v", err)
|
|
}
|
|
server.conn = conn
|
|
|
|
log.Printf("World server started on UDP port %s (ID: %s)", *port, server.serverID)
|
|
log.Printf("World bounds: Min(%v), Max(%v)", server.world.MinBounds, server.world.MaxBounds)
|
|
|
|
go server.gameLoop()
|
|
go server.updateTimeOfDay()
|
|
|
|
// Main packet processing loop
|
|
buffer := make([]byte, 1024)
|
|
for {
|
|
n, clientAddr, err := conn.ReadFromUDP(buffer)
|
|
if err != nil {
|
|
log.Printf("Read error: %v", err)
|
|
continue
|
|
}
|
|
|
|
go server.handlePacket(buffer[:n], clientAddr)
|
|
}
|
|
}
|
|
|
|
func (s *WorldServer) handlePacket(data []byte, addr *net.UDPAddr) {
|
|
if len(data) == 0 {
|
|
return
|
|
}
|
|
|
|
msgType := data[0]
|
|
|
|
switch msgType {
|
|
case packets.MSG_AUTH:
|
|
s.handleAuth(data, addr)
|
|
case packets.MSG_MOVE:
|
|
s.handleMove(data, addr)
|
|
case packets.MSG_HEARTBEAT:
|
|
s.handleHeartbeat(data, addr)
|
|
case packets.MSG_LOGOUT:
|
|
s.handleLogout(data, addr)
|
|
}
|
|
}
|
|
|
|
func (s *WorldServer) handleAuth(data []byte, addr *net.UDPAddr) {
|
|
tokenBytes, ok := packets.DecodeAuth(data)
|
|
if !ok {
|
|
response := packets.EncodeAuthResponse(false, "Invalid auth packet")
|
|
s.conn.WriteToUDP(response, addr)
|
|
return
|
|
}
|
|
|
|
tokenHex := hex.EncodeToString(tokenBytes)
|
|
session, err := s.database.ValidateSession(tokenHex)
|
|
if err != nil {
|
|
response := packets.EncodeAuthResponse(false, "Invalid or expired session")
|
|
s.conn.WriteToUDP(response, addr)
|
|
return
|
|
}
|
|
|
|
player, err := s.database.GetPlayerByID(session.PlayerID)
|
|
if err != nil {
|
|
response := packets.EncodeAuthResponse(false, "Player not found")
|
|
s.conn.WriteToUDP(response, addr)
|
|
return
|
|
}
|
|
|
|
// Send auth success
|
|
response := packets.EncodeAuthResponse(true, "Authenticated")
|
|
s.conn.WriteToUDP(response, addr)
|
|
|
|
// Get saved position or default
|
|
position, _ := s.database.GetPlayerPosition(player.ID)
|
|
|
|
// Create client
|
|
s.clientLock.Lock()
|
|
clientID := s.nextID
|
|
s.nextID++
|
|
|
|
client := &Client{
|
|
ID: clientID,
|
|
PlayerID: player.ID,
|
|
Username: player.Username,
|
|
Addr: addr,
|
|
Position: packets.Vec3{X: position.X, Y: position.Y, Z: position.Z},
|
|
LastUpdate: time.Now(),
|
|
Authed: true,
|
|
}
|
|
s.clients[clientID] = client
|
|
s.clientLock.Unlock()
|
|
|
|
// Send spawn packet
|
|
spawnMsg := packets.EncodeSpawnPacket(clientID, client.Position)
|
|
s.conn.WriteToUDP(spawnMsg, addr)
|
|
|
|
// Send time sync
|
|
timeMsg := packets.EncodeTimeSyncPacket(s.timeOfDay)
|
|
s.conn.WriteToUDP(timeMsg, addr)
|
|
|
|
// Send existing players to new client
|
|
s.sendPlayerList(client)
|
|
|
|
// Broadcast new player to others
|
|
s.broadcastPlayerJoined(client)
|
|
|
|
log.Printf("Player %s (ID: %d) joined the world", player.Username, clientID)
|
|
}
|
|
|
|
func (s *WorldServer) handleMove(data []byte, addr *net.UDPAddr) {
|
|
playerID, delta, ok := packets.DecodeMovePacket(data)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
s.clientLock.Lock()
|
|
client, exists := s.clients[playerID]
|
|
if !exists || !client.Authed {
|
|
s.clientLock.Unlock()
|
|
return
|
|
}
|
|
|
|
// Update position with delta (server authoritative)
|
|
moveSpeed := float32(10.0)
|
|
deltaTime := float32(0.016) // Assume 60 FPS
|
|
|
|
client.Position.X += delta.X * moveSpeed * deltaTime
|
|
client.Position.Z += delta.Z * moveSpeed * deltaTime
|
|
|
|
// Clamp to world bounds
|
|
client.Position = s.world.ClampPosition(client.Position)
|
|
|
|
// Check terrain height
|
|
terrainHeight := s.world.GetHeightAt(s.heightmap, client.Position.X, client.Position.Z)
|
|
if client.Position.Y < terrainHeight {
|
|
client.Position.Y = terrainHeight
|
|
}
|
|
|
|
client.LastUpdate = time.Now()
|
|
s.clientLock.Unlock()
|
|
|
|
// Send position update back to client
|
|
updateMsg := packets.EncodeUpdatePacket(playerID, client.Position)
|
|
s.conn.WriteToUDP(updateMsg, addr)
|
|
|
|
// Broadcast to other players
|
|
s.broadcastMovement(client)
|
|
}
|
|
|
|
func (s *WorldServer) handleHeartbeat(data []byte, addr *net.UDPAddr) {
|
|
playerID, ok := packets.DecodeHeartbeatPacket(data)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
s.clientLock.Lock()
|
|
if client, exists := s.clients[playerID]; exists {
|
|
client.LastUpdate = time.Now()
|
|
}
|
|
s.clientLock.Unlock()
|
|
}
|
|
|
|
func (s *WorldServer) handleLogout(data []byte, addr *net.UDPAddr) {
|
|
playerID, ok := packets.DecodeLogoutPacket(data)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
s.clientLock.Lock()
|
|
client, exists := s.clients[playerID]
|
|
if !exists {
|
|
s.clientLock.Unlock()
|
|
return
|
|
}
|
|
|
|
// Save position before removing
|
|
s.database.SavePlayerPosition(client.PlayerID, &db.Position{
|
|
X: client.Position.X,
|
|
Y: client.Position.Y,
|
|
Z: client.Position.Z,
|
|
World: "main",
|
|
})
|
|
|
|
delete(s.clients, playerID)
|
|
s.clientLock.Unlock()
|
|
|
|
// Broadcast player left
|
|
leftMsg := packets.EncodePlayerLeftPacket(playerID)
|
|
s.broadcast(leftMsg, playerID)
|
|
|
|
log.Printf("Player %s (ID: %d) logged out", client.Username, playerID)
|
|
}
|
|
|
|
func (s *WorldServer) sendPlayerList(newClient *Client) {
|
|
s.clientLock.RLock()
|
|
defer s.clientLock.RUnlock()
|
|
|
|
for _, client := range s.clients {
|
|
if client.ID != newClient.ID {
|
|
msg := packets.EncodePlayerJoinedPacket(client.ID, client.Position, client.Username)
|
|
s.conn.WriteToUDP(msg, newClient.Addr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *WorldServer) broadcastPlayerJoined(newClient *Client) {
|
|
msg := packets.EncodePlayerJoinedPacket(newClient.ID, newClient.Position, newClient.Username)
|
|
s.broadcast(msg, newClient.ID)
|
|
}
|
|
|
|
func (s *WorldServer) broadcastMovement(movedClient *Client) {
|
|
msg := packets.EncodeUpdatePacket(movedClient.ID, movedClient.Position)
|
|
s.broadcast(msg, movedClient.ID)
|
|
}
|
|
|
|
func (s *WorldServer) broadcast(msg []byte, excludeID uint32) {
|
|
s.clientLock.RLock()
|
|
defer s.clientLock.RUnlock()
|
|
|
|
for _, client := range s.clients {
|
|
if client.ID != excludeID && client.Authed {
|
|
s.conn.WriteToUDP(msg, client.Addr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *WorldServer) gameLoop() {
|
|
ticker := time.NewTicker(50 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
now := time.Now()
|
|
s.clientLock.Lock()
|
|
for id, client := range s.clients {
|
|
if now.Sub(client.LastUpdate) > 30*time.Second {
|
|
// Save position before timeout
|
|
s.database.SavePlayerPosition(client.PlayerID, &db.Position{
|
|
X: client.Position.X,
|
|
Y: client.Position.Y,
|
|
Z: client.Position.Z,
|
|
World: "main",
|
|
})
|
|
delete(s.clients, id)
|
|
|
|
// Broadcast player left
|
|
leftMsg := packets.EncodePlayerLeftPacket(id)
|
|
s.broadcast(leftMsg, id)
|
|
|
|
log.Printf("Player %s (ID: %d) timed out", client.Username, id)
|
|
}
|
|
}
|
|
s.clientLock.Unlock()
|
|
}
|
|
}
|
|
|
|
func (s *WorldServer) updateTimeOfDay() {
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
s.timeOfDay += 0.1
|
|
if s.timeOfDay >= 24.0 {
|
|
s.timeOfDay -= 24.0
|
|
}
|
|
|
|
// Broadcast time update
|
|
msg := packets.EncodeTimeSyncPacket(s.timeOfDay)
|
|
s.clientLock.RLock()
|
|
for _, client := range s.clients {
|
|
if client.Authed {
|
|
s.conn.WriteToUDP(msg, client.Addr)
|
|
}
|
|
}
|
|
s.clientLock.RUnlock()
|
|
}
|
|
}
|
|
|
|
func generateDefaultHeightmap(size int) [][]float32 {
|
|
heightmap := make([][]float32, size)
|
|
for i := range heightmap {
|
|
heightmap[i] = make([]float32, size)
|
|
for j := range heightmap[i] {
|
|
x := float64(j-size/2) / float64(size) * 10
|
|
z := float64(i-size/2) / float64(size) * 10
|
|
heightmap[i][j] = float32(10 * (math.Sin(x) + math.Cos(z)))
|
|
}
|
|
}
|
|
return heightmap
|
|
}
|
|
|
|
func generateServerID() string {
|
|
return fmt.Sprintf("world-%d", time.Now().Unix())
|
|
} |