diff --git a/internal/packets/parser/DOCS.md b/internal/packets/parser/DOCS.md new file mode 100644 index 0000000..8da2d81 --- /dev/null +++ b/internal/packets/parser/DOCS.md @@ -0,0 +1,131 @@ +# 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"` +``` + +### 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"` +``` + +### 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"` +} + +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` | \ No newline at end of file diff --git a/internal/packets/parser/array_parser.go b/internal/packets/parser/array_parser.go index 01f3a36..1930c24 100644 --- a/internal/packets/parser/array_parser.go +++ b/internal/packets/parser/array_parser.go @@ -62,7 +62,7 @@ func (p *Parser) readSubstruct(field reflect.Value, length int) error { elemType := field.Type().Elem() slice := reflect.MakeSlice(field.Type(), length, length) - for i := 0; i < length; i++ { + for i := range length { elem := slice.Index(i) if err := p.parseStructElement(elem, elemType); err != nil { return fmt.Errorf("substruct element %d: %w", i, err) @@ -95,7 +95,7 @@ func (p *Parser) parseStructElement(elem reflect.Value, elemType reflect.Type) e p.fieldCache = make(map[string]any) p.structStack = append(p.structStack, elem) - for i := 0; i < elem.NumField(); i++ { + for i := range elem.NumField() { field := elem.Field(i) fieldType := elemType.Field(i) diff --git a/internal/packets/parser/conditions.go b/internal/packets/parser/conditions.go index dec7a4a..556c573 100644 --- a/internal/packets/parser/conditions.go +++ b/internal/packets/parser/conditions.go @@ -6,7 +6,7 @@ import ( ) // evaluateAllConditions checks all conditional logic -func (p *Parser) evaluateAllConditions(fieldTag *FieldTag, field reflect.Value) bool { +func (p *Parser) evaluateAllConditions(fieldTag *FieldTag, _ reflect.Value) bool { if fieldTag.Condition != nil && !p.evaluateCondition(fieldTag.Condition) { return false } diff --git a/internal/packets/parser/type_readers.go b/internal/packets/parser/type_readers.go index 9fd87f8..ba3f92f 100644 --- a/internal/packets/parser/type_readers.go +++ b/internal/packets/parser/type_readers.go @@ -5,7 +5,6 @@ import ( "reflect" ) -// readInt8 handles uint8/int8 reading func (p *Parser) readInt8(field reflect.Value, length int) error { if length == 1 { val, err := p.readUint8() @@ -17,7 +16,7 @@ func (p *Parser) readInt8(field reflect.Value, length int) error { } slice := make([]uint8, length) - for i := 0; i < length; i++ { + for i := range length { val, err := p.readUint8() if err != nil { return err @@ -39,7 +38,7 @@ func (p *Parser) readInt16(field reflect.Value, length int) error { } slice := make([]uint16, length) - for i := 0; i < length; i++ { + for i := range length { val, err := p.readUint16() if err != nil { return err @@ -67,7 +66,7 @@ func (p *Parser) readInt32(field reflect.Value, length int) error { } slice := make([]uint32, length) - for i := 0; i < length; i++ { + for i := range length { val, err := p.readUint32() if err != nil { return err @@ -89,7 +88,7 @@ func (p *Parser) readInt64(field reflect.Value, length int) error { } slice := make([]uint64, length) - for i := 0; i < length; i++ { + for i := range length { val, err := p.readUint64() if err != nil { return err @@ -111,7 +110,7 @@ func (p *Parser) readSInt8(field reflect.Value, length int) error { } slice := make([]int8, length) - for i := 0; i < length; i++ { + for i := range length { val, err := p.readUint8() if err != nil { return err @@ -133,7 +132,7 @@ func (p *Parser) readSInt16(field reflect.Value, length int) error { } slice := make([]int16, length) - for i := 0; i < length; i++ { + for i := range length { val, err := p.readUint16() if err != nil { return err @@ -155,7 +154,7 @@ func (p *Parser) readSInt32(field reflect.Value, length int) error { } slice := make([]int32, length) - for i := 0; i < length; i++ { + for i := range length { val, err := p.readUint32() if err != nil { return err @@ -224,7 +223,7 @@ func (p *Parser) readFloat(field reflect.Value, length int) error { } slice := make([]float32, length) - for i := 0; i < length; i++ { + for i := range length { val, err := p.readFloat32() if err != nil { return err @@ -246,7 +245,7 @@ func (p *Parser) readDouble(field reflect.Value, length int) error { } slice := make([]float64, length) - for i := 0; i < length; i++ { + for i := range length { val, err := p.readFloat64() if err != nil { return err @@ -338,7 +337,7 @@ func (p *Parser) readColor(field reflect.Value, length int) error { } slice := make([]common.EQ2Color, length) - for i := 0; i < length; i++ { + for i := range length { var err error slice[i].Red, err = p.readUint8() if err != nil { @@ -397,7 +396,7 @@ func (p *Parser) readEquipment(field reflect.Value, length int) error { } slice := make([]common.EQ2EquipmentItem, length) - for i := 0; i < length; i++ { + for i := range length { var err error slice[i].Type, err = p.readUint16() if err != nil {