250 lines
8.5 KiB
Markdown
250 lines
8.5 KiB
Markdown
# 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` | |