Common opcodes library with database support.
| tools | ||
| constants_login.go | ||
| constants_shared.go | ||
| constants_world.go | ||
| go.mod | ||
| go.sum | ||
| integration_test.go | ||
| loader.go | ||
| manager.go | ||
| manager_test.go | ||
| names.go | ||
| opcode.go | ||
| README.md | ||
| sets.go | ||
EQ2 Opcodes Package
Go package for managing EQ2 emulator opcodes with database loading and version support.
Overview
This package provides type-safe opcode management for EQ2 emulator servers, mirroring the C++ opcode system with improved testability through dependency injection. It supports multiple client versions and maintains bidirectional translation between internal opcodes and wire protocol values.
Features
- Type-safe opcodes:
Opcodetype (int16) with string names - Database loading: Load opcodes from MySQL database using the same queries as C++
- Version management: Support multiple client versions simultaneously
- Bidirectional translation: Convert between internal opcodes and wire protocol values
- Thread-safe: Concurrent access via RWMutex
- Independently testable: Mock loader for unit tests without database
- No build flags: Function-based API for login vs world server opcodes
Architecture
Core Types
- Opcode (int16): Type-safe opcode identifier
- Manager: Handles opcode translation for a specific client version
- Registry: Manages multiple opcode managers for different versions
- Loader: Interface for loading opcodes from database or mock sources
Opcode Organization
Opcodes are organized into three categories:
- Shared (21 opcodes): Used by both login and world servers
- Login (18 opcodes): Login server specific
- World (465 opcodes): World server specific
Total: 504 opcodes
Usage
Basic Example
package main
import (
"context"
"fmt"
"git.sharkk.net/eq2go/db"
"git.sharkk.net/eq2go/opcodes"
)
func main() {
// Connect to database
backend, err := db.New("mysql", db.Config{
Host: "localhost",
Database: "eq2ls",
Username: "eq2",
Password: "eq2pass",
})
if err != nil {
panic(err)
}
defer backend.Close()
// Create loader and registry
loader := opcodes.NewDatabaseLoader(backend)
registry := opcodes.NewRegistry()
// Load all available client versions
if err := registry.LoadAllVersions(context.Background(), loader); err != nil {
panic(err)
}
// Get manager for client version 1193
mgr, ok := registry.GetManager(1193)
if !ok {
panic("Version 1193 not found")
}
// Translate internal opcode to wire protocol
wireOp, ok := mgr.EmuToEQ(opcodes.OP_LoginRequestMsg)
if !ok {
panic("Opcode not found for this version")
}
fmt.Printf("OP_LoginRequestMsg -> 0x%04x\n", wireOp)
// Translate wire protocol back to internal
internalOp, ok := mgr.EQToEmu(wireOp)
if !ok {
panic("Wire opcode not found")
}
fmt.Printf("0x%04x -> %s\n", wireOp, internalOp.String())
}
Login Server Example
// Get opcodes available to login server
loginOpcodes := opcodes.LoginOpcodeSet()
fmt.Printf("Login server has %d opcodes\n", len(loginOpcodes))
// Use manager to translate login opcodes
wireOp, _ := mgr.EmuToEQ(opcodes.OP_LoginRequestMsg)
World Server Example
// Get opcodes available to world server
worldOpcodes := opcodes.WorldOpcodeSet()
fmt.Printf("World server has %d opcodes\n", len(worldOpcodes))
// Use manager to translate world opcodes
wireOp, _ := mgr.EmuToEQ(opcodes.OP_ESInitMsg)
Testing
Run Unit Tests (No Database Required)
cd go/opcodes
go test -v -short
Run All Tests (Requires Database)
cd go/opcodes
go test -v
Run Integration Tests Only
cd go/opcodes
go test -v -run Integration
Configure Database for Integration Tests
Set environment variables:
export EQ2_DB_HOST=localhost
export EQ2_DB_NAME=eq2ls
export EQ2_DB_USER=eq2
export EQ2_DB_PASS=eq2pass
go test -v
Regenerating Opcode Constants
When C++ opcode definitions change, regenerate the Go constants:
cd go/opcodes/tools
go run generate_constants.go
This reads the C++ header files:
/include/opcodes/emu_oplist_shared.h/cmd/login/emu_oplist_login.h/cmd/world/emu_oplist_world.h
And generates:
constants_shared.goconstants_login.goconstants_world.gonames.go
Design Differences from C++
Advantages
- No build flags: Uses
LoginOpcodeSet()andWorldOpcodeSet()functions instead of preprocessor flags - Dependency injection:
Loaderinterface enables testing without database - Type safety:
Opcodeis a distinct type, not raw int16 - Built-in thread safety: RWMutex instead of manual mutex management
- Auto-generation: Script ensures constants stay in sync with C++
Compatibility
- Uses identical database queries as C++
- Maintains same bidirectional translation semantics
- Supports same version range logic (BETWEEN version_range1 AND version_range2)
- Produces identical wire protocol values for each client version
Package API
Opcode Type
type Opcode int16
func (o Opcode) String() string // Returns opcode name
func (o Opcode) IsValid() bool // Checks if opcode is recognized
Manager
func NewManager(version int16) *Manager
func (m *Manager) LoadFromLoader(ctx context.Context, loader Loader) error
func (m *Manager) EmuToEQ(op Opcode) (uint16, bool)
func (m *Manager) EQToEmu(wireOp uint16) (Opcode, bool)
func (m *Manager) HasOpcode(op Opcode) bool
func (m *Manager) Version() int16
func (m *Manager) OpcodeCount() int
Registry
func NewRegistry() *Registry
func (r *Registry) LoadAllVersions(ctx context.Context, loader Loader) error
func (r *Registry) GetManager(version int16) (*Manager, bool)
func (r *Registry) VersionsLoaded() []int16
func (r *Registry) ManagerCount() int
Loader Interface
type Loader interface {
GetVersions(ctx context.Context) ([]VersionRange, error)
GetOpcodes(ctx context.Context, version int16) (map[string]uint16, error)
}
func NewDatabaseLoader(backend db.Backend) *DatabaseLoader
Opcode Sets
func SharedOpcodes() []Opcode // 21 shared opcodes
func LoginOpcodes() []Opcode // 18 login-specific opcodes
func WorldOpcodes() []Opcode // 465 world-specific opcodes
func LoginOpcodeSet() []Opcode // Shared + Login (39 total)
func WorldOpcodeSet() []Opcode // Shared + World (486 total)
Database Schema
The package expects the following database table:
CREATE TABLE opcodes (
id INT(10) PRIMARY KEY AUTO_INCREMENT,
version_range1 SMALLINT(5) NOT NULL DEFAULT 0,
version_range2 SMALLINT(5) NOT NULL DEFAULT 0,
name VARCHAR(255) NOT NULL,
opcode SMALLINT(5) NOT NULL,
table_data_version SMALLINT(5) NOT NULL DEFAULT 1,
UNIQUE KEY newindex (version_range1, name, version_range2)
);
License
EQ2Emu Copyright (C) 2007-2025 EQ2Emu, Sharkk See LICENSE