From ca46c5617dc5cf4bbdaa05d858b67e81cfe13712 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Sun, 27 Jul 2025 22:16:36 -0500 Subject: [PATCH] new parser attempt --- internal/packets/PARSER.md | 273 ++++++++++++++++++++++++++ internal/packets/parser/conditions.go | 239 ++++++++++++++++++++++ internal/packets/parser/context.go | 145 ++++++++++++++ internal/packets/parser/parser.go | 84 ++++++++ internal/packets/parser/types.go | 18 ++ 5 files changed, 759 insertions(+) create mode 100644 internal/packets/PARSER.md create mode 100644 internal/packets/parser/conditions.go create mode 100644 internal/packets/parser/context.go create mode 100644 internal/packets/parser/parser.go create mode 100644 internal/packets/parser/types.go diff --git a/internal/packets/PARSER.md b/internal/packets/PARSER.md new file mode 100644 index 0000000..e904cfc --- /dev/null +++ b/internal/packets/PARSER.md @@ -0,0 +1,273 @@ +# 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 + +```go +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 +```go +"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 +```go +"flag:loot" // Parse if loot flag is set +"!flag:loot" // Parse if loot flag is not set +``` + +### Version Checks +```go +"version>=1188" // Parse if version 1188 or higher +"version<562" // Parse if version below 562 +``` + +### Comparisons +```go +"stat_type!=6" // Parse if stat_type is not 6 +"level>=10" // Parse if level is 10 or higher +``` + +### String Length +```go +"name!>5" // Parse if name is longer than 5 characters +"description!<=100" // Parse if description is 100 chars or less +``` + +### Bitwise Operations +```go +"header_flags&0x01" // Parse if bit 1 is set in header_flags +``` + +### Combining Conditions +```go +"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: + +```go +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: + +```go +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: +```go +{ + 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: +```go +{ + Type: common.TypeInt16, + Oversized: 127, // Switch to 16-bit if first byte is 127 +} +``` + +### Array Index Substitution +Reference array indices in conditions: +```go +"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: + +```go +// 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 +```go +// 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 +```go +// 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 + +```go +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. \ No newline at end of file diff --git a/internal/packets/parser/conditions.go b/internal/packets/parser/conditions.go new file mode 100644 index 0000000..8ac7001 --- /dev/null +++ b/internal/packets/parser/conditions.go @@ -0,0 +1,239 @@ +package parser + +import ( + "eq2emu/internal/common" + "strconv" + "strings" +) + +func (ctx *ParseContext) checkCondition(condition string) bool { + if condition == "" { + return true + } + + // Handle comma-separated OR conditions + if strings.Contains(condition, ",") { + parts := strings.Split(condition, ",") + for _, part := range parts { + if ctx.evaluateCondition(strings.TrimSpace(part)) { + return true + } + } + return false + } + + // Handle AND conditions with & + if strings.Contains(condition, "&") && !strings.Contains(condition, "0x") { + parts := strings.Split(condition, "&") + for _, part := range parts { + if !ctx.evaluateCondition(strings.TrimSpace(part)) { + return false + } + } + return true + } + + return ctx.evaluateCondition(condition) +} + +func (ctx *ParseContext) evaluateCondition(condition string) bool { + // Flag conditions: flag:name or !flag:name + if strings.HasPrefix(condition, "flag:") { + flagName := condition[5:] + return (ctx.flags & ctx.getFlagValue(flagName)) != 0 + } + if strings.HasPrefix(condition, "!flag:") { + flagName := condition[6:] + return (ctx.flags & ctx.getFlagValue(flagName)) == 0 + } + + // Variable conditions: var:name or !var:name + if strings.HasPrefix(condition, "var:") { + varName := condition[4:] + return ctx.hasVar(varName) + } + if strings.HasPrefix(condition, "!var:") { + varName := condition[5:] + return !ctx.hasVar(varName) + } + + // Version comparisons + if strings.HasPrefix(condition, "version") { + return ctx.evaluateVersionCondition(condition) + } + + // String length operators: name!>5, name!<=10 + stringOps := []string{"!>=", "!<=", "!>", "!<", "!="} + for _, op := range stringOps { + if idx := strings.Index(condition, op); idx > 0 { + varName := condition[:idx] + valueStr := condition[idx+len(op):] + return ctx.evaluateStringLength(varName, valueStr, op) + } + } + + // Comparison operators: >=, <=, >, <, ==, != + compOps := []string{">=", "<=", ">", "<", "==", "!="} + for _, op := range compOps { + if idx := strings.Index(condition, op); idx > 0 { + varName := condition[:idx] + valueStr := condition[idx+len(op):] + return ctx.evaluateComparison(varName, valueStr, op) + } + } + + // Bitwise AND: header_flag&0x01 + if strings.Contains(condition, "&0x") { + parts := strings.SplitN(condition, "&", 2) + varName := parts[0] + hexValue, _ := strconv.ParseUint(parts[1], 0, 64) + varValue := ctx.getVarValue(varName) + return (varValue & hexValue) != 0 + } + + // Simple variable existence + return ctx.hasVar(condition) +} + +func (ctx *ParseContext) evaluateVersionCondition(condition string) bool { + if strings.Contains(condition, ">=") { + versionStr := condition[strings.Index(condition, ">=")+2:] + targetVersion, _ := strconv.ParseUint(versionStr, 10, 32) + return ctx.version >= uint32(targetVersion) + } + if strings.Contains(condition, "<=") { + versionStr := condition[strings.Index(condition, "<=")+2:] + targetVersion, _ := strconv.ParseUint(versionStr, 10, 32) + return ctx.version <= uint32(targetVersion) + } + if strings.Contains(condition, ">") { + versionStr := condition[strings.Index(condition, ">")+1:] + targetVersion, _ := strconv.ParseUint(versionStr, 10, 32) + return ctx.version > uint32(targetVersion) + } + if strings.Contains(condition, "<") { + versionStr := condition[strings.Index(condition, "<")+1:] + targetVersion, _ := strconv.ParseUint(versionStr, 10, 32) + return ctx.version < uint32(targetVersion) + } + return false +} + +func (ctx *ParseContext) evaluateStringLength(varName, valueStr, op string) bool { + str := ctx.getStringVar(varName) + targetLen, _ := strconv.Atoi(valueStr) + strLen := len(str) + + switch op { + case "!>": + return strLen > targetLen + case "!<": + return strLen < targetLen + case "!>=": + return strLen >= targetLen + case "!<=": + return strLen <= targetLen + case "!=": + return strLen != targetLen + } + return false +} + +func (ctx *ParseContext) evaluateComparison(varName, valueStr, op string) bool { + varValue := ctx.getVarValue(varName) + targetValue, _ := strconv.ParseUint(valueStr, 0, 64) + + switch op { + case ">=": + return varValue >= targetValue + case "<=": + return varValue <= targetValue + case ">": + return varValue > targetValue + case "<": + return varValue < targetValue + case "==": + return varValue == targetValue + case "!=": + return varValue != targetValue + } + return false +} + +func (ctx *ParseContext) hasVar(name string) bool { + // Handle %i substitution + if strings.Contains(name, "%i") && len(ctx.arrayStack) > 0 { + currentIndex := ctx.arrayStack[len(ctx.arrayStack)-1] + name = strings.ReplaceAll(name, "%i", strconv.Itoa(currentIndex)) + } + + if val, exists := ctx.vars[name]; exists { + switch v := val.(type) { + case uint8, uint16, uint32, uint64: + return ctx.getVarValue(name) != 0 + case string: + return v != "" + case common.EQ2String8: + return v.Data != "" + case common.EQ2String16: + return v.Data != "" + case common.EQ2String32: + return v.Data != "" + default: + return true + } + } + return false +} + +func (ctx *ParseContext) getVarValue(name string) uint64 { + if val, exists := ctx.vars[name]; exists { + switch v := val.(type) { + case uint8: + return uint64(v) + case uint16: + return uint64(v) + case uint32: + return uint64(v) + case uint64: + return v + } + } + return 0 +} + +func (ctx *ParseContext) getStringVar(name string) string { + if val, exists := ctx.vars[name]; exists { + switch v := val.(type) { + case string: + return v + case common.EQ2String8: + return v.Data + case common.EQ2String16: + return v.Data + case common.EQ2String32: + return v.Data + } + } + return "" +} + +func (ctx *ParseContext) getFlagValue(flagName string) uint64 { + flagMap := map[string]uint64{ + "loot": 0x01, + "has_equipment": 0x02, + "no_colors": 0x04, + } + if val, exists := flagMap[flagName]; exists { + return val + } + return 0 +} + +func (ctx *ParseContext) getArraySize(condition string) int { + if strings.HasPrefix(condition, "var:") { + varName := condition[4:] + return int(ctx.getVarValue(varName)) + } + return 0 +} diff --git a/internal/packets/parser/context.go b/internal/packets/parser/context.go new file mode 100644 index 0000000..a1c2fa0 --- /dev/null +++ b/internal/packets/parser/context.go @@ -0,0 +1,145 @@ +package parser + +import ( + "encoding/binary" + "eq2emu/internal/common" + "unsafe" +) + +type ParseContext struct { + data []byte + offset int + version uint32 + flags uint64 + vars map[string]any + arrayStack []int +} + +func NewContext(data []byte, version uint32, flags uint64) *ParseContext { + return &ParseContext{ + data: data, + version: version, + flags: flags, + vars: make(map[string]any), + } +} + +func (ctx *ParseContext) readUint8() uint8 { + val := ctx.data[ctx.offset] + ctx.offset++ + return val +} + +func (ctx *ParseContext) readUint16() uint16 { + val := binary.LittleEndian.Uint16(ctx.data[ctx.offset:]) + ctx.offset += 2 + return val +} + +func (ctx *ParseContext) readUint32() uint32 { + val := binary.LittleEndian.Uint32(ctx.data[ctx.offset:]) + ctx.offset += 4 + return val +} + +func (ctx *ParseContext) readOversizedUint8(threshold int) uint8 { + if ctx.data[ctx.offset] == byte(threshold) { + ctx.offset++ + return ctx.readUint8() + } + return ctx.readUint8() +} + +func (ctx *ParseContext) readOversizedUint16(threshold int) uint16 { + if ctx.data[ctx.offset] == byte(threshold) { + ctx.offset++ + return ctx.readUint16() + } + return uint16(ctx.readUint8()) +} + +func (ctx *ParseContext) readString8() string { + length := ctx.readUint8() + str := string(ctx.data[ctx.offset : ctx.offset+int(length)]) + ctx.offset += int(length) + return str +} + +func (ctx *ParseContext) readString16() string { + length := ctx.readUint16() + str := string(ctx.data[ctx.offset : ctx.offset+int(length)]) + ctx.offset += int(length) + return str +} + +func (ctx *ParseContext) readEQ2String8() common.EQ2String8 { + size := ctx.readUint8() + data := string(ctx.data[ctx.offset : ctx.offset+int(size)]) + ctx.offset += int(size) + return common.EQ2String8{ + Size: size, + Data: data, + } +} + +func (ctx *ParseContext) readEQ2String16() common.EQ2String16 { + size := ctx.readUint16() + data := string(ctx.data[ctx.offset : ctx.offset+int(size)]) + ctx.offset += int(size) + return common.EQ2String16{ + Size: size, + Data: data, + } +} + +func (ctx *ParseContext) readEQ2String32() common.EQ2String32 { + size := ctx.readUint32() + data := string(ctx.data[ctx.offset : ctx.offset+int(size)]) + ctx.offset += int(size) + return common.EQ2String32{ + Size: size, + Data: data, + } +} + +func (ctx *ParseContext) readEQ2Color() common.EQ2Color { + return common.EQ2Color{ + Red: ctx.readUint8(), + Green: ctx.readUint8(), + Blue: ctx.readUint8(), + } +} + +func (ctx *ParseContext) readEQ2Equipment() common.EQ2EquipmentItem { + return common.EQ2EquipmentItem{ + Type: ctx.readUint16(), + Color: ctx.readEQ2Color(), + Highlight: ctx.readEQ2Color(), + } +} + +func (ctx *ParseContext) readBytes(count int) []byte { + val := make([]byte, count) + copy(val, ctx.data[ctx.offset:ctx.offset+count]) + ctx.offset += count + return val +} + +func (ctx *ParseContext) readFloat32() float32 { + val := ctx.readUint32() + return *(*float32)(unsafe.Pointer(&val)) +} + +func (ctx *ParseContext) setVar(name string, value any) { + ctx.vars[name] = value +} + +func (ctx *ParseContext) pushArrayIndex(index int) { + ctx.arrayStack = append(ctx.arrayStack, index) +} + +func (ctx *ParseContext) popArrayIndex() { + if len(ctx.arrayStack) > 0 { + ctx.arrayStack = ctx.arrayStack[:len(ctx.arrayStack)-1] + } +} diff --git a/internal/packets/parser/parser.go b/internal/packets/parser/parser.go new file mode 100644 index 0000000..c3088c1 --- /dev/null +++ b/internal/packets/parser/parser.go @@ -0,0 +1,84 @@ +package parser + +import "eq2emu/internal/common" + +func (def *PacketDef) Parse(data []byte, version uint32, flags uint64) (map[string]any, error) { + ctx := NewContext(data, version, flags) + return def.parseStruct(ctx) +} + +func (def *PacketDef) parseStruct(ctx *ParseContext) (map[string]any, error) { + result := make(map[string]any) + order := def.getVersionOrder(ctx.version) + + for _, fieldName := range order { + field := def.Fields[fieldName] + + if !ctx.checkCondition(field.Condition) { + continue + } + + fieldType := field.Type + if field.Type2 != 0 && ctx.checkCondition(field.Type2Cond) { + fieldType = field.Type2 + } + + value := def.parseField(ctx, field, fieldType, fieldName) + result[fieldName] = value + ctx.setVar(fieldName, value) + } + + return result, nil +} + +func (def *PacketDef) parseField(ctx *ParseContext, field FieldDesc, fieldType common.EQ2DataType, fieldName string) any { + switch fieldType { + case common.TypeInt8, common.TypeSInt8: + if field.Oversized > 0 { + return ctx.readOversizedUint8(field.Oversized) + } + return ctx.readUint8() + case common.TypeInt16, common.TypeSInt16: + if field.Oversized > 0 { + return ctx.readOversizedUint16(field.Oversized) + } + return ctx.readUint16() + case common.TypeInt32, common.TypeSInt32: + return ctx.readUint32() + case common.TypeString8: + return ctx.readEQ2String8() + case common.TypeString16: + return ctx.readEQ2String16() + case common.TypeString32: + return ctx.readEQ2String32() + case common.TypeChar: + return ctx.readBytes(field.Length) + case common.TypeFloat: + return ctx.readFloat32() + case common.TypeColor: + return ctx.readEQ2Color() + case common.TypeEquipment: + return ctx.readEQ2Equipment() + case common.TypeArray: + size := ctx.getArraySize(field.Condition) + result := make([]map[string]any, size) + for i := 0; i < size; i++ { + ctx.pushArrayIndex(i) + item, _ := field.SubDef.parseStruct(ctx) + result[i] = item + ctx.popArrayIndex() + } + return result + } + return nil +} + +func (def *PacketDef) getVersionOrder(version uint32) []string { + var bestVersion uint32 + for v := range def.Orders { + if v <= version && v > bestVersion { + bestVersion = v + } + } + return def.Orders[bestVersion] +} diff --git a/internal/packets/parser/types.go b/internal/packets/parser/types.go new file mode 100644 index 0000000..fd5d6ab --- /dev/null +++ b/internal/packets/parser/types.go @@ -0,0 +1,18 @@ +package parser + +import "eq2emu/internal/common" + +type PacketDef struct { + Fields map[string]FieldDesc + Orders map[uint32][]string +} + +type FieldDesc struct { + Type common.EQ2DataType + Condition string + Length int + SubDef *PacketDef + Type2 common.EQ2DataType + Type2Cond string + Oversized int +}