3.8 KiB
3.8 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"`
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"`
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"`
}
type InventoryItem struct {
ItemID uint32 `eq2:"int32"`
Quantity uint16 `eq2:"int16"`
Color common.EQ2Color `eq2:"color"`
}
type PlayerStats struct {
Health uint32 `eq2:"int32"`
Mana uint32 `eq2:"int32"`
Stamina uint32 `eq2:"int32"`
}
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 |
Size="5" |
len=5 |