8.5 KiB

EQ2 Packet Parser

A dead-simple way to turn EverQuest 2's binary packet data into Go structs. Just add some tags to your struct fields and let reflection do the heavy lifting.

How it works

// Your packet structure
type LoginPacket struct {
    UserID   uint32             `eq2:"int32"`
    Username common.EQ2String16 `eq2:"string16"`
    Level    uint8              `eq2:"int8"`
}

// Parse some bytes
data := []byte{...} // whatever bytes you got
parser := parser.NewParser(data)

var packet LoginPacket
err := parser.ParseStruct(&packet)
// boom, packet is now filled with data

Tag Syntax

Just slap eq2:"type,options" on your fields and you're good to go.

The usual suspects

PlayerID    uint32  `eq2:"int32"`     // 32-bit integer
Health      uint16  `eq2:"int16"`     // 16-bit integer  
Alive       uint8   `eq2:"int8"`      // 8-bit integer
Damage      float32 `eq2:"float"`     // 32-bit float
Name        common.EQ2String16 `eq2:"string16"` // EQ2's weird string format
Color       common.EQ2Color    `eq2:"color"`    // RGB color

Arrays (because everything's an array in EQ2)

// Fixed size - always 5 items
Stats []uint16 `eq2:"int16,len=5"`

// Dynamic size - read ItemCount first, then that many items
ItemCount uint8  `eq2:"int8"`
Items     []Item `eq2:"array,arraysize=ItemCount"`

Conditionals (the fun stuff)

// Only parse this if Channel equals 1
MessageType uint8 `eq2:"int8,if=Channel==1"`

// Only parse if HasRewards is truthy
HasRewards  uint8       `eq2:"int8"`
RewardData  *RewardInfo `eq2:"substruct,ifvariableset=HasRewards"`

// Only parse if we set the "has_equipment" flag
Equipment []Equipment `eq2:"equipment,ifflag=has_equipment"`

// Array index variables - access specific array elements
ModCount uint8 `eq2:"int8"`
Mods     []Mod `eq2:"array,arraysize=ModCount"`
// This checks if Mods[0] exists and is truthy
ExtraData []byte `eq2:"char,len=10,ifvariableset=header_info_mod_need_0"`

// Dynamic array index using %i (replaced with current array index)
StatTypes  []uint8 `eq2:"int8,len=5"`
StatValues []any   `eq2:"int32,type2=float,type2criteria=stat_type_%i!=6"`

Comma-separated conditions

Multiple variables can be checked in a single condition using comma-separated lists:

// Parse only if NONE of the listed variables are set
NumEffects uint8 `eq2:"int8,ifvariablenotset=header_info_header_unknown_0_0,header_unknown_0"`

// Parse if ANY of the listed variables are set
BonusData []byte `eq2:"char,len=20,ifvariableset=has_bonus,has_special,has_extra"`

// Parse if ANY of the listed flags are set
OptionalField uint32 `eq2:"int32,ifflag=debug_mode,test_mode,dev_mode"`

// Parse if ALL of the listed flags are NOT set
ProductionData []byte `eq2:"char,len=50,ifflagnotset=debug_mode,test_mode"`

// Multiple equals conditions (ANY must be true)
TypeData any `eq2:"int32,ifequals=type=1,category=special"`

// Multiple not-equals conditions (ALL must be true)
Value uint16 `eq2:"int16,ifnotequals=status=disabled,flag=hidden"`

Comma-separated logic rules:

  • ifvariableset - TRUE if ANY variable is set
  • ifvariablenotset - TRUE if ALL variables are NOT set
  • ifflag - TRUE if ANY flag is set
  • ifflagnotset - TRUE if ALL flags are NOT set
  • ifequals - TRUE if ANY condition matches
  • ifnotequals - TRUE if ALL conditions are true (none match)

Type switching (when EQ2 reuses the same bytes for different things)

// Normally parse as int32, but if StatType != 6, parse as float instead
StatType  uint8 `eq2:"int8"`
StatValue any   `eq2:"int32,type2=float,type2criteria=StatType!=6"`

// String length operators for type switching
NameLength uint8  `eq2:"int8"`
Name       string `eq2:"string16,type2=string8,type2criteria=stat_name!>10"`

String length operators

Use !>, !<, !>=, !<=, != for string length comparisons:

// Switch to string8 if name length > 10 characters
Name string `eq2:"string16,type2=string8,type2criteria=player_name!>10"`

// Only parse if description is not empty
HasDesc     uint8  `eq2:"int8"`
Description string `eq2:"string16,ifvariableset=HasDesc,if=description!>0"`

Size limits (because EQ2 packets can get weird)

// If the data is bigger than 1000 bytes, just truncate it
DataSize uint16 `eq2:"int16"`
Data     []byte `eq2:"char,len=DataSize,maxsize=1000,skipoversized"`

Multiple client versions

EQ2 has like 50 different client versions, each with slightly different packet layouts. Handle it like this:

registry := parser.NewVersionRegistry()
registry.RegisterStruct("LoginReply", "1.0", reflect.TypeOf(LoginReplyV1{}))
registry.RegisterStruct("LoginReply", "2.0", reflect.TypeOf(LoginReplyV2{}))

// Parser picks the right version (or falls back to closest match)
result, err := parser.ParseWithVersion(registry, "LoginReply", clientVersion)

Real example

type CharacterData struct {
    // Basic stuff
    CharID   uint32             `eq2:"int32"`
    Name     common.EQ2String16 `eq2:"string16"`
    Level    uint8              `eq2:"int8"`
    
    // Guild info (only if they're in a guild)
    HasGuild uint8              `eq2:"int8"`
    GuildID  uint32             `eq2:"int32,ifvariableset=HasGuild"`
    
    // Variable number of items
    ItemCount uint16            `eq2:"int16"`
    Items     []InventoryItem   `eq2:"array,arraysize=ItemCount"`
    
    // Nested stuff
    Stats     PlayerStats       `eq2:"substruct"`
    
    // Array index access example
    BuffCount uint8             `eq2:"int8"`
    Buffs     []BuffData        `eq2:"array,arraysize=BuffCount"`
    // Only parse extended data if first buff exists
    ExtendedBuffData []byte     `eq2:"char,len=20,ifvariableset=buffs_0"`
    
    // Comma-separated conditions example
    HeaderFlags uint8           `eq2:"int8"`
    // Parse effects only if neither unknown field is set
    NumEffects uint8            `eq2:"int8,ifvariablenotset=header_info_header_unknown_0_0,header_unknown_0"`
    Effects    []EffectData     `eq2:"array,arraysize=NumEffects"`
}

type InventoryItem struct {
    ItemID   uint32            `eq2:"int32"`
    Quantity uint16            `eq2:"int16"`
    Color    common.EQ2Color   `eq2:"color"`
    
    // Type switching based on string length
    NameType uint8             `eq2:"int8"`
    Name     any               `eq2:"string16,type2=string8,type2criteria=item_name!<=8"`
}

type PlayerStats struct {
    Health  uint32 `eq2:"int32"`
    Mana    uint32 `eq2:"int32"`
    Stamina uint32 `eq2:"int32"`
}

type BuffData struct {
    BuffID   uint32 `eq2:"int32"`
    Duration uint16 `eq2:"int16"`
}

type EffectData struct {
    Effect     common.EQ2String16 `eq2:"string16"`
    Percentage uint8              `eq2:"int8"`
}

Advanced conditional patterns

type ComplexPacket struct {
    // Array with per-element conditionals using %i
    StatCount  uint8   `eq2:"int8"`
    StatTypes  []uint8 `eq2:"int8,len=StatCount"`
    StatValues []any   `eq2:"int32,type2=float,type2criteria=stat_type_%i!=6"`
    
    // Array index access for conditionals
    ModCount uint8 `eq2:"int8"`
    Mods     []Mod `eq2:"array,arraysize=ModCount"`
    // Parse only if specific array elements exist
    Bonus1   uint32 `eq2:"int32,ifvariableset=header_info_mod_need_0"`
    Bonus2   uint32 `eq2:"int32,ifvariableset=header_info_mod_need_1"`
    
    // String length conditionals
    PlayerName string `eq2:"string16"`
    ShortName  string `eq2:"string8,if=player_name!<=8"`
    LongDesc   string `eq2:"string32,if=player_name!>15"`
    
    // Comma-separated multi-condition examples
    DebugInfo  []byte `eq2:"char,len=100,ifflag=debug_mode,test_mode,dev_mode"`
    ProdData   []byte `eq2:"char,len=50,ifflagnotset=debug_mode,test_mode,dev_mode"`
    
    // Multiple variable checks
    OptionalData []byte `eq2:"char,len=20,ifvariableset=has_optional,has_extended"`
    CleanupData  []byte `eq2:"char,len=10,ifvariablenotset=dirty_flag,temp_flag,cache_flag"`
}

Converting from XML

If you've got EQ2's XML packet definitions, the conversion is pretty straightforward:

XML Go Tag
Type="int32" eq2:"int32"
ArraySizeVariable="count" arraysize=Count
IfVariableSet="flag" ifvariableset=Flag
IfVariableNotSet="var1,var2" ifvariablenotset=var1,var2
IfFlag="flag1,flag2" ifflag=flag1,flag2
Size="5" len=5
Type2Criteria="field!=value" type2criteria=Field!=value
Type2Criteria="name!>10" type2criteria=name!>10
Array index access ifvariableset=array_name_0
Dynamic index patterns type2criteria=field_%i!=value