1
0

opcode handling

This commit is contained in:
Sky Johnson 2025-09-01 14:03:38 -05:00
parent 1561c74fdf
commit e5189486af
5 changed files with 955 additions and 1 deletions

188
cmd_opcodes_example.go Normal file
View 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
View File

@ -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
View File

@ -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
View 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
View 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))
}
})
}