opcode handling
This commit is contained in:
parent
1561c74fdf
commit
e5189486af
188
cmd_opcodes_example.go
Normal file
188
cmd_opcodes_example.go
Normal file
@ -0,0 +1,188 @@
|
||||
package eq2net
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// ExampleOpcodeData provides sample opcode data for different client versions
|
||||
// In production, this would be loaded from files or generated from packet captures
|
||||
var ExampleOpcodeData = map[uint16]map[string]uint16{
|
||||
// Version 1193 opcodes (example values)
|
||||
1193: {
|
||||
"OP_LoginRequestMsg": 0x00B3,
|
||||
"OP_LoginByNumRequestMsg": 0x00B4,
|
||||
"OP_WSLoginRequestMsg": 0x00B5,
|
||||
"OP_LoginReplyMsg": 0x00B6,
|
||||
"OP_WSStatusReplyMsg": 0x00B7,
|
||||
"OP_WorldListMsg": 0x00B8,
|
||||
"OP_WorldStatusMsg": 0x00B9,
|
||||
"OP_DeleteCharacterRequestMsg": 0x00BA,
|
||||
"OP_DeleteCharacterReplyMsg": 0x00BB,
|
||||
"OP_CreateCharacterRequestMsg": 0x00BC,
|
||||
"OP_CreateCharacterReplyMsg": 0x00BD,
|
||||
"OP_PlayCharacterRequestMsg": 0x00BE,
|
||||
"OP_PlayCharacterReplyMsg": 0x00BF,
|
||||
"OP_ServerListRequestMsg": 0x00C0,
|
||||
"OP_ServerListReplyMsg": 0x00C1,
|
||||
"OP_CharacterListRequestMsg": 0x00C2,
|
||||
"OP_CharacterListReplyMsg": 0x00C3,
|
||||
},
|
||||
|
||||
// Version 1208 opcodes (example values)
|
||||
1208: {
|
||||
"OP_LoginRequestMsg": 0x00D1,
|
||||
"OP_LoginByNumRequestMsg": 0x00D2,
|
||||
"OP_WSLoginRequestMsg": 0x00D3,
|
||||
"OP_LoginReplyMsg": 0x00D4,
|
||||
"OP_WSStatusReplyMsg": 0x00D5,
|
||||
"OP_WorldListMsg": 0x00D6,
|
||||
"OP_WorldStatusMsg": 0x00D7,
|
||||
"OP_DeleteCharacterRequestMsg": 0x00D8,
|
||||
"OP_DeleteCharacterReplyMsg": 0x00D9,
|
||||
"OP_CreateCharacterRequestMsg": 0x00DA,
|
||||
"OP_CreateCharacterReplyMsg": 0x00DB,
|
||||
"OP_PlayCharacterRequestMsg": 0x00DC,
|
||||
"OP_PlayCharacterReplyMsg": 0x00DD,
|
||||
"OP_ServerListRequestMsg": 0x00DE,
|
||||
"OP_ServerListReplyMsg": 0x00DF,
|
||||
"OP_CharacterListRequestMsg": 0x00E0,
|
||||
"OP_CharacterListReplyMsg": 0x00E1,
|
||||
},
|
||||
}
|
||||
|
||||
// ImportExampleOpcodes imports the example opcode data into the database
|
||||
func ImportExampleOpcodes(service *OpcodeService) error {
|
||||
for version, opcodes := range ExampleOpcodeData {
|
||||
log.Printf("Importing %d opcodes for version %d", len(opcodes), version)
|
||||
if err := service.ImportOpcodes(version, opcodes); err != nil {
|
||||
return fmt.Errorf("failed to import opcodes for version %d: %w", version, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Successfully imported all example opcodes")
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintOpcodeTable prints a formatted table of opcodes for a version
|
||||
func PrintOpcodeTable(service *OpcodeService, version uint16) error {
|
||||
manager, err := service.GetManager(version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get opcode manager for version %d: %w", version, err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== Opcodes for Client Version %d ===\n", version)
|
||||
fmt.Printf("%-30s | %-10s | %-10s\n", "Opcode Name", "Emu Value", "EQ Value")
|
||||
fmt.Printf("%-30s-+-%-10s-+-%-10s\n", "------------------------------",
|
||||
"----------", "----------")
|
||||
|
||||
// Print all known opcodes
|
||||
for emuOpcode, name := range OpcodeNames {
|
||||
eqOpcode := manager.EmuToEQ(emuOpcode)
|
||||
if eqOpcode != 0xFFFF {
|
||||
fmt.Printf("%-30s | 0x%04X | 0x%04X\n", name, emuOpcode, eqOpcode)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyOpcodeIntegrity checks that opcode mappings are bidirectional
|
||||
func VerifyOpcodeIntegrity(service *OpcodeService, version uint16) error {
|
||||
manager, err := service.GetManager(version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get opcode manager for version %d: %w", version, err)
|
||||
}
|
||||
|
||||
errors := 0
|
||||
for emuOpcode := range OpcodeNames {
|
||||
eqOpcode := manager.EmuToEQ(emuOpcode)
|
||||
if eqOpcode != 0xFFFF {
|
||||
// Verify reverse mapping
|
||||
reverseEmu := manager.EQToEmu(eqOpcode)
|
||||
if reverseEmu != emuOpcode {
|
||||
fmt.Printf("ERROR: Bidirectional mapping failed for %s (0x%04X)\n",
|
||||
OpcodeNames[emuOpcode], emuOpcode)
|
||||
fmt.Printf(" EmuToEQ: 0x%04X -> 0x%04X\n", emuOpcode, eqOpcode)
|
||||
fmt.Printf(" EQToEmu: 0x%04X -> 0x%04X\n", eqOpcode, reverseEmu)
|
||||
errors++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors > 0 {
|
||||
return fmt.Errorf("found %d opcode integrity errors", errors)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ All opcode mappings verified for version %d\n", version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExampleUsage demonstrates how to use the opcode system
|
||||
func ExampleUsage() {
|
||||
// Connect to database
|
||||
dsn := "root:Root12!@tcp(localhost:3306)/eq2test?parseTime=true"
|
||||
db, err := ConnectDB(dsn)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Create tables if needed
|
||||
if err := CreateOpcodeTable(db); err != nil {
|
||||
log.Fatalf("Failed to create opcodes table: %v", err)
|
||||
}
|
||||
|
||||
// Create opcode service
|
||||
service := NewOpcodeService(db)
|
||||
|
||||
// Import example opcodes
|
||||
if err := ImportExampleOpcodes(service); err != nil {
|
||||
log.Fatalf("Failed to import example opcodes: %v", err)
|
||||
}
|
||||
|
||||
// Print opcode tables
|
||||
for version := range ExampleOpcodeData {
|
||||
if err := PrintOpcodeTable(service, version); err != nil {
|
||||
log.Printf("Failed to print opcodes for version %d: %v", version, err)
|
||||
}
|
||||
|
||||
if err := VerifyOpcodeIntegrity(service, version); err != nil {
|
||||
log.Printf("Integrity check failed for version %d: %v", version, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Get manager for specific client version
|
||||
clientVersion := uint16(1193)
|
||||
opcodeVersion := GetOpcodeVersion(clientVersion)
|
||||
manager, err := service.GetManager(opcodeVersion)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get opcode manager: %v", err)
|
||||
}
|
||||
|
||||
// Example: Convert opcodes
|
||||
fmt.Printf("\n=== Example Opcode Conversions (Version %d) ===\n", opcodeVersion)
|
||||
|
||||
// Emu to EQ
|
||||
emuOp := OP_LoginRequestMsg
|
||||
eqOp := manager.EmuToEQ(emuOp)
|
||||
fmt.Printf("EmuToEQ: %s (0x%04X) -> 0x%04X\n",
|
||||
OpcodeNames[emuOp], emuOp, eqOp)
|
||||
|
||||
// EQ to Emu
|
||||
eqOp = 0x00B8 // OP_WorldListMsg for version 1193
|
||||
emuOp = manager.EQToEmu(eqOp)
|
||||
fmt.Printf("EQToEmu: 0x%04X -> %s (0x%04X)\n",
|
||||
eqOp, OpcodeNames[emuOp], emuOp)
|
||||
|
||||
// Get all supported versions
|
||||
versions, err := service.GetSupportedVersions()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get supported versions: %v", err)
|
||||
} else {
|
||||
fmt.Printf("\n=== Supported Client Versions ===\n")
|
||||
for _, v := range versions {
|
||||
fmt.Printf(" Version %d\n", v)
|
||||
}
|
||||
}
|
||||
}
|
4
go.mod
4
go.mod
@ -1,8 +1,10 @@
|
||||
module git.sharkk.net/EQ2/Protocol
|
||||
|
||||
go 1.21
|
||||
go 1.21.0
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/panjf2000/ants/v2 v2.11.3 // indirect
|
||||
github.com/panjf2000/gnet/v2 v2.9.3 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -1,3 +1,7 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
|
||||
github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
|
||||
github.com/panjf2000/gnet/v2 v2.9.3 h1:auV3/A9Na3jiBDmYAAU00rPhFKnsAI+TnI1F7YUJMHQ=
|
||||
|
416
opcodes.go
Normal file
416
opcodes.go
Normal file
@ -0,0 +1,416 @@
|
||||
package eq2net
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// EmuOpcode represents an emulator-side opcode
|
||||
type EmuOpcode uint16
|
||||
|
||||
// Common emulator opcodes
|
||||
const (
|
||||
OP_Unknown EmuOpcode = 0x0000
|
||||
OP_LoginRequestMsg EmuOpcode = 0x0001
|
||||
OP_LoginByNumRequestMsg EmuOpcode = 0x0002
|
||||
OP_WSLoginRequestMsg EmuOpcode = 0x0003
|
||||
OP_ESLoginRequestMsg EmuOpcode = 0x0004
|
||||
OP_LoginReplyMsg EmuOpcode = 0x0005
|
||||
OP_WSStatusReplyMsg EmuOpcode = 0x0006
|
||||
OP_WorldListMsg EmuOpcode = 0x0007
|
||||
OP_WorldStatusMsg EmuOpcode = 0x0008
|
||||
OP_DeleteCharacterRequestMsg EmuOpcode = 0x0009
|
||||
OP_DeleteCharacterReplyMsg EmuOpcode = 0x000A
|
||||
OP_CreateCharacterRequestMsg EmuOpcode = 0x000B
|
||||
OP_CreateCharacterReplyMsg EmuOpcode = 0x000C
|
||||
OP_PlayCharacterRequestMsg EmuOpcode = 0x000D
|
||||
OP_PlayCharacterReplyMsg EmuOpcode = 0x000E
|
||||
OP_ServerListRequestMsg EmuOpcode = 0x000F
|
||||
OP_ServerListReplyMsg EmuOpcode = 0x0010
|
||||
OP_CharacterListRequestMsg EmuOpcode = 0x0011
|
||||
OP_CharacterListReplyMsg EmuOpcode = 0x0012
|
||||
)
|
||||
|
||||
// OpcodeNames maps emulator opcodes to their string names
|
||||
var OpcodeNames = map[EmuOpcode]string{
|
||||
OP_Unknown: "OP_Unknown",
|
||||
OP_LoginRequestMsg: "OP_LoginRequestMsg",
|
||||
OP_LoginByNumRequestMsg: "OP_LoginByNumRequestMsg",
|
||||
OP_WSLoginRequestMsg: "OP_WSLoginRequestMsg",
|
||||
OP_ESLoginRequestMsg: "OP_ESLoginRequestMsg",
|
||||
OP_LoginReplyMsg: "OP_LoginReplyMsg",
|
||||
OP_WSStatusReplyMsg: "OP_WSStatusReplyMsg",
|
||||
OP_WorldListMsg: "OP_WorldListMsg",
|
||||
OP_WorldStatusMsg: "OP_WorldStatusMsg",
|
||||
OP_DeleteCharacterRequestMsg: "OP_DeleteCharacterRequestMsg",
|
||||
OP_DeleteCharacterReplyMsg: "OP_DeleteCharacterReplyMsg",
|
||||
OP_CreateCharacterRequestMsg: "OP_CreateCharacterRequestMsg",
|
||||
OP_CreateCharacterReplyMsg: "OP_CreateCharacterReplyMsg",
|
||||
OP_PlayCharacterRequestMsg: "OP_PlayCharacterRequestMsg",
|
||||
OP_PlayCharacterReplyMsg: "OP_PlayCharacterReplyMsg",
|
||||
OP_ServerListRequestMsg: "OP_ServerListRequestMsg",
|
||||
OP_ServerListReplyMsg: "OP_ServerListReplyMsg",
|
||||
OP_CharacterListRequestMsg: "OP_CharacterListRequestMsg",
|
||||
OP_CharacterListReplyMsg: "OP_CharacterListReplyMsg",
|
||||
}
|
||||
|
||||
// OpcodeManager manages opcode mappings for different client versions
|
||||
type OpcodeManager struct {
|
||||
version uint16
|
||||
emuToEQ map[EmuOpcode]uint16
|
||||
eqToEmu map[uint16]EmuOpcode
|
||||
mu sync.RWMutex
|
||||
lastModified time.Time
|
||||
}
|
||||
|
||||
// NewOpcodeManager creates a new opcode manager for a specific version
|
||||
func NewOpcodeManager(version uint16) *OpcodeManager {
|
||||
return &OpcodeManager{
|
||||
version: version,
|
||||
emuToEQ: make(map[EmuOpcode]uint16),
|
||||
eqToEmu: make(map[uint16]EmuOpcode),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadOpcodes loads opcode mappings from a map
|
||||
func (om *OpcodeManager) LoadOpcodes(opcodes map[string]uint16) error {
|
||||
om.mu.Lock()
|
||||
defer om.mu.Unlock()
|
||||
|
||||
// Clear existing mappings
|
||||
om.emuToEQ = make(map[EmuOpcode]uint16)
|
||||
om.eqToEmu = make(map[uint16]EmuOpcode)
|
||||
|
||||
// Build bidirectional mappings
|
||||
for name, eqOpcode := range opcodes {
|
||||
// Find the emulator opcode by name
|
||||
var emuOpcode EmuOpcode = OP_Unknown
|
||||
for emu, opcName := range OpcodeNames {
|
||||
if opcName == name {
|
||||
emuOpcode = emu
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if emuOpcode != OP_Unknown {
|
||||
om.emuToEQ[emuOpcode] = eqOpcode
|
||||
om.eqToEmu[eqOpcode] = emuOpcode
|
||||
}
|
||||
}
|
||||
|
||||
om.lastModified = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmuToEQ converts an emulator opcode to EQ network opcode
|
||||
func (om *OpcodeManager) EmuToEQ(emu EmuOpcode) uint16 {
|
||||
om.mu.RLock()
|
||||
defer om.mu.RUnlock()
|
||||
|
||||
if eq, exists := om.emuToEQ[emu]; exists {
|
||||
return eq
|
||||
}
|
||||
return 0xFFFF // Invalid opcode marker
|
||||
}
|
||||
|
||||
// EQToEmu converts an EQ network opcode to emulator opcode
|
||||
func (om *OpcodeManager) EQToEmu(eq uint16) EmuOpcode {
|
||||
om.mu.RLock()
|
||||
defer om.mu.RUnlock()
|
||||
|
||||
if emu, exists := om.eqToEmu[eq]; exists {
|
||||
return emu
|
||||
}
|
||||
return OP_Unknown
|
||||
}
|
||||
|
||||
// EmuToName returns the name of an emulator opcode
|
||||
func (om *OpcodeManager) EmuToName(emu EmuOpcode) string {
|
||||
if name, exists := OpcodeNames[emu]; exists {
|
||||
return name
|
||||
}
|
||||
return "OP_Unknown"
|
||||
}
|
||||
|
||||
// EQToName returns the name of an EQ network opcode
|
||||
func (om *OpcodeManager) EQToName(eq uint16) string {
|
||||
emu := om.EQToEmu(eq)
|
||||
return om.EmuToName(emu)
|
||||
}
|
||||
|
||||
// GetVersion returns the client version this manager handles
|
||||
func (om *OpcodeManager) GetVersion() uint16 {
|
||||
return om.version
|
||||
}
|
||||
|
||||
// OpcodeService manages opcode managers for all client versions
|
||||
type OpcodeService struct {
|
||||
db *sql.DB
|
||||
managers map[uint16]*OpcodeManager
|
||||
mu sync.RWMutex
|
||||
|
||||
// Cache settings
|
||||
cacheExpiry time.Duration
|
||||
lastRefresh time.Time
|
||||
}
|
||||
|
||||
// NewOpcodeService creates a new opcode service
|
||||
func NewOpcodeService(db *sql.DB) *OpcodeService {
|
||||
return &OpcodeService{
|
||||
db: db,
|
||||
managers: make(map[uint16]*OpcodeManager),
|
||||
cacheExpiry: 5 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// GetManager returns the opcode manager for a specific client version
|
||||
func (s *OpcodeService) GetManager(version uint16) (*OpcodeManager, error) {
|
||||
s.mu.RLock()
|
||||
manager, exists := s.managers[version]
|
||||
s.mu.RUnlock()
|
||||
|
||||
// Check if we have a cached manager that's still fresh
|
||||
if exists && time.Since(manager.lastModified) < s.cacheExpiry {
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// Load or reload from database
|
||||
return s.loadManager(version)
|
||||
}
|
||||
|
||||
// loadManager loads opcode mappings from the database
|
||||
func (s *OpcodeService) loadManager(version uint16) (*OpcodeManager, error) {
|
||||
// Query opcodes for this version
|
||||
query := `
|
||||
SELECT opcode_name, opcode_value
|
||||
FROM opcodes
|
||||
WHERE version = ?
|
||||
ORDER BY opcode_name
|
||||
`
|
||||
|
||||
rows, err := s.db.Query(query, version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query opcodes: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
opcodes := make(map[string]uint16)
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var value uint16
|
||||
if err := rows.Scan(&name, &value); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan opcode row: %w", err)
|
||||
}
|
||||
opcodes[name] = value
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating opcode rows: %w", err)
|
||||
}
|
||||
|
||||
if len(opcodes) == 0 {
|
||||
return nil, fmt.Errorf("no opcodes found for version %d", version)
|
||||
}
|
||||
|
||||
// Create or update manager
|
||||
manager := NewOpcodeManager(version)
|
||||
if err := manager.LoadOpcodes(opcodes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache the manager
|
||||
s.mu.Lock()
|
||||
s.managers[version] = manager
|
||||
s.mu.Unlock()
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// RefreshAll refreshes all cached opcode managers
|
||||
func (s *OpcodeService) RefreshAll() error {
|
||||
// Get all versions from database
|
||||
query := `SELECT DISTINCT version FROM opcodes ORDER BY version`
|
||||
|
||||
rows, err := s.db.Query(query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query versions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var versions []uint16
|
||||
for rows.Next() {
|
||||
var version uint16
|
||||
if err := rows.Scan(&version); err != nil {
|
||||
return fmt.Errorf("failed to scan version: %w", err)
|
||||
}
|
||||
versions = append(versions, version)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf("error iterating versions: %w", err)
|
||||
}
|
||||
|
||||
// Load each version
|
||||
for _, version := range versions {
|
||||
if _, err := s.loadManager(version); err != nil {
|
||||
return fmt.Errorf("failed to load opcodes for version %d: %w", version, err)
|
||||
}
|
||||
}
|
||||
|
||||
s.lastRefresh = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSupportedVersions returns all client versions with opcode mappings
|
||||
func (s *OpcodeService) GetSupportedVersions() ([]uint16, error) {
|
||||
query := `SELECT DISTINCT version FROM opcodes ORDER BY version`
|
||||
|
||||
rows, err := s.db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query versions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var versions []uint16
|
||||
for rows.Next() {
|
||||
var version uint16
|
||||
if err := rows.Scan(&version); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan version: %w", err)
|
||||
}
|
||||
versions = append(versions, version)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating versions: %w", err)
|
||||
}
|
||||
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
// SaveOpcode saves or updates an opcode mapping in the database
|
||||
func (s *OpcodeService) SaveOpcode(version uint16, name string, value uint16) error {
|
||||
query := `
|
||||
INSERT INTO opcodes (version, opcode_name, opcode_value)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE opcode_value = VALUES(opcode_value)
|
||||
`
|
||||
|
||||
_, err := s.db.Exec(query, version, name, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save opcode: %w", err)
|
||||
}
|
||||
|
||||
// Invalidate cache for this version
|
||||
s.mu.Lock()
|
||||
delete(s.managers, version)
|
||||
s.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportOpcodes imports a batch of opcodes for a version
|
||||
func (s *OpcodeService) ImportOpcodes(version uint16, opcodes map[string]uint16) error {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Prepare the insert statement
|
||||
stmt, err := tx.Prepare(`
|
||||
INSERT INTO opcodes (version, opcode_name, opcode_value)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE opcode_value = VALUES(opcode_value)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare statement: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
// Insert all opcodes
|
||||
for name, value := range opcodes {
|
||||
if _, err := stmt.Exec(version, name, value); err != nil {
|
||||
return fmt.Errorf("failed to insert opcode %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
// Invalidate cache for this version
|
||||
s.mu.Lock()
|
||||
delete(s.managers, version)
|
||||
s.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOpcodeVersion maps client version to opcode version
|
||||
// This is a simplified version - in production, this might be more complex
|
||||
func GetOpcodeVersion(clientVersion uint16) uint16 {
|
||||
// Map client versions to opcode table versions
|
||||
// These are example mappings - adjust based on your actual data
|
||||
switch {
|
||||
case clientVersion < 900:
|
||||
return 1
|
||||
case clientVersion < 1100:
|
||||
return 900
|
||||
case clientVersion < 1193:
|
||||
return 1100
|
||||
case clientVersion == 1193:
|
||||
return 1193
|
||||
case clientVersion < 1300:
|
||||
return 1193
|
||||
default:
|
||||
return clientVersion
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectDB establishes a connection to the MySQL database
|
||||
func ConnectDB(dsn string) (*sql.DB, error) {
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// Configure connection pool
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(5)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
// Test the connection
|
||||
if err := db.Ping(); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// CreateOpcodeTable creates the opcodes table if it doesn't exist
|
||||
func CreateOpcodeTable(db *sql.DB) error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS opcodes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
version INT NOT NULL,
|
||||
opcode_name VARCHAR(64) NOT NULL,
|
||||
opcode_value INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY idx_version_name (version, opcode_name),
|
||||
INDEX idx_version (version)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`
|
||||
|
||||
_, err := db.Exec(query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create opcodes table: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
344
opcodes_test.go
Normal file
344
opcodes_test.go
Normal file
@ -0,0 +1,344 @@
|
||||
package eq2net
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test database configuration
|
||||
const (
|
||||
testDBHost = "localhost"
|
||||
testDBPort = "3306"
|
||||
testDBUser = "root"
|
||||
testDBPass = "Root12!"
|
||||
testDBName = "eq2test"
|
||||
)
|
||||
|
||||
// getTestDB creates a test database connection
|
||||
func getTestDB(t *testing.T) *sql.DB {
|
||||
// Connect without database first to create it if needed
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/", testDBUser, testDBPass, testDBHost, testDBPort)
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
t.Skipf("Cannot connect to MySQL: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create test database if it doesn't exist
|
||||
_, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + testDBName)
|
||||
if err != nil {
|
||||
t.Skipf("Cannot create test database: %v", err)
|
||||
return nil
|
||||
}
|
||||
db.Close()
|
||||
|
||||
// Connect to test database
|
||||
dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true",
|
||||
testDBUser, testDBPass, testDBHost, testDBPort, testDBName)
|
||||
|
||||
db, err = ConnectDB(dsn)
|
||||
if err != nil {
|
||||
t.Skipf("Cannot connect to test database: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create tables
|
||||
if err := CreateOpcodeTable(db); err != nil {
|
||||
t.Fatalf("Failed to create opcodes table: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// cleanupTestDB removes test data
|
||||
func cleanupTestDB(db *sql.DB) {
|
||||
if db != nil {
|
||||
db.Exec("DELETE FROM opcodes WHERE version >= 9999") // Clean test versions
|
||||
db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpcodeManager(t *testing.T) {
|
||||
manager := NewOpcodeManager(1193)
|
||||
|
||||
// Test loading opcodes
|
||||
opcodes := map[string]uint16{
|
||||
"OP_LoginRequestMsg": 0x0001,
|
||||
"OP_LoginReplyMsg": 0x0002,
|
||||
"OP_WorldListMsg": 0x0003,
|
||||
"OP_PlayCharacterRequestMsg": 0x0004,
|
||||
}
|
||||
|
||||
err := manager.LoadOpcodes(opcodes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load opcodes: %v", err)
|
||||
}
|
||||
|
||||
// Test EmuToEQ conversion
|
||||
eq := manager.EmuToEQ(OP_LoginRequestMsg)
|
||||
if eq != 0x0001 {
|
||||
t.Errorf("Expected EQ opcode 0x0001, got 0x%04x", eq)
|
||||
}
|
||||
|
||||
// Test EQToEmu conversion
|
||||
emu := manager.EQToEmu(0x0002)
|
||||
if emu != OP_LoginReplyMsg {
|
||||
t.Errorf("Expected emu opcode %v, got %v", OP_LoginReplyMsg, emu)
|
||||
}
|
||||
|
||||
// Test unknown opcode
|
||||
eq = manager.EmuToEQ(OP_Unknown)
|
||||
if eq != 0xFFFF {
|
||||
t.Errorf("Expected 0xFFFF for unknown opcode, got 0x%04x", eq)
|
||||
}
|
||||
|
||||
// Test name lookups
|
||||
name := manager.EmuToName(OP_LoginRequestMsg)
|
||||
if name != "OP_LoginRequestMsg" {
|
||||
t.Errorf("Expected 'OP_LoginRequestMsg', got '%s'", name)
|
||||
}
|
||||
|
||||
name = manager.EQToName(0x0003)
|
||||
if name != "OP_WorldListMsg" {
|
||||
t.Errorf("Expected 'OP_WorldListMsg', got '%s'", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpcodeService(t *testing.T) {
|
||||
db := getTestDB(t)
|
||||
if db == nil {
|
||||
return // Test skipped
|
||||
}
|
||||
defer cleanupTestDB(db)
|
||||
|
||||
service := NewOpcodeService(db)
|
||||
|
||||
// Insert test opcodes
|
||||
testVersion := uint16(9999)
|
||||
testOpcodes := map[string]uint16{
|
||||
"OP_LoginRequestMsg": 0x1001,
|
||||
"OP_LoginReplyMsg": 0x1002,
|
||||
"OP_WorldListMsg": 0x1003,
|
||||
"OP_PlayCharacterRequestMsg": 0x1004,
|
||||
"OP_DeleteCharacterRequestMsg": 0x1005,
|
||||
}
|
||||
|
||||
// Import opcodes
|
||||
err := service.ImportOpcodes(testVersion, testOpcodes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to import opcodes: %v", err)
|
||||
}
|
||||
|
||||
// Get manager for version
|
||||
manager, err := service.GetManager(testVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get manager: %v", err)
|
||||
}
|
||||
|
||||
if manager.GetVersion() != testVersion {
|
||||
t.Errorf("Expected version %d, got %d", testVersion, manager.GetVersion())
|
||||
}
|
||||
|
||||
// Test opcode conversions
|
||||
eq := manager.EmuToEQ(OP_LoginRequestMsg)
|
||||
if eq != 0x1001 {
|
||||
t.Errorf("Expected EQ opcode 0x1001, got 0x%04x", eq)
|
||||
}
|
||||
|
||||
emu := manager.EQToEmu(0x1002)
|
||||
if emu != OP_LoginReplyMsg {
|
||||
t.Errorf("Expected emu opcode %v, got %v", OP_LoginReplyMsg, emu)
|
||||
}
|
||||
|
||||
// Test single opcode save
|
||||
err = service.SaveOpcode(testVersion, "OP_ServerListRequestMsg", 0x1006)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save opcode: %v", err)
|
||||
}
|
||||
|
||||
// Force reload and verify
|
||||
service.mu.Lock()
|
||||
delete(service.managers, testVersion)
|
||||
service.mu.Unlock()
|
||||
|
||||
manager, err = service.GetManager(testVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to reload manager: %v", err)
|
||||
}
|
||||
|
||||
eq = manager.EmuToEQ(OP_ServerListRequestMsg)
|
||||
if eq != 0x1006 {
|
||||
t.Errorf("Expected EQ opcode 0x1006 after save, got 0x%04x", eq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSupportedVersions(t *testing.T) {
|
||||
db := getTestDB(t)
|
||||
if db == nil {
|
||||
return // Test skipped
|
||||
}
|
||||
defer cleanupTestDB(db)
|
||||
|
||||
service := NewOpcodeService(db)
|
||||
|
||||
// Add opcodes for multiple versions
|
||||
versions := []uint16{9999, 9998, 9997}
|
||||
for _, v := range versions {
|
||||
opcodes := map[string]uint16{
|
||||
"OP_LoginRequestMsg": uint16(v),
|
||||
}
|
||||
if err := service.ImportOpcodes(v, opcodes); err != nil {
|
||||
t.Fatalf("Failed to import opcodes for version %d: %v", v, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get supported versions
|
||||
supported, err := service.GetSupportedVersions()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get supported versions: %v", err)
|
||||
}
|
||||
|
||||
// Check that our test versions are included
|
||||
found := make(map[uint16]bool)
|
||||
for _, v := range supported {
|
||||
found[v] = true
|
||||
}
|
||||
|
||||
for _, v := range versions {
|
||||
if !found[v] {
|
||||
t.Errorf("Version %d not found in supported versions", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshAll(t *testing.T) {
|
||||
db := getTestDB(t)
|
||||
if db == nil {
|
||||
return // Test skipped
|
||||
}
|
||||
defer cleanupTestDB(db)
|
||||
|
||||
service := NewOpcodeService(db)
|
||||
|
||||
// Add opcodes for multiple versions
|
||||
versions := []uint16{9999, 9998}
|
||||
for _, v := range versions {
|
||||
opcodes := map[string]uint16{
|
||||
"OP_LoginRequestMsg": uint16(v),
|
||||
"OP_LoginReplyMsg": uint16(v + 1000),
|
||||
}
|
||||
if err := service.ImportOpcodes(v, opcodes); err != nil {
|
||||
t.Fatalf("Failed to import opcodes for version %d: %v", v, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh all
|
||||
err := service.RefreshAll()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to refresh all: %v", err)
|
||||
}
|
||||
|
||||
// Verify all versions are loaded
|
||||
for _, v := range versions {
|
||||
manager, err := service.GetManager(v)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get manager for version %d after refresh: %v", v, err)
|
||||
}
|
||||
|
||||
eq := manager.EmuToEQ(OP_LoginRequestMsg)
|
||||
if eq != v {
|
||||
t.Errorf("Version %d: expected opcode %d, got %d", v, v, eq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOpcodeVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
clientVersion uint16
|
||||
expectedOpcode uint16
|
||||
}{
|
||||
{800, 1},
|
||||
{899, 1},
|
||||
{900, 900},
|
||||
{1099, 900},
|
||||
{1100, 1100},
|
||||
{1192, 1100},
|
||||
{1193, 1193},
|
||||
{1199, 1193},
|
||||
{1200, 1193},
|
||||
{1300, 1300},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := GetOpcodeVersion(tt.clientVersion)
|
||||
if result != tt.expectedOpcode {
|
||||
t.Errorf("GetOpcodeVersion(%d) = %d, want %d",
|
||||
tt.clientVersion, result, tt.expectedOpcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpcodeNames(t *testing.T) {
|
||||
// Verify all defined opcodes have names
|
||||
knownOpcodes := []EmuOpcode{
|
||||
OP_Unknown,
|
||||
OP_LoginRequestMsg,
|
||||
OP_LoginByNumRequestMsg,
|
||||
OP_WSLoginRequestMsg,
|
||||
OP_ESLoginRequestMsg,
|
||||
OP_LoginReplyMsg,
|
||||
OP_WSStatusReplyMsg,
|
||||
OP_WorldListMsg,
|
||||
OP_WorldStatusMsg,
|
||||
OP_DeleteCharacterRequestMsg,
|
||||
OP_DeleteCharacterReplyMsg,
|
||||
OP_CreateCharacterRequestMsg,
|
||||
OP_CreateCharacterReplyMsg,
|
||||
OP_PlayCharacterRequestMsg,
|
||||
OP_PlayCharacterReplyMsg,
|
||||
OP_ServerListRequestMsg,
|
||||
OP_ServerListReplyMsg,
|
||||
OP_CharacterListRequestMsg,
|
||||
OP_CharacterListReplyMsg,
|
||||
}
|
||||
|
||||
for _, opcode := range knownOpcodes {
|
||||
name, exists := OpcodeNames[opcode]
|
||||
if !exists {
|
||||
t.Errorf("Opcode %v has no name defined", opcode)
|
||||
}
|
||||
if name == "" {
|
||||
t.Errorf("Opcode %v has empty name", opcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkOpcodeConversion benchmarks opcode conversion performance
|
||||
func BenchmarkOpcodeConversion(b *testing.B) {
|
||||
manager := NewOpcodeManager(1193)
|
||||
opcodes := make(map[string]uint16)
|
||||
|
||||
// Add many opcodes
|
||||
for i := uint16(1); i <= 100; i++ {
|
||||
name := fmt.Sprintf("OP_Test%d", i)
|
||||
opcodes[name] = i
|
||||
OpcodeNames[EmuOpcode(i)] = name
|
||||
}
|
||||
|
||||
manager.LoadOpcodes(opcodes)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.Run("EmuToEQ", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = manager.EmuToEQ(EmuOpcode(i%100 + 1))
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("EQToEmu", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = manager.EQToEmu(uint16(i%100 + 1))
|
||||
}
|
||||
})
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user