package packets import ( "fmt" "sync" "database/sql" "log" ) // OpcodeManager manages opcode mappings for different client versions type OpcodeManager struct { versions map[int32]int32 // Maps version range start to end opcodes map[int32]map[string]uint16 // Maps version to opcode name->value internalOpcodes map[int32]map[uint16]InternalOpcode // Maps version to wire opcode->internal mu sync.RWMutex } // NewOpcodeManager creates a new opcode manager func NewOpcodeManager() *OpcodeManager { return &OpcodeManager{ versions: make(map[int32]int32), opcodes: make(map[int32]map[string]uint16), internalOpcodes: make(map[int32]map[uint16]InternalOpcode), } } // LoadVersionsFromDB loads version ranges from the database func (om *OpcodeManager) LoadVersionsFromDB(db *sql.DB) error { om.mu.Lock() defer om.mu.Unlock() query := `SELECT DISTINCT version_range1, version_range2 FROM opcodes` rows, err := db.Query(query) if err != nil { return fmt.Errorf("failed to query versions: %w", err) } defer rows.Close() for rows.Next() { var start, end int32 if err := rows.Scan(&start, &end); err != nil { return fmt.Errorf("failed to scan version row: %w", err) } om.versions[start] = end } return rows.Err() } // LoadOpcodesFromDB loads opcodes for all versions from the database func (om *OpcodeManager) LoadOpcodesFromDB(db *sql.DB) error { om.mu.Lock() defer om.mu.Unlock() for versionStart := range om.versions { query := `SELECT name, opcode FROM opcodes WHERE ? BETWEEN version_range1 AND version_range2 ORDER BY version_range1, id` rows, err := db.Query(query, versionStart) if err != nil { return fmt.Errorf("failed to query opcodes for version %d: %w", versionStart, err) } opcodes := make(map[string]uint16) internal := make(map[uint16]InternalOpcode) for rows.Next() { var name string var opcode uint16 if err := rows.Scan(&name, &opcode); err != nil { rows.Close() return fmt.Errorf("failed to scan opcode row: %w", err) } opcodes[name] = opcode // Map to internal opcodes if internalOp, ok := nameToInternalOpcode[name]; ok { internal[opcode] = internalOp } } rows.Close() om.opcodes[versionStart] = opcodes om.internalOpcodes[versionStart] = internal // Silent - we'll log summary later } log.Printf("Loaded opcodes for %d client versions from database", len(om.versions)) return nil } // LoadDefaultOpcodes loads hardcoded opcodes for when database is not available func (om *OpcodeManager) LoadDefaultOpcodes() { om.mu.Lock() defer om.mu.Unlock() // Default version range (1119 is a common EQ2 client version) defaultVersion := int32(1119) om.versions[defaultVersion] = defaultVersion // These are the default opcodes from the C++ implementation opcodes := map[string]uint16{ "OP_LoginRequestMsg": 0x0001, "OP_LoginByNumRequestMsg": 0x0002, "OP_WSLoginRequestMsg": 0x0003, "OP_ESLoginRequestMsg": 0x0004, "OP_LoginReplyMsg": 0x0005, "OP_WorldListMsg": 0x0006, "OP_WorldStatusChangeMsg": 0x0007, "OP_AllWSDescRequestMsg": 0x0008, "OP_WSStatusReplyMsg": 0x0009, "OP_AllCharactersDescRequestMsg": 0x000A, "OP_AllCharactersDescReplyMsg": 0x000B, "OP_CreateCharacterRequestMsg": 0x000C, "OP_ReskinCharacterRequestMsg": 0x000D, "OP_CreateCharacterReplyMsg": 0x000E, "OP_WSCreateCharacterRequestMsg": 0x000F, "OP_WSCreateCharacterReplyMsg": 0x0010, "OP_DeleteCharacterRequestMsg": 0x0011, "OP_DeleteCharacterReplyMsg": 0x0012, "OP_PlayCharacterRequestMsg": 0x0013, "OP_PlayCharacterReplyMsg": 0x0014, "OP_ServerPlayCharacterRequestMsg": 0x0015, "OP_ServerPlayCharacterReplyMsg": 0x0016, "OP_KeymapLoadMsg": 0x0017, "OP_KeymapNoneMsg": 0x0018, "OP_KeymapDataMsg": 0x0019, "OP_KeymapSaveMsg": 0x001A, "OP_LSCheckAcctLockMsg": 0x001B, "OP_WSAcctLockStatusMsg": 0x001C, "OP_LsRequestClientCrashLogMsg": 0x001D, "OP_LsClientBaselogReplyMsg": 0x001E, "OP_LsClientCrashlogReplyMsg": 0x001F, "OP_LsClientAlertlogReplyMsg": 0x0020, "OP_LsClientVerifylogReplyMsg": 0x0021, "OP_BadLanguageFilter": 0x0022, "OP_WSServerLockMsg": 0x0023, "OP_WSServerHideMsg": 0x0024, "OP_LSServerLockMsg": 0x0025, "OP_UpdateCharacterSheetMsg": 0x0026, "OP_UpdateInventoryMsg": 0x0027, } internal := make(map[uint16]InternalOpcode) for name, opcode := range opcodes { if internalOp, ok := nameToInternalOpcode[name]; ok { internal[opcode] = internalOp } } om.opcodes[defaultVersion] = opcodes om.internalOpcodes[defaultVersion] = internal log.Printf("Loaded default opcodes for version %d", defaultVersion) } // GetOpcodeVersion returns the version range start for a given client version // This implements the same logic as the C++ GetOpcodeVersion function func (om *OpcodeManager) GetOpcodeVersion(clientVersion int32) int32 { om.mu.RLock() defer om.mu.RUnlock() for versionStart, versionEnd := range om.versions { if clientVersion >= versionStart && clientVersion <= versionEnd { return versionStart } } // If no match found, return the client version itself return clientVersion } // GetOpcodeByName returns the wire opcode value for a given opcode name and client version func (om *OpcodeManager) GetOpcodeByName(clientVersion int32, name string) (uint16, bool) { om.mu.RLock() defer om.mu.RUnlock() version := om.GetOpcodeVersion(clientVersion) if opcodes, ok := om.opcodes[version]; ok { if opcode, ok := opcodes[name]; ok { return opcode, true } } return 0, false } // GetInternalOpcode converts a wire opcode to an internal opcode for a given client version func (om *OpcodeManager) GetInternalOpcode(clientVersion int32, wireOpcode uint16) (InternalOpcode, bool) { om.mu.RLock() defer om.mu.RUnlock() version := om.GetOpcodeVersion(clientVersion) if internal, ok := om.internalOpcodes[version]; ok { if internalOp, ok := internal[wireOpcode]; ok { return internalOp, true } } return OP_Unknown, false } // GetWireOpcode converts an internal opcode to a wire opcode for a given client version func (om *OpcodeManager) GetWireOpcode(clientVersion int32, internalOpcode InternalOpcode) (uint16, bool) { om.mu.RLock() defer om.mu.RUnlock() // Get the name for this internal opcode name := "" for n, op := range nameToInternalOpcode { if op == internalOpcode { name = n break } } if name == "" { return 0, false } return om.GetOpcodeByName(clientVersion, name) } // nameToInternalOpcode maps opcode names to internal opcodes var nameToInternalOpcode = map[string]InternalOpcode{ "OP_LoginRequestMsg": OP_LoginRequestMsg, "OP_LoginByNumRequestMsg": OP_LoginByNumRequestMsg, "OP_WSLoginRequestMsg": OP_WSLoginRequestMsg, "OP_LoginReplyMsg": OP_LoginReplyMsg, "OP_PlayCharacterRequestMsg": OP_PlayCharacterRequest, "OP_CreateCharacterRequestMsg": OP_CharacterCreate, "OP_DeleteCharacterRequestMsg": OP_CharacterDelete, "OP_AllCharactersDescRequestMsg": OP_CharacterList, "OP_WorldListMsg": OP_ServerList, "OP_WorldStatusChangeMsg": OP_SendServerStatus, // Add more mappings as needed }