8.6 KiB
8.6 KiB
Packet Parser
Write packet definitions as data instead of code! This parser handles complex binary protocols with versioning, conditional fields, and nested structures.
Quick Start
import "your-project/internal/common"
var MyPacketDef = &PacketDef{
Fields: map[string]FieldDesc{
"count": {Type: common.TypeInt16},
"items": {Type: common.TypeArray, Condition: "var:count", SubDef: ItemDef},
"name": {Type: common.TypeString8},
},
Orders: map[uint32][]string{
1: {"count", "items", "name"},
},
}
// Parse it
result, err := MyPacketDef.Parse(data, version, flags)
Field Types
Integer Types
Type | Description | Size | Example |
---|---|---|---|
common.TypeInt8 |
Unsigned 8-bit integer | 1 byte | {Type: common.TypeInt8} |
common.TypeInt16 |
Unsigned 16-bit integer | 2 bytes | {Type: common.TypeInt16} |
common.TypeInt32 |
Unsigned 32-bit integer | 4 bytes | {Type: common.TypeInt32} |
common.TypeInt64 |
Unsigned 64-bit integer | 8 bytes | {Type: common.TypeInt64} |
common.TypeSInt8 |
Signed 8-bit integer | 1 byte | {Type: common.TypeSInt8} |
common.TypeSInt16 |
Signed 16-bit integer | 2 bytes | {Type: common.TypeSInt16} |
common.TypeSInt32 |
Signed 32-bit integer | 4 bytes | {Type: common.TypeSInt32} |
common.TypeSInt64 |
Signed 64-bit integer | 8 bytes | {Type: common.TypeSInt64} |
String Types
Type | Description | Returns | Example |
---|---|---|---|
common.TypeString8 |
1-byte length prefix | EQ2String8{Size, Data} |
{Type: common.TypeString8} |
common.TypeString16 |
2-byte length prefix | EQ2String16{Size, Data} |
{Type: common.TypeString16} |
common.TypeString32 |
4-byte length prefix | EQ2String32{Size, Data} |
{Type: common.TypeString32} |
Other Types
Type | Description | Returns | Example |
---|---|---|---|
common.TypeChar |
Fixed-size byte array | []byte |
{Type: common.TypeChar, Length: 10} |
common.TypeFloat |
32-bit float | float32 |
{Type: common.TypeFloat} |
common.TypeDouble |
64-bit float | float64 |
{Type: common.TypeDouble} |
common.TypeColor |
RGB color value | EQ2Color{Red, Green, Blue} |
{Type: common.TypeColor} |
common.TypeEquipment |
Equipment item | EQ2EquipmentItem{Type, Color, Highlight} |
{Type: common.TypeEquipment} |
common.TypeArray |
Array of substructures | []map[string]any |
{Type: common.TypeArray, SubDef: ItemDef} |
Conditions
Control when fields are parsed using simple condition strings:
Variable Checks
"var:item_count" // Parse if item_count exists and is non-zero
"!var:item_count" // Parse if item_count doesn't exist or is zero
Flag Checks
"flag:loot" // Parse if loot flag is set
"!flag:loot" // Parse if loot flag is not set
Version Checks
"version>=1188" // Parse if version 1188 or higher
"version<562" // Parse if version below 562
Comparisons
"stat_type!=6" // Parse if stat_type is not 6
"level>=10" // Parse if level is 10 or higher
String Length
"name!>5" // Parse if name is longer than 5 characters
"description!<=100" // Parse if description is 100 chars or less
Bitwise Operations
"header_flags&0x01" // Parse if bit 1 is set in header_flags
Combining Conditions
"var:count,var:size" // OR: either count or size exists
"version>=562&version<1188" // AND: version between 562-1187
Version Ordering
Different versions can have different field orders:
Orders: map[uint32][]string{
373: {"basic_field", "name"},
1188: {"basic_field", "new_field", "name"},
2000: {"basic_field", "new_field", "another_field", "name"},
}
The parser automatically uses the highest version ≤ your target version.
Substructures
Create reusable packet definitions:
var ItemDef = &PacketDef{
Fields: map[string]FieldDesc{
"item_id": {Type: common.TypeInt32},
"item_name": {Type: common.TypeString16},
"quantity": {Type: common.TypeInt8, Condition: "version>=546"},
"color": {Type: common.TypeColor, Condition: "flag:has_colors"},
},
Orders: map[uint32][]string{
1: {"item_id", "item_name", "quantity", "color"},
},
}
var InventoryDef = &PacketDef{
Fields: map[string]FieldDesc{
"item_count": {Type: common.TypeInt8},
"items": {Type: common.TypeArray, Condition: "var:item_count", SubDef: ItemDef},
},
Orders: map[uint32][]string{
1: {"item_count", "items"},
},
}
Advanced Features
Type Switching
Use different types based on conditions:
{
Type: common.TypeInt32,
Type2: common.TypeFloat,
Type2Cond: "stat_type!=6", // Use float if stat_type is not 6
}
Oversized Fields
Handle fields that can exceed normal size limits:
{
Type: common.TypeInt16,
Oversized: 127, // Switch to 16-bit if first byte is 127
}
Array Index Substitution
Reference array indices in conditions:
"var:item_type_%i" // Substitutes %i with current array index
Working with Structured Types
String Types
String types return structured objects with size and data:
// TypeString8 returns: EQ2String8{Size: uint8, Data: string}
// TypeString16 returns: EQ2String16{Size: uint16, Data: string}
// TypeString32 returns: EQ2String32{Size: uint32, Data: string}
result, _ := packet.Parse(data, version, flags)
if nameField, ok := result["player_name"].(common.EQ2String16); ok {
fmt.Printf("Name: %s (length: %d)\n", nameField.Data, nameField.Size)
}
Color Types
// TypeColor returns: EQ2Color{Red: uint8, Green: uint8, Blue: uint8}
if colorField, ok := result["shirt_color"].(common.EQ2Color); ok {
fmt.Printf("RGB: %d,%d,%d\n", colorField.Red, colorField.Green, colorField.Blue)
}
Equipment Types
// TypeEquipment returns: EQ2EquipmentItem{Type: uint16, Color: EQ2Color, Highlight: EQ2Color}
if equipField, ok := result["helmet"].(common.EQ2EquipmentItem); ok {
fmt.Printf("Equipment Type: %d\n", equipField.Type)
fmt.Printf("Color: RGB(%d,%d,%d)\n",
equipField.Color.Red, equipField.Color.Green, equipField.Color.Blue)
}
Complete Example
import "your-project/internal/common"
var QuestPacketDef = &PacketDef{
Fields: map[string]FieldDesc{
"quest_id": {Type: common.TypeInt32},
"has_rewards": {Type: common.TypeInt8},
"reward_count": {Type: common.TypeInt8, Condition: "var:has_rewards"},
"rewards": {Type: common.TypeArray, Condition: "var:reward_count", SubDef: RewardDef},
"is_complete": {Type: common.TypeInt8},
"complete_text": {Type: common.TypeString16, Condition: "var:is_complete"},
"quest_color": {Type: common.TypeColor, Condition: "version>=1200"},
"version_field": {Type: common.TypeInt32, Condition: "version>=1200"},
},
Orders: map[uint32][]string{
1: {"quest_id", "has_rewards", "reward_count", "rewards", "is_complete", "complete_text"},
1200: {"quest_id", "has_rewards", "reward_count", "rewards", "is_complete", "complete_text", "quest_color", "version_field"},
},
}
var RewardDef = &PacketDef{
Fields: map[string]FieldDesc{
"reward_type": {Type: common.TypeInt8},
"amount": {Type: common.TypeInt32},
"bonus": {Type: common.TypeInt16, Condition: "reward_type==1"},
"item_color": {Type: common.TypeColor, Condition: "reward_type==2"},
},
Orders: map[uint32][]string{
1: {"reward_type", "amount", "bonus", "item_color"},
},
}
// Usage
result, err := QuestPacketDef.Parse(packetData, 1200, questFlags)
if err != nil {
log.Fatal(err)
}
// Access structured data
if questText, ok := result["complete_text"].(common.EQ2String16); ok {
fmt.Printf("Quest completion text: %s\n", questText.Data)
}
if questColor, ok := result["quest_color"].(common.EQ2Color); ok {
fmt.Printf("Quest color: #%02x%02x%02x\n",
questColor.Red, questColor.Green, questColor.Blue)
}
Migration from Old Types
Old Type | New Type | Breaking Change |
---|---|---|
Uint8 |
common.TypeInt8 |
Type name only |
Uint16 |
common.TypeInt16 |
Type name only |
Uint32 |
common.TypeInt32 |
Type name only |
String8 |
common.TypeString8 |
Returns EQ2String8 struct |
String16 |
common.TypeString16 |
Returns EQ2String16 struct |
ByteArray |
common.TypeChar |
Type name only |
Float32 |
common.TypeFloat |
Type name only |
SubArray |
common.TypeArray |
Type name only |
Note: String types now return structured objects instead of plain strings. Access the string data via the .Data
field.