398 lines
8.8 KiB
Go
398 lines
8.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"net"
|
|
"server/internal/db"
|
|
"server/internal/net/packets"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type WorldServer struct {
|
|
listener net.Listener
|
|
database *db.Database
|
|
serverID string
|
|
clients map[int]*Client
|
|
clientLock sync.RWMutex
|
|
world *packets.WorldConfig
|
|
heightmap [][]float32
|
|
ticker *time.Ticker
|
|
timeOfDay float32
|
|
}
|
|
|
|
type Client struct {
|
|
ID int
|
|
PlayerID int64
|
|
Username string
|
|
Conn net.Conn
|
|
Encoder *json.Encoder
|
|
Decoder *json.Decoder
|
|
Position packets.Vec3
|
|
Rotation packets.Vec2
|
|
Velocity packets.Vec3
|
|
LastUpdate time.Time
|
|
}
|
|
|
|
type AuthRequest struct {
|
|
Type string `json:"type"`
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
type AuthResponse struct {
|
|
Type string `json:"type"`
|
|
Success bool `json:"success"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
func main() {
|
|
var (
|
|
port = flag.String("port", "8082", "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[int]*Client),
|
|
world: worldConfig,
|
|
heightmap: heightmap,
|
|
timeOfDay: 12.0,
|
|
}
|
|
|
|
listener, err := net.Listen("tcp", ":"+*port)
|
|
if err != nil {
|
|
log.Fatalf("Failed to start world server: %v", err)
|
|
}
|
|
server.listener = listener
|
|
|
|
log.Printf("World server started on 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()
|
|
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
log.Printf("Accept error: %v", err)
|
|
continue
|
|
}
|
|
go server.handleConnection(conn)
|
|
}
|
|
}
|
|
|
|
func (s *WorldServer) handleConnection(conn net.Conn) {
|
|
defer conn.Close()
|
|
|
|
decoder := json.NewDecoder(conn)
|
|
encoder := json.NewEncoder(conn)
|
|
|
|
var authReq AuthRequest
|
|
if err := decoder.Decode(&authReq); err != nil || authReq.Type != "auth" {
|
|
encoder.Encode(AuthResponse{
|
|
Type: "authResponse",
|
|
Success: false,
|
|
Message: "Authentication required",
|
|
})
|
|
return
|
|
}
|
|
|
|
session, err := s.database.ValidateSession(authReq.Token)
|
|
if err != nil {
|
|
encoder.Encode(AuthResponse{
|
|
Type: "authResponse",
|
|
Success: false,
|
|
Message: "Invalid or expired session",
|
|
})
|
|
return
|
|
}
|
|
|
|
player, err := s.database.GetPlayerByID(session.PlayerID)
|
|
if err != nil {
|
|
encoder.Encode(AuthResponse{
|
|
Type: "authResponse",
|
|
Success: false,
|
|
Message: "Player not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
encoder.Encode(AuthResponse{
|
|
Type: "authResponse",
|
|
Success: true,
|
|
})
|
|
|
|
position, _ := s.database.GetPlayerPosition(player.ID)
|
|
|
|
client := &Client{
|
|
ID: s.getNextClientID(),
|
|
PlayerID: player.ID,
|
|
Username: player.Username,
|
|
Conn: conn,
|
|
Encoder: encoder,
|
|
Decoder: decoder,
|
|
Position: packets.Vec3{X: position.X, Y: position.Y, Z: position.Z},
|
|
Rotation: packets.Vec2{X: position.Yaw, Y: position.Pitch},
|
|
LastUpdate: time.Now(),
|
|
}
|
|
|
|
s.addClient(client)
|
|
defer s.removeClient(client)
|
|
|
|
s.sendInitialState(client)
|
|
s.broadcastPlayerJoined(client)
|
|
|
|
for {
|
|
var msg json.RawMessage
|
|
if err := decoder.Decode(&msg); err != nil {
|
|
break
|
|
}
|
|
|
|
var baseMsg struct {
|
|
Type string `json:"type"`
|
|
}
|
|
if err := json.Unmarshal(msg, &baseMsg); err != nil {
|
|
continue
|
|
}
|
|
|
|
switch baseMsg.Type {
|
|
case "movement":
|
|
var moveMsg packets.MovementMessage
|
|
if err := json.Unmarshal(msg, &moveMsg); err != nil {
|
|
continue
|
|
}
|
|
s.handleMovement(client, moveMsg)
|
|
|
|
case "chat":
|
|
var chatMsg packets.ChatMessage
|
|
if err := json.Unmarshal(msg, &chatMsg); err != nil {
|
|
continue
|
|
}
|
|
s.handleChat(client, chatMsg)
|
|
}
|
|
}
|
|
|
|
s.database.SavePlayerPosition(client.PlayerID, &db.Position{
|
|
X: client.Position.X,
|
|
Y: client.Position.Y,
|
|
Z: client.Position.Z,
|
|
Yaw: client.Rotation.X,
|
|
Pitch: client.Rotation.Y,
|
|
World: "main",
|
|
})
|
|
}
|
|
|
|
func (s *WorldServer) handleMovement(client *Client, msg packets.MovementMessage) {
|
|
newPos := packets.Vec3{X: msg.X, Y: msg.Y, Z: msg.Z}
|
|
|
|
if !s.world.IsInBounds(newPos) {
|
|
newPos = s.world.ClampPosition(newPos)
|
|
client.Encoder.Encode(packets.PositionCorrectionMessage{
|
|
Type: "positionCorrection",
|
|
X: newPos.X,
|
|
Y: newPos.Y,
|
|
Z: newPos.Z,
|
|
})
|
|
}
|
|
|
|
terrainHeight := s.world.GetHeightAt(s.heightmap, newPos.X, newPos.Z)
|
|
if newPos.Y < terrainHeight {
|
|
newPos.Y = terrainHeight
|
|
client.Encoder.Encode(packets.PositionCorrectionMessage{
|
|
Type: "positionCorrection",
|
|
X: newPos.X,
|
|
Y: newPos.Y,
|
|
Z: newPos.Z,
|
|
})
|
|
}
|
|
|
|
client.Position = newPos
|
|
client.Rotation = packets.Vec2{X: msg.Yaw, Y: msg.Pitch}
|
|
client.Velocity = packets.Vec3{X: msg.VelX, Y: msg.VelY, Z: msg.VelZ}
|
|
client.LastUpdate = time.Now()
|
|
|
|
s.broadcastMovement(client)
|
|
}
|
|
|
|
func (s *WorldServer) handleChat(client *Client, msg packets.ChatMessage) {
|
|
fullMsg := packets.ChatMessage{
|
|
Type: "chat",
|
|
Username: client.Username,
|
|
Message: msg.Message,
|
|
}
|
|
s.broadcast(fullMsg)
|
|
}
|
|
|
|
func (s *WorldServer) sendInitialState(client *Client) {
|
|
client.Encoder.Encode(packets.InitMessage{
|
|
Type: "init",
|
|
PlayerID: client.ID,
|
|
X: client.Position.X,
|
|
Y: client.Position.Y,
|
|
Z: client.Position.Z,
|
|
Yaw: client.Rotation.X,
|
|
Pitch: client.Rotation.Y,
|
|
TimeOfDay: s.timeOfDay,
|
|
})
|
|
|
|
s.clientLock.RLock()
|
|
for _, other := range s.clients {
|
|
if other.ID != client.ID {
|
|
client.Encoder.Encode(packets.PlayerJoinedMessage{
|
|
Type: "playerJoined",
|
|
PlayerID: other.ID,
|
|
Username: other.Username,
|
|
X: other.Position.X,
|
|
Y: other.Position.Y,
|
|
Z: other.Position.Z,
|
|
})
|
|
}
|
|
}
|
|
s.clientLock.RUnlock()
|
|
}
|
|
|
|
func (s *WorldServer) broadcastPlayerJoined(client *Client) {
|
|
msg := packets.PlayerJoinedMessage{
|
|
Type: "playerJoined",
|
|
PlayerID: client.ID,
|
|
Username: client.Username,
|
|
X: client.Position.X,
|
|
Y: client.Position.Y,
|
|
Z: client.Position.Z,
|
|
}
|
|
s.broadcastExcept(msg, client.ID)
|
|
}
|
|
|
|
func (s *WorldServer) broadcastMovement(client *Client) {
|
|
msg := packets.PlayerMovementMessage{
|
|
Type: "playerMovement",
|
|
PlayerID: client.ID,
|
|
X: client.Position.X,
|
|
Y: client.Position.Y,
|
|
Z: client.Position.Z,
|
|
Yaw: client.Rotation.X,
|
|
Pitch: client.Rotation.Y,
|
|
VelX: client.Velocity.X,
|
|
VelY: client.Velocity.Y,
|
|
VelZ: client.Velocity.Z,
|
|
}
|
|
s.broadcastExcept(msg, client.ID)
|
|
}
|
|
|
|
func (s *WorldServer) broadcast(msg interface{}) {
|
|
s.clientLock.RLock()
|
|
defer s.clientLock.RUnlock()
|
|
|
|
for _, client := range s.clients {
|
|
client.Encoder.Encode(msg)
|
|
}
|
|
}
|
|
|
|
func (s *WorldServer) broadcastExcept(msg interface{}, excludeID int) {
|
|
s.clientLock.RLock()
|
|
defer s.clientLock.RUnlock()
|
|
|
|
for _, client := range s.clients {
|
|
if client.ID != excludeID {
|
|
client.Encoder.Encode(msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *WorldServer) addClient(client *Client) {
|
|
s.clientLock.Lock()
|
|
s.clients[client.ID] = client
|
|
s.clientLock.Unlock()
|
|
log.Printf("Player %s (ID: %d) joined the world", client.Username, client.ID)
|
|
}
|
|
|
|
func (s *WorldServer) removeClient(client *Client) {
|
|
s.clientLock.Lock()
|
|
delete(s.clients, client.ID)
|
|
s.clientLock.Unlock()
|
|
|
|
s.broadcast(packets.PlayerLeftMessage{
|
|
Type: "playerLeft",
|
|
PlayerID: client.ID,
|
|
})
|
|
log.Printf("Player %s (ID: %d) left the world", client.Username, client.ID)
|
|
}
|
|
|
|
func (s *WorldServer) gameLoop() {
|
|
ticker := time.NewTicker(50 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
s.clientLock.RLock()
|
|
for _, client := range s.clients {
|
|
if time.Since(client.LastUpdate) > 30*time.Second {
|
|
go func(c *Client) {
|
|
c.Conn.Close()
|
|
}(client)
|
|
}
|
|
}
|
|
s.clientLock.RUnlock()
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
s.broadcast(packets.TimeUpdateMessage{
|
|
Type: "timeUpdate",
|
|
TimeOfDay: s.timeOfDay,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *WorldServer) getNextClientID() int {
|
|
s.clientLock.Lock()
|
|
defer s.clientLock.Unlock()
|
|
|
|
id := 1
|
|
for {
|
|
if _, exists := s.clients[id]; !exists {
|
|
return id
|
|
}
|
|
id++
|
|
}
|
|
}
|
|
|
|
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())
|
|
} |