416 lines
11 KiB
Go
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
|
|
} |