1
0
game/server/cmd/world/main.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())
}