package main import ( "context" "log" "os" "os/signal" "sync" "syscall" "time" "github.com/panjf2000/gnet/v2" "github.com/valyala/fasthttp" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) type LoginServer struct { config *Config db *sqlitex.Pool clients *ClientManager worlds *WorldManager opcodes map[int16]*OpcodeManager webServer *fasthttp.Server ctx context.Context cancel context.CancelFunc mu sync.RWMutex running bool startTime time.Time } func main() { server := &LoginServer{ startTime: time.Now(), } if err := server.initialize(); err != nil { log.Fatalf("Failed to initialize server: %v", err) } server.printHeader() if err := server.start(); err != nil { log.Fatalf("Failed to start server: %v", err) } // Wait for shutdown signal sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh log.Println("Shutting down...") server.shutdown() } func (s *LoginServer) initialize() error { s.ctx, s.cancel = context.WithCancel(context.Background()) // Load configuration config, err := LoadConfig("login_config.json") if err != nil { return err } s.config = config // Initialize database if err := s.initDatabase(); err != nil { return err } // Initialize managers s.clients = NewClientManager(s.db) s.worlds = NewWorldManager(s.db) // Load opcodes if err := s.loadOpcodes(); err != nil { return err } // Initialize web server s.initWebServer() return nil } func (s *LoginServer) initDatabase() error { pool, err := sqlitex.Open(s.config.Database.Path, 0, 10) if err != nil { return err } s.db = pool // Create tables conn := s.db.Get(s.ctx) defer s.db.Put(conn) return s.createTables(conn) } func (s *LoginServer) createTables(conn *sqlite.Conn) error { schema := ` CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, password TEXT NOT NULL, created_date INTEGER DEFAULT CURRENT_TIMESTAMP, ip_address TEXT, last_client_version INTEGER ); CREATE TABLE IF NOT EXISTS login_characters ( id INTEGER PRIMARY KEY AUTOINCREMENT, account_id INTEGER NOT NULL, server_id INTEGER NOT NULL, char_id INTEGER NOT NULL, name TEXT NOT NULL, race INTEGER NOT NULL, class INTEGER NOT NULL, gender INTEGER NOT NULL, deity INTEGER NOT NULL, body_size REAL NOT NULL, body_age REAL NOT NULL, level INTEGER DEFAULT 1, current_zone_id INTEGER DEFAULT 1, created_date INTEGER DEFAULT CURRENT_TIMESTAMP, last_played INTEGER, deleted INTEGER DEFAULT 0, FOREIGN KEY(account_id) REFERENCES accounts(id) ); CREATE TABLE IF NOT EXISTS login_worldservers ( id INTEGER PRIMARY KEY AUTOINCREMENT, account TEXT UNIQUE NOT NULL, name TEXT NOT NULL, password TEXT NOT NULL, admin_id INTEGER DEFAULT 0, disabled INTEGER DEFAULT 0, ip_address TEXT, lastseen INTEGER ); CREATE TABLE IF NOT EXISTS login_equipment ( id INTEGER PRIMARY KEY AUTOINCREMENT, login_characters_id INTEGER NOT NULL, equip_type INTEGER NOT NULL, red INTEGER NOT NULL, green INTEGER NOT NULL, blue INTEGER NOT NULL, highlight_red INTEGER NOT NULL, highlight_green INTEGER NOT NULL, highlight_blue INTEGER NOT NULL, slot INTEGER NOT NULL, FOREIGN KEY(login_characters_id) REFERENCES login_characters(id) );` return sqlitex.ExecScript(conn, schema) } func (s *LoginServer) loadOpcodes() error { s.opcodes = make(map[int16]*OpcodeManager) // For demo, loading basic opcodes - in real implementation, // these would be loaded from database manager := &OpcodeManager{ opcodes: map[string]uint16{ "OP_LoginRequestMsg": 0x0001, "OP_LoginReplyMsg": 0x0002, "OP_AllWSDescRequestMsg": 0x0003, "OP_CreateCharacterRequestMsg": 0x0004, "OP_PlayCharacterRequestMsg": 0x0005, "OP_DeleteCharacterRequestMsg": 0x0006, }, } s.opcodes[1208] = manager // Default version return nil } func (s *LoginServer) initWebServer() { router := &WebRouter{server: s} s.webServer = &fasthttp.Server{ Handler: router.Handler, Name: "EQ2LoginWeb", } } func (s *LoginServer) start() error { s.mu.Lock() s.running = true s.mu.Unlock() // Start TCP server for game clients go func() { tcpServer := &TCPServer{ server: s, clients: s.clients, worlds: s.worlds, } log.Printf("Starting TCP server on port %d", s.config.Server.Port) if err := gnet.Run(tcpServer, "tcp://:"+string(rune(s.config.Server.Port)), gnet.WithMulticore(true), gnet.WithTCPKeepAlive(time.Minute*5)); err != nil { log.Printf("TCP server error: %v", err) } }() // Start web server if s.config.Web.Enabled { go func() { addr := s.config.Web.Address + ":" + string(rune(s.config.Web.Port)) log.Printf("Starting web server on %s", addr) if err := s.webServer.ListenAndServe(addr); err != nil { log.Printf("Web server error: %v", err) } }() } // Start periodic tasks go s.runPeriodicTasks() return nil } func (s *LoginServer) runPeriodicTasks() { ticker := time.NewTicker(time.Minute) defer ticker.Stop() for { select { case <-ticker.C: s.clients.CleanupExpired() s.worlds.UpdateStats() case <-s.ctx.Done(): return } } } func (s *LoginServer) shutdown() { s.mu.Lock() s.running = false s.mu.Unlock() s.cancel() if s.webServer != nil { s.webServer.Shutdown() } if s.db != nil { s.db.Close() } } func (s *LoginServer) printHeader() { log.Println("===============================================") log.Println(" EverQuest II Login Server - Go Edition") log.Println(" High Performance Game Authentication Server") log.Println("===============================================") log.Printf("Server Port: %d", s.config.Server.Port) if s.config.Web.Enabled { log.Printf("Web Interface: %s:%d", s.config.Web.Address, s.config.Web.Port) } log.Println("Server starting...") }