opcodes and packet parser
This commit is contained in:
parent
e5189486af
commit
1a9f194f6a
@ -1,188 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
394
opcodes.go
394
opcodes.go
@ -1,18 +1,41 @@
|
|||||||
package eq2net
|
package eq2net
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// VersionRange represents a client version range mapped to an opcode version
|
||||||
|
type VersionRange struct {
|
||||||
|
MinVersion uint16 // version_range1 in database
|
||||||
|
MaxVersion uint16 // version_range2 in database
|
||||||
|
OpcodeVersion uint16 // The opcode version for this range (usually MinVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpcodeVersionMap maps version ranges for opcode lookups
|
||||||
|
// This replaces the C++ EQOpcodeVersions map<int16, int16>
|
||||||
|
type OpcodeVersionMap map[uint16]uint16 // Key: version_range1, Value: version_range2
|
||||||
|
|
||||||
|
// GetOpcodeVersion returns the opcode version for a given client version
|
||||||
|
// This is a direct port of the C++ GetOpcodeVersion function
|
||||||
|
func GetOpcodeVersion(clientVersion uint16, versionMap OpcodeVersionMap) uint16 {
|
||||||
|
ret := clientVersion
|
||||||
|
|
||||||
|
// Iterate through version ranges to find a match
|
||||||
|
for minVersion, maxVersion := range versionMap {
|
||||||
|
if clientVersion >= minVersion && clientVersion <= maxVersion {
|
||||||
|
ret = minVersion
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
// EmuOpcode represents an emulator-side opcode
|
// EmuOpcode represents an emulator-side opcode
|
||||||
type EmuOpcode uint16
|
type EmuOpcode uint16
|
||||||
|
|
||||||
// Common emulator opcodes
|
// Common emulator opcodes - these match the C++ emu_opcodes.h
|
||||||
const (
|
const (
|
||||||
OP_Unknown EmuOpcode = 0x0000
|
OP_Unknown EmuOpcode = 0x0000
|
||||||
OP_LoginRequestMsg EmuOpcode = 0x0001
|
OP_LoginRequestMsg EmuOpcode = 0x0001
|
||||||
@ -33,9 +56,11 @@ const (
|
|||||||
OP_ServerListReplyMsg EmuOpcode = 0x0010
|
OP_ServerListReplyMsg EmuOpcode = 0x0010
|
||||||
OP_CharacterListRequestMsg EmuOpcode = 0x0011
|
OP_CharacterListRequestMsg EmuOpcode = 0x0011
|
||||||
OP_CharacterListReplyMsg EmuOpcode = 0x0012
|
OP_CharacterListReplyMsg EmuOpcode = 0x0012
|
||||||
|
// Add more opcodes as needed
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpcodeNames maps emulator opcodes to their string names
|
// OpcodeNames maps emulator opcodes to their string names
|
||||||
|
// This matches the C++ OpcodeNames array
|
||||||
var OpcodeNames = map[EmuOpcode]string{
|
var OpcodeNames = map[EmuOpcode]string{
|
||||||
OP_Unknown: "OP_Unknown",
|
OP_Unknown: "OP_Unknown",
|
||||||
OP_LoginRequestMsg: "OP_LoginRequestMsg",
|
OP_LoginRequestMsg: "OP_LoginRequestMsg",
|
||||||
@ -58,18 +83,18 @@ var OpcodeNames = map[EmuOpcode]string{
|
|||||||
OP_CharacterListReplyMsg: "OP_CharacterListReplyMsg",
|
OP_CharacterListReplyMsg: "OP_CharacterListReplyMsg",
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpcodeManager manages opcode mappings for different client versions
|
// RegularOpcodeManager manages opcode mappings for a specific version
|
||||||
type OpcodeManager struct {
|
// This is equivalent to the C++ RegularOpcodeManager class
|
||||||
|
type RegularOpcodeManager struct {
|
||||||
version uint16
|
version uint16
|
||||||
emuToEQ map[EmuOpcode]uint16
|
emuToEQ map[EmuOpcode]uint16
|
||||||
eqToEmu map[uint16]EmuOpcode
|
eqToEmu map[uint16]EmuOpcode
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
lastModified time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOpcodeManager creates a new opcode manager for a specific version
|
// NewRegularOpcodeManager creates a new opcode manager
|
||||||
func NewOpcodeManager(version uint16) *OpcodeManager {
|
func NewRegularOpcodeManager(version uint16) *RegularOpcodeManager {
|
||||||
return &OpcodeManager{
|
return &RegularOpcodeManager{
|
||||||
version: version,
|
version: version,
|
||||||
emuToEQ: make(map[EmuOpcode]uint16),
|
emuToEQ: make(map[EmuOpcode]uint16),
|
||||||
eqToEmu: make(map[uint16]EmuOpcode),
|
eqToEmu: make(map[uint16]EmuOpcode),
|
||||||
@ -77,7 +102,8 @@ func NewOpcodeManager(version uint16) *OpcodeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadOpcodes loads opcode mappings from a map
|
// LoadOpcodes loads opcode mappings from a map
|
||||||
func (om *OpcodeManager) LoadOpcodes(opcodes map[string]uint16) error {
|
// Input format matches database: map[opcode_name]opcode_value
|
||||||
|
func (om *RegularOpcodeManager) LoadOpcodes(opcodes map[string]uint16) bool {
|
||||||
om.mu.Lock()
|
om.mu.Lock()
|
||||||
defer om.mu.Unlock()
|
defer om.mu.Unlock()
|
||||||
|
|
||||||
@ -102,23 +128,22 @@ func (om *OpcodeManager) LoadOpcodes(opcodes map[string]uint16) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
om.lastModified = time.Now()
|
return true
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmuToEQ converts an emulator opcode to EQ network opcode
|
// EmuToEQ converts an emulator opcode to EQ network opcode
|
||||||
func (om *OpcodeManager) EmuToEQ(emu EmuOpcode) uint16 {
|
func (om *RegularOpcodeManager) EmuToEQ(emu EmuOpcode) uint16 {
|
||||||
om.mu.RLock()
|
om.mu.RLock()
|
||||||
defer om.mu.RUnlock()
|
defer om.mu.RUnlock()
|
||||||
|
|
||||||
if eq, exists := om.emuToEQ[emu]; exists {
|
if eq, exists := om.emuToEQ[emu]; exists {
|
||||||
return eq
|
return eq
|
||||||
}
|
}
|
||||||
return 0xFFFF // Invalid opcode marker
|
return 0xCDCD // Invalid opcode marker (matches C++)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EQToEmu converts an EQ network opcode to emulator opcode
|
// EQToEmu converts an EQ network opcode to emulator opcode
|
||||||
func (om *OpcodeManager) EQToEmu(eq uint16) EmuOpcode {
|
func (om *RegularOpcodeManager) EQToEmu(eq uint16) EmuOpcode {
|
||||||
om.mu.RLock()
|
om.mu.RLock()
|
||||||
defer om.mu.RUnlock()
|
defer om.mu.RUnlock()
|
||||||
|
|
||||||
@ -129,7 +154,7 @@ func (om *OpcodeManager) EQToEmu(eq uint16) EmuOpcode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EmuToName returns the name of an emulator opcode
|
// EmuToName returns the name of an emulator opcode
|
||||||
func (om *OpcodeManager) EmuToName(emu EmuOpcode) string {
|
func (om *RegularOpcodeManager) EmuToName(emu EmuOpcode) string {
|
||||||
if name, exists := OpcodeNames[emu]; exists {
|
if name, exists := OpcodeNames[emu]; exists {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
@ -137,280 +162,111 @@ func (om *OpcodeManager) EmuToName(emu EmuOpcode) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EQToName returns the name of an EQ network opcode
|
// EQToName returns the name of an EQ network opcode
|
||||||
func (om *OpcodeManager) EQToName(eq uint16) string {
|
func (om *RegularOpcodeManager) EQToName(eq uint16) string {
|
||||||
emu := om.EQToEmu(eq)
|
emu := om.EQToEmu(eq)
|
||||||
return om.EmuToName(emu)
|
return om.EmuToName(emu)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVersion returns the client version this manager handles
|
// NameSearch finds an emulator opcode by name
|
||||||
func (om *OpcodeManager) GetVersion() uint16 {
|
func NameSearch(name string) EmuOpcode {
|
||||||
return om.version
|
for opcode, opcName := range OpcodeNames {
|
||||||
}
|
if opcName == name {
|
||||||
|
return opcode
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return OP_Unknown
|
||||||
s.lastRefresh = time.Now()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSupportedVersions returns all client versions with opcode mappings
|
// EQOpcodeManager is the global opcode manager map
|
||||||
func (s *OpcodeService) GetSupportedVersions() ([]uint16, error) {
|
// Maps opcode version to manager instance
|
||||||
query := `SELECT DISTINCT version FROM opcodes ORDER BY version`
|
// This replaces the C++ map<int16, OpcodeManager*> EQOpcodeManager
|
||||||
|
type EQOpcodeManagerMap map[uint16]*RegularOpcodeManager
|
||||||
rows, err := s.db.Query(query)
|
|
||||||
if err != nil {
|
// NewEQOpcodeManager creates and initializes the global opcode manager
|
||||||
return nil, fmt.Errorf("failed to query versions: %w", err)
|
func NewEQOpcodeManager() EQOpcodeManagerMap {
|
||||||
}
|
return make(EQOpcodeManagerMap)
|
||||||
defer rows.Close()
|
}
|
||||||
|
|
||||||
var versions []uint16
|
// LoadFromDatabase simulates loading opcodes from database results
|
||||||
for rows.Next() {
|
// This would be called by your application after querying the database
|
||||||
var version uint16
|
func (m EQOpcodeManagerMap) LoadFromDatabase(versions OpcodeVersionMap, opcodesByVersion map[uint16]map[string]uint16) error {
|
||||||
if err := rows.Scan(&version); err != nil {
|
// For each version range, create an opcode manager
|
||||||
return nil, fmt.Errorf("failed to scan version: %w", err)
|
for minVersion := range versions {
|
||||||
|
manager := NewRegularOpcodeManager(minVersion)
|
||||||
|
|
||||||
|
// Load opcodes for this version
|
||||||
|
if opcodes, exists := opcodesByVersion[minVersion]; exists {
|
||||||
|
if !manager.LoadOpcodes(opcodes) {
|
||||||
|
return fmt.Errorf("failed to load opcodes for version %d", minVersion)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("no opcodes found for version %d", minVersion)
|
||||||
}
|
}
|
||||||
versions = append(versions, version)
|
|
||||||
|
m[minVersion] = manager
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportOpcodes imports a batch of opcodes for a version
|
// GetManagerForClient returns the appropriate opcode manager for a client version
|
||||||
func (s *OpcodeService) ImportOpcodes(version uint16, opcodes map[string]uint16) error {
|
func (m EQOpcodeManagerMap) GetManagerForClient(clientVersion uint16, versionMap OpcodeVersionMap) *RegularOpcodeManager {
|
||||||
tx, err := s.db.Begin()
|
opcodeVersion := GetOpcodeVersion(clientVersion, versionMap)
|
||||||
if err != nil {
|
return m[opcodeVersion]
|
||||||
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
|
// Example helper functions for database integration
|
||||||
// This is a simplified version - in production, this might be more complex
|
// These would be implemented by the application using this library
|
||||||
func GetOpcodeVersion(clientVersion uint16) uint16 {
|
|
||||||
// Map client versions to opcode table versions
|
// LoadVersionsFromDB would execute:
|
||||||
// These are example mappings - adjust based on your actual data
|
// SELECT DISTINCT version_range1, version_range2 FROM opcodes
|
||||||
switch {
|
func LoadVersionsFromDB() OpcodeVersionMap {
|
||||||
case clientVersion < 900:
|
// This is just an example - actual implementation would query the database
|
||||||
return 1
|
return OpcodeVersionMap{
|
||||||
case clientVersion < 1100:
|
1: 546, // Version range 1-546 uses opcode version 1
|
||||||
return 900
|
547: 889, // Version range 547-889 uses opcode version 547
|
||||||
case clientVersion < 1193:
|
890: 1027, // etc.
|
||||||
return 1100
|
1028: 1048,
|
||||||
case clientVersion == 1193:
|
1049: 1095,
|
||||||
return 1193
|
1096: 1184,
|
||||||
case clientVersion < 1300:
|
1185: 1197,
|
||||||
return 1193
|
1198: 1207,
|
||||||
default:
|
1208: 1211,
|
||||||
return clientVersion
|
1212: 9999,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectDB establishes a connection to the MySQL database
|
// LoadOpcodesFromDB would execute:
|
||||||
func ConnectDB(dsn string) (*sql.DB, error) {
|
// SELECT name, opcode FROM opcodes WHERE ? BETWEEN version_range1 AND version_range2
|
||||||
db, err := sql.Open("mysql", dsn)
|
func LoadOpcodesFromDB(version uint16) map[string]uint16 {
|
||||||
if err != nil {
|
// This is just an example - actual implementation would query the database
|
||||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
return map[string]uint16{
|
||||||
|
"OP_LoginRequestMsg": 0x00B3,
|
||||||
|
"OP_LoginReplyMsg": 0x00B6,
|
||||||
|
// ... etc
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// InitializeOpcodeSystem shows how to initialize the opcode system
|
||||||
func CreateOpcodeTable(db *sql.DB) error {
|
// This would be called during server startup
|
||||||
query := `
|
func InitializeOpcodeSystem() (EQOpcodeManagerMap, OpcodeVersionMap, error) {
|
||||||
CREATE TABLE IF NOT EXISTS opcodes (
|
// Load version ranges from database
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
versions := LoadVersionsFromDB()
|
||||||
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)
|
// Create the global opcode manager
|
||||||
if err != nil {
|
opcodeManager := NewEQOpcodeManager()
|
||||||
return fmt.Errorf("failed to create opcodes table: %w", err)
|
|
||||||
|
// Load opcodes for each version
|
||||||
|
opcodesByVersion := make(map[uint16]map[string]uint16)
|
||||||
|
for minVersion := range versions {
|
||||||
|
opcodes := LoadOpcodesFromDB(minVersion)
|
||||||
|
opcodesByVersion[minVersion] = opcodes
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// Initialize the manager
|
||||||
|
if err := opcodeManager.LoadFromDatabase(versions, opcodesByVersion); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return opcodeManager, versions, nil
|
||||||
}
|
}
|
252
opcodes_db_example.go
Normal file
252
opcodes_db_example.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
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
|
||||||
|
}
|
411
opcodes_test.go
411
opcodes_test.go
@ -1,68 +1,85 @@
|
|||||||
package eq2net
|
package eq2net
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Test database configuration
|
func TestGetOpcodeVersionV2(t *testing.T) {
|
||||||
const (
|
// Create a version map that matches the C++ implementation
|
||||||
testDBHost = "localhost"
|
versionMap := OpcodeVersionMap{
|
||||||
testDBPort = "3306"
|
1: 546,
|
||||||
testDBUser = "root"
|
547: 889,
|
||||||
testDBPass = "Root12!"
|
890: 1027,
|
||||||
testDBName = "eq2test"
|
1028: 1048,
|
||||||
)
|
1049: 1095,
|
||||||
|
1096: 1184,
|
||||||
// getTestDB creates a test database connection
|
1185: 1197,
|
||||||
func getTestDB(t *testing.T) *sql.DB {
|
1198: 1207,
|
||||||
// Connect without database first to create it if needed
|
1208: 1211,
|
||||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/", testDBUser, testDBPass, testDBHost, testDBPort)
|
1212: 9999,
|
||||||
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
|
tests := []struct {
|
||||||
_, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + testDBName)
|
clientVersion uint16
|
||||||
if err != nil {
|
expectedOpcode uint16
|
||||||
t.Skipf("Cannot create test database: %v", err)
|
}{
|
||||||
return nil
|
// Test first range
|
||||||
}
|
{1, 1},
|
||||||
db.Close()
|
{100, 1},
|
||||||
|
{546, 1},
|
||||||
// Connect to test database
|
|
||||||
dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true",
|
// Test second range
|
||||||
testDBUser, testDBPass, testDBHost, testDBPort, testDBName)
|
{547, 547},
|
||||||
|
{700, 547},
|
||||||
db, err = ConnectDB(dsn)
|
{889, 547},
|
||||||
if err != nil {
|
|
||||||
t.Skipf("Cannot connect to test database: %v", err)
|
// Test middle ranges
|
||||||
return nil
|
{890, 890},
|
||||||
|
{1000, 890},
|
||||||
|
{1027, 890},
|
||||||
|
|
||||||
|
{1028, 1028},
|
||||||
|
{1048, 1028},
|
||||||
|
|
||||||
|
{1096, 1096},
|
||||||
|
{1100, 1096},
|
||||||
|
{1184, 1096},
|
||||||
|
|
||||||
|
{1185, 1185},
|
||||||
|
{1193, 1185},
|
||||||
|
{1197, 1185},
|
||||||
|
|
||||||
|
{1198, 1198},
|
||||||
|
{1200, 1198},
|
||||||
|
{1207, 1198},
|
||||||
|
|
||||||
|
{1208, 1208},
|
||||||
|
{1210, 1208},
|
||||||
|
{1211, 1208},
|
||||||
|
|
||||||
|
// Test last range
|
||||||
|
{1212, 1212},
|
||||||
|
{2000, 1212},
|
||||||
|
{9999, 1212},
|
||||||
|
|
||||||
|
// Test out of range (should return client version)
|
||||||
|
{10000, 10000},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create tables
|
for _, tt := range tests {
|
||||||
if err := CreateOpcodeTable(db); err != nil {
|
result := GetOpcodeVersion(tt.clientVersion, versionMap)
|
||||||
t.Fatalf("Failed to create opcodes table: %v", err)
|
if result != tt.expectedOpcode {
|
||||||
}
|
t.Errorf("GetOpcodeVersion(%d) = %d, want %d",
|
||||||
|
tt.clientVersion, result, tt.expectedOpcode)
|
||||||
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) {
|
func TestRegularOpcodeManagerV2(t *testing.T) {
|
||||||
manager := NewOpcodeManager(1193)
|
manager := NewRegularOpcodeManager(1193)
|
||||||
|
|
||||||
// Test loading opcodes
|
// Test loading opcodes (simulating database results)
|
||||||
opcodes := map[string]uint16{
|
opcodes := map[string]uint16{
|
||||||
"OP_LoginRequestMsg": 0x0001,
|
"OP_LoginRequestMsg": 0x0001,
|
||||||
"OP_LoginReplyMsg": 0x0002,
|
"OP_LoginReplyMsg": 0x0002,
|
||||||
@ -70,9 +87,8 @@ func TestOpcodeManager(t *testing.T) {
|
|||||||
"OP_PlayCharacterRequestMsg": 0x0004,
|
"OP_PlayCharacterRequestMsg": 0x0004,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := manager.LoadOpcodes(opcodes)
|
if !manager.LoadOpcodes(opcodes) {
|
||||||
if err != nil {
|
t.Fatal("Failed to load opcodes")
|
||||||
t.Fatalf("Failed to load opcodes: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test EmuToEQ conversion
|
// Test EmuToEQ conversion
|
||||||
@ -81,16 +97,22 @@ func TestOpcodeManager(t *testing.T) {
|
|||||||
t.Errorf("Expected EQ opcode 0x0001, got 0x%04x", eq)
|
t.Errorf("Expected EQ opcode 0x0001, got 0x%04x", eq)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test invalid opcode returns 0xCDCD
|
||||||
|
eq = manager.EmuToEQ(OP_Unknown)
|
||||||
|
if eq != 0xCDCD {
|
||||||
|
t.Errorf("Expected 0xCDCD for unknown opcode, got 0x%04x", eq)
|
||||||
|
}
|
||||||
|
|
||||||
// Test EQToEmu conversion
|
// Test EQToEmu conversion
|
||||||
emu := manager.EQToEmu(0x0002)
|
emu := manager.EQToEmu(0x0002)
|
||||||
if emu != OP_LoginReplyMsg {
|
if emu != OP_LoginReplyMsg {
|
||||||
t.Errorf("Expected emu opcode %v, got %v", OP_LoginReplyMsg, emu)
|
t.Errorf("Expected emu opcode %v, got %v", OP_LoginReplyMsg, emu)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test unknown opcode
|
// Test unknown EQ opcode
|
||||||
eq = manager.EmuToEQ(OP_Unknown)
|
emu = manager.EQToEmu(0xFFFF)
|
||||||
if eq != 0xFFFF {
|
if emu != OP_Unknown {
|
||||||
t.Errorf("Expected 0xFFFF for unknown opcode, got 0x%04x", eq)
|
t.Errorf("Expected OP_Unknown for unknown EQ opcode, got %v", emu)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test name lookups
|
// Test name lookups
|
||||||
@ -105,225 +127,103 @@ func TestOpcodeManager(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOpcodeService(t *testing.T) {
|
func TestEQOpcodeManagerMapV2(t *testing.T) {
|
||||||
db := getTestDB(t)
|
// Create version map
|
||||||
if db == nil {
|
versions := OpcodeVersionMap{
|
||||||
return // Test skipped
|
1185: 1197,
|
||||||
}
|
1198: 1207,
|
||||||
defer cleanupTestDB(db)
|
1208: 1211,
|
||||||
|
|
||||||
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
|
// Create opcodes for each version
|
||||||
err := service.ImportOpcodes(testVersion, testOpcodes)
|
opcodesByVersion := map[uint16]map[string]uint16{
|
||||||
|
1185: {
|
||||||
|
"OP_LoginRequestMsg": 0x00B3,
|
||||||
|
"OP_LoginReplyMsg": 0x00B6,
|
||||||
|
},
|
||||||
|
1198: {
|
||||||
|
"OP_LoginRequestMsg": 0x00C1,
|
||||||
|
"OP_LoginReplyMsg": 0x00C4,
|
||||||
|
},
|
||||||
|
1208: {
|
||||||
|
"OP_LoginRequestMsg": 0x00D1,
|
||||||
|
"OP_LoginReplyMsg": 0x00D4,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the manager map
|
||||||
|
managerMap := NewEQOpcodeManager()
|
||||||
|
err := managerMap.LoadFromDatabase(versions, opcodesByVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to import opcodes: %v", err)
|
t.Fatalf("Failed to load from database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get manager for version
|
// Test getting manager for client version 1193 (should use 1185 opcodes)
|
||||||
manager, err := service.GetManager(testVersion)
|
manager := managerMap.GetManagerForClient(1193, versions)
|
||||||
if err != nil {
|
if manager == nil {
|
||||||
t.Fatalf("Failed to get manager: %v", err)
|
t.Fatal("Failed to get manager for client version 1193")
|
||||||
}
|
}
|
||||||
|
|
||||||
if manager.GetVersion() != testVersion {
|
|
||||||
t.Errorf("Expected version %d, got %d", testVersion, manager.GetVersion())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test opcode conversions
|
|
||||||
eq := manager.EmuToEQ(OP_LoginRequestMsg)
|
eq := manager.EmuToEQ(OP_LoginRequestMsg)
|
||||||
if eq != 0x1001 {
|
if eq != 0x00B3 {
|
||||||
t.Errorf("Expected EQ opcode 0x1001, got 0x%04x", eq)
|
t.Errorf("Expected 0x00B3 for version 1193, got 0x%04x", eq)
|
||||||
}
|
}
|
||||||
|
|
||||||
emu := manager.EQToEmu(0x1002)
|
// Test getting manager for client version 1200 (should use 1198 opcodes)
|
||||||
if emu != OP_LoginReplyMsg {
|
manager = managerMap.GetManagerForClient(1200, versions)
|
||||||
t.Errorf("Expected emu opcode %v, got %v", OP_LoginReplyMsg, emu)
|
if manager == nil {
|
||||||
|
t.Fatal("Failed to get manager for client version 1200")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test single opcode save
|
eq = manager.EmuToEQ(OP_LoginRequestMsg)
|
||||||
err = service.SaveOpcode(testVersion, "OP_ServerListRequestMsg", 0x1006)
|
if eq != 0x00C1 {
|
||||||
if err != nil {
|
t.Errorf("Expected 0x00C1 for version 1200, got 0x%04x", eq)
|
||||||
t.Fatalf("Failed to save opcode: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force reload and verify
|
// Test getting manager for client version 1210 (should use 1208 opcodes)
|
||||||
service.mu.Lock()
|
manager = managerMap.GetManagerForClient(1210, versions)
|
||||||
delete(service.managers, testVersion)
|
if manager == nil {
|
||||||
service.mu.Unlock()
|
t.Fatal("Failed to get manager for client version 1210")
|
||||||
|
|
||||||
manager, err = service.GetManager(testVersion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to reload manager: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eq = manager.EmuToEQ(OP_ServerListRequestMsg)
|
eq = manager.EmuToEQ(OP_LoginRequestMsg)
|
||||||
if eq != 0x1006 {
|
if eq != 0x00D1 {
|
||||||
t.Errorf("Expected EQ opcode 0x1006 after save, got 0x%04x", eq)
|
t.Errorf("Expected 0x00D1 for version 1210, got 0x%04x", eq)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetSupportedVersions(t *testing.T) {
|
func TestNameSearch(t *testing.T) {
|
||||||
db := getTestDB(t)
|
// Test finding opcodes by name
|
||||||
if db == nil {
|
opcode := NameSearch("OP_LoginRequestMsg")
|
||||||
return // Test skipped
|
if opcode != OP_LoginRequestMsg {
|
||||||
}
|
t.Errorf("Expected %v, got %v", OP_LoginRequestMsg, opcode)
|
||||||
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
|
opcode = NameSearch("OP_WorldListMsg")
|
||||||
supported, err := service.GetSupportedVersions()
|
if opcode != OP_WorldListMsg {
|
||||||
if err != nil {
|
t.Errorf("Expected %v, got %v", OP_WorldListMsg, opcode)
|
||||||
t.Fatalf("Failed to get supported versions: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that our test versions are included
|
// Test unknown name
|
||||||
found := make(map[uint16]bool)
|
opcode = NameSearch("OP_NonExistent")
|
||||||
for _, v := range supported {
|
if opcode != OP_Unknown {
|
||||||
found[v] = true
|
t.Errorf("Expected OP_Unknown for non-existent name, got %v", opcode)
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range versions {
|
|
||||||
if !found[v] {
|
|
||||||
t.Errorf("Version %d not found in supported versions", v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRefreshAll(t *testing.T) {
|
// BenchmarkOpcodeConversionV2 benchmarks the new implementation
|
||||||
db := getTestDB(t)
|
func BenchmarkOpcodeConversionV2(b *testing.B) {
|
||||||
if db == nil {
|
manager := NewRegularOpcodeManager(1193)
|
||||||
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)
|
opcodes := make(map[string]uint16)
|
||||||
|
|
||||||
// Add many opcodes
|
// Add many opcodes for benchmarking
|
||||||
for i := uint16(1); i <= 100; i++ {
|
for i := uint16(1); i <= 100; i++ {
|
||||||
name := fmt.Sprintf("OP_Test%d", i)
|
name := OpcodeNames[EmuOpcode(i)]
|
||||||
|
if name == "" {
|
||||||
|
name = fmt.Sprintf("OP_Test%d", i)
|
||||||
|
OpcodeNames[EmuOpcode(i)] = name
|
||||||
|
}
|
||||||
opcodes[name] = i
|
opcodes[name] = i
|
||||||
OpcodeNames[EmuOpcode(i)] = name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.LoadOpcodes(opcodes)
|
manager.LoadOpcodes(opcodes)
|
||||||
@ -341,4 +241,23 @@ func BenchmarkOpcodeConversion(b *testing.B) {
|
|||||||
_ = manager.EQToEmu(uint16(i%100 + 1))
|
_ = manager.EQToEmu(uint16(i%100 + 1))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
b.Run("GetOpcodeVersion", func(b *testing.B) {
|
||||||
|
versionMap := OpcodeVersionMap{
|
||||||
|
1: 546,
|
||||||
|
547: 889,
|
||||||
|
890: 1027,
|
||||||
|
1028: 1048,
|
||||||
|
1049: 1095,
|
||||||
|
1096: 1184,
|
||||||
|
1185: 1197,
|
||||||
|
1198: 1207,
|
||||||
|
1208: 1211,
|
||||||
|
1212: 9999,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = GetOpcodeVersion(uint16(i%2000+1), versionMap)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
174
structs/README.md
Normal file
174
structs/README.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# Packet Structure Parser
|
||||||
|
|
||||||
|
This package provides a Go implementation of the EQ2 packet structure system, compatible with the C++ ConfigReader and PacketStruct classes. It parses XML packet definitions and provides runtime serialization/deserialization capabilities.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **XML Parsing**: Reads packet and substruct definitions from XML files
|
||||||
|
- **Version Management**: Handles multiple versions of packet structures
|
||||||
|
- **Type Support**: All EQ2 data types including integers, floats, strings, and colors
|
||||||
|
- **Conditional Fields**: Support for if-set/if-not-set field conditions
|
||||||
|
- **Zero Allocation**: Optimized for performance with minimal allocations
|
||||||
|
- **ConfigReader Compatible**: Drop-in replacement for C++ ConfigReader
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Loading Packet Definitions
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create a config reader
|
||||||
|
cr := structs.NewConfigReader("./")
|
||||||
|
|
||||||
|
// Load all XML files from structs/xml directory
|
||||||
|
err := cr.LoadStructs()
|
||||||
|
|
||||||
|
// Or load specific files
|
||||||
|
err := cr.LoadFiles([]string{"structs/xml/login/LoginRequest.xml"})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating and Using Packet Structs
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get a packet struct for a specific version
|
||||||
|
ps, err := cr.GetStruct("LoginRequest", 1193)
|
||||||
|
|
||||||
|
// Set field values
|
||||||
|
ps.Set("username", "player123")
|
||||||
|
ps.Set("password", "secret")
|
||||||
|
ps.Set("version", uint16(1193))
|
||||||
|
|
||||||
|
// Serialize to bytes
|
||||||
|
data, err := ps.Serialize()
|
||||||
|
|
||||||
|
// Deserialize from bytes
|
||||||
|
ps2, err := cr.GetStruct("LoginRequest", 1193)
|
||||||
|
err = ps2.Deserialize(data)
|
||||||
|
|
||||||
|
// Get field values
|
||||||
|
username, _ := ps2.Get("username")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Direct Parser Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create a parser
|
||||||
|
parser := structs.NewParser("structs")
|
||||||
|
|
||||||
|
// Load all XML files
|
||||||
|
err := parser.LoadAll()
|
||||||
|
|
||||||
|
// Get a specific packet version
|
||||||
|
version, err := parser.GetPacketVersion("LoginRequest", 1193)
|
||||||
|
|
||||||
|
// Create packet struct directly
|
||||||
|
ps := structs.NewPacketStruct(version)
|
||||||
|
```
|
||||||
|
|
||||||
|
## XML Format
|
||||||
|
|
||||||
|
Packet definitions use the following XML format:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<packet name="LoginRequest">
|
||||||
|
<version number="1">
|
||||||
|
<str16 name="username"/>
|
||||||
|
<str16 name="password"/>
|
||||||
|
<u32 name="acctNum"/>
|
||||||
|
<u16 name="version"/>
|
||||||
|
</version>
|
||||||
|
<version number="562">
|
||||||
|
<str16 name="accesscode"/>
|
||||||
|
<str16 name="username"/>
|
||||||
|
<str16 name="password"/>
|
||||||
|
<u8 name="unknown" size="8"/>
|
||||||
|
<u32 name="version"/>
|
||||||
|
</version>
|
||||||
|
</packet>
|
||||||
|
```
|
||||||
|
|
||||||
|
Substructs use a similar format:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<substruct name="Item">
|
||||||
|
<version number="1">
|
||||||
|
<u32 name="unique_id"/>
|
||||||
|
<u32 name="bag_id"/>
|
||||||
|
<u8 name="slot_id"/>
|
||||||
|
<char name="name" size="64"/>
|
||||||
|
</version>
|
||||||
|
</substruct>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Field Types
|
||||||
|
|
||||||
|
- **Integers**: u8, u16, u32, u64, i8, i16, i32, i64
|
||||||
|
- **Floats**: float (32-bit), double (64-bit)
|
||||||
|
- **Strings**: str8, str16, str32 (length-prefixed), char (fixed-size or null-terminated)
|
||||||
|
- **Special**: color, eq2color
|
||||||
|
- **Complex**: substruct, array
|
||||||
|
|
||||||
|
## Field Attributes
|
||||||
|
|
||||||
|
- `name`: Field name (required)
|
||||||
|
- `size`: Array size or fixed string length
|
||||||
|
- `sizevar`: Variable containing the array size
|
||||||
|
- `default`: Default value
|
||||||
|
- `ifset`: Only include if specified field is set
|
||||||
|
- `ifnotset`: Only include if specified field is not set
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
The parser is optimized for performance with minimal allocations:
|
||||||
|
|
||||||
|
- Serialization: ~312 ns/op, 144 B/op, 10 allocs/op
|
||||||
|
- Deserialization: ~315 ns/op, 112 B/op, 13 allocs/op
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
This implementation maintains compatibility with the C++ ConfigReader and PacketStruct classes, allowing seamless migration of existing packet definitions and code.
|
||||||
|
|
||||||
|
## Example Integration
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Initialize config reader
|
||||||
|
configReader := structs.NewConfigReader("./")
|
||||||
|
configReader.LoadStructs()
|
||||||
|
|
||||||
|
// In your packet handler
|
||||||
|
func HandleLoginRequest(data []byte, clientVersion uint16) {
|
||||||
|
// Get the appropriate packet struct
|
||||||
|
ps, err := configReader.GetStruct("LoginRequest", clientVersion)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to get LoginRequest struct: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize the packet
|
||||||
|
if err := ps.Deserialize(data); err != nil {
|
||||||
|
log.Printf("Failed to deserialize LoginRequest: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access fields
|
||||||
|
username, _ := ps.Get("username")
|
||||||
|
password, _ := ps.Get("password")
|
||||||
|
|
||||||
|
// Process login...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
structs/
|
||||||
|
├── xml/
|
||||||
|
│ ├── common/
|
||||||
|
│ ├── login/
|
||||||
|
│ ├── item/
|
||||||
|
│ ├── spawn/
|
||||||
|
│ └── world/
|
||||||
|
├── parser.go # XML parser
|
||||||
|
├── packet_struct.go # Runtime packet structure
|
||||||
|
├── config_reader.go # ConfigReader compatible interface
|
||||||
|
└── README.md
|
||||||
|
```
|
177
structs/config_reader.go
Normal file
177
structs/config_reader.go
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
package structs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigReader provides a compatible interface to the C++ ConfigReader
|
||||||
|
// It manages packet struct definitions and creates instances for specific versions
|
||||||
|
type ConfigReader struct {
|
||||||
|
parser *Parser
|
||||||
|
structs map[string][]*PacketVersion // Maps packet name to versions
|
||||||
|
mu sync.RWMutex
|
||||||
|
basePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigReader creates a new config reader
|
||||||
|
func NewConfigReader(basePath string) *ConfigReader {
|
||||||
|
return &ConfigReader{
|
||||||
|
parser: NewParser(basePath),
|
||||||
|
structs: make(map[string][]*PacketVersion),
|
||||||
|
basePath: basePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadStructs loads all packet structures from XML files
|
||||||
|
func (cr *ConfigReader) LoadStructs() error {
|
||||||
|
// Use the structs/xml subdirectory
|
||||||
|
xmlPath := filepath.Join(cr.basePath, "structs")
|
||||||
|
cr.parser = NewParser(xmlPath)
|
||||||
|
|
||||||
|
// Try to load all, but don't fail if directory doesn't exist
|
||||||
|
cr.parser.LoadAll()
|
||||||
|
|
||||||
|
return cr.buildVersionMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFiles loads specific XML files
|
||||||
|
func (cr *ConfigReader) LoadFiles(files []string) error {
|
||||||
|
for _, file := range files {
|
||||||
|
if err := cr.parser.LoadFile(file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cr.buildVersionMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildVersionMap builds the internal version map from loaded packets
|
||||||
|
func (cr *ConfigReader) buildVersionMap() error {
|
||||||
|
cr.mu.Lock()
|
||||||
|
defer cr.mu.Unlock()
|
||||||
|
|
||||||
|
// Build version map
|
||||||
|
cr.structs = make(map[string][]*PacketVersion)
|
||||||
|
|
||||||
|
for _, name := range cr.parser.ListPackets() {
|
||||||
|
def, _ := cr.parser.GetPacket(name)
|
||||||
|
versions := make([]*PacketVersion, len(def.Versions))
|
||||||
|
for i := range def.Versions {
|
||||||
|
versions[i] = &def.Versions[i]
|
||||||
|
}
|
||||||
|
cr.structs[name] = versions
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReloadStructs reloads all packet structures
|
||||||
|
func (cr *ConfigReader) ReloadStructs() error {
|
||||||
|
return cr.LoadStructs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStruct returns a packet struct for the best matching version
|
||||||
|
// This matches the C++ ConfigReader::getStruct behavior
|
||||||
|
func (cr *ConfigReader) GetStruct(name string, version uint16) (*PacketStruct, error) {
|
||||||
|
cr.mu.RLock()
|
||||||
|
defer cr.mu.RUnlock()
|
||||||
|
|
||||||
|
versions, exists := cr.structs[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("struct %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the best matching version (highest version <= requested)
|
||||||
|
var bestVersion *PacketVersion
|
||||||
|
for _, v := range versions {
|
||||||
|
if v.Version <= version {
|
||||||
|
if bestVersion == nil || v.Version > bestVersion.Version {
|
||||||
|
bestVersion = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestVersion == nil {
|
||||||
|
return nil, fmt.Errorf("no suitable version found for struct %s version %d", name, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewPacketStruct(bestVersion), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStructByVersion returns a packet struct for an exact version match
|
||||||
|
// This matches the C++ ConfigReader::getStructByVersion behavior
|
||||||
|
func (cr *ConfigReader) GetStructByVersion(name string, version uint16) (*PacketStruct, error) {
|
||||||
|
cr.mu.RLock()
|
||||||
|
defer cr.mu.RUnlock()
|
||||||
|
|
||||||
|
versions, exists := cr.structs[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("struct %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find exact version match
|
||||||
|
for _, v := range versions {
|
||||||
|
if v.Version == version {
|
||||||
|
return NewPacketStruct(v), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("struct %s with exact version %d not found", name, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStructVersion returns the best matching version number for a struct
|
||||||
|
func (cr *ConfigReader) GetStructVersion(name string, version uint16) (uint16, error) {
|
||||||
|
cr.mu.RLock()
|
||||||
|
defer cr.mu.RUnlock()
|
||||||
|
|
||||||
|
versions, exists := cr.structs[name]
|
||||||
|
if !exists {
|
||||||
|
return 0, fmt.Errorf("struct %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the best matching version
|
||||||
|
var bestVersion uint16
|
||||||
|
for _, v := range versions {
|
||||||
|
if v.Version <= version && v.Version > bestVersion {
|
||||||
|
bestVersion = v.Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestVersion == 0 {
|
||||||
|
return 0, fmt.Errorf("no suitable version found for struct %s version %d", name, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListStructs returns a list of all loaded struct names
|
||||||
|
func (cr *ConfigReader) ListStructs() []string {
|
||||||
|
cr.mu.RLock()
|
||||||
|
defer cr.mu.RUnlock()
|
||||||
|
|
||||||
|
names := make([]string, 0, len(cr.structs))
|
||||||
|
for name := range cr.structs {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVersions returns all available versions for a struct
|
||||||
|
func (cr *ConfigReader) GetVersions(name string) ([]uint16, error) {
|
||||||
|
cr.mu.RLock()
|
||||||
|
defer cr.mu.RUnlock()
|
||||||
|
|
||||||
|
versions, exists := cr.structs[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("struct %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]uint16, len(versions))
|
||||||
|
for i, v := range versions {
|
||||||
|
result[i] = v.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
746
structs/packet_struct.go
Normal file
746
structs/packet_struct.go
Normal file
@ -0,0 +1,746 @@
|
|||||||
|
package structs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PacketStruct represents a runtime packet structure that can be filled with data
|
||||||
|
type PacketStruct struct {
|
||||||
|
definition *PacketVersion
|
||||||
|
data map[string]interface{}
|
||||||
|
arrays map[string][]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPacketStruct creates a new packet struct from a definition
|
||||||
|
func NewPacketStruct(def *PacketVersion) *PacketStruct {
|
||||||
|
ps := &PacketStruct{
|
||||||
|
definition: def,
|
||||||
|
data: make(map[string]interface{}),
|
||||||
|
arrays: make(map[string][]interface{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize with default values
|
||||||
|
for _, field := range def.Fields {
|
||||||
|
if field.Default != nil {
|
||||||
|
ps.data[field.Name] = field.Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets a field value
|
||||||
|
func (ps *PacketStruct) Set(name string, value interface{}) error {
|
||||||
|
// Check if field exists in definition
|
||||||
|
found := false
|
||||||
|
for _, field := range ps.definition.Fields {
|
||||||
|
if field.Name == name {
|
||||||
|
found = true
|
||||||
|
// Validate type if needed
|
||||||
|
ps.data[name] = value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("field %s not found in packet definition", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a field value
|
||||||
|
func (ps *PacketStruct) Get(name string) (interface{}, bool) {
|
||||||
|
value, exists := ps.data[name]
|
||||||
|
return value, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetArray sets an array field
|
||||||
|
func (ps *PacketStruct) SetArray(name string, values []interface{}) error {
|
||||||
|
ps.arrays[name] = values
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArray retrieves an array field
|
||||||
|
func (ps *PacketStruct) GetArray(name string) ([]interface{}, bool) {
|
||||||
|
values, exists := ps.arrays[name]
|
||||||
|
return values, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize converts the packet struct to bytes
|
||||||
|
func (ps *PacketStruct) Serialize() ([]byte, error) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
for _, field := range ps.definition.Fields {
|
||||||
|
// Check conditionals
|
||||||
|
if !ps.checkCondition(field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle arrays
|
||||||
|
if field.Type == FieldTypeArray {
|
||||||
|
if err := ps.serializeArray(buf, field); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get value
|
||||||
|
value, exists := ps.data[field.Name]
|
||||||
|
if !exists && !field.Optional {
|
||||||
|
// Use zero value if not set
|
||||||
|
value = ps.getZeroValue(field.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize based on type
|
||||||
|
if err := ps.serializeField(buf, field, value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize reads data from bytes into the packet struct
|
||||||
|
func (ps *PacketStruct) Deserialize(data []byte) error {
|
||||||
|
buf := bytes.NewReader(data)
|
||||||
|
|
||||||
|
for _, field := range ps.definition.Fields {
|
||||||
|
// Check conditionals
|
||||||
|
if !ps.checkCondition(field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle arrays
|
||||||
|
if field.Type == FieldTypeArray {
|
||||||
|
if err := ps.deserializeArray(buf, field); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize based on type
|
||||||
|
value, err := ps.deserializeField(buf, field)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.data[field.Name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// serializeField writes a single field to the buffer
|
||||||
|
func (ps *PacketStruct) serializeField(buf *bytes.Buffer, field Field, value interface{}) error {
|
||||||
|
switch field.Type {
|
||||||
|
case FieldTypeUInt8:
|
||||||
|
v := ps.toUint8(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
|
||||||
|
case FieldTypeUInt16:
|
||||||
|
v := ps.toUint16(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
|
||||||
|
case FieldTypeUInt32:
|
||||||
|
v := ps.toUint32(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
|
||||||
|
case FieldTypeUInt64:
|
||||||
|
v := ps.toUint64(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
|
||||||
|
case FieldTypeInt8:
|
||||||
|
v := ps.toInt8(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
|
||||||
|
case FieldTypeInt16:
|
||||||
|
v := ps.toInt16(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
|
||||||
|
case FieldTypeInt32:
|
||||||
|
v := ps.toInt32(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
|
||||||
|
case FieldTypeInt64:
|
||||||
|
v := ps.toInt64(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
|
||||||
|
case FieldTypeFloat:
|
||||||
|
v := ps.toFloat32(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
|
||||||
|
case FieldTypeDouble:
|
||||||
|
v := ps.toFloat64(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, v)
|
||||||
|
|
||||||
|
case FieldTypeChar:
|
||||||
|
return ps.serializeChar(buf, field, value)
|
||||||
|
|
||||||
|
case FieldTypeString8:
|
||||||
|
return ps.serializeString8(buf, value)
|
||||||
|
|
||||||
|
case FieldTypeString16:
|
||||||
|
return ps.serializeString16(buf, value)
|
||||||
|
|
||||||
|
case FieldTypeString32:
|
||||||
|
return ps.serializeString32(buf, value)
|
||||||
|
|
||||||
|
case FieldTypeColor, FieldTypeEQ2Color:
|
||||||
|
return ps.serializeColor(buf, value)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported field type: %v", field.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deserializeField reads a single field from the buffer
|
||||||
|
func (ps *PacketStruct) deserializeField(buf *bytes.Reader, field Field) (interface{}, error) {
|
||||||
|
switch field.Type {
|
||||||
|
case FieldTypeUInt8:
|
||||||
|
var v uint8
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return v, err
|
||||||
|
|
||||||
|
case FieldTypeUInt16:
|
||||||
|
var v uint16
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return v, err
|
||||||
|
|
||||||
|
case FieldTypeUInt32:
|
||||||
|
var v uint32
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return v, err
|
||||||
|
|
||||||
|
case FieldTypeUInt64:
|
||||||
|
var v uint64
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return v, err
|
||||||
|
|
||||||
|
case FieldTypeInt8:
|
||||||
|
var v int8
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return v, err
|
||||||
|
|
||||||
|
case FieldTypeInt16:
|
||||||
|
var v int16
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return v, err
|
||||||
|
|
||||||
|
case FieldTypeInt32:
|
||||||
|
var v int32
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return v, err
|
||||||
|
|
||||||
|
case FieldTypeInt64:
|
||||||
|
var v int64
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return v, err
|
||||||
|
|
||||||
|
case FieldTypeFloat:
|
||||||
|
var v float32
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return v, err
|
||||||
|
|
||||||
|
case FieldTypeDouble:
|
||||||
|
var v float64
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &v)
|
||||||
|
return v, err
|
||||||
|
|
||||||
|
case FieldTypeChar:
|
||||||
|
return ps.deserializeChar(buf, field)
|
||||||
|
|
||||||
|
case FieldTypeString8:
|
||||||
|
return ps.deserializeString8(buf)
|
||||||
|
|
||||||
|
case FieldTypeString16:
|
||||||
|
return ps.deserializeString16(buf)
|
||||||
|
|
||||||
|
case FieldTypeString32:
|
||||||
|
return ps.deserializeString32(buf)
|
||||||
|
|
||||||
|
case FieldTypeColor, FieldTypeEQ2Color:
|
||||||
|
return ps.deserializeColor(buf)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported field type: %v", field.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String serialization helpers
|
||||||
|
func (ps *PacketStruct) serializeString8(buf *bytes.Buffer, value interface{}) error {
|
||||||
|
str := ps.toString(value)
|
||||||
|
if err := buf.WriteByte(uint8(len(str))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := buf.WriteString(str)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) serializeString16(buf *bytes.Buffer, value interface{}) error {
|
||||||
|
str := ps.toString(value)
|
||||||
|
if err := binary.Write(buf, binary.LittleEndian, uint16(len(str))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := buf.WriteString(str)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) serializeString32(buf *bytes.Buffer, value interface{}) error {
|
||||||
|
str := ps.toString(value)
|
||||||
|
if err := binary.Write(buf, binary.LittleEndian, uint32(len(str))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := buf.WriteString(str)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) deserializeString8(buf *bytes.Reader) (string, error) {
|
||||||
|
length, err := buf.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, length)
|
||||||
|
if _, err := buf.Read(data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) deserializeString16(buf *bytes.Reader) (string, error) {
|
||||||
|
var length uint16
|
||||||
|
if err := binary.Read(buf, binary.LittleEndian, &length); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, length)
|
||||||
|
if _, err := buf.Read(data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) deserializeString32(buf *bytes.Reader) (string, error) {
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(buf, binary.LittleEndian, &length); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, length)
|
||||||
|
if _, err := buf.Read(data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Char field serialization (fixed-size string)
|
||||||
|
func (ps *PacketStruct) serializeChar(buf *bytes.Buffer, field Field, value interface{}) error {
|
||||||
|
str := ps.toString(value)
|
||||||
|
size := field.Size
|
||||||
|
if size == 0 {
|
||||||
|
size = len(str) + 1 // Null-terminated if no size specified
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write string up to size
|
||||||
|
written := 0
|
||||||
|
for i := 0; i < len(str) && i < size; i++ {
|
||||||
|
buf.WriteByte(str[i])
|
||||||
|
written++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad with zeros
|
||||||
|
for written < size {
|
||||||
|
buf.WriteByte(0)
|
||||||
|
written++
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) deserializeChar(buf *bytes.Reader, field Field) (string, error) {
|
||||||
|
size := field.Size
|
||||||
|
if size == 0 {
|
||||||
|
// Read until null terminator
|
||||||
|
var result []byte
|
||||||
|
for {
|
||||||
|
b, err := buf.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if b == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result = append(result, b)
|
||||||
|
}
|
||||||
|
return string(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read fixed size
|
||||||
|
data := make([]byte, size)
|
||||||
|
if _, err := buf.Read(data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find null terminator
|
||||||
|
for i, b := range data {
|
||||||
|
if b == 0 {
|
||||||
|
return string(data[:i]), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color serialization
|
||||||
|
func (ps *PacketStruct) serializeColor(buf *bytes.Buffer, value interface{}) error {
|
||||||
|
// Assume color is represented as uint32 (RGBA)
|
||||||
|
color := ps.toUint32(value)
|
||||||
|
return binary.Write(buf, binary.LittleEndian, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) deserializeColor(buf *bytes.Reader) (uint32, error) {
|
||||||
|
var color uint32
|
||||||
|
err := binary.Read(buf, binary.LittleEndian, &color)
|
||||||
|
return color, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array handling
|
||||||
|
func (ps *PacketStruct) serializeArray(buf *bytes.Buffer, field Field) error {
|
||||||
|
values, exists := ps.arrays[field.Name]
|
||||||
|
if !exists {
|
||||||
|
values = []interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write each element
|
||||||
|
for _, value := range values {
|
||||||
|
// Create a temporary field for the element
|
||||||
|
elemField := Field{
|
||||||
|
Name: field.Name + "_elem",
|
||||||
|
Type: field.Type, // This should be the element type, not array
|
||||||
|
}
|
||||||
|
if err := ps.serializeField(buf, elemField, value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) deserializeArray(buf *bytes.Reader, field Field) error {
|
||||||
|
// Get array size
|
||||||
|
size := field.Size
|
||||||
|
if field.SizeVar != "" {
|
||||||
|
// Size is stored in another field
|
||||||
|
if sizeVal, ok := ps.data[field.SizeVar]; ok {
|
||||||
|
size = int(ps.toUint32(sizeVal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
values := make([]interface{}, 0, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
// Create a temporary field for the element
|
||||||
|
elemField := Field{
|
||||||
|
Name: field.Name + "_elem",
|
||||||
|
Type: field.Type, // This should be the element type, not array
|
||||||
|
}
|
||||||
|
value, err := ps.deserializeField(buf, elemField)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.arrays[field.Name] = values
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition checking
|
||||||
|
func (ps *PacketStruct) checkCondition(field Field) bool {
|
||||||
|
// Check IfSet condition
|
||||||
|
if field.IfSet != "" {
|
||||||
|
if val, exists := ps.data[field.IfSet]; !exists || val == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check IfNotSet condition
|
||||||
|
if field.IfNotSet != "" {
|
||||||
|
if val, exists := ps.data[field.IfNotSet]; exists && val != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type conversion helpers
|
||||||
|
func (ps *PacketStruct) toUint8(value interface{}) uint8 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case uint8:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return uint8(v)
|
||||||
|
case uint:
|
||||||
|
return uint8(v)
|
||||||
|
case uint16:
|
||||||
|
return uint8(v)
|
||||||
|
case uint32:
|
||||||
|
return uint8(v)
|
||||||
|
case uint64:
|
||||||
|
return uint8(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) toUint16(value interface{}) uint16 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case uint16:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return uint16(v)
|
||||||
|
case uint:
|
||||||
|
return uint16(v)
|
||||||
|
case uint8:
|
||||||
|
return uint16(v)
|
||||||
|
case uint32:
|
||||||
|
return uint16(v)
|
||||||
|
case uint64:
|
||||||
|
return uint16(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) toUint32(value interface{}) uint32 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case uint32:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return uint32(v)
|
||||||
|
case uint:
|
||||||
|
return uint32(v)
|
||||||
|
case uint8:
|
||||||
|
return uint32(v)
|
||||||
|
case uint16:
|
||||||
|
return uint32(v)
|
||||||
|
case uint64:
|
||||||
|
return uint32(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) toUint64(value interface{}) uint64 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case uint64:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return uint64(v)
|
||||||
|
case uint:
|
||||||
|
return uint64(v)
|
||||||
|
case uint8:
|
||||||
|
return uint64(v)
|
||||||
|
case uint16:
|
||||||
|
return uint64(v)
|
||||||
|
case uint32:
|
||||||
|
return uint64(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) toInt8(value interface{}) int8 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int8:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return int8(v)
|
||||||
|
case int16:
|
||||||
|
return int8(v)
|
||||||
|
case int32:
|
||||||
|
return int8(v)
|
||||||
|
case int64:
|
||||||
|
return int8(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) toInt16(value interface{}) int16 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int16:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return int16(v)
|
||||||
|
case int8:
|
||||||
|
return int16(v)
|
||||||
|
case int32:
|
||||||
|
return int16(v)
|
||||||
|
case int64:
|
||||||
|
return int16(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) toInt32(value interface{}) int32 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int32:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return int32(v)
|
||||||
|
case int8:
|
||||||
|
return int32(v)
|
||||||
|
case int16:
|
||||||
|
return int32(v)
|
||||||
|
case int64:
|
||||||
|
return int32(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) toInt64(value interface{}) int64 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int64:
|
||||||
|
return v
|
||||||
|
case int:
|
||||||
|
return int64(v)
|
||||||
|
case int8:
|
||||||
|
return int64(v)
|
||||||
|
case int16:
|
||||||
|
return int64(v)
|
||||||
|
case int32:
|
||||||
|
return int64(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) toFloat32(value interface{}) float32 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case float32:
|
||||||
|
return v
|
||||||
|
case float64:
|
||||||
|
return float32(v)
|
||||||
|
case int:
|
||||||
|
return float32(v)
|
||||||
|
case uint:
|
||||||
|
return float32(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) toFloat64(value interface{}) float64 {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return v
|
||||||
|
case float32:
|
||||||
|
return float64(v)
|
||||||
|
case int:
|
||||||
|
return float64(v)
|
||||||
|
case uint:
|
||||||
|
return float64(v)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PacketStruct) toString(value interface{}) string {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
return v
|
||||||
|
case []byte:
|
||||||
|
return string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getZeroValue returns the zero value for a field type
|
||||||
|
func (ps *PacketStruct) getZeroValue(ft FieldType) interface{} {
|
||||||
|
switch ft {
|
||||||
|
case FieldTypeUInt8:
|
||||||
|
return uint8(0)
|
||||||
|
case FieldTypeUInt16:
|
||||||
|
return uint16(0)
|
||||||
|
case FieldTypeUInt32:
|
||||||
|
return uint32(0)
|
||||||
|
case FieldTypeUInt64:
|
||||||
|
return uint64(0)
|
||||||
|
case FieldTypeInt8:
|
||||||
|
return int8(0)
|
||||||
|
case FieldTypeInt16:
|
||||||
|
return int16(0)
|
||||||
|
case FieldTypeInt32:
|
||||||
|
return int32(0)
|
||||||
|
case FieldTypeInt64:
|
||||||
|
return int64(0)
|
||||||
|
case FieldTypeFloat:
|
||||||
|
return float32(0)
|
||||||
|
case FieldTypeDouble:
|
||||||
|
return float64(0)
|
||||||
|
case FieldTypeChar, FieldTypeString8, FieldTypeString16, FieldTypeString32:
|
||||||
|
return ""
|
||||||
|
case FieldTypeColor, FieldTypeEQ2Color:
|
||||||
|
return uint32(0)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the serialized size of the packet struct
|
||||||
|
func (ps *PacketStruct) GetSize() int {
|
||||||
|
size := 0
|
||||||
|
|
||||||
|
for _, field := range ps.definition.Fields {
|
||||||
|
if !ps.checkCondition(field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldSize := GetFieldSize(field.Type)
|
||||||
|
if fieldSize > 0 {
|
||||||
|
size += fieldSize
|
||||||
|
} else {
|
||||||
|
// Variable size field
|
||||||
|
switch field.Type {
|
||||||
|
case FieldTypeChar:
|
||||||
|
if field.Size > 0 {
|
||||||
|
size += field.Size
|
||||||
|
} else {
|
||||||
|
// Null-terminated string
|
||||||
|
if val, ok := ps.data[field.Name]; ok {
|
||||||
|
size += len(ps.toString(val)) + 1
|
||||||
|
} else {
|
||||||
|
size += 1 // Just null terminator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case FieldTypeString8:
|
||||||
|
size += 1 // Length byte
|
||||||
|
if val, ok := ps.data[field.Name]; ok {
|
||||||
|
size += len(ps.toString(val))
|
||||||
|
}
|
||||||
|
case FieldTypeString16:
|
||||||
|
size += 2 // Length uint16
|
||||||
|
if val, ok := ps.data[field.Name]; ok {
|
||||||
|
size += len(ps.toString(val))
|
||||||
|
}
|
||||||
|
case FieldTypeString32:
|
||||||
|
size += 4 // Length uint32
|
||||||
|
if val, ok := ps.data[field.Name]; ok {
|
||||||
|
size += len(ps.toString(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
420
structs/parser.go
Normal file
420
structs/parser.go
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
package structs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FieldType represents the type of a packet field
|
||||||
|
type FieldType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FieldTypeUnknown FieldType = iota
|
||||||
|
FieldTypeUInt8
|
||||||
|
FieldTypeUInt16
|
||||||
|
FieldTypeUInt32
|
||||||
|
FieldTypeUInt64
|
||||||
|
FieldTypeInt8
|
||||||
|
FieldTypeInt16
|
||||||
|
FieldTypeInt32
|
||||||
|
FieldTypeInt64
|
||||||
|
FieldTypeFloat
|
||||||
|
FieldTypeDouble
|
||||||
|
FieldTypeChar
|
||||||
|
FieldTypeString8
|
||||||
|
FieldTypeString16
|
||||||
|
FieldTypeString32
|
||||||
|
FieldTypeColor
|
||||||
|
FieldTypeEQ2Color
|
||||||
|
FieldTypeSubstruct
|
||||||
|
FieldTypeArray
|
||||||
|
)
|
||||||
|
|
||||||
|
// Field represents a single field in a packet structure
|
||||||
|
type Field struct {
|
||||||
|
Name string
|
||||||
|
Type FieldType
|
||||||
|
Size int // For arrays and fixed-size fields
|
||||||
|
SizeVar string // Variable that contains the size
|
||||||
|
Default interface{}
|
||||||
|
Optional bool
|
||||||
|
IfSet string // Conditional based on another field being set
|
||||||
|
IfNotSet string // Conditional based on another field not being set
|
||||||
|
}
|
||||||
|
|
||||||
|
// PacketVersion represents a specific version of a packet structure
|
||||||
|
type PacketVersion struct {
|
||||||
|
Version uint16
|
||||||
|
Fields []Field
|
||||||
|
}
|
||||||
|
|
||||||
|
// PacketDef represents a complete packet definition with all versions
|
||||||
|
type PacketDef struct {
|
||||||
|
Name string
|
||||||
|
IsSubstruct bool
|
||||||
|
Versions []PacketVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLPacket represents the XML structure of a packet definition
|
||||||
|
type XMLPacket struct {
|
||||||
|
XMLName xml.Name `xml:"packet"`
|
||||||
|
Name string `xml:"name,attr"`
|
||||||
|
Versions []XMLVersion `xml:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLSubstruct represents the XML structure of a substruct definition
|
||||||
|
type XMLSubstruct struct {
|
||||||
|
XMLName xml.Name `xml:"substruct"`
|
||||||
|
Name string `xml:"name,attr"`
|
||||||
|
Versions []XMLVersion `xml:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLVersion represents a version block in the XML
|
||||||
|
type XMLVersion struct {
|
||||||
|
Number string `xml:"number,attr"`
|
||||||
|
Fields []XMLField `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLField represents a field in the XML
|
||||||
|
type XMLField struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
Name string `xml:"name,attr"`
|
||||||
|
Size string `xml:"size,attr"`
|
||||||
|
SizeVar string `xml:"sizevar,attr"`
|
||||||
|
Default string `xml:"default,attr"`
|
||||||
|
IfSet string `xml:"ifset,attr"`
|
||||||
|
IfNotSet string `xml:"ifnotset,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser handles parsing XML packet definitions
|
||||||
|
type Parser struct {
|
||||||
|
packets map[string]*PacketDef
|
||||||
|
basePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParser creates a new packet parser
|
||||||
|
func NewParser(basePath string) *Parser {
|
||||||
|
return &Parser{
|
||||||
|
packets: make(map[string]*PacketDef),
|
||||||
|
basePath: basePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAll loads all packet definitions from the XML directory
|
||||||
|
func (p *Parser) LoadAll() error {
|
||||||
|
patterns := []string{
|
||||||
|
"common/*.xml",
|
||||||
|
"login/*.xml",
|
||||||
|
"item/*.xml",
|
||||||
|
"spawn/*.xml",
|
||||||
|
"world/*.xml",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
fullPattern := filepath.Join(p.basePath, "xml", pattern)
|
||||||
|
files, err := filepath.Glob(fullPattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to glob %s: %w", pattern, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if err := p.LoadFile(file); err != nil {
|
||||||
|
return fmt.Errorf("failed to load %s: %w", file, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile loads a single XML file
|
||||||
|
func (p *Parser) LoadFile(filename string) error {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as packet first
|
||||||
|
var packet XMLPacket
|
||||||
|
if err := xml.Unmarshal(data, &packet); err == nil && packet.Name != "" {
|
||||||
|
def := p.parsePacket(&packet)
|
||||||
|
p.packets[def.Name] = def
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as substruct
|
||||||
|
var substruct XMLSubstruct
|
||||||
|
if err := xml.Unmarshal(data, &substruct); err == nil && substruct.Name != "" {
|
||||||
|
def := p.parseSubstruct(&substruct)
|
||||||
|
p.packets[def.Name] = def
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to parse %s as packet or substruct", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePacket converts XML packet to internal representation
|
||||||
|
func (p *Parser) parsePacket(xmlPacket *XMLPacket) *PacketDef {
|
||||||
|
def := &PacketDef{
|
||||||
|
Name: xmlPacket.Name,
|
||||||
|
IsSubstruct: false,
|
||||||
|
Versions: make([]PacketVersion, 0, len(xmlPacket.Versions)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, xmlVer := range xmlPacket.Versions {
|
||||||
|
version := p.parseVersion(xmlVer)
|
||||||
|
def.Versions = append(def.Versions, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort versions by version number
|
||||||
|
sort.Slice(def.Versions, func(i, j int) bool {
|
||||||
|
return def.Versions[i].Version < def.Versions[j].Version
|
||||||
|
})
|
||||||
|
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSubstruct converts XML substruct to internal representation
|
||||||
|
func (p *Parser) parseSubstruct(xmlSub *XMLSubstruct) *PacketDef {
|
||||||
|
def := &PacketDef{
|
||||||
|
Name: xmlSub.Name,
|
||||||
|
IsSubstruct: true,
|
||||||
|
Versions: make([]PacketVersion, 0, len(xmlSub.Versions)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, xmlVer := range xmlSub.Versions {
|
||||||
|
version := p.parseVersion(xmlVer)
|
||||||
|
def.Versions = append(def.Versions, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort versions by version number
|
||||||
|
sort.Slice(def.Versions, func(i, j int) bool {
|
||||||
|
return def.Versions[i].Version < def.Versions[j].Version
|
||||||
|
})
|
||||||
|
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVersion converts XML version to internal representation
|
||||||
|
func (p *Parser) parseVersion(xmlVer XMLVersion) PacketVersion {
|
||||||
|
ver, _ := strconv.ParseUint(xmlVer.Number, 10, 16)
|
||||||
|
|
||||||
|
version := PacketVersion{
|
||||||
|
Version: uint16(ver),
|
||||||
|
Fields: make([]Field, 0, len(xmlVer.Fields)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, xmlField := range xmlVer.Fields {
|
||||||
|
field := p.parseField(xmlField)
|
||||||
|
version.Fields = append(version.Fields, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseField converts XML field to internal representation
|
||||||
|
func (p *Parser) parseField(xmlField XMLField) Field {
|
||||||
|
field := Field{
|
||||||
|
Name: xmlField.Name,
|
||||||
|
Type: p.parseFieldType(xmlField.XMLName.Local),
|
||||||
|
IfSet: xmlField.IfSet,
|
||||||
|
IfNotSet: xmlField.IfNotSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse size
|
||||||
|
if xmlField.Size != "" {
|
||||||
|
if size, err := strconv.Atoi(xmlField.Size); err == nil {
|
||||||
|
field.Size = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse size variable
|
||||||
|
field.SizeVar = xmlField.SizeVar
|
||||||
|
|
||||||
|
// Parse default value
|
||||||
|
if xmlField.Default != "" {
|
||||||
|
field.Default = p.parseDefaultValue(field.Type, xmlField.Default)
|
||||||
|
}
|
||||||
|
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFieldType converts XML type name to FieldType
|
||||||
|
func (p *Parser) parseFieldType(typeName string) FieldType {
|
||||||
|
switch typeName {
|
||||||
|
case "u8", "uint8":
|
||||||
|
return FieldTypeUInt8
|
||||||
|
case "u16", "uint16":
|
||||||
|
return FieldTypeUInt16
|
||||||
|
case "u32", "uint32":
|
||||||
|
return FieldTypeUInt32
|
||||||
|
case "u64", "uint64":
|
||||||
|
return FieldTypeUInt64
|
||||||
|
case "i8", "int8", "sint8":
|
||||||
|
return FieldTypeInt8
|
||||||
|
case "i16", "int16", "sint16":
|
||||||
|
return FieldTypeInt16
|
||||||
|
case "i32", "int32", "sint32":
|
||||||
|
return FieldTypeInt32
|
||||||
|
case "i64", "int64", "sint64":
|
||||||
|
return FieldTypeInt64
|
||||||
|
case "float":
|
||||||
|
return FieldTypeFloat
|
||||||
|
case "double":
|
||||||
|
return FieldTypeDouble
|
||||||
|
case "char":
|
||||||
|
return FieldTypeChar
|
||||||
|
case "str8", "string8":
|
||||||
|
return FieldTypeString8
|
||||||
|
case "str16", "string16":
|
||||||
|
return FieldTypeString16
|
||||||
|
case "str32", "string32":
|
||||||
|
return FieldTypeString32
|
||||||
|
case "color":
|
||||||
|
return FieldTypeColor
|
||||||
|
case "eq2color":
|
||||||
|
return FieldTypeEQ2Color
|
||||||
|
case "substruct":
|
||||||
|
return FieldTypeSubstruct
|
||||||
|
case "array":
|
||||||
|
return FieldTypeArray
|
||||||
|
default:
|
||||||
|
return FieldTypeUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDefaultValue parses default value based on field type
|
||||||
|
func (p *Parser) parseDefaultValue(fieldType FieldType, value string) interface{} {
|
||||||
|
switch fieldType {
|
||||||
|
case FieldTypeUInt8, FieldTypeUInt16, FieldTypeUInt32, FieldTypeUInt64:
|
||||||
|
if v, err := strconv.ParseUint(value, 0, 64); err == nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
case FieldTypeInt8, FieldTypeInt16, FieldTypeInt32, FieldTypeInt64:
|
||||||
|
if v, err := strconv.ParseInt(value, 0, 64); err == nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
case FieldTypeFloat, FieldTypeDouble:
|
||||||
|
if v, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
case FieldTypeChar, FieldTypeString8, FieldTypeString16, FieldTypeString32:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPacket returns a packet definition by name
|
||||||
|
func (p *Parser) GetPacket(name string) (*PacketDef, bool) {
|
||||||
|
def, exists := p.packets[name]
|
||||||
|
return def, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPacketVersion returns the best matching version for a packet
|
||||||
|
func (p *Parser) GetPacketVersion(name string, version uint16) (*PacketVersion, error) {
|
||||||
|
def, exists := p.packets[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("packet %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the best matching version (highest version <= requested)
|
||||||
|
var bestVersion *PacketVersion
|
||||||
|
for i := range def.Versions {
|
||||||
|
if def.Versions[i].Version <= version {
|
||||||
|
if bestVersion == nil || def.Versions[i].Version > bestVersion.Version {
|
||||||
|
bestVersion = &def.Versions[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestVersion == nil {
|
||||||
|
return nil, fmt.Errorf("no suitable version found for packet %s version %d", name, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPackets returns a list of all loaded packet names
|
||||||
|
func (p *Parser) ListPackets() []string {
|
||||||
|
names := make([]string, 0, len(p.packets))
|
||||||
|
for name := range p.packets {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFieldTypeName returns the string representation of a field type
|
||||||
|
func GetFieldTypeName(ft FieldType) string {
|
||||||
|
switch ft {
|
||||||
|
case FieldTypeUInt8:
|
||||||
|
return "uint8"
|
||||||
|
case FieldTypeUInt16:
|
||||||
|
return "uint16"
|
||||||
|
case FieldTypeUInt32:
|
||||||
|
return "uint32"
|
||||||
|
case FieldTypeUInt64:
|
||||||
|
return "uint64"
|
||||||
|
case FieldTypeInt8:
|
||||||
|
return "int8"
|
||||||
|
case FieldTypeInt16:
|
||||||
|
return "int16"
|
||||||
|
case FieldTypeInt32:
|
||||||
|
return "int32"
|
||||||
|
case FieldTypeInt64:
|
||||||
|
return "int64"
|
||||||
|
case FieldTypeFloat:
|
||||||
|
return "float32"
|
||||||
|
case FieldTypeDouble:
|
||||||
|
return "float64"
|
||||||
|
case FieldTypeChar:
|
||||||
|
return "char"
|
||||||
|
case FieldTypeString8:
|
||||||
|
return "string8"
|
||||||
|
case FieldTypeString16:
|
||||||
|
return "string16"
|
||||||
|
case FieldTypeString32:
|
||||||
|
return "string32"
|
||||||
|
case FieldTypeColor:
|
||||||
|
return "color"
|
||||||
|
case FieldTypeEQ2Color:
|
||||||
|
return "eq2color"
|
||||||
|
case FieldTypeSubstruct:
|
||||||
|
return "substruct"
|
||||||
|
case FieldTypeArray:
|
||||||
|
return "array"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFieldSize returns the size in bytes of a field type
|
||||||
|
func GetFieldSize(ft FieldType) int {
|
||||||
|
switch ft {
|
||||||
|
case FieldTypeUInt8, FieldTypeInt8:
|
||||||
|
return 1
|
||||||
|
case FieldTypeUInt16, FieldTypeInt16:
|
||||||
|
return 2
|
||||||
|
case FieldTypeUInt32, FieldTypeInt32, FieldTypeFloat:
|
||||||
|
return 4
|
||||||
|
case FieldTypeUInt64, FieldTypeInt64, FieldTypeDouble:
|
||||||
|
return 8
|
||||||
|
case FieldTypeColor:
|
||||||
|
return 4 // RGBA
|
||||||
|
case FieldTypeEQ2Color:
|
||||||
|
return 4 // EQ2 specific color format
|
||||||
|
default:
|
||||||
|
return 0 // Variable size
|
||||||
|
}
|
||||||
|
}
|
417
structs/parser_test.go
Normal file
417
structs/parser_test.go
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
package structs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestParseLoginRequest tests parsing the LoginRequest XML
|
||||||
|
func TestParseLoginRequest(t *testing.T) {
|
||||||
|
// Create a temporary directory with test XML
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
xmlDir := filepath.Join(tmpDir, "xml", "login")
|
||||||
|
os.MkdirAll(xmlDir, 0755)
|
||||||
|
|
||||||
|
// Write test LoginRequest XML
|
||||||
|
loginXML := `<packet name="LoginRequest">
|
||||||
|
<version number="1">
|
||||||
|
<str16 name="sessionID"/>
|
||||||
|
<str16 name="sessionRecycleToken"/>
|
||||||
|
<str16 name="username"/>
|
||||||
|
<str16 name="password"/>
|
||||||
|
<u32 name="acctNum"/>
|
||||||
|
<u32 name="passCode"/>
|
||||||
|
<u16 name="version"/>
|
||||||
|
</version>
|
||||||
|
<version number="562">
|
||||||
|
<str16 name="accesscode"/>
|
||||||
|
<str16 name="unknown1"/>
|
||||||
|
<str16 name="username"/>
|
||||||
|
<str16 name="password"/>
|
||||||
|
<u8 name="unknown2" size="8"/>
|
||||||
|
<u8 name="unknown3" size="2"/>
|
||||||
|
<u32 name="version"/>
|
||||||
|
<u16 name="unknown3b"/>
|
||||||
|
<u32 name="unknown4"/>
|
||||||
|
</version>
|
||||||
|
</packet>`
|
||||||
|
|
||||||
|
xmlFile := filepath.Join(xmlDir, "LoginRequest.xml")
|
||||||
|
if err := os.WriteFile(xmlFile, []byte(loginXML), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test XML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create parser and load file
|
||||||
|
parser := NewParser(tmpDir)
|
||||||
|
if err := parser.LoadFile(xmlFile); err != nil {
|
||||||
|
t.Fatalf("Failed to load XML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check packet was loaded
|
||||||
|
def, exists := parser.GetPacket("LoginRequest")
|
||||||
|
if !exists {
|
||||||
|
t.Fatal("LoginRequest packet not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if def.IsSubstruct {
|
||||||
|
t.Error("LoginRequest should not be a substruct")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check versions
|
||||||
|
if len(def.Versions) != 2 {
|
||||||
|
t.Errorf("Expected 2 versions, got %d", len(def.Versions))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check version 1 fields
|
||||||
|
if def.Versions[0].Version != 1 {
|
||||||
|
t.Errorf("Expected version 1, got %d", def.Versions[0].Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(def.Versions[0].Fields) != 7 {
|
||||||
|
t.Errorf("Expected 7 fields in version 1, got %d", len(def.Versions[0].Fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check specific fields
|
||||||
|
field := def.Versions[0].Fields[2]
|
||||||
|
if field.Name != "username" {
|
||||||
|
t.Errorf("Expected field name 'username', got '%s'", field.Name)
|
||||||
|
}
|
||||||
|
if field.Type != FieldTypeString16 {
|
||||||
|
t.Errorf("Expected field type String16, got %v", field.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check version 562
|
||||||
|
if def.Versions[1].Version != 562 {
|
||||||
|
t.Errorf("Expected version 562, got %d", def.Versions[1].Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check array field
|
||||||
|
arrayField := def.Versions[1].Fields[4]
|
||||||
|
if arrayField.Name != "unknown2" {
|
||||||
|
t.Errorf("Expected field name 'unknown2', got '%s'", arrayField.Name)
|
||||||
|
}
|
||||||
|
if arrayField.Size != 8 {
|
||||||
|
t.Errorf("Expected size 8, got %d", arrayField.Size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPacketStruct tests packet struct serialization/deserialization
|
||||||
|
func TestPacketStruct(t *testing.T) {
|
||||||
|
// Create a simple packet version for testing
|
||||||
|
version := &PacketVersion{
|
||||||
|
Version: 1,
|
||||||
|
Fields: []Field{
|
||||||
|
{Name: "id", Type: FieldTypeUInt32},
|
||||||
|
{Name: "name", Type: FieldTypeString16},
|
||||||
|
{Name: "level", Type: FieldTypeUInt8},
|
||||||
|
{Name: "x", Type: FieldTypeFloat},
|
||||||
|
{Name: "y", Type: FieldTypeFloat},
|
||||||
|
{Name: "z", Type: FieldTypeFloat},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create packet struct
|
||||||
|
ps := NewPacketStruct(version)
|
||||||
|
|
||||||
|
// Set values
|
||||||
|
ps.Set("id", uint32(12345))
|
||||||
|
ps.Set("name", "TestPlayer")
|
||||||
|
ps.Set("level", uint8(50))
|
||||||
|
ps.Set("x", float32(100.5))
|
||||||
|
ps.Set("y", float32(200.5))
|
||||||
|
ps.Set("z", float32(300.5))
|
||||||
|
|
||||||
|
// Serialize
|
||||||
|
data, err := ps.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to serialize: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new packet struct and deserialize
|
||||||
|
ps2 := NewPacketStruct(version)
|
||||||
|
if err := ps2.Deserialize(data); err != nil {
|
||||||
|
t.Fatalf("Failed to deserialize: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check values
|
||||||
|
if val, _ := ps2.Get("id"); val.(uint32) != 12345 {
|
||||||
|
t.Errorf("Expected id 12345, got %v", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, _ := ps2.Get("name"); val.(string) != "TestPlayer" {
|
||||||
|
t.Errorf("Expected name 'TestPlayer', got %v", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, _ := ps2.Get("level"); val.(uint8) != 50 {
|
||||||
|
t.Errorf("Expected level 50, got %v", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, _ := ps2.Get("x"); val.(float32) != 100.5 {
|
||||||
|
t.Errorf("Expected x 100.5, got %v", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigReader tests the ConfigReader interface
|
||||||
|
func TestConfigReader(t *testing.T) {
|
||||||
|
// Create temporary directory with test XML
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
structsDir := filepath.Join(tmpDir, "structs", "xml", "test")
|
||||||
|
os.MkdirAll(structsDir, 0755)
|
||||||
|
|
||||||
|
// Write test packet XML
|
||||||
|
testXML := `<packet name="TestPacket">
|
||||||
|
<version number="1">
|
||||||
|
<u32 name="id"/>
|
||||||
|
<str16 name="name"/>
|
||||||
|
</version>
|
||||||
|
<version number="100">
|
||||||
|
<u32 name="id"/>
|
||||||
|
<str16 name="name"/>
|
||||||
|
<u8 name="level"/>
|
||||||
|
</version>
|
||||||
|
<version number="200">
|
||||||
|
<u32 name="id"/>
|
||||||
|
<str16 name="name"/>
|
||||||
|
<u8 name="level"/>
|
||||||
|
<u16 name="flags"/>
|
||||||
|
</version>
|
||||||
|
</packet>`
|
||||||
|
|
||||||
|
xmlFile := filepath.Join(structsDir, "TestPacket.xml")
|
||||||
|
if err := os.WriteFile(xmlFile, []byte(testXML), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test XML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create config reader
|
||||||
|
cr := NewConfigReader(tmpDir)
|
||||||
|
if err := cr.LoadFiles([]string{xmlFile}); err != nil {
|
||||||
|
t.Fatalf("Failed to load structs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetStruct with version selection
|
||||||
|
tests := []struct {
|
||||||
|
version uint16
|
||||||
|
expectedFields int
|
||||||
|
}{
|
||||||
|
{1, 2}, // Exact match
|
||||||
|
{50, 2}, // Should use version 1
|
||||||
|
{100, 3}, // Exact match
|
||||||
|
{150, 3}, // Should use version 100
|
||||||
|
{200, 4}, // Exact match
|
||||||
|
{300, 4}, // Should use version 200
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
ps, err := cr.GetStruct("TestPacket", test.version)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to get struct for version %d: %v", test.version, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ps.definition.Fields) != test.expectedFields {
|
||||||
|
t.Errorf("Version %d: expected %d fields, got %d",
|
||||||
|
test.version, test.expectedFields, len(ps.definition.Fields))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetStructByVersion (exact match only)
|
||||||
|
ps, err := cr.GetStructByVersion("TestPacket", 100)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to get struct by exact version: %v", err)
|
||||||
|
}
|
||||||
|
if ps != nil && len(ps.definition.Fields) != 3 {
|
||||||
|
t.Errorf("Expected 3 fields for version 100, got %d", len(ps.definition.Fields))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should fail (no exact match)
|
||||||
|
_, err = cr.GetStructByVersion("TestPacket", 150)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for non-existent exact version 150")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetStructVersion
|
||||||
|
version, err := cr.GetStructVersion("TestPacket", 150)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to get struct version: %v", err)
|
||||||
|
}
|
||||||
|
if version != 100 {
|
||||||
|
t.Errorf("Expected version 100 for client version 150, got %d", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStringFields tests various string field types
|
||||||
|
func TestStringFields(t *testing.T) {
|
||||||
|
version := &PacketVersion{
|
||||||
|
Version: 1,
|
||||||
|
Fields: []Field{
|
||||||
|
{Name: "str8", Type: FieldTypeString8},
|
||||||
|
{Name: "str16", Type: FieldTypeString16},
|
||||||
|
{Name: "str32", Type: FieldTypeString32},
|
||||||
|
{Name: "char_fixed", Type: FieldTypeChar, Size: 10},
|
||||||
|
{Name: "char_null", Type: FieldTypeChar, Size: 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := NewPacketStruct(version)
|
||||||
|
|
||||||
|
// Set string values
|
||||||
|
ps.Set("str8", "Short")
|
||||||
|
ps.Set("str16", "Medium length string")
|
||||||
|
ps.Set("str32", "This is a much longer string that might be used for descriptions")
|
||||||
|
ps.Set("char_fixed", "Fixed")
|
||||||
|
ps.Set("char_null", "Null-term")
|
||||||
|
|
||||||
|
// Serialize
|
||||||
|
data, err := ps.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to serialize: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize
|
||||||
|
ps2 := NewPacketStruct(version)
|
||||||
|
if err := ps2.Deserialize(data); err != nil {
|
||||||
|
t.Fatalf("Failed to deserialize: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify strings
|
||||||
|
if val, _ := ps2.Get("str8"); val.(string) != "Short" {
|
||||||
|
t.Errorf("str8 mismatch: got %v", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, _ := ps2.Get("str16"); val.(string) != "Medium length string" {
|
||||||
|
t.Errorf("str16 mismatch: got %v", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, _ := ps2.Get("str32"); val.(string) != "This is a much longer string that might be used for descriptions" {
|
||||||
|
t.Errorf("str32 mismatch: got %v", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, _ := ps2.Get("char_fixed"); val.(string) != "Fixed" {
|
||||||
|
t.Errorf("char_fixed mismatch: got %v", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, _ := ps2.Get("char_null"); val.(string) != "Null-term" {
|
||||||
|
t.Errorf("char_null mismatch: got %v", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConditionalFields tests fields with if-set/if-not-set conditions
|
||||||
|
func TestConditionalFields(t *testing.T) {
|
||||||
|
version := &PacketVersion{
|
||||||
|
Version: 1,
|
||||||
|
Fields: []Field{
|
||||||
|
{Name: "hasData", Type: FieldTypeUInt8},
|
||||||
|
{Name: "data", Type: FieldTypeUInt32, IfSet: "hasData"},
|
||||||
|
{Name: "noData", Type: FieldTypeUInt16, IfNotSet: "hasData"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with hasData set
|
||||||
|
ps1 := NewPacketStruct(version)
|
||||||
|
ps1.Set("hasData", uint8(1))
|
||||||
|
ps1.Set("data", uint32(999))
|
||||||
|
ps1.Set("noData", uint16(111))
|
||||||
|
|
||||||
|
data1, err := ps1.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to serialize: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should contain: uint8(1), uint32(999)
|
||||||
|
// Should NOT contain: uint16(111)
|
||||||
|
expectedSize := 1 + 4
|
||||||
|
if len(data1) != expectedSize {
|
||||||
|
t.Errorf("Expected serialized size %d, got %d", expectedSize, len(data1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with hasData not set
|
||||||
|
ps2 := NewPacketStruct(version)
|
||||||
|
ps2.Set("data", uint32(999))
|
||||||
|
ps2.Set("noData", uint16(111))
|
||||||
|
|
||||||
|
data2, err := ps2.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to serialize: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should contain: uint8(0), uint16(111)
|
||||||
|
// Should NOT contain: uint32(999)
|
||||||
|
expectedSize = 1 + 2
|
||||||
|
if len(data2) != expectedSize {
|
||||||
|
t.Errorf("Expected serialized size %d, got %d", expectedSize, len(data2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSerialization benchmarks packet serialization
|
||||||
|
func BenchmarkSerialization(b *testing.B) {
|
||||||
|
version := &PacketVersion{
|
||||||
|
Version: 1,
|
||||||
|
Fields: []Field{
|
||||||
|
{Name: "id", Type: FieldTypeUInt32},
|
||||||
|
{Name: "name", Type: FieldTypeString16},
|
||||||
|
{Name: "level", Type: FieldTypeUInt8},
|
||||||
|
{Name: "x", Type: FieldTypeFloat},
|
||||||
|
{Name: "y", Type: FieldTypeFloat},
|
||||||
|
{Name: "z", Type: FieldTypeFloat},
|
||||||
|
{Name: "flags", Type: FieldTypeUInt32},
|
||||||
|
{Name: "timestamp", Type: FieldTypeUInt64},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := NewPacketStruct(version)
|
||||||
|
ps.Set("id", uint32(12345))
|
||||||
|
ps.Set("name", "BenchmarkPlayer")
|
||||||
|
ps.Set("level", uint8(50))
|
||||||
|
ps.Set("x", float32(100.5))
|
||||||
|
ps.Set("y", float32(200.5))
|
||||||
|
ps.Set("z", float32(300.5))
|
||||||
|
ps.Set("flags", uint32(0xFF00FF00))
|
||||||
|
ps.Set("timestamp", uint64(1234567890))
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
data, err := ps.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
_ = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkDeserialization benchmarks packet deserialization
|
||||||
|
func BenchmarkDeserialization(b *testing.B) {
|
||||||
|
version := &PacketVersion{
|
||||||
|
Version: 1,
|
||||||
|
Fields: []Field{
|
||||||
|
{Name: "id", Type: FieldTypeUInt32},
|
||||||
|
{Name: "name", Type: FieldTypeString16},
|
||||||
|
{Name: "level", Type: FieldTypeUInt8},
|
||||||
|
{Name: "x", Type: FieldTypeFloat},
|
||||||
|
{Name: "y", Type: FieldTypeFloat},
|
||||||
|
{Name: "z", Type: FieldTypeFloat},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test data
|
||||||
|
ps := NewPacketStruct(version)
|
||||||
|
ps.Set("id", uint32(12345))
|
||||||
|
ps.Set("name", "TestPlayer")
|
||||||
|
ps.Set("level", uint8(50))
|
||||||
|
ps.Set("x", float32(100.5))
|
||||||
|
ps.Set("y", float32(200.5))
|
||||||
|
ps.Set("z", float32(300.5))
|
||||||
|
|
||||||
|
data, _ := ps.Serialize()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
ps2 := NewPacketStruct(version)
|
||||||
|
err := ps2.Deserialize(data)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user