diff --git a/cmd_opcodes_example.go b/cmd_opcodes_example.go new file mode 100644 index 0000000..5fe0697 --- /dev/null +++ b/cmd_opcodes_example.go @@ -0,0 +1,188 @@ +package eq2net + +import ( + "fmt" + "log" +) + +// ExampleOpcodeData provides sample opcode data for different client versions +// In production, this would be loaded from files or generated from packet captures +var ExampleOpcodeData = map[uint16]map[string]uint16{ + // Version 1193 opcodes (example values) + 1193: { + "OP_LoginRequestMsg": 0x00B3, + "OP_LoginByNumRequestMsg": 0x00B4, + "OP_WSLoginRequestMsg": 0x00B5, + "OP_LoginReplyMsg": 0x00B6, + "OP_WSStatusReplyMsg": 0x00B7, + "OP_WorldListMsg": 0x00B8, + "OP_WorldStatusMsg": 0x00B9, + "OP_DeleteCharacterRequestMsg": 0x00BA, + "OP_DeleteCharacterReplyMsg": 0x00BB, + "OP_CreateCharacterRequestMsg": 0x00BC, + "OP_CreateCharacterReplyMsg": 0x00BD, + "OP_PlayCharacterRequestMsg": 0x00BE, + "OP_PlayCharacterReplyMsg": 0x00BF, + "OP_ServerListRequestMsg": 0x00C0, + "OP_ServerListReplyMsg": 0x00C1, + "OP_CharacterListRequestMsg": 0x00C2, + "OP_CharacterListReplyMsg": 0x00C3, + }, + + // Version 1208 opcodes (example values) + 1208: { + "OP_LoginRequestMsg": 0x00D1, + "OP_LoginByNumRequestMsg": 0x00D2, + "OP_WSLoginRequestMsg": 0x00D3, + "OP_LoginReplyMsg": 0x00D4, + "OP_WSStatusReplyMsg": 0x00D5, + "OP_WorldListMsg": 0x00D6, + "OP_WorldStatusMsg": 0x00D7, + "OP_DeleteCharacterRequestMsg": 0x00D8, + "OP_DeleteCharacterReplyMsg": 0x00D9, + "OP_CreateCharacterRequestMsg": 0x00DA, + "OP_CreateCharacterReplyMsg": 0x00DB, + "OP_PlayCharacterRequestMsg": 0x00DC, + "OP_PlayCharacterReplyMsg": 0x00DD, + "OP_ServerListRequestMsg": 0x00DE, + "OP_ServerListReplyMsg": 0x00DF, + "OP_CharacterListRequestMsg": 0x00E0, + "OP_CharacterListReplyMsg": 0x00E1, + }, +} + +// ImportExampleOpcodes imports the example opcode data into the database +func ImportExampleOpcodes(service *OpcodeService) error { + for version, opcodes := range ExampleOpcodeData { + log.Printf("Importing %d opcodes for version %d", len(opcodes), version) + if err := service.ImportOpcodes(version, opcodes); err != nil { + return fmt.Errorf("failed to import opcodes for version %d: %w", version, err) + } + } + + log.Println("Successfully imported all example opcodes") + return nil +} + +// PrintOpcodeTable prints a formatted table of opcodes for a version +func PrintOpcodeTable(service *OpcodeService, version uint16) error { + manager, err := service.GetManager(version) + if err != nil { + return fmt.Errorf("failed to get opcode manager for version %d: %w", version, err) + } + + fmt.Printf("\n=== Opcodes for Client Version %d ===\n", version) + fmt.Printf("%-30s | %-10s | %-10s\n", "Opcode Name", "Emu Value", "EQ Value") + fmt.Printf("%-30s-+-%-10s-+-%-10s\n", "------------------------------", + "----------", "----------") + + // Print all known opcodes + for emuOpcode, name := range OpcodeNames { + eqOpcode := manager.EmuToEQ(emuOpcode) + if eqOpcode != 0xFFFF { + fmt.Printf("%-30s | 0x%04X | 0x%04X\n", name, emuOpcode, eqOpcode) + } + } + + return nil +} + +// VerifyOpcodeIntegrity checks that opcode mappings are bidirectional +func VerifyOpcodeIntegrity(service *OpcodeService, version uint16) error { + manager, err := service.GetManager(version) + if err != nil { + return fmt.Errorf("failed to get opcode manager for version %d: %w", version, err) + } + + errors := 0 + for emuOpcode := range OpcodeNames { + eqOpcode := manager.EmuToEQ(emuOpcode) + if eqOpcode != 0xFFFF { + // Verify reverse mapping + reverseEmu := manager.EQToEmu(eqOpcode) + if reverseEmu != emuOpcode { + fmt.Printf("ERROR: Bidirectional mapping failed for %s (0x%04X)\n", + OpcodeNames[emuOpcode], emuOpcode) + fmt.Printf(" EmuToEQ: 0x%04X -> 0x%04X\n", emuOpcode, eqOpcode) + fmt.Printf(" EQToEmu: 0x%04X -> 0x%04X\n", eqOpcode, reverseEmu) + errors++ + } + } + } + + if errors > 0 { + return fmt.Errorf("found %d opcode integrity errors", errors) + } + + fmt.Printf("✓ All opcode mappings verified for version %d\n", version) + return nil +} + +// ExampleUsage demonstrates how to use the opcode system +func ExampleUsage() { + // Connect to database + dsn := "root:Root12!@tcp(localhost:3306)/eq2test?parseTime=true" + db, err := ConnectDB(dsn) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + defer db.Close() + + // Create tables if needed + if err := CreateOpcodeTable(db); err != nil { + log.Fatalf("Failed to create opcodes table: %v", err) + } + + // Create opcode service + service := NewOpcodeService(db) + + // Import example opcodes + if err := ImportExampleOpcodes(service); err != nil { + log.Fatalf("Failed to import example opcodes: %v", err) + } + + // Print opcode tables + for version := range ExampleOpcodeData { + if err := PrintOpcodeTable(service, version); err != nil { + log.Printf("Failed to print opcodes for version %d: %v", version, err) + } + + if err := VerifyOpcodeIntegrity(service, version); err != nil { + log.Printf("Integrity check failed for version %d: %v", version, err) + } + } + + // Example: Get manager for specific client version + clientVersion := uint16(1193) + opcodeVersion := GetOpcodeVersion(clientVersion) + manager, err := service.GetManager(opcodeVersion) + if err != nil { + log.Fatalf("Failed to get opcode manager: %v", err) + } + + // Example: Convert opcodes + fmt.Printf("\n=== Example Opcode Conversions (Version %d) ===\n", opcodeVersion) + + // Emu to EQ + emuOp := OP_LoginRequestMsg + eqOp := manager.EmuToEQ(emuOp) + fmt.Printf("EmuToEQ: %s (0x%04X) -> 0x%04X\n", + OpcodeNames[emuOp], emuOp, eqOp) + + // EQ to Emu + eqOp = 0x00B8 // OP_WorldListMsg for version 1193 + emuOp = manager.EQToEmu(eqOp) + fmt.Printf("EQToEmu: 0x%04X -> %s (0x%04X)\n", + eqOp, OpcodeNames[emuOp], emuOp) + + // Get all supported versions + versions, err := service.GetSupportedVersions() + if err != nil { + log.Printf("Failed to get supported versions: %v", err) + } else { + fmt.Printf("\n=== Supported Client Versions ===\n") + for _, v := range versions { + fmt.Printf(" Version %d\n", v) + } + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 48356ea..4a5d069 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,10 @@ module git.sharkk.net/EQ2/Protocol -go 1.21 +go 1.21.0 require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/panjf2000/ants/v2 v2.11.3 // indirect github.com/panjf2000/gnet/v2 v2.9.3 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index 58b6804..1de0f99 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/panjf2000/gnet/v2 v2.9.3 h1:auV3/A9Na3jiBDmYAAU00rPhFKnsAI+TnI1F7YUJMHQ= diff --git a/opcodes.go b/opcodes.go new file mode 100644 index 0000000..6f5db24 --- /dev/null +++ b/opcodes.go @@ -0,0 +1,416 @@ +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 +} \ No newline at end of file diff --git a/opcodes_test.go b/opcodes_test.go new file mode 100644 index 0000000..395806a --- /dev/null +++ b/opcodes_test.go @@ -0,0 +1,344 @@ +package eq2net + +import ( + "database/sql" + "fmt" + "testing" +) + +// Test database configuration +const ( + testDBHost = "localhost" + testDBPort = "3306" + testDBUser = "root" + testDBPass = "Root12!" + testDBName = "eq2test" +) + +// getTestDB creates a test database connection +func getTestDB(t *testing.T) *sql.DB { + // Connect without database first to create it if needed + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/", testDBUser, testDBPass, testDBHost, testDBPort) + db, err := sql.Open("mysql", dsn) + if err != nil { + t.Skipf("Cannot connect to MySQL: %v", err) + return nil + } + + // Create test database if it doesn't exist + _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + testDBName) + if err != nil { + t.Skipf("Cannot create test database: %v", err) + return nil + } + db.Close() + + // Connect to test database + dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", + testDBUser, testDBPass, testDBHost, testDBPort, testDBName) + + db, err = ConnectDB(dsn) + if err != nil { + t.Skipf("Cannot connect to test database: %v", err) + return nil + } + + // Create tables + if err := CreateOpcodeTable(db); err != nil { + t.Fatalf("Failed to create opcodes table: %v", err) + } + + return db +} + +// cleanupTestDB removes test data +func cleanupTestDB(db *sql.DB) { + if db != nil { + db.Exec("DELETE FROM opcodes WHERE version >= 9999") // Clean test versions + db.Close() + } +} + +func TestOpcodeManager(t *testing.T) { + manager := NewOpcodeManager(1193) + + // Test loading opcodes + opcodes := map[string]uint16{ + "OP_LoginRequestMsg": 0x0001, + "OP_LoginReplyMsg": 0x0002, + "OP_WorldListMsg": 0x0003, + "OP_PlayCharacterRequestMsg": 0x0004, + } + + err := manager.LoadOpcodes(opcodes) + if err != nil { + t.Fatalf("Failed to load opcodes: %v", err) + } + + // Test EmuToEQ conversion + eq := manager.EmuToEQ(OP_LoginRequestMsg) + if eq != 0x0001 { + t.Errorf("Expected EQ opcode 0x0001, got 0x%04x", eq) + } + + // Test EQToEmu conversion + emu := manager.EQToEmu(0x0002) + if emu != OP_LoginReplyMsg { + t.Errorf("Expected emu opcode %v, got %v", OP_LoginReplyMsg, emu) + } + + // Test unknown opcode + eq = manager.EmuToEQ(OP_Unknown) + if eq != 0xFFFF { + t.Errorf("Expected 0xFFFF for unknown opcode, got 0x%04x", eq) + } + + // Test name lookups + name := manager.EmuToName(OP_LoginRequestMsg) + if name != "OP_LoginRequestMsg" { + t.Errorf("Expected 'OP_LoginRequestMsg', got '%s'", name) + } + + name = manager.EQToName(0x0003) + if name != "OP_WorldListMsg" { + t.Errorf("Expected 'OP_WorldListMsg', got '%s'", name) + } +} + +func TestOpcodeService(t *testing.T) { + db := getTestDB(t) + if db == nil { + return // Test skipped + } + defer cleanupTestDB(db) + + service := NewOpcodeService(db) + + // Insert test opcodes + testVersion := uint16(9999) + testOpcodes := map[string]uint16{ + "OP_LoginRequestMsg": 0x1001, + "OP_LoginReplyMsg": 0x1002, + "OP_WorldListMsg": 0x1003, + "OP_PlayCharacterRequestMsg": 0x1004, + "OP_DeleteCharacterRequestMsg": 0x1005, + } + + // Import opcodes + err := service.ImportOpcodes(testVersion, testOpcodes) + if err != nil { + t.Fatalf("Failed to import opcodes: %v", err) + } + + // Get manager for version + manager, err := service.GetManager(testVersion) + if err != nil { + t.Fatalf("Failed to get manager: %v", err) + } + + if manager.GetVersion() != testVersion { + t.Errorf("Expected version %d, got %d", testVersion, manager.GetVersion()) + } + + // Test opcode conversions + eq := manager.EmuToEQ(OP_LoginRequestMsg) + if eq != 0x1001 { + t.Errorf("Expected EQ opcode 0x1001, got 0x%04x", eq) + } + + emu := manager.EQToEmu(0x1002) + if emu != OP_LoginReplyMsg { + t.Errorf("Expected emu opcode %v, got %v", OP_LoginReplyMsg, emu) + } + + // Test single opcode save + err = service.SaveOpcode(testVersion, "OP_ServerListRequestMsg", 0x1006) + if err != nil { + t.Fatalf("Failed to save opcode: %v", err) + } + + // Force reload and verify + service.mu.Lock() + delete(service.managers, testVersion) + service.mu.Unlock() + + manager, err = service.GetManager(testVersion) + if err != nil { + t.Fatalf("Failed to reload manager: %v", err) + } + + eq = manager.EmuToEQ(OP_ServerListRequestMsg) + if eq != 0x1006 { + t.Errorf("Expected EQ opcode 0x1006 after save, got 0x%04x", eq) + } +} + +func TestGetSupportedVersions(t *testing.T) { + db := getTestDB(t) + if db == nil { + return // Test skipped + } + defer cleanupTestDB(db) + + service := NewOpcodeService(db) + + // Add opcodes for multiple versions + versions := []uint16{9999, 9998, 9997} + for _, v := range versions { + opcodes := map[string]uint16{ + "OP_LoginRequestMsg": uint16(v), + } + if err := service.ImportOpcodes(v, opcodes); err != nil { + t.Fatalf("Failed to import opcodes for version %d: %v", v, err) + } + } + + // Get supported versions + supported, err := service.GetSupportedVersions() + if err != nil { + t.Fatalf("Failed to get supported versions: %v", err) + } + + // Check that our test versions are included + found := make(map[uint16]bool) + for _, v := range supported { + found[v] = true + } + + for _, v := range versions { + if !found[v] { + t.Errorf("Version %d not found in supported versions", v) + } + } +} + +func TestRefreshAll(t *testing.T) { + db := getTestDB(t) + if db == nil { + return // Test skipped + } + defer cleanupTestDB(db) + + service := NewOpcodeService(db) + + // Add opcodes for multiple versions + versions := []uint16{9999, 9998} + for _, v := range versions { + opcodes := map[string]uint16{ + "OP_LoginRequestMsg": uint16(v), + "OP_LoginReplyMsg": uint16(v + 1000), + } + if err := service.ImportOpcodes(v, opcodes); err != nil { + t.Fatalf("Failed to import opcodes for version %d: %v", v, err) + } + } + + // Refresh all + err := service.RefreshAll() + if err != nil { + t.Fatalf("Failed to refresh all: %v", err) + } + + // Verify all versions are loaded + for _, v := range versions { + manager, err := service.GetManager(v) + if err != nil { + t.Errorf("Failed to get manager for version %d after refresh: %v", v, err) + } + + eq := manager.EmuToEQ(OP_LoginRequestMsg) + if eq != v { + t.Errorf("Version %d: expected opcode %d, got %d", v, v, eq) + } + } +} + +func TestGetOpcodeVersion(t *testing.T) { + tests := []struct { + clientVersion uint16 + expectedOpcode uint16 + }{ + {800, 1}, + {899, 1}, + {900, 900}, + {1099, 900}, + {1100, 1100}, + {1192, 1100}, + {1193, 1193}, + {1199, 1193}, + {1200, 1193}, + {1300, 1300}, + } + + for _, tt := range tests { + result := GetOpcodeVersion(tt.clientVersion) + if result != tt.expectedOpcode { + t.Errorf("GetOpcodeVersion(%d) = %d, want %d", + tt.clientVersion, result, tt.expectedOpcode) + } + } +} + +func TestOpcodeNames(t *testing.T) { + // Verify all defined opcodes have names + knownOpcodes := []EmuOpcode{ + OP_Unknown, + OP_LoginRequestMsg, + OP_LoginByNumRequestMsg, + OP_WSLoginRequestMsg, + OP_ESLoginRequestMsg, + OP_LoginReplyMsg, + OP_WSStatusReplyMsg, + OP_WorldListMsg, + OP_WorldStatusMsg, + OP_DeleteCharacterRequestMsg, + OP_DeleteCharacterReplyMsg, + OP_CreateCharacterRequestMsg, + OP_CreateCharacterReplyMsg, + OP_PlayCharacterRequestMsg, + OP_PlayCharacterReplyMsg, + OP_ServerListRequestMsg, + OP_ServerListReplyMsg, + OP_CharacterListRequestMsg, + OP_CharacterListReplyMsg, + } + + for _, opcode := range knownOpcodes { + name, exists := OpcodeNames[opcode] + if !exists { + t.Errorf("Opcode %v has no name defined", opcode) + } + if name == "" { + t.Errorf("Opcode %v has empty name", opcode) + } + } +} + +// BenchmarkOpcodeConversion benchmarks opcode conversion performance +func BenchmarkOpcodeConversion(b *testing.B) { + manager := NewOpcodeManager(1193) + opcodes := make(map[string]uint16) + + // Add many opcodes + for i := uint16(1); i <= 100; i++ { + name := fmt.Sprintf("OP_Test%d", i) + opcodes[name] = i + OpcodeNames[EmuOpcode(i)] = name + } + + manager.LoadOpcodes(opcodes) + + b.ResetTimer() + + b.Run("EmuToEQ", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = manager.EmuToEQ(EmuOpcode(i%100 + 1)) + } + }) + + b.Run("EQToEmu", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = manager.EQToEmu(uint16(i%100 + 1)) + } + }) +} \ No newline at end of file