1
0
2025-07-02 13:14:42 -05:00

268 lines
5.8 KiB
Go

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...")
}