world server skeleton
This commit is contained in:
parent
4bae02bec0
commit
fd75638fc6
137
cmd/world_server/main.go
Normal file
137
cmd/world_server/main.go
Normal file
@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigFile = "world_config.json"
|
||||
Version = "0.1.0"
|
||||
)
|
||||
|
||||
// printHeader displays the EQ2Emu banner and copyright info
|
||||
func printHeader() {
|
||||
fmt.Println("EQ2Emulator World Server")
|
||||
fmt.Printf("Version: %s\n", Version)
|
||||
fmt.Println()
|
||||
fmt.Println("Copyright (C) 2007-2026 EQ2Emulator Development Team")
|
||||
fmt.Println("https://www.eq2emu.com")
|
||||
fmt.Println()
|
||||
fmt.Println("EQ2Emulator is free software licensed under the GNU GPL v3")
|
||||
fmt.Println("See LICENSE file for details")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// loadConfig loads configuration from JSON file with command line overrides
|
||||
func loadConfig() (*WorldConfig, error) {
|
||||
// Default configuration
|
||||
config := &WorldConfig{
|
||||
ListenAddr: "0.0.0.0",
|
||||
ListenPort: 9000,
|
||||
MaxClients: 1000,
|
||||
BufferSize: 8192,
|
||||
WebAddr: "0.0.0.0",
|
||||
WebPort: 8080,
|
||||
DatabasePath: "world.db",
|
||||
XPRate: 1.0,
|
||||
TSXPRate: 1.0,
|
||||
VitalityRate: 1.0,
|
||||
LogLevel: "info",
|
||||
ThreadedLoad: true,
|
||||
}
|
||||
|
||||
// Load from config file if it exists
|
||||
if data, err := os.ReadFile(ConfigFile); err == nil {
|
||||
if err := json.Unmarshal(data, config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
log.Printf("Loaded configuration from %s", ConfigFile)
|
||||
} else {
|
||||
log.Printf("Config file %s not found, using defaults", ConfigFile)
|
||||
}
|
||||
|
||||
// Command line overrides
|
||||
flag.StringVar(&config.ListenAddr, "listen-addr", config.ListenAddr, "UDP listen address")
|
||||
flag.IntVar(&config.ListenPort, "listen-port", config.ListenPort, "UDP listen port")
|
||||
flag.IntVar(&config.MaxClients, "max-clients", config.MaxClients, "Maximum client connections")
|
||||
flag.StringVar(&config.WebAddr, "web-addr", config.WebAddr, "Web server address")
|
||||
flag.IntVar(&config.WebPort, "web-port", config.WebPort, "Web server port")
|
||||
flag.StringVar(&config.DatabasePath, "db-path", config.DatabasePath, "Database file path")
|
||||
flag.StringVar(&config.LogLevel, "log-level", config.LogLevel, "Log level (debug, info, warn, error)")
|
||||
flag.BoolVar(&config.ThreadedLoad, "threaded-load", config.ThreadedLoad, "Use threaded loading")
|
||||
flag.Parse()
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// saveConfig saves the current configuration to file
|
||||
func saveConfig(config *WorldConfig) error {
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(ConfigFile, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupSignalHandlers sets up graceful shutdown on SIGINT/SIGTERM
|
||||
func setupSignalHandlers(world *World) <-chan os.Signal {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
sig := <-sigChan
|
||||
log.Printf("Received signal %v, initiating graceful shutdown...", sig)
|
||||
world.Shutdown()
|
||||
}()
|
||||
|
||||
return sigChan
|
||||
}
|
||||
|
||||
func main() {
|
||||
printHeader()
|
||||
|
||||
// Load configuration
|
||||
config, err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Configuration error: %v", err)
|
||||
}
|
||||
|
||||
// Save config file with any command line overrides
|
||||
if err := saveConfig(config); err != nil {
|
||||
log.Printf("Warning: failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Create world server instance
|
||||
world, err := NewWorld(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create world server: %v", err)
|
||||
}
|
||||
|
||||
// Initialize all components
|
||||
log.Println("Initializing EQ2Emulator World Server...")
|
||||
if err := world.Initialize(); err != nil {
|
||||
log.Fatalf("Failed to initialize world server: %v", err)
|
||||
}
|
||||
|
||||
// Setup signal handlers for graceful shutdown
|
||||
setupSignalHandlers(world)
|
||||
|
||||
// Run the server
|
||||
log.Println("Starting World Server...")
|
||||
if err := world.Run(); err != nil {
|
||||
log.Fatalf("World server error: %v", err)
|
||||
}
|
||||
|
||||
log.Println("World Server stopped gracefully")
|
||||
}
|
298
cmd/world_server/web.go
Normal file
298
cmd/world_server/web.go
Normal file
@ -0,0 +1,298 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// setupWebServer initializes the HTTP server for admin interface
|
||||
func (w *World) setupWebServer() error {
|
||||
if w.config.WebPort == 0 {
|
||||
return nil // Web server disabled
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// API endpoints
|
||||
mux.HandleFunc("/api/status", w.handleStatus)
|
||||
mux.HandleFunc("/api/clients", w.handleClients)
|
||||
mux.HandleFunc("/api/zones", w.handleZones)
|
||||
mux.HandleFunc("/api/stats", w.handleStats)
|
||||
mux.HandleFunc("/api/time", w.handleWorldTime)
|
||||
mux.HandleFunc("/api/shutdown", w.handleShutdown)
|
||||
|
||||
// Administrative endpoints
|
||||
mux.HandleFunc("/api/admin/reload", w.handleReload)
|
||||
mux.HandleFunc("/api/admin/broadcast", w.handleBroadcast)
|
||||
mux.HandleFunc("/api/admin/kick", w.handleKickClient)
|
||||
|
||||
// Peer management endpoints
|
||||
mux.HandleFunc("/api/peers", w.handlePeers)
|
||||
mux.HandleFunc("/api/peers/sync", w.handlePeerSync)
|
||||
|
||||
// Console command interface
|
||||
mux.HandleFunc("/api/console", w.handleConsoleCommand)
|
||||
|
||||
// Static health check
|
||||
mux.HandleFunc("/health", w.handleHealth)
|
||||
|
||||
// @TODO: Add authentication middleware
|
||||
// @TODO: Add rate limiting middleware
|
||||
// @TODO: Add CORS middleware for browser access
|
||||
// @TODO: Add TLS support with cert/key files
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", w.config.WebAddr, w.config.WebPort)
|
||||
w.webServer = &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Core API handlers
|
||||
|
||||
// handleHealth provides a simple health check endpoint
|
||||
func (w *World) handleHealth(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// handleStatus returns comprehensive server status information
|
||||
func (w *World) handleStatus(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
status := map[string]any{
|
||||
"status": "running",
|
||||
"uptime": time.Since(w.stats.StartTime).Seconds(),
|
||||
"version": Version,
|
||||
"locked": w.config.WorldLocked,
|
||||
"primary": w.config.IsPrimary,
|
||||
"threaded": w.config.ThreadedLoad,
|
||||
"data_loaded": w.isDataLoaded(),
|
||||
"world_time": w.getWorldTime(),
|
||||
}
|
||||
|
||||
json.NewEncoder(rw).Encode(status)
|
||||
}
|
||||
|
||||
// handleClients returns list of connected clients
|
||||
func (w *World) handleClients(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.clientsMux.RLock()
|
||||
clients := make([]*ClientInfo, 0, len(w.clients))
|
||||
for _, client := range w.clients {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
w.clientsMux.RUnlock()
|
||||
|
||||
json.NewEncoder(rw).Encode(map[string]any{
|
||||
"count": len(clients),
|
||||
"clients": clients,
|
||||
})
|
||||
}
|
||||
|
||||
// handleZones returns list of zone servers
|
||||
func (w *World) handleZones(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.zonesMux.RLock()
|
||||
zones := make([]*ZoneInfo, 0, len(w.zones))
|
||||
for _, zone := range w.zones {
|
||||
zones = append(zones, zone)
|
||||
}
|
||||
w.zonesMux.RUnlock()
|
||||
|
||||
json.NewEncoder(rw).Encode(map[string]any{
|
||||
"count": len(zones),
|
||||
"zones": zones,
|
||||
})
|
||||
}
|
||||
|
||||
// handleStats returns detailed server statistics
|
||||
func (w *World) handleStats(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.statsMux.RLock()
|
||||
stats := w.stats
|
||||
w.statsMux.RUnlock()
|
||||
|
||||
// Add UDP server stats if available
|
||||
if w.udpServer != nil {
|
||||
serverStats := w.udpServer.GetStats()
|
||||
stats.TotalConnections = int64(serverStats.ConnectionCount)
|
||||
}
|
||||
|
||||
json.NewEncoder(rw).Encode(stats)
|
||||
}
|
||||
|
||||
// handleWorldTime returns current game world time
|
||||
func (w *World) handleWorldTime(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(w.getWorldTime())
|
||||
}
|
||||
|
||||
// Administrative handlers
|
||||
|
||||
// handleShutdown initiates graceful server shutdown
|
||||
func (w *World) handleShutdown(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// @TODO: Add authentication check
|
||||
// @TODO: Add confirmation parameter
|
||||
// @TODO: Add delay parameter
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(map[string]string{"status": "shutdown initiated"})
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second) // Allow response to be sent
|
||||
w.Shutdown()
|
||||
}()
|
||||
}
|
||||
|
||||
// handleReload reloads game data
|
||||
func (w *World) handleReload(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// @TODO: Add authentication check
|
||||
// @TODO: Implement selective reloading (items, spells, quests, etc.)
|
||||
// @TODO: Add progress reporting
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(map[string]string{"status": "reload not implemented"})
|
||||
}
|
||||
|
||||
// handleBroadcast sends server-wide message
|
||||
func (w *World) handleBroadcast(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// @TODO: Add authentication check
|
||||
// @TODO: Parse message from request body
|
||||
// @TODO: Validate message content
|
||||
// @TODO: Send to all connected clients
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(map[string]string{"status": "broadcast not implemented"})
|
||||
}
|
||||
|
||||
// handleKickClient disconnects a specific client
|
||||
func (w *World) handleKickClient(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// @TODO: Add authentication check
|
||||
// @TODO: Parse client ID from request
|
||||
// @TODO: Find and disconnect client
|
||||
// @TODO: Log kick action
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(map[string]string{"status": "kick not implemented"})
|
||||
}
|
||||
|
||||
// Peer management handlers
|
||||
|
||||
// handlePeers returns list of peer servers
|
||||
func (w *World) handlePeers(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
peers := make([]map[string]any, 0)
|
||||
for _, peer := range w.config.PeerServers {
|
||||
peerInfo := map[string]any{
|
||||
"address": peer.Address,
|
||||
"port": peer.Port,
|
||||
"status": "unknown", // @TODO: Implement peer status checking
|
||||
}
|
||||
peers = append(peers, peerInfo)
|
||||
}
|
||||
|
||||
json.NewEncoder(rw).Encode(map[string]any{
|
||||
"count": len(peers),
|
||||
"peers": peers,
|
||||
})
|
||||
}
|
||||
|
||||
// handlePeerSync synchronizes data with peer servers
|
||||
func (w *World) handlePeerSync(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// @TODO: Add authentication check
|
||||
// @TODO: Implement peer synchronization
|
||||
// @TODO: Return sync status and results
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(map[string]string{"status": "peer sync not implemented"})
|
||||
}
|
||||
|
||||
// Console command handler
|
||||
|
||||
// handleConsoleCommand executes administrative commands
|
||||
func (w *World) handleConsoleCommand(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// @TODO: Add authentication check
|
||||
// @TODO: Parse command from request body
|
||||
// @TODO: Validate command permissions
|
||||
// @TODO: Execute command and return results
|
||||
// @TODO: Log command execution
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(map[string]string{"status": "console commands not implemented"})
|
||||
}
|
||||
|
||||
// Helper methods for web handlers
|
||||
|
||||
// getWorldTime returns thread-safe copy of world time
|
||||
func (w *World) getWorldTime() WorldTime {
|
||||
w.worldTimeMux.RLock()
|
||||
defer w.worldTimeMux.RUnlock()
|
||||
return w.worldTime
|
||||
}
|
||||
|
||||
// startWebServer starts the web server in a goroutine
|
||||
func (w *World) startWebServer() {
|
||||
if w.webServer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := w.webServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Printf("Web server error: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// stopWebServer gracefully stops the web server
|
||||
func (w *World) stopWebServer() error {
|
||||
if w.webServer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
return w.webServer.Shutdown(ctx)
|
||||
}
|
830
cmd/world_server/world.go
Normal file
830
cmd/world_server/world.go
Normal file
@ -0,0 +1,830 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
"eq2emu/internal/udp"
|
||||
)
|
||||
|
||||
// WorldTime represents the in-game time
|
||||
type WorldTime struct {
|
||||
Year int32 `json:"year"`
|
||||
Month int32 `json:"month"`
|
||||
Day int32 `json:"day"`
|
||||
Hour int32 `json:"hour"`
|
||||
Minute int32 `json:"minute"`
|
||||
}
|
||||
|
||||
// WorldConfig holds all world server configuration
|
||||
type WorldConfig struct {
|
||||
// Network settings
|
||||
ListenAddr string `json:"listen_addr"`
|
||||
ListenPort int `json:"listen_port"`
|
||||
MaxClients int `json:"max_clients"`
|
||||
BufferSize int `json:"buffer_size"`
|
||||
|
||||
// Web server settings
|
||||
WebAddr string `json:"web_addr"`
|
||||
WebPort int `json:"web_port"`
|
||||
CertFile string `json:"cert_file"`
|
||||
KeyFile string `json:"key_file"`
|
||||
KeyPassword string `json:"key_password"`
|
||||
WebUser string `json:"web_user"`
|
||||
WebPassword string `json:"web_password"`
|
||||
|
||||
// Database settings
|
||||
DatabasePath string `json:"database_path"`
|
||||
|
||||
// Game settings
|
||||
XPRate float64 `json:"xp_rate"`
|
||||
TSXPRate float64 `json:"ts_xp_rate"`
|
||||
VitalityRate float64 `json:"vitality_rate"`
|
||||
|
||||
// Server settings
|
||||
LogLevel string `json:"log_level"`
|
||||
ThreadedLoad bool `json:"threaded_load"`
|
||||
WorldLocked bool `json:"world_locked"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
|
||||
// Login server settings
|
||||
LoginServers []LoginServerInfo `json:"login_servers"`
|
||||
|
||||
// Peer server settings
|
||||
PeerServers []PeerServerInfo `json:"peer_servers"`
|
||||
PeerPriority int `json:"peer_priority"`
|
||||
}
|
||||
|
||||
// LoginServerInfo represents login server connection details
|
||||
type LoginServerInfo struct {
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
Account string `json:"account"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// PeerServerInfo represents peer server connection details
|
||||
type PeerServerInfo struct {
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
// ClientInfo represents a connected client
|
||||
type ClientInfo struct {
|
||||
ID int32 `json:"id"`
|
||||
AccountID int32 `json:"account_id"`
|
||||
CharacterID int32 `json:"character_id"`
|
||||
Name string `json:"name"`
|
||||
ZoneID int32 `json:"zone_id"`
|
||||
ConnectedAt time.Time `json:"connected_at"`
|
||||
LastActive time.Time `json:"last_active"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
}
|
||||
|
||||
// ZoneInfo represents zone server information
|
||||
type ZoneInfo struct {
|
||||
ID int32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
PlayerCount int32 `json:"player_count"`
|
||||
MaxPlayers int32 `json:"max_players"`
|
||||
IsShutdown bool `json:"is_shutdown"`
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
// ServerStats holds server statistics
|
||||
type ServerStats struct {
|
||||
StartTime time.Time `json:"start_time"`
|
||||
ClientCount int32 `json:"client_count"`
|
||||
ZoneCount int32 `json:"zone_count"`
|
||||
TotalConnections int64 `json:"total_connections"`
|
||||
PacketsProcessed int64 `json:"packets_processed"`
|
||||
DataLoaded bool `json:"data_loaded"`
|
||||
ItemsLoaded bool `json:"items_loaded"`
|
||||
SpellsLoaded bool `json:"spells_loaded"`
|
||||
QuestsLoaded bool `json:"quests_loaded"`
|
||||
}
|
||||
|
||||
// World represents the main world server
|
||||
type World struct {
|
||||
config *WorldConfig
|
||||
db *database.DB
|
||||
|
||||
// Network components
|
||||
udpServer *udp.Server
|
||||
webServer *http.Server
|
||||
|
||||
// Game state
|
||||
worldTime WorldTime
|
||||
worldTimeMux sync.RWMutex
|
||||
|
||||
// Client management
|
||||
clients map[int32]*ClientInfo
|
||||
clientsMux sync.RWMutex
|
||||
|
||||
// Zone management
|
||||
zones map[int32]*ZoneInfo
|
||||
zonesMux sync.RWMutex
|
||||
|
||||
// Statistics
|
||||
stats ServerStats
|
||||
statsMux sync.RWMutex
|
||||
|
||||
// Control
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
shutdownWg *sync.WaitGroup
|
||||
|
||||
// Timers
|
||||
timeTickTimer *time.Ticker
|
||||
saveTimer *time.Ticker
|
||||
vitalityTimer *time.Ticker
|
||||
statsTimer *time.Ticker
|
||||
watchdogTimer *time.Ticker
|
||||
loginCheckTimer *time.Ticker
|
||||
|
||||
// Loading state
|
||||
loadingMux sync.RWMutex
|
||||
itemsLoaded bool
|
||||
spellsLoaded bool
|
||||
questsLoaded bool
|
||||
traitsLoaded bool
|
||||
dataLoaded bool
|
||||
}
|
||||
|
||||
// NewWorld creates a new world server instance
|
||||
func NewWorld(config *WorldConfig) (*World, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
db, err := database.Open(config.DatabasePath)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
w := &World{
|
||||
config: config,
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
shutdownWg: &sync.WaitGroup{},
|
||||
clients: make(map[int32]*ClientInfo),
|
||||
zones: make(map[int32]*ZoneInfo),
|
||||
stats: ServerStats{
|
||||
StartTime: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize world time from database
|
||||
if err := w.loadWorldTime(); err != nil {
|
||||
log.Printf("Warning: failed to load world time: %v", err)
|
||||
w.setDefaultWorldTime()
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Initialize sets up all world server components
|
||||
func (w *World) Initialize() error {
|
||||
log.Println("Loading System Data...")
|
||||
|
||||
// Initialize database schema
|
||||
if err := w.initializeDatabase(); err != nil {
|
||||
return fmt.Errorf("database initialization failed: %w", err)
|
||||
}
|
||||
|
||||
// Load game data (threaded or sequential)
|
||||
if w.config.ThreadedLoad {
|
||||
log.Println("Using threaded loading of static data...")
|
||||
if err := w.loadGameDataThreaded(); err != nil {
|
||||
return fmt.Errorf("threaded game data loading failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := w.loadGameData(); err != nil {
|
||||
return fmt.Errorf("game data loading failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup UDP server for game connections
|
||||
if err := w.setupUDPServer(); err != nil {
|
||||
return fmt.Errorf("UDP server setup failed: %w", err)
|
||||
}
|
||||
|
||||
// Setup web server for admin/API
|
||||
if err := w.setupWebServer(); err != nil {
|
||||
return fmt.Errorf("web server setup failed: %w", err)
|
||||
}
|
||||
|
||||
// Initialize timers
|
||||
w.initializeTimers()
|
||||
|
||||
log.Println("World Server initialization complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run starts the world server main loop
|
||||
func (w *World) Run() error {
|
||||
// Start background processes
|
||||
w.shutdownWg.Add(6)
|
||||
go w.processTimeUpdates()
|
||||
go w.processSaveOperations()
|
||||
go w.processVitalityUpdates()
|
||||
go w.processStatsUpdates()
|
||||
go w.processWatchdog()
|
||||
go w.processLoginCheck()
|
||||
|
||||
// Start network servers
|
||||
if w.udpServer != nil {
|
||||
go func() {
|
||||
if err := w.udpServer.Start(); err != nil {
|
||||
log.Printf("UDP server error: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Start web server
|
||||
w.startWebServer()
|
||||
|
||||
log.Printf("World Server running on UDP %s:%d, Web %s:%d",
|
||||
w.config.ListenAddr, w.config.ListenPort,
|
||||
w.config.WebAddr, w.config.WebPort)
|
||||
|
||||
// Wait for shutdown signal
|
||||
<-w.ctx.Done()
|
||||
|
||||
return w.shutdown()
|
||||
}
|
||||
|
||||
// Shutdown gracefully stops the world server
|
||||
func (w *World) Shutdown() {
|
||||
log.Println("Initiating World Server shutdown...")
|
||||
w.cancel()
|
||||
}
|
||||
|
||||
// setupUDPServer initializes the UDP server for game client connections
|
||||
func (w *World) setupUDPServer() error {
|
||||
handler := func(conn *udp.Connection, packet *udp.ApplicationPacket) {
|
||||
w.handleGamePacket(conn, packet)
|
||||
}
|
||||
|
||||
config := udp.DefaultConfig()
|
||||
config.MaxConnections = w.config.MaxClients
|
||||
config.BufferSize = w.config.BufferSize
|
||||
config.EnableCompression = true
|
||||
config.EnableEncryption = true
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", w.config.ListenAddr, w.config.ListenPort)
|
||||
server, err := udp.NewServer(addr, handler, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.udpServer = server
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeTimers sets up all periodic timers
|
||||
func (w *World) initializeTimers() {
|
||||
w.timeTickTimer = time.NewTicker(5 * time.Second) // Game time updates
|
||||
w.saveTimer = time.NewTicker(5 * time.Minute) // Save operations
|
||||
w.vitalityTimer = time.NewTicker(1 * time.Hour) // Vitality updates
|
||||
w.statsTimer = time.NewTicker(1 * time.Minute) // Statistics updates
|
||||
w.watchdogTimer = time.NewTicker(30 * time.Second) // Watchdog checks
|
||||
w.loginCheckTimer = time.NewTicker(30 * time.Second) // Login server check
|
||||
}
|
||||
|
||||
// Background processes
|
||||
|
||||
// processTimeUpdates handles game world time progression
|
||||
func (w *World) processTimeUpdates() {
|
||||
defer w.shutdownWg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-w.timeTickTimer.C:
|
||||
w.updateWorldTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processSaveOperations handles periodic save operations
|
||||
func (w *World) processSaveOperations() {
|
||||
defer w.shutdownWg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-w.saveTimer.C:
|
||||
w.saveWorldState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processVitalityUpdates handles vitality system updates
|
||||
func (w *World) processVitalityUpdates() {
|
||||
defer w.shutdownWg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-w.vitalityTimer.C:
|
||||
w.updateVitality()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processStatsUpdates handles statistics collection
|
||||
func (w *World) processStatsUpdates() {
|
||||
defer w.shutdownWg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-w.statsTimer.C:
|
||||
w.updateStatistics()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processWatchdog handles connection timeouts and cleanup
|
||||
func (w *World) processWatchdog() {
|
||||
defer w.shutdownWg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-w.watchdogTimer.C:
|
||||
w.cleanupInactiveClients()
|
||||
w.cleanupTimeoutConnections()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processLoginCheck handles login server connectivity
|
||||
func (w *World) processLoginCheck() {
|
||||
defer w.shutdownWg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-w.loginCheckTimer.C:
|
||||
w.checkLoginServers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Game packet handling
|
||||
func (w *World) handleGamePacket(conn *udp.Connection, packet *udp.ApplicationPacket) {
|
||||
// Update connection activity
|
||||
w.updateConnectionActivity(conn)
|
||||
|
||||
// Route packet based on opcode
|
||||
switch packet.Opcode {
|
||||
case 0x2000: // Login request
|
||||
w.handleLoginRequest(conn, packet)
|
||||
case 0x0020: // Zone change request
|
||||
w.handleZoneChange(conn, packet)
|
||||
case 0x0080: // Client command
|
||||
w.handleClientCommand(conn, packet)
|
||||
case 0x01F0: // Chat message
|
||||
w.handleChatMessage(conn, packet)
|
||||
default:
|
||||
// @TODO: Implement comprehensive packet routing
|
||||
log.Printf("Unhandled packet opcode: 0x%04X, size: %d", packet.Opcode, len(packet.Data))
|
||||
}
|
||||
|
||||
// Update packet statistics
|
||||
w.statsMux.Lock()
|
||||
w.stats.PacketsProcessed++
|
||||
w.statsMux.Unlock()
|
||||
}
|
||||
|
||||
// Game packet handlers
|
||||
func (w *World) handleLoginRequest(conn *udp.Connection, packet *udp.ApplicationPacket) {
|
||||
// @TODO: Parse login request packet
|
||||
// @TODO: Validate credentials with login server
|
||||
// @TODO: Create client session
|
||||
// @TODO: Send login response
|
||||
|
||||
log.Printf("Login request from connection %d", conn.GetSessionID())
|
||||
}
|
||||
|
||||
func (w *World) handleZoneChange(conn *udp.Connection, packet *udp.ApplicationPacket) {
|
||||
// @TODO: Parse zone change request
|
||||
// @TODO: Validate zone transfer
|
||||
// @TODO: Coordinate with zone servers
|
||||
// @TODO: Send zone change response
|
||||
|
||||
log.Printf("Zone change request from connection %d", conn.GetSessionID())
|
||||
}
|
||||
|
||||
func (w *World) handleClientCommand(conn *udp.Connection, packet *udp.ApplicationPacket) {
|
||||
// @TODO: Parse client command packet
|
||||
// @TODO: Process administrative commands
|
||||
// @TODO: Route to appropriate handlers
|
||||
|
||||
log.Printf("Client command from connection %d", conn.GetSessionID())
|
||||
}
|
||||
|
||||
func (w *World) handleChatMessage(conn *udp.Connection, packet *udp.ApplicationPacket) {
|
||||
// @TODO: Parse chat message packet
|
||||
// @TODO: Handle channel routing
|
||||
// @TODO: Apply filters and permissions
|
||||
// @TODO: Broadcast to appropriate recipients
|
||||
|
||||
log.Printf("Chat message from connection %d", conn.GetSessionID())
|
||||
}
|
||||
|
||||
// Game state management
|
||||
func (w *World) updateWorldTime() {
|
||||
w.worldTimeMux.Lock()
|
||||
defer w.worldTimeMux.Unlock()
|
||||
|
||||
w.worldTime.Minute++
|
||||
if w.worldTime.Minute >= 60 {
|
||||
w.worldTime.Minute = 0
|
||||
w.worldTime.Hour++
|
||||
if w.worldTime.Hour >= 24 {
|
||||
w.worldTime.Hour = 0
|
||||
w.worldTime.Day++
|
||||
if w.worldTime.Day >= 30 {
|
||||
w.worldTime.Day = 0
|
||||
w.worldTime.Month++
|
||||
if w.worldTime.Month >= 12 {
|
||||
w.worldTime.Month = 0
|
||||
w.worldTime.Year++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO: Broadcast time update to all zones/clients
|
||||
// @TODO: Save time to database periodically
|
||||
}
|
||||
|
||||
func (w *World) saveWorldState() {
|
||||
// @TODO: Save world time to database
|
||||
// @TODO: Save player data
|
||||
// @TODO: Save guild data
|
||||
// @TODO: Save zone states
|
||||
// @TODO: Save server statistics
|
||||
|
||||
log.Println("Saving world state...")
|
||||
}
|
||||
|
||||
func (w *World) updateVitality() {
|
||||
// @TODO: Update player vitality for offline/resting players
|
||||
// @TODO: Broadcast vitality updates to zones
|
||||
// @TODO: Apply vitality bonuses
|
||||
|
||||
log.Println("Updating vitality...")
|
||||
}
|
||||
|
||||
func (w *World) updateStatistics() {
|
||||
w.statsMux.Lock()
|
||||
defer w.statsMux.Unlock()
|
||||
|
||||
// Update client count
|
||||
w.clientsMux.RLock()
|
||||
w.stats.ClientCount = int32(len(w.clients))
|
||||
w.clientsMux.RUnlock()
|
||||
|
||||
// Update zone count
|
||||
w.zonesMux.RLock()
|
||||
w.stats.ZoneCount = int32(len(w.zones))
|
||||
w.zonesMux.RUnlock()
|
||||
|
||||
// Update loading status
|
||||
w.loadingMux.RLock()
|
||||
w.stats.DataLoaded = w.dataLoaded
|
||||
w.stats.ItemsLoaded = w.itemsLoaded
|
||||
w.stats.SpellsLoaded = w.spellsLoaded
|
||||
w.stats.QuestsLoaded = w.questsLoaded
|
||||
w.loadingMux.RUnlock()
|
||||
}
|
||||
|
||||
func (w *World) cleanupInactiveClients() {
|
||||
w.clientsMux.Lock()
|
||||
defer w.clientsMux.Unlock()
|
||||
|
||||
timeout := time.Now().Add(-5 * time.Minute)
|
||||
for id, client := range w.clients {
|
||||
if client.LastActive.Before(timeout) {
|
||||
log.Printf("Removing inactive client %d (%s)", id, client.Name)
|
||||
delete(w.clients, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *World) cleanupTimeoutConnections() {
|
||||
// @TODO: Clean up timed out UDP connections
|
||||
// @TODO: Update connection statistics
|
||||
}
|
||||
|
||||
func (w *World) checkLoginServers() {
|
||||
// @TODO: Check connectivity to login servers
|
||||
// @TODO: Attempt reconnection if disconnected
|
||||
// @TODO: Update server status
|
||||
}
|
||||
|
||||
func (w *World) updateConnectionActivity(conn *udp.Connection) {
|
||||
sessionID := conn.GetSessionID()
|
||||
|
||||
w.clientsMux.Lock()
|
||||
if client, exists := w.clients[int32(sessionID)]; exists {
|
||||
client.LastActive = time.Now()
|
||||
}
|
||||
w.clientsMux.Unlock()
|
||||
}
|
||||
|
||||
// Database operations
|
||||
func (w *World) initializeDatabase() error {
|
||||
// @TODO: Create/update database schema tables
|
||||
// @TODO: Initialize character tables
|
||||
// @TODO: Initialize guild tables
|
||||
// @TODO: Initialize item tables
|
||||
// @TODO: Initialize zone tables
|
||||
|
||||
log.Println("Database schema initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) loadGameData() error {
|
||||
log.Println("Loading game data sequentially...")
|
||||
|
||||
// Load items
|
||||
log.Println("Loading items...")
|
||||
if err := w.loadItems(); err != nil {
|
||||
return fmt.Errorf("failed to load items: %w", err)
|
||||
}
|
||||
|
||||
// Load spells
|
||||
log.Println("Loading spells...")
|
||||
if err := w.loadSpells(); err != nil {
|
||||
return fmt.Errorf("failed to load spells: %w", err)
|
||||
}
|
||||
|
||||
// Load quests
|
||||
log.Println("Loading quests...")
|
||||
if err := w.loadQuests(); err != nil {
|
||||
return fmt.Errorf("failed to load quests: %w", err)
|
||||
}
|
||||
|
||||
// Load additional data
|
||||
if err := w.loadTraits(); err != nil {
|
||||
return fmt.Errorf("failed to load traits: %w", err)
|
||||
}
|
||||
|
||||
if err := w.loadNPCs(); err != nil {
|
||||
return fmt.Errorf("failed to load NPCs: %w", err)
|
||||
}
|
||||
|
||||
if err := w.loadZones(); err != nil {
|
||||
return fmt.Errorf("failed to load zones: %w", err)
|
||||
}
|
||||
|
||||
w.loadingMux.Lock()
|
||||
w.dataLoaded = true
|
||||
w.loadingMux.Unlock()
|
||||
|
||||
log.Println("Game data loading complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) loadGameDataThreaded() error {
|
||||
log.Println("Loading game data with threads...")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errChan := make(chan error, 10)
|
||||
|
||||
// Load items in thread
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Println("Loading items...")
|
||||
if err := w.loadItems(); err != nil {
|
||||
errChan <- fmt.Errorf("failed to load items: %w", err)
|
||||
return
|
||||
}
|
||||
w.loadingMux.Lock()
|
||||
w.itemsLoaded = true
|
||||
w.loadingMux.Unlock()
|
||||
log.Println("Items loaded")
|
||||
}()
|
||||
|
||||
// Load spells in thread
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Println("Loading spells...")
|
||||
if err := w.loadSpells(); err != nil {
|
||||
errChan <- fmt.Errorf("failed to load spells: %w", err)
|
||||
return
|
||||
}
|
||||
w.loadingMux.Lock()
|
||||
w.spellsLoaded = true
|
||||
w.loadingMux.Unlock()
|
||||
log.Println("Spells loaded")
|
||||
}()
|
||||
|
||||
// Load quests in thread
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Println("Loading quests...")
|
||||
if err := w.loadQuests(); err != nil {
|
||||
errChan <- fmt.Errorf("failed to load quests: %w", err)
|
||||
return
|
||||
}
|
||||
w.loadingMux.Lock()
|
||||
w.questsLoaded = true
|
||||
w.loadingMux.Unlock()
|
||||
log.Println("Quests loaded")
|
||||
}()
|
||||
|
||||
// Wait for completion
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
}()
|
||||
|
||||
// Check for errors
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Load additional data sequentially
|
||||
if err := w.loadTraits(); err != nil {
|
||||
return fmt.Errorf("failed to load traits: %w", err)
|
||||
}
|
||||
|
||||
if err := w.loadNPCs(); err != nil {
|
||||
return fmt.Errorf("failed to load NPCs: %w", err)
|
||||
}
|
||||
|
||||
if err := w.loadZones(); err != nil {
|
||||
return fmt.Errorf("failed to load zones: %w", err)
|
||||
}
|
||||
|
||||
// Wait for threaded loads to complete
|
||||
for !w.isDataLoaded() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
w.loadingMux.Lock()
|
||||
w.dataLoaded = true
|
||||
w.loadingMux.Unlock()
|
||||
|
||||
log.Println("Threaded game data loading complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Data loading functions
|
||||
func (w *World) loadItems() error {
|
||||
// @TODO: Load items from database
|
||||
// @TODO: Build item lookup tables
|
||||
// @TODO: Load item templates
|
||||
// @TODO: Initialize item factories
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) loadSpells() error {
|
||||
// @TODO: Load spells from database
|
||||
// @TODO: Build spell lookup tables
|
||||
// @TODO: Load spell effects
|
||||
// @TODO: Initialize spell system
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) loadQuests() error {
|
||||
// @TODO: Load quests from database
|
||||
// @TODO: Build quest lookup tables
|
||||
// @TODO: Load quest rewards
|
||||
// @TODO: Initialize quest system
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) loadTraits() error {
|
||||
// @TODO: Load traits from database
|
||||
// @TODO: Build trait trees
|
||||
// @TODO: Initialize trait system
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) loadNPCs() error {
|
||||
// @TODO: Load NPCs from database
|
||||
// @TODO: Load NPC templates
|
||||
// @TODO: Load NPC spawn data
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) loadZones() error {
|
||||
// @TODO: Load zone definitions
|
||||
// @TODO: Load zone spawn points
|
||||
// @TODO: Initialize zone management
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) loadWorldTime() error {
|
||||
// @TODO: Load world time from database
|
||||
w.worldTime = WorldTime{
|
||||
Year: 3800,
|
||||
Month: 0,
|
||||
Day: 0,
|
||||
Hour: 8,
|
||||
Minute: 30,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *World) setDefaultWorldTime() {
|
||||
w.worldTimeMux.Lock()
|
||||
defer w.worldTimeMux.Unlock()
|
||||
|
||||
w.worldTime = WorldTime{
|
||||
Year: 3800,
|
||||
Month: 0,
|
||||
Day: 0,
|
||||
Hour: 8,
|
||||
Minute: 30,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *World) isDataLoaded() bool {
|
||||
w.loadingMux.RLock()
|
||||
defer w.loadingMux.RUnlock()
|
||||
|
||||
if w.config.ThreadedLoad {
|
||||
return w.itemsLoaded && w.spellsLoaded && w.questsLoaded && w.traitsLoaded
|
||||
}
|
||||
return w.dataLoaded
|
||||
}
|
||||
|
||||
// Cleanup and shutdown
|
||||
func (w *World) shutdown() error {
|
||||
log.Println("Shutting down World Server...")
|
||||
|
||||
// Stop timers
|
||||
if w.timeTickTimer != nil {
|
||||
w.timeTickTimer.Stop()
|
||||
}
|
||||
if w.saveTimer != nil {
|
||||
w.saveTimer.Stop()
|
||||
}
|
||||
if w.vitalityTimer != nil {
|
||||
w.vitalityTimer.Stop()
|
||||
}
|
||||
if w.statsTimer != nil {
|
||||
w.statsTimer.Stop()
|
||||
}
|
||||
if w.watchdogTimer != nil {
|
||||
w.watchdogTimer.Stop()
|
||||
}
|
||||
if w.loginCheckTimer != nil {
|
||||
w.loginCheckTimer.Stop()
|
||||
}
|
||||
|
||||
// Stop network servers
|
||||
if err := w.stopWebServer(); err != nil {
|
||||
log.Printf("Error stopping web server: %v", err)
|
||||
}
|
||||
|
||||
if w.udpServer != nil {
|
||||
w.udpServer.Stop()
|
||||
}
|
||||
|
||||
// Wait for background processes
|
||||
w.shutdownWg.Wait()
|
||||
|
||||
// Save final state
|
||||
w.saveWorldState()
|
||||
|
||||
// Close database
|
||||
if w.db != nil {
|
||||
w.db.Close()
|
||||
}
|
||||
|
||||
log.Println("World Server shutdown complete")
|
||||
return nil
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user