1
0
Protocol/opcodes.go
2025-09-01 14:03:38 -05:00

416 lines
11 KiB
Go

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
}