package eq2net import ( "database/sql" "fmt" "log" "time" _ "github.com/go-sql-driver/mysql" ) // OpcodeDBLoader handles loading opcodes from a MySQL database // This keeps database concerns separate from the core opcode system type OpcodeDBLoader struct { db *sql.DB } // NewOpcodeDBLoader creates a new database loader func NewOpcodeDBLoader(db *sql.DB) *OpcodeDBLoader { return &OpcodeDBLoader{db: db} } // LoadVersions loads version ranges from the database // Executes: SELECT DISTINCT version_range1, version_range2 FROM opcodes func (l *OpcodeDBLoader) LoadVersions() (OpcodeVersionMap, error) { query := `SELECT DISTINCT version_range1, version_range2 FROM opcodes ORDER BY version_range1` rows, err := l.db.Query(query) if err != nil { return nil, fmt.Errorf("failed to query version ranges: %w", err) } defer rows.Close() versions := make(OpcodeVersionMap) for rows.Next() { var minVersion, maxVersion uint16 if err := rows.Scan(&minVersion, &maxVersion); err != nil { return nil, fmt.Errorf("failed to scan version range: %w", err) } versions[minVersion] = maxVersion } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating version rows: %w", err) } return versions, nil } // LoadOpcodes loads opcodes for a specific version // Executes: SELECT name, opcode FROM opcodes WHERE ? BETWEEN version_range1 AND version_range2 func (l *OpcodeDBLoader) LoadOpcodes(version uint16) (map[string]uint16, error) { query := ` SELECT name, opcode FROM opcodes WHERE ? BETWEEN version_range1 AND version_range2 ORDER BY version_range1, id ` rows, err := l.db.Query(query, version) if err != nil { return nil, fmt.Errorf("failed to query opcodes for version %d: %w", version, err) } defer rows.Close() opcodes := make(map[string]uint16) for rows.Next() { var name string var opcode uint16 if err := rows.Scan(&name, &opcode); err != nil { return nil, fmt.Errorf("failed to scan opcode row: %w", err) } opcodes[name] = opcode } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating opcode rows: %w", err) } return opcodes, nil } // LoadAllOpcodes loads all opcodes for all versions at once // More efficient for server initialization func (l *OpcodeDBLoader) LoadAllOpcodes() (map[uint16]map[string]uint16, error) { // First get all unique version ranges versions, err := l.LoadVersions() if err != nil { return nil, err } // Load opcodes for each version result := make(map[uint16]map[string]uint16) for minVersion := range versions { opcodes, err := l.LoadOpcodes(minVersion) if err != nil { return nil, fmt.Errorf("failed to load opcodes for version %d: %w", minVersion, err) } result[minVersion] = opcodes } return result, nil } // InitializeOpcodeSystemFromDB initializes the opcode system from a database func InitializeOpcodeSystemFromDB(db *sql.DB) (EQOpcodeManagerMap, OpcodeVersionMap, error) { loader := NewOpcodeDBLoader(db) // Load version ranges versions, err := loader.LoadVersions() if err != nil { return nil, nil, fmt.Errorf("failed to load version ranges: %w", err) } // Load all opcodes opcodesByVersion, err := loader.LoadAllOpcodes() if err != nil { return nil, nil, fmt.Errorf("failed to load opcodes: %w", err) } // Create and initialize the opcode manager opcodeManager := NewEQOpcodeManager() if err := opcodeManager.LoadFromDatabase(versions, opcodesByVersion); err != nil { return nil, nil, fmt.Errorf("failed to initialize opcode manager: %w", err) } log.Printf("Loaded opcodes for %d version ranges", len(versions)) for minVersion, maxVersion := range versions { if opcodes, exists := opcodesByVersion[minVersion]; exists { log.Printf(" Version %d-%d: %d opcodes", minVersion, maxVersion, len(opcodes)) } } return opcodeManager, versions, nil } // Example usage showing how to use the opcode system with a database func ExampleDatabaseUsage() { // Connect to database dsn := "root:Root12!@tcp(localhost:3306)/eq2db?parseTime=true" db, err := sql.Open("mysql", dsn) if err != nil { log.Fatalf("Failed to open database: %v", err) } defer db.Close() // Configure connection pool db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(5 * time.Minute) // Test connection if err := db.Ping(); err != nil { log.Fatalf("Failed to ping database: %v", err) } // Initialize the opcode system opcodeManager, versionMap, err := InitializeOpcodeSystemFromDB(db) if err != nil { log.Fatalf("Failed to initialize opcode system: %v", err) } // Example: Handle a client with version 1193 clientVersion := uint16(1193) // Get the appropriate opcode manager for this client manager := opcodeManager.GetManagerForClient(clientVersion, versionMap) if manager == nil { log.Fatalf("No opcode manager available for client version %d", clientVersion) } // Convert opcodes as needed emuOpcode := OP_LoginRequestMsg eqOpcode := manager.EmuToEQ(emuOpcode) log.Printf("Client %d: %s -> 0x%04X", clientVersion, manager.EmuToName(emuOpcode), eqOpcode) // Reverse conversion emuOpcode = manager.EQToEmu(eqOpcode) log.Printf("Client %d: 0x%04X -> %s", clientVersion, eqOpcode, manager.EmuToName(emuOpcode)) } // CreateOpcodeTableSQL returns the SQL to create the opcodes table // This matches the existing EQ2 schema func CreateOpcodeTableSQL() string { return ` CREATE TABLE IF NOT EXISTS opcodes ( id INT AUTO_INCREMENT PRIMARY KEY, version_range1 INT NOT NULL, version_range2 INT NOT NULL, name VARCHAR(64) NOT NULL, opcode INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_version_range (version_range1, version_range2), INDEX idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ` } // InsertExampleOpcodes inserts example opcodes for testing func InsertExampleOpcodes(db *sql.DB) error { // Example data for version 1185-1197 (includes client version 1193) opcodes := []struct { versionMin uint16 versionMax uint16 name string opcode uint16 }{ {1185, 1197, "OP_LoginRequestMsg", 0x00B3}, {1185, 1197, "OP_LoginReplyMsg", 0x00B6}, {1185, 1197, "OP_WorldListMsg", 0x00B8}, {1185, 1197, "OP_PlayCharacterRequestMsg", 0x00BE}, {1185, 1197, "OP_PlayCharacterReplyMsg", 0x00BF}, {1185, 1197, "OP_DeleteCharacterRequestMsg", 0x00BA}, {1185, 1197, "OP_CreateCharacterRequestMsg", 0x00BC}, // Example data for version 1198-1207 {1198, 1207, "OP_LoginRequestMsg", 0x00C3}, {1198, 1207, "OP_LoginReplyMsg", 0x00C6}, {1198, 1207, "OP_WorldListMsg", 0x00C8}, {1198, 1207, "OP_PlayCharacterRequestMsg", 0x00CE}, {1198, 1207, "OP_PlayCharacterReplyMsg", 0x00CF}, // Example data for version 1208-1211 {1208, 1211, "OP_LoginRequestMsg", 0x00D3}, {1208, 1211, "OP_LoginReplyMsg", 0x00D6}, {1208, 1211, "OP_WorldListMsg", 0x00D8}, {1208, 1211, "OP_PlayCharacterRequestMsg", 0x00DE}, {1208, 1211, "OP_PlayCharacterReplyMsg", 0x00DF}, } // Prepare insert statement stmt, err := db.Prepare(` INSERT INTO opcodes (version_range1, version_range2, name, opcode) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE opcode = VALUES(opcode) `) if err != nil { return fmt.Errorf("failed to prepare statement: %w", err) } defer stmt.Close() // Insert all opcodes for _, op := range opcodes { if _, err := stmt.Exec(op.versionMin, op.versionMax, op.name, op.opcode); err != nil { return fmt.Errorf("failed to insert opcode %s: %w", op.name, err) } } log.Printf("Inserted %d example opcodes", len(opcodes)) return nil }