# 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 ```go // 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 ```go 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) ```go // 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) ```go // 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: ```go // 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) ```go // 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: ```go // 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) ```go // 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: ```go 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 ```go 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 ```go 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` |