package eq2net import ( "database/sql" "fmt" "sync" "time" _ "github.com/go-sql-driver/mysql" ) // EmuOpcode represents an emulator-side opcode type EmuOpcode uint16 // Common emulator opcodes const ( OP_Unknown EmuOpcode = 0x0000 OP_LoginRequestMsg EmuOpcode = 0x0001 OP_LoginByNumRequestMsg EmuOpcode = 0x0002 OP_WSLoginRequestMsg EmuOpcode = 0x0003 OP_ESLoginRequestMsg EmuOpcode = 0x0004 OP_LoginReplyMsg EmuOpcode = 0x0005 OP_WSStatusReplyMsg EmuOpcode = 0x0006 OP_WorldListMsg EmuOpcode = 0x0007 OP_WorldStatusMsg EmuOpcode = 0x0008 OP_DeleteCharacterRequestMsg EmuOpcode = 0x0009 OP_DeleteCharacterReplyMsg EmuOpcode = 0x000A OP_CreateCharacterRequestMsg EmuOpcode = 0x000B OP_CreateCharacterReplyMsg EmuOpcode = 0x000C OP_PlayCharacterRequestMsg EmuOpcode = 0x000D OP_PlayCharacterReplyMsg EmuOpcode = 0x000E OP_ServerListRequestMsg EmuOpcode = 0x000F OP_ServerListReplyMsg EmuOpcode = 0x0010 OP_CharacterListRequestMsg EmuOpcode = 0x0011 OP_CharacterListReplyMsg EmuOpcode = 0x0012 ) // OpcodeNames maps emulator opcodes to their string names var OpcodeNames = map[EmuOpcode]string{ OP_Unknown: "OP_Unknown", OP_LoginRequestMsg: "OP_LoginRequestMsg", OP_LoginByNumRequestMsg: "OP_LoginByNumRequestMsg", OP_WSLoginRequestMsg: "OP_WSLoginRequestMsg", OP_ESLoginRequestMsg: "OP_ESLoginRequestMsg", OP_LoginReplyMsg: "OP_LoginReplyMsg", OP_WSStatusReplyMsg: "OP_WSStatusReplyMsg", OP_WorldListMsg: "OP_WorldListMsg", OP_WorldStatusMsg: "OP_WorldStatusMsg", OP_DeleteCharacterRequestMsg: "OP_DeleteCharacterRequestMsg", OP_DeleteCharacterReplyMsg: "OP_DeleteCharacterReplyMsg", OP_CreateCharacterRequestMsg: "OP_CreateCharacterRequestMsg", OP_CreateCharacterReplyMsg: "OP_CreateCharacterReplyMsg", OP_PlayCharacterRequestMsg: "OP_PlayCharacterRequestMsg", OP_PlayCharacterReplyMsg: "OP_PlayCharacterReplyMsg", OP_ServerListRequestMsg: "OP_ServerListRequestMsg", OP_ServerListReplyMsg: "OP_ServerListReplyMsg", OP_CharacterListRequestMsg: "OP_CharacterListRequestMsg", OP_CharacterListReplyMsg: "OP_CharacterListReplyMsg", } // OpcodeManager manages opcode mappings for different client versions type OpcodeManager struct { version uint16 emuToEQ map[EmuOpcode]uint16 eqToEmu map[uint16]EmuOpcode mu sync.RWMutex lastModified time.Time } // NewOpcodeManager creates a new opcode manager for a specific version func NewOpcodeManager(version uint16) *OpcodeManager { return &OpcodeManager{ version: version, emuToEQ: make(map[EmuOpcode]uint16), eqToEmu: make(map[uint16]EmuOpcode), } } // LoadOpcodes loads opcode mappings from a map func (om *OpcodeManager) LoadOpcodes(opcodes map[string]uint16) error { om.mu.Lock() defer om.mu.Unlock() // Clear existing mappings om.emuToEQ = make(map[EmuOpcode]uint16) om.eqToEmu = make(map[uint16]EmuOpcode) // Build bidirectional mappings for name, eqOpcode := range opcodes { // Find the emulator opcode by name var emuOpcode EmuOpcode = OP_Unknown for emu, opcName := range OpcodeNames { if opcName == name { emuOpcode = emu break } } if emuOpcode != OP_Unknown { om.emuToEQ[emuOpcode] = eqOpcode om.eqToEmu[eqOpcode] = emuOpcode } } om.lastModified = time.Now() return nil } // EmuToEQ converts an emulator opcode to EQ network opcode func (om *OpcodeManager) EmuToEQ(emu EmuOpcode) uint16 { om.mu.RLock() defer om.mu.RUnlock() if eq, exists := om.emuToEQ[emu]; exists { return eq } return 0xFFFF // Invalid opcode marker } // EQToEmu converts an EQ network opcode to emulator opcode func (om *OpcodeManager) EQToEmu(eq uint16) EmuOpcode { om.mu.RLock() defer om.mu.RUnlock() if emu, exists := om.eqToEmu[eq]; exists { return emu } return OP_Unknown } // EmuToName returns the name of an emulator opcode func (om *OpcodeManager) EmuToName(emu EmuOpcode) string { if name, exists := OpcodeNames[emu]; exists { return name } return "OP_Unknown" } // EQToName returns the name of an EQ network opcode func (om *OpcodeManager) EQToName(eq uint16) string { emu := om.EQToEmu(eq) return om.EmuToName(emu) } // GetVersion returns the client version this manager handles func (om *OpcodeManager) GetVersion() uint16 { return om.version } // OpcodeService manages opcode managers for all client versions type OpcodeService struct { db *sql.DB managers map[uint16]*OpcodeManager mu sync.RWMutex // Cache settings cacheExpiry time.Duration lastRefresh time.Time } // NewOpcodeService creates a new opcode service func NewOpcodeService(db *sql.DB) *OpcodeService { return &OpcodeService{ db: db, managers: make(map[uint16]*OpcodeManager), cacheExpiry: 5 * time.Minute, } } // GetManager returns the opcode manager for a specific client version func (s *OpcodeService) GetManager(version uint16) (*OpcodeManager, error) { s.mu.RLock() manager, exists := s.managers[version] s.mu.RUnlock() // Check if we have a cached manager that's still fresh if exists && time.Since(manager.lastModified) < s.cacheExpiry { return manager, nil } // Load or reload from database return s.loadManager(version) } // loadManager loads opcode mappings from the database func (s *OpcodeService) loadManager(version uint16) (*OpcodeManager, error) { // Query opcodes for this version query := ` SELECT opcode_name, opcode_value FROM opcodes WHERE version = ? ORDER BY opcode_name ` rows, err := s.db.Query(query, version) if err != nil { return nil, fmt.Errorf("failed to query opcodes: %w", err) } defer rows.Close() opcodes := make(map[string]uint16) for rows.Next() { var name string var value uint16 if err := rows.Scan(&name, &value); err != nil { return nil, fmt.Errorf("failed to scan opcode row: %w", err) } opcodes[name] = value } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating opcode rows: %w", err) } if len(opcodes) == 0 { return nil, fmt.Errorf("no opcodes found for version %d", version) } // Create or update manager manager := NewOpcodeManager(version) if err := manager.LoadOpcodes(opcodes); err != nil { return nil, err } // Cache the manager s.mu.Lock() s.managers[version] = manager s.mu.Unlock() return manager, nil } // RefreshAll refreshes all cached opcode managers func (s *OpcodeService) RefreshAll() error { // Get all versions from database query := `SELECT DISTINCT version FROM opcodes ORDER BY version` rows, err := s.db.Query(query) if err != nil { return fmt.Errorf("failed to query versions: %w", err) } defer rows.Close() var versions []uint16 for rows.Next() { var version uint16 if err := rows.Scan(&version); err != nil { return fmt.Errorf("failed to scan version: %w", err) } versions = append(versions, version) } if err := rows.Err(); err != nil { return fmt.Errorf("error iterating versions: %w", err) } // Load each version for _, version := range versions { if _, err := s.loadManager(version); err != nil { return fmt.Errorf("failed to load opcodes for version %d: %w", version, err) } } s.lastRefresh = time.Now() return nil } // GetSupportedVersions returns all client versions with opcode mappings func (s *OpcodeService) GetSupportedVersions() ([]uint16, error) { query := `SELECT DISTINCT version FROM opcodes ORDER BY version` rows, err := s.db.Query(query) if err != nil { return nil, fmt.Errorf("failed to query versions: %w", err) } defer rows.Close() var versions []uint16 for rows.Next() { var version uint16 if err := rows.Scan(&version); err != nil { return nil, fmt.Errorf("failed to scan version: %w", err) } versions = append(versions, version) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating versions: %w", err) } return versions, nil } // SaveOpcode saves or updates an opcode mapping in the database func (s *OpcodeService) SaveOpcode(version uint16, name string, value uint16) error { query := ` INSERT INTO opcodes (version, opcode_name, opcode_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE opcode_value = VALUES(opcode_value) ` _, err := s.db.Exec(query, version, name, value) if err != nil { return fmt.Errorf("failed to save opcode: %w", err) } // Invalidate cache for this version s.mu.Lock() delete(s.managers, version) s.mu.Unlock() return nil } // ImportOpcodes imports a batch of opcodes for a version func (s *OpcodeService) ImportOpcodes(version uint16, opcodes map[string]uint16) error { tx, err := s.db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Prepare the insert statement stmt, err := tx.Prepare(` INSERT INTO opcodes (version, opcode_name, opcode_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE opcode_value = VALUES(opcode_value) `) if err != nil { return fmt.Errorf("failed to prepare statement: %w", err) } defer stmt.Close() // Insert all opcodes for name, value := range opcodes { if _, err := stmt.Exec(version, name, value); err != nil { return fmt.Errorf("failed to insert opcode %s: %w", name, err) } } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } // Invalidate cache for this version s.mu.Lock() delete(s.managers, version) s.mu.Unlock() return nil } // GetOpcodeVersion maps client version to opcode version // This is a simplified version - in production, this might be more complex func GetOpcodeVersion(clientVersion uint16) uint16 { // Map client versions to opcode table versions // These are example mappings - adjust based on your actual data switch { case clientVersion < 900: return 1 case clientVersion < 1100: return 900 case clientVersion < 1193: return 1100 case clientVersion == 1193: return 1193 case clientVersion < 1300: return 1193 default: return clientVersion } } // ConnectDB establishes a connection to the MySQL database func ConnectDB(dsn string) (*sql.DB, error) { db, err := sql.Open("mysql", dsn) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } // Configure connection pool db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(5 * time.Minute) // Test the connection if err := db.Ping(); err != nil { db.Close() return nil, fmt.Errorf("failed to ping database: %w", err) } return db, nil } // CreateOpcodeTable creates the opcodes table if it doesn't exist func CreateOpcodeTable(db *sql.DB) error { query := ` CREATE TABLE IF NOT EXISTS opcodes ( id INT AUTO_INCREMENT PRIMARY KEY, version INT NOT NULL, opcode_name VARCHAR(64) NOT NULL, opcode_value INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY idx_version_name (version, opcode_name), INDEX idx_version (version) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ` _, err := db.Exec(query) if err != nil { return fmt.Errorf("failed to create opcodes table: %w", err) } return nil }