1
0
Protocol/opcodes_db_example.go

253 lines
7.5 KiB
Go

package eq2net
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
// OpcodeDBLoader handles loading opcodes from a MySQL database
// This keeps database concerns separate from the core opcode system
type OpcodeDBLoader struct {
db *sql.DB
}
// NewOpcodeDBLoader creates a new database loader
func NewOpcodeDBLoader(db *sql.DB) *OpcodeDBLoader {
return &OpcodeDBLoader{db: db}
}
// LoadVersions loads version ranges from the database
// Executes: SELECT DISTINCT version_range1, version_range2 FROM opcodes
func (l *OpcodeDBLoader) LoadVersions() (OpcodeVersionMap, error) {
query := `SELECT DISTINCT version_range1, version_range2 FROM opcodes ORDER BY version_range1`
rows, err := l.db.Query(query)
if err != nil {
return nil, fmt.Errorf("failed to query version ranges: %w", err)
}
defer rows.Close()
versions := make(OpcodeVersionMap)
for rows.Next() {
var minVersion, maxVersion uint16
if err := rows.Scan(&minVersion, &maxVersion); err != nil {
return nil, fmt.Errorf("failed to scan version range: %w", err)
}
versions[minVersion] = maxVersion
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating version rows: %w", err)
}
return versions, nil
}
// LoadOpcodes loads opcodes for a specific version
// Executes: SELECT name, opcode FROM opcodes WHERE ? BETWEEN version_range1 AND version_range2
func (l *OpcodeDBLoader) LoadOpcodes(version uint16) (map[string]uint16, error) {
query := `
SELECT name, opcode
FROM opcodes
WHERE ? BETWEEN version_range1 AND version_range2
ORDER BY version_range1, id
`
rows, err := l.db.Query(query, version)
if err != nil {
return nil, fmt.Errorf("failed to query opcodes for version %d: %w", version, err)
}
defer rows.Close()
opcodes := make(map[string]uint16)
for rows.Next() {
var name string
var opcode uint16
if err := rows.Scan(&name, &opcode); err != nil {
return nil, fmt.Errorf("failed to scan opcode row: %w", err)
}
opcodes[name] = opcode
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating opcode rows: %w", err)
}
return opcodes, nil
}
// LoadAllOpcodes loads all opcodes for all versions at once
// More efficient for server initialization
func (l *OpcodeDBLoader) LoadAllOpcodes() (map[uint16]map[string]uint16, error) {
// First get all unique version ranges
versions, err := l.LoadVersions()
if err != nil {
return nil, err
}
// Load opcodes for each version
result := make(map[uint16]map[string]uint16)
for minVersion := range versions {
opcodes, err := l.LoadOpcodes(minVersion)
if err != nil {
return nil, fmt.Errorf("failed to load opcodes for version %d: %w", minVersion, err)
}
result[minVersion] = opcodes
}
return result, nil
}
// InitializeOpcodeSystemFromDB initializes the opcode system from a database
func InitializeOpcodeSystemFromDB(db *sql.DB) (EQOpcodeManagerMap, OpcodeVersionMap, error) {
loader := NewOpcodeDBLoader(db)
// Load version ranges
versions, err := loader.LoadVersions()
if err != nil {
return nil, nil, fmt.Errorf("failed to load version ranges: %w", err)
}
// Load all opcodes
opcodesByVersion, err := loader.LoadAllOpcodes()
if err != nil {
return nil, nil, fmt.Errorf("failed to load opcodes: %w", err)
}
// Create and initialize the opcode manager
opcodeManager := NewEQOpcodeManager()
if err := opcodeManager.LoadFromDatabase(versions, opcodesByVersion); err != nil {
return nil, nil, fmt.Errorf("failed to initialize opcode manager: %w", err)
}
log.Printf("Loaded opcodes for %d version ranges", len(versions))
for minVersion, maxVersion := range versions {
if opcodes, exists := opcodesByVersion[minVersion]; exists {
log.Printf(" Version %d-%d: %d opcodes", minVersion, maxVersion, len(opcodes))
}
}
return opcodeManager, versions, nil
}
// Example usage showing how to use the opcode system with a database
func ExampleDatabaseUsage() {
// Connect to database
dsn := "root:Root12!@tcp(localhost:3306)/eq2db?parseTime=true"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
// Configure connection pool
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
// Test connection
if err := db.Ping(); err != nil {
log.Fatalf("Failed to ping database: %v", err)
}
// Initialize the opcode system
opcodeManager, versionMap, err := InitializeOpcodeSystemFromDB(db)
if err != nil {
log.Fatalf("Failed to initialize opcode system: %v", err)
}
// Example: Handle a client with version 1193
clientVersion := uint16(1193)
// Get the appropriate opcode manager for this client
manager := opcodeManager.GetManagerForClient(clientVersion, versionMap)
if manager == nil {
log.Fatalf("No opcode manager available for client version %d", clientVersion)
}
// Convert opcodes as needed
emuOpcode := OP_LoginRequestMsg
eqOpcode := manager.EmuToEQ(emuOpcode)
log.Printf("Client %d: %s -> 0x%04X", clientVersion, manager.EmuToName(emuOpcode), eqOpcode)
// Reverse conversion
emuOpcode = manager.EQToEmu(eqOpcode)
log.Printf("Client %d: 0x%04X -> %s", clientVersion, eqOpcode, manager.EmuToName(emuOpcode))
}
// CreateOpcodeTableSQL returns the SQL to create the opcodes table
// This matches the existing EQ2 schema
func CreateOpcodeTableSQL() string {
return `
CREATE TABLE IF NOT EXISTS opcodes (
id INT AUTO_INCREMENT PRIMARY KEY,
version_range1 INT NOT NULL,
version_range2 INT NOT NULL,
name VARCHAR(64) NOT NULL,
opcode INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_version_range (version_range1, version_range2),
INDEX idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`
}
// InsertExampleOpcodes inserts example opcodes for testing
func InsertExampleOpcodes(db *sql.DB) error {
// Example data for version 1185-1197 (includes client version 1193)
opcodes := []struct {
versionMin uint16
versionMax uint16
name string
opcode uint16
}{
{1185, 1197, "OP_LoginRequestMsg", 0x00B3},
{1185, 1197, "OP_LoginReplyMsg", 0x00B6},
{1185, 1197, "OP_WorldListMsg", 0x00B8},
{1185, 1197, "OP_PlayCharacterRequestMsg", 0x00BE},
{1185, 1197, "OP_PlayCharacterReplyMsg", 0x00BF},
{1185, 1197, "OP_DeleteCharacterRequestMsg", 0x00BA},
{1185, 1197, "OP_CreateCharacterRequestMsg", 0x00BC},
// Example data for version 1198-1207
{1198, 1207, "OP_LoginRequestMsg", 0x00C3},
{1198, 1207, "OP_LoginReplyMsg", 0x00C6},
{1198, 1207, "OP_WorldListMsg", 0x00C8},
{1198, 1207, "OP_PlayCharacterRequestMsg", 0x00CE},
{1198, 1207, "OP_PlayCharacterReplyMsg", 0x00CF},
// Example data for version 1208-1211
{1208, 1211, "OP_LoginRequestMsg", 0x00D3},
{1208, 1211, "OP_LoginReplyMsg", 0x00D6},
{1208, 1211, "OP_WorldListMsg", 0x00D8},
{1208, 1211, "OP_PlayCharacterRequestMsg", 0x00DE},
{1208, 1211, "OP_PlayCharacterReplyMsg", 0x00DF},
}
// Prepare insert statement
stmt, err := db.Prepare(`
INSERT INTO opcodes (version_range1, version_range2, name, opcode)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE opcode = VALUES(opcode)
`)
if err != nil {
return fmt.Errorf("failed to prepare statement: %w", err)
}
defer stmt.Close()
// Insert all opcodes
for _, op := range opcodes {
if _, err := stmt.Exec(op.versionMin, op.versionMax, op.name, op.opcode); err != nil {
return fmt.Errorf("failed to insert opcode %s: %w", op.name, err)
}
}
log.Printf("Inserted %d example opcodes", len(opcodes))
return nil
}