diff --git a/internal/packets/PARSER.md b/internal/packets/PARSER.md index e904cfc..4ff4614 100644 --- a/internal/packets/PARSER.md +++ b/internal/packets/PARSER.md @@ -1,273 +1,132 @@ -# Packet Parser +# Packet Definition Parser -Write packet definitions as data instead of code! This parser handles complex binary protocols with versioning, conditional fields, and nested structures. +Fast XML-like parser for binary packet structures with versioning and conditional fields. -## Quick Start +## Basic Syntax -```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) +```xml + + + + + + + ``` ## Field Types -### Integer Types +| Type | Size | Description | +|------|------|-------------| +| `i8`, `i16`, `i32`, `i64` | 1-8 bytes | Unsigned integers | +| `si8`, `si16`, `si32`, `si64` | 1-8 bytes | Signed integers | +| `f32`, `f64` | 4-8 bytes | Floating point | +| `str8`, `str16`, `str32` | Variable | Length-prefixed strings | +| `char` | Fixed | Fixed-size byte array | +| `color` | 3 bytes | RGB color (r,g,b) | +| `equip` | 8 bytes | Equipment item | +| `array` | Variable | Array of substructures | -| 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}` | +## Multiple Field Names -### 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 +```xml + + ``` -### Flag Checks -```go -"flag:loot" // Parse if loot flag is set -"!flag:loot" // Parse if loot flag is not set +## Conditional Fields + +```xml + + + ``` -### Version Checks -```go -"version>=1188" // Parse if version 1188 or higher -"version<562" // Parse if version below 562 +### Condition Types +- `flag:name` - Flag is set +- `!flag:name` - Flag not set +- `var:name` - Variable exists +- `!var:name` - Variable doesn't exist +- `field>=value` - Comparison operators: `>=`, `<=`, `>`, `<`, `==`, `!=` +- `field&0x01` - Bitwise AND + +## Arrays + +```xml + + + + + + + ``` -### Comparisons -```go -"stat_type!=6" // Parse if stat_type is not 6 -"level>=10" // Parse if level is 10 or higher +## Reusable Substructs + +```xml + + + + + + + + + + + ``` -### String Length -```go -"name!>5" // Parse if name is longer than 5 characters -"description!<=100" // Parse if description is 100 chars or less +## Field Attributes + +- `name="field1,field2"` - Field name(s) +- `if="condition"` - Conditional parsing +- `size="10"` - Fixed array size for `char` type +- `count="var:name"` - Array size variable +- `substruct="Name"` - Reference to substruct + +## Multiple Versions + +```xml + + + + + + + + + + + ``` -### Bitwise Operations -```go -"header_flags&0x01" // Parse if bit 1 is set in header_flags +## Comments + +```xml + + + + + + ``` -### 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: +## Usage ```go -Orders: map[uint32][]string{ - 373: {"basic_field", "name"}, - 1188: {"basic_field", "new_field", "name"}, - 2000: {"basic_field", "new_field", "another_field", "name"}, -} -``` +import "eq2emu/internal/parser" -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) +// Parse PML content +packets, err := parser.Parse(pmlContent) 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) -} +// Get packet definition +packet := packets["PacketName"] -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 +// Parse binary data +result, err := packet.Parse(data, version, flags) +``` \ No newline at end of file diff --git a/internal/packets/parser/conditions.go b/internal/packets/parser/conditions.go deleted file mode 100644 index 8ac7001..0000000 --- a/internal/packets/parser/conditions.go +++ /dev/null @@ -1,239 +0,0 @@ -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 deleted file mode 100644 index a1c2fa0..0000000 --- a/internal/packets/parser/context.go +++ /dev/null @@ -1,145 +0,0 @@ -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/lexer.go b/internal/packets/parser/lexer.go new file mode 100644 index 0000000..a22ab01 --- /dev/null +++ b/internal/packets/parser/lexer.go @@ -0,0 +1,354 @@ +package parser + +import ( + "fmt" + "sync" + "unicode" +) + +// Object pools for heavy reuse +var tokenPool = sync.Pool{ + New: func() any { + return &Token{ + Attributes: make(map[string]string, 8), + TagStart: -1, + TagEnd: -1, + TextStart: -1, + TextEnd: -1, + } + }, +} + +// More efficient lexer using byte operations and minimal allocations +type Lexer struct { + input []byte // Use byte slice for faster operations + pos int + line int + col int +} + +// Creates a new lexer +func NewLexer(input string) *Lexer { + return &Lexer{ + input: []byte(input), + line: 1, + col: 1, + } +} + +// Returns next byte without advancing +func (l *Lexer) peek() byte { + if l.pos >= len(l.input) { + return 0 + } + return l.input[l.pos] +} + +// Advances and returns next byte +func (l *Lexer) next() byte { + if l.pos >= len(l.input) { + return 0 + } + ch := l.input[l.pos] + l.pos++ + if ch == '\n' { + l.line++ + l.col = 1 + } else { + l.col++ + } + return ch +} + +// Checks if a tag should be treated as self-closing (using byte comparison) +func (l *Lexer) isSelfClosingTag(start, end int) bool { + length := end - start + if length < 2 || length > 5 { + return false + } + + // Fast byte-based comparison + switch length { + case 2: + return (l.input[start] == 'i' && l.input[start+1] == '8') || + (l.input[start] == 'f' && l.input[start+1] == '2') + case 3: + return (l.input[start] == 'i' && l.input[start+1] == '1' && l.input[start+2] == '6') || + (l.input[start] == 'i' && l.input[start+1] == '3' && l.input[start+2] == '2') || + (l.input[start] == 'i' && l.input[start+1] == '6' && l.input[start+2] == '4') || + (l.input[start] == 's' && l.input[start+1] == 'i' && l.input[start+2] == '8') || + (l.input[start] == 'f' && l.input[start+1] == '3' && l.input[start+2] == '2') || + (l.input[start] == 'f' && l.input[start+1] == '6' && l.input[start+2] == '4') + case 4: + return (l.input[start] == 's' && l.input[start+1] == 'i' && + l.input[start+2] == '1' && l.input[start+3] == '6') || + (l.input[start] == 's' && l.input[start+1] == 'i' && + l.input[start+2] == '3' && l.input[start+3] == '2') || + (l.input[start] == 's' && l.input[start+1] == 'i' && + l.input[start+2] == '6' && l.input[start+3] == '4') || + (l.input[start] == 'c' && l.input[start+1] == 'h' && + l.input[start+2] == 'a' && l.input[start+3] == 'r') || + (l.input[start] == 's' && l.input[start+1] == 't' && + l.input[start+2] == 'r' && l.input[start+3] == '8') + case 5: + return (l.input[start] == 'c' && l.input[start+1] == 'o' && + l.input[start+2] == 'l' && l.input[start+3] == 'o' && + l.input[start+4] == 'r') || + (l.input[start] == 'e' && l.input[start+1] == 'q' && + l.input[start+2] == 'u' && l.input[start+3] == 'i' && + l.input[start+4] == 'p') || + (l.input[start] == 's' && l.input[start+1] == 't' && + l.input[start+2] == 'r' && l.input[start+3] == '1' && + l.input[start+4] == '6') || + (l.input[start] == 's' && l.input[start+1] == 't' && + l.input[start+2] == 'r' && l.input[start+3] == '3' && + l.input[start+4] == '2') + } + return false +} + +// Skips whitespace using byte operations +func (l *Lexer) skipWhitespace() { + for l.pos < len(l.input) { + ch := l.input[l.pos] + if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' { + if ch == '\n' { + l.line++ + l.col = 1 + } else { + l.col++ + } + l.pos++ + } else { + break + } + } +} + +// Optimized attribute parsing with minimal allocations - FIXED BUG +func (l *Lexer) parseAttributes(attrs map[string]string) error { + // Clear existing attributes without deallocating + for k := range attrs { + delete(attrs, k) + } + + for { + l.skipWhitespace() + if l.pos >= len(l.input) || l.peek() == '>' || + (l.peek() == '/' && l.pos+1 < len(l.input) && l.input[l.pos+1] == '>') { + break + } + + // Read attribute name using byte operations + nameStart := l.pos + for l.pos < len(l.input) { + ch := l.input[l.pos] + if ch == '=' || ch == ' ' || ch == '\t' || ch == '\n' || ch == '>' { + break + } + l.pos++ + if ch != '\n' { + l.col++ + } + } + + nameEnd := l.pos // FIXED: Store end of name here + + if nameStart == nameEnd { + break + } + + l.skipWhitespace() + if l.peek() != '=' { + return fmt.Errorf("expected '=' after attribute name") + } + l.next() // skip '=' + l.skipWhitespace() + + // Read attribute value + quote := l.peek() + if quote != '"' && quote != '\'' { + return fmt.Errorf("attribute value must be quoted") + } + l.next() // skip opening quote + + valueStart := l.pos + for l.pos < len(l.input) && l.input[l.pos] != quote { + if l.input[l.pos] == '\n' { + l.line++ + l.col = 1 + } else { + l.col++ + } + l.pos++ + } + + if l.pos >= len(l.input) { + return fmt.Errorf("unclosed attribute value") + } + + // FIXED: Correct name and value extraction + name := string(l.input[nameStart:nameEnd]) + value := string(l.input[valueStart:l.pos]) + attrs[name] = value + + l.next() // skip closing quote + } + + return nil +} + +// Optimized token generation with pooling +func (l *Lexer) NextToken() *Token { + token := tokenPool.Get().(*Token) + token.Type = TokenError + token.TagStart = -1 + token.TagEnd = -1 + token.TextStart = -1 + token.TextEnd = -1 + token.Line = l.line + token.Col = l.col + + l.skipWhitespace() + if l.pos >= len(l.input) { + token.Type = TokenEOF + return token + } + + if l.peek() == '<' { + l.next() // skip '<' + + // Check for comment using byte comparison + if l.pos+2 < len(l.input) && + l.input[l.pos] == '!' && l.input[l.pos+1] == '-' && l.input[l.pos+2] == '-' { + l.pos += 3 + start := l.pos + // Find end of comment efficiently + for l.pos+2 < len(l.input) { + if l.input[l.pos] == '-' && l.input[l.pos+1] == '-' && l.input[l.pos+2] == '>' { + token.Type = TokenComment + token.TextStart = start + token.TextEnd = l.pos + l.pos += 3 + return token + } + if l.input[l.pos] == '\n' { + l.line++ + l.col = 1 + } else { + l.col++ + } + l.pos++ + } + token.Type = TokenError + return token + } + + // Check for closing tag + if l.peek() == '/' { + l.next() // skip '/' + start := l.pos + for l.pos < len(l.input) && l.input[l.pos] != '>' { + l.pos++ + l.col++ + } + if l.pos >= len(l.input) { + token.Type = TokenError + return token + } + token.Type = TokenCloseTag + token.TagStart = start + token.TagEnd = l.pos + l.next() // skip '>' + return token + } + + // Opening or self-closing tag + start := l.pos + for l.pos < len(l.input) { + ch := l.input[l.pos] + if ch == '>' || ch == '/' || ch == ' ' || ch == '\t' || ch == '\n' { + break + } + l.pos++ + l.col++ + } + + if start == l.pos { + token.Type = TokenError + return token + } + + token.TagStart = start + token.TagEnd = l.pos + + if err := l.parseAttributes(token.Attributes); err != nil { + token.Type = TokenError + return token + } + + l.skipWhitespace() + if l.pos >= len(l.input) { + token.Type = TokenError + return token + } + + if l.peek() == '/' && l.pos+1 < len(l.input) && l.input[l.pos+1] == '>' { + token.Type = TokenSelfCloseTag + l.pos += 2 + } else { + // Check if this is a self-closing field type + if l.isSelfClosingTag(token.TagStart, token.TagEnd) { + token.Type = TokenSelfCloseTag + } else { + token.Type = TokenOpenTag + } + if l.peek() == '>' { + l.next() + } else { + token.Type = TokenError + return token + } + } + + return token + } + + // Text content - find range without copying + start := l.pos + for l.pos < len(l.input) && l.input[l.pos] != '<' { + if l.input[l.pos] == '\n' { + l.line++ + l.col = 1 + } else { + l.col++ + } + l.pos++ + } + + // Trim whitespace from range + for start < l.pos && unicode.IsSpace(rune(l.input[start])) { + start++ + } + end := l.pos + for end > start && unicode.IsSpace(rune(l.input[end-1])) { + end-- + } + + if start < end { + token.Type = TokenText + token.TextStart = start + token.TextEnd = end + return token + } + + // Skip empty text, get next token + return l.NextToken() +} + +// Returns token to pool +func (l *Lexer) ReleaseToken(token *Token) { + if token != nil { + tokenPool.Put(token) + } +} diff --git a/internal/packets/parser/parser.go b/internal/packets/parser/parser.go index c3088c1..ddfd145 100644 --- a/internal/packets/parser/parser.go +++ b/internal/packets/parser/parser.go @@ -1,84 +1,666 @@ package parser -import "eq2emu/internal/common" +import ( + "eq2emu/internal/common" + "fmt" + "strconv" + "strings" + "sync" +) -func (def *PacketDef) Parse(data []byte, version uint32, flags uint64) (map[string]any, error) { - ctx := NewContext(data, version, flags) - return def.parseStruct(ctx) +// Object pools for reducing allocations +var ( + fieldOrderPool = sync.Pool{ + New: func() any { + slice := make([]string, 0, 32) + return &slice + }, + } + conditionBuilder = sync.Pool{ + New: func() any { + buf := make([]byte, 0, 128) + return &buf + }, + } + stringBuilderPool = sync.Pool{ + New: func() any { + return &stringBuilder{buf: make([]byte, 0, 64)} + }, + } +) + +// String builder for efficient concatenation +type stringBuilder struct { + buf []byte } -func (def *PacketDef) parseStruct(ctx *ParseContext) (map[string]any, error) { - result := make(map[string]any) - order := def.getVersionOrder(ctx.version) +func (sb *stringBuilder) reset() { + sb.buf = sb.buf[:0] +} - for _, fieldName := range order { - field := def.Fields[fieldName] +func (sb *stringBuilder) writeString(s string) { + sb.buf = append(sb.buf, s...) +} - if !ctx.checkCondition(field.Condition) { - continue - } +func (sb *stringBuilder) string() string { + return string(sb.buf) +} - fieldType := field.Type - if field.Type2 != 0 && ctx.checkCondition(field.Type2Cond) { - fieldType = field.Type2 - } +// Parses PML into PacketDef structures +type Parser struct { + lexer *Lexer + current *Token + input string + substructs map[string]*PacketDef + tagStack []string + fieldNames []string +} - value := def.parseField(ctx, field, fieldType, fieldName) - result[fieldName] = value - ctx.setVar(fieldName, value) +// Type mapping for efficient lookup +var typeMap = map[string]common.EQ2DataType{ + "i8": common.TypeInt8, + "i16": common.TypeInt16, + "i32": common.TypeInt32, + "i64": common.TypeInt64, + "si8": common.TypeSInt8, + "si16": common.TypeSInt16, + "si32": common.TypeSInt32, + "si64": common.TypeSInt64, + "f32": common.TypeFloat, + "f64": common.TypeDouble, + "str8": common.TypeString8, + "str16": common.TypeString16, + "str32": common.TypeString32, + "char": common.TypeChar, + "color": common.TypeColor, + "equip": common.TypeEquipment, + "array": common.TypeArray, +} + +// Creates a new parser +func NewParser(input string) *Parser { + parser := &Parser{ + lexer: NewLexer(input), + input: input, + substructs: make(map[string]*PacketDef), + tagStack: make([]string, 0, 16), + fieldNames: make([]string, 0, 8), + } + parser.advance() + return parser +} + +// Moves to next token with cleanup +func (p *Parser) advance() { + if p.current != nil { + p.lexer.ReleaseToken(p.current) + } + p.current = p.lexer.NextToken() + + // Skip comments + for p.current.Type == TokenComment { + p.lexer.ReleaseToken(p.current) + p.current = p.lexer.NextToken() + } +} + +// Cleanup resources +func (p *Parser) cleanup() { + if p.current != nil { + p.lexer.ReleaseToken(p.current) + p.current = nil + } +} + +// Pushes tag onto stack for validation +func (p *Parser) pushTag(tag string) { + p.tagStack = append(p.tagStack, tag) +} + +// Pops and validates closing tag +func (p *Parser) popTag(expectedTag string) error { + if len(p.tagStack) == 0 { + return fmt.Errorf("unexpected closing tag '%s' at line %d", expectedTag, p.current.Line) } - return result, nil + lastTag := p.tagStack[len(p.tagStack)-1] + if lastTag != expectedTag { + return fmt.Errorf("mismatched closing tag: expected '%s', got '%s' at line %d", lastTag, expectedTag, p.current.Line) + } + + p.tagStack = p.tagStack[:len(p.tagStack)-1] + return 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 +// Checks for unclosed tags +func (p *Parser) validateAllTagsClosed() error { + if len(p.tagStack) > 0 { + return fmt.Errorf("unclosed tag '%s'", p.tagStack[len(p.tagStack)-1]) } return nil } -func (def *PacketDef) getVersionOrder(version uint32) []string { - var bestVersion uint32 - for v := range def.Orders { - if v <= version && v > bestVersion { - bestVersion = v +// Fast type lookup using first character +func getDataType(tag string) (common.EQ2DataType, bool) { + if len(tag) == 0 { + return 0, false + } + + // Fast path for common types + switch tag[0] { + case 'i': + switch tag { + case "i8": + return common.TypeInt8, true + case "i16": + return common.TypeInt16, true + case "i32": + return common.TypeInt32, true + case "i64": + return common.TypeInt64, true + } + case 's': + switch tag { + case "si8": + return common.TypeSInt8, true + case "si16": + return common.TypeSInt16, true + case "si32": + return common.TypeSInt32, true + case "si64": + return common.TypeSInt64, true + case "str8": + return common.TypeString8, true + case "str16": + return common.TypeString16, true + case "str32": + return common.TypeString32, true + } + case 'f': + switch tag { + case "f32": + return common.TypeFloat, true + case "f64": + return common.TypeDouble, true + } + case 'c': + switch tag { + case "char": + return common.TypeChar, true + case "color": + return common.TypeColor, true + } + case 'e': + if tag == "equip" { + return common.TypeEquipment, true + } + case 'a': + if tag == "array" { + return common.TypeArray, true } } - return def.Orders[bestVersion] + + // Fallback to map lookup + if dataType, exists := typeMap[tag]; exists { + return dataType, true + } + + return 0, false } + +// Parses field names with minimal allocations +func (p *Parser) parseFieldNames(nameAttr string) []string { + if nameAttr == "" { + return nil + } + + // Fast path for single name + if strings.IndexByte(nameAttr, ',') == -1 { + name := strings.TrimSpace(nameAttr) + if name != "" { + p.fieldNames = p.fieldNames[:0] + p.fieldNames = append(p.fieldNames, name) + return p.fieldNames + } + return nil + } + + // Parse multiple names efficiently + p.fieldNames = p.fieldNames[:0] + start := 0 + for i := 0; i < len(nameAttr); i++ { + if nameAttr[i] == ',' { + if name := strings.TrimSpace(nameAttr[start:i]); name != "" { + p.fieldNames = append(p.fieldNames, name) + } + start = i + 1 + } + } + if name := strings.TrimSpace(nameAttr[start:]); name != "" { + p.fieldNames = append(p.fieldNames, name) + } + + return p.fieldNames +} + +// Builds field name with prefix efficiently +func buildFieldName(prefix, name string) string { + if prefix == "" { + return name + } + + sb := stringBuilderPool.Get().(*stringBuilder) + sb.reset() + defer stringBuilderPool.Put(sb) + + sb.writeString(prefix) + sb.writeString(name) + return sb.string() +} + +// Combines conditions with AND logic +func combineConditions(cond1, cond2 string) string { + if cond1 == "" { + return cond2 + } + if cond2 == "" { + return cond1 + } + + bufPtr := conditionBuilder.Get().(*[]byte) + buf := *bufPtr + buf = buf[:0] + defer conditionBuilder.Put(bufPtr) + + buf = append(buf, cond1...) + buf = append(buf, '&') + buf = append(buf, cond2...) + *bufPtr = buf + + return string(buf) +} + +// Creates packet definition with estimated capacity +func NewPacketDef(estimatedFields int) *PacketDef { + return &PacketDef{ + Fields: make(map[string]FieldDesc, estimatedFields), + Orders: make(map[uint32][]string, 4), + } +} + +// Parses the entire PML document +func (p *Parser) Parse() (map[string]*PacketDef, error) { + packets := make(map[string]*PacketDef) + + for p.current.Type != TokenEOF { + if p.current.Type == TokenError { + return nil, fmt.Errorf("parse error at line %d, col %d", p.current.Line, p.current.Col) + } + + if p.current.Type == TokenOpenTag || p.current.Type == TokenSelfCloseTag { + switch p.current.Tag(p.input) { + case "packet": + name := p.current.Attributes["name"] + packet, err := p.parsePacket() + if err != nil { + return nil, err + } + if name != "" { + packets[name] = packet + } + case "substruct": + name := p.current.Attributes["name"] + substruct, err := p.parseSubstruct() + if err != nil { + return nil, err + } + if name != "" { + p.substructs[name] = substruct + } + default: + p.advance() + } + } else { + p.advance() + } + } + + if err := p.validateAllTagsClosed(); err != nil { + return nil, err + } + + return packets, nil +} + +// Parses a packet element with version children +func (p *Parser) parsePacket() (*PacketDef, error) { + packetDef := NewPacketDef(16) + + if p.current.Type == TokenSelfCloseTag { + p.advance() + return packetDef, nil + } + + p.pushTag("packet") + p.advance() + + for p.current.Type != TokenEOF && !(p.current.Type == TokenCloseTag && p.current.Tag(p.input) == "packet") { + if p.current.Type == TokenOpenTag && p.current.Tag(p.input) == "version" { + err := p.parseVersion(packetDef) + if err != nil { + return nil, err + } + } else { + p.advance() + } + } + + if p.current.Type == TokenCloseTag && p.current.Tag(p.input) == "packet" { + if err := p.popTag("packet"); err != nil { + return nil, err + } + p.advance() + } else { + return nil, fmt.Errorf("expected closing tag for packet at line %d", p.current.Line) + } + + return packetDef, nil +} + +// Parses a version element +func (p *Parser) parseVersion(packetDef *PacketDef) error { + attrs := p.current.Attributes + version := uint32(1) + if v := attrs["number"]; v != "" { + if parsed, err := strconv.ParseUint(v, 10, 32); err == nil { + version = uint32(parsed) + } + } + + fieldOrder := fieldOrderPool.Get().(*[]string) + *fieldOrder = (*fieldOrder)[:0] + defer fieldOrderPool.Put(fieldOrder) + + if p.current.Type == TokenSelfCloseTag { + p.advance() + packetDef.Orders[version] = make([]string, len(*fieldOrder)) + copy(packetDef.Orders[version], *fieldOrder) + return nil + } + + p.pushTag("version") + p.advance() + + err := p.parseElements(packetDef, fieldOrder, "") + if err != nil { + return err + } + + if p.current.Type == TokenCloseTag && p.current.Tag(p.input) == "version" { + if err := p.popTag("version"); err != nil { + return err + } + p.advance() + } else { + return fmt.Errorf("expected closing tag for version at line %d", p.current.Line) + } + + packetDef.Orders[version] = make([]string, len(*fieldOrder)) + copy(packetDef.Orders[version], *fieldOrder) + return nil +} + +// Parses a substruct element +func (p *Parser) parseSubstruct() (*PacketDef, error) { + packetDef := NewPacketDef(16) + + fieldOrder := fieldOrderPool.Get().(*[]string) + *fieldOrder = (*fieldOrder)[:0] + defer fieldOrderPool.Put(fieldOrder) + + if p.current.Type == TokenSelfCloseTag { + p.advance() + packetDef.Orders[1] = make([]string, len(*fieldOrder)) + copy(packetDef.Orders[1], *fieldOrder) + return packetDef, nil + } + + p.pushTag("substruct") + p.advance() + + err := p.parseElements(packetDef, fieldOrder, "") + if err != nil { + return nil, err + } + + if p.current.Type == TokenCloseTag && p.current.Tag(p.input) == "substruct" { + if err := p.popTag("substruct"); err != nil { + return nil, err + } + p.advance() + } else { + return nil, fmt.Errorf("expected closing tag for substruct") + } + + packetDef.Orders[1] = make([]string, len(*fieldOrder)) + copy(packetDef.Orders[1], *fieldOrder) + return packetDef, nil +} + +// Processes child elements +func (p *Parser) parseElements(packetDef *PacketDef, fieldOrder *[]string, prefix string) error { + for p.current.Type != TokenEOF && !(p.current.Type == TokenCloseTag) { + switch p.current.Type { + case TokenOpenTag, TokenSelfCloseTag: + switch p.current.Tag(p.input) { + case "group": + err := p.parseGroup(packetDef, fieldOrder, prefix) + if err != nil { + return err + } + case "array": + err := p.parseArray(packetDef, fieldOrder, prefix) + if err != nil { + return err + } + default: + err := p.parseField(packetDef, fieldOrder, prefix) + if err != nil { + return err + } + } + case TokenText: + p.advance() + default: + p.advance() + } + } + return nil +} + +// Handles group elements +func (p *Parser) parseGroup(packetDef *PacketDef, fieldOrder *[]string, prefix string) error { + attrs := p.current.Attributes + groupPrefix := prefix + if name := attrs["name"]; name != "" { + if prefix == "" { + groupPrefix = name + "_" + } else { + groupPrefix = prefix + name + "_" + } + } + + if p.current.Type == TokenSelfCloseTag { + p.advance() + return nil + } + + p.pushTag("group") + p.advance() + + err := p.parseElements(packetDef, fieldOrder, groupPrefix) + if err != nil { + return err + } + + if p.current.Type == TokenCloseTag && p.current.Tag(p.input) == "group" { + if err := p.popTag("group"); err != nil { + return err + } + p.advance() + } else { + return fmt.Errorf("expected closing tag for group at line %d", p.current.Line) + } + + return nil +} + +// Handles array elements +func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix string) error { + attrs := p.current.Attributes + + var arrayName string + if prefix == "" { + arrayName = attrs["name"] + } else { + arrayName = buildFieldName(prefix, attrs["name"]) + } + + fieldDesc := FieldDesc{ + Type: common.TypeArray, + Condition: attrs["count"], + } + + if ifCond := attrs["if"]; ifCond != "" { + fieldDesc.Condition = combineConditions(fieldDesc.Condition, ifCond) + } + + // Handle substruct reference + if substruct := attrs["substruct"]; substruct != "" { + if subDef, exists := p.substructs[substruct]; exists { + fieldDesc.SubDef = subDef + } + } + + if p.current.Type == TokenSelfCloseTag { + p.advance() + packetDef.Fields[arrayName] = fieldDesc + *fieldOrder = append(*fieldOrder, arrayName) + return nil + } + + p.pushTag("array") + p.advance() + + // If we have a substruct reference and no inline content + if fieldDesc.SubDef != nil && (p.current.Type == TokenCloseTag || + (p.current.Type == TokenOpenTag && p.current.Tag(p.input) != "substruct")) { + + if p.current.Type == TokenCloseTag && p.current.Tag(p.input) == "array" { + if err := p.popTag("array"); err != nil { + return err + } + p.advance() + } else { + p.tagStack = p.tagStack[:len(p.tagStack)-1] + } + packetDef.Fields[arrayName] = fieldDesc + *fieldOrder = append(*fieldOrder, arrayName) + return nil + } + + // Handle inline substruct + if fieldDesc.SubDef == nil && p.current.Type == TokenOpenTag && p.current.Tag(p.input) == "substruct" { + subDef := NewPacketDef(16) + + p.pushTag("substruct") + p.advance() + + subOrder := fieldOrderPool.Get().(*[]string) + *subOrder = (*subOrder)[:0] + defer fieldOrderPool.Put(subOrder) + + err := p.parseElements(subDef, subOrder, "") + if err != nil { + return err + } + + if p.current.Type == TokenCloseTag && p.current.Tag(p.input) == "substruct" { + if err := p.popTag("substruct"); err != nil { + return err + } + p.advance() + } else { + return fmt.Errorf("expected closing tag for substruct at line %d", p.current.Line) + } + + subDef.Orders[1] = make([]string, len(*subOrder)) + copy(subDef.Orders[1], *subOrder) + fieldDesc.SubDef = subDef + } + + if p.current.Type == TokenCloseTag && p.current.Tag(p.input) == "array" { + if err := p.popTag("array"); err != nil { + return err + } + p.advance() + } else { + return fmt.Errorf("expected closing tag for array at line %d", p.current.Line) + } + + packetDef.Fields[arrayName] = fieldDesc + *fieldOrder = append(*fieldOrder, arrayName) + return nil +} + +// Handles field elements +func (p *Parser) parseField(packetDef *PacketDef, fieldOrder *[]string, prefix string) error { + tagName := p.current.Tag(p.input) + attrs := p.current.Attributes + + dataType, exists := getDataType(tagName) + if !exists { + p.advance() + return nil + } + + nameAttr := attrs["name"] + if nameAttr == "" { + p.advance() + return nil + } + + names := p.parseFieldNames(nameAttr) + for _, name := range names { + var fullName string + if prefix == "" { + fullName = name + } else { + fullName = buildFieldName(prefix, name) + } + + fieldDesc := FieldDesc{ + Type: dataType, + Condition: attrs["if"], + } + + if size := attrs["size"]; size != "" { + if s, err := strconv.Atoi(size); err == nil { + fieldDesc.Length = s + } + } + + packetDef.Fields[fullName] = fieldDesc + *fieldOrder = append(*fieldOrder, fullName) + } + + p.advance() + return nil +} + +// Parses PML content and returns PacketDef map +func Parse(content string) (map[string]*PacketDef, error) { + parser := NewParser(content) + defer parser.cleanup() + return parser.Parse() +} \ No newline at end of file diff --git a/internal/packets/parser/parser_test.go b/internal/packets/parser/parser_test.go new file mode 100644 index 0000000..5a08023 --- /dev/null +++ b/internal/packets/parser/parser_test.go @@ -0,0 +1,396 @@ +package parser + +import ( + "eq2emu/internal/common" + "testing" +) + +func TestBasicParsing(t *testing.T) { + pml := ` + + + + + + ` + + packets, err := Parse(pml) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + packet := packets["Test"] + if packet == nil { + t.Fatal("Test packet not found") + } + + // Check fields + if len(packet.Fields) != 3 { + t.Errorf("Expected 3 fields, got %d", len(packet.Fields)) + } + + if packet.Fields["player_id"].Type != common.TypeInt32 { + t.Error("player_id should be TypeInt32") + } + + if packet.Fields["player_name"].Type != common.TypeString16 { + t.Error("player_name should be TypeString16") + } + + if packet.Fields["skin_color"].Type != common.TypeColor { + t.Error("skin_color should be TypeColor") + } + + // Check order + order := packet.Orders[1] + expected := []string{"player_id", "player_name", "skin_color"} + if !equalSlices(order, expected) { + t.Errorf("Expected order %v, got %v", expected, order) + } +} + +func TestMultipleVersions(t *testing.T) { + pml := ` + + + + + + + + + + ` + + packets, err := Parse(pml) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + packet := packets["MultiVersion"] + if packet == nil { + t.Fatal("MultiVersion packet not found") + } + + // Check both versions exist + if len(packet.Orders) != 2 { + t.Errorf("Expected 2 versions, got %d", len(packet.Orders)) + } + + v1Order := packet.Orders[1] + v562Order := packet.Orders[562] + + if len(v1Order) != 2 { + t.Errorf("Version 1 should have 2 fields, got %d", len(v1Order)) + } + + if len(v562Order) != 3 { + t.Errorf("Version 562 should have 3 fields, got %d", len(v562Order)) + } +} + +func TestArrayParsing(t *testing.T) { + pml := ` + + + + + + + + + + ` + + packets, err := Parse(pml) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + packet := packets["ArrayTest"] + itemsField := packet.Fields["items"] + + if itemsField.Type != common.TypeArray { + t.Error("items should be TypeArray") + } + + if itemsField.Condition != "var:item_count" { + t.Errorf("Expected condition 'var:item_count', got '%s'", itemsField.Condition) + } + + if itemsField.SubDef == nil { + t.Fatal("SubDef should not be nil") + } + + // Check substruct fields + if len(itemsField.SubDef.Fields) != 2 { + t.Errorf("Expected 2 substruct fields, got %d", len(itemsField.SubDef.Fields)) + } + + if itemsField.SubDef.Fields["item_id"].Type != common.TypeInt32 { + t.Error("item_id should be TypeInt32") + } +} + +func TestConditionalParsing(t *testing.T) { + pml := ` + + + + + + + ` + + packets, err := Parse(pml) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + packet := packets["ConditionalTest"] + + if packet.Fields["guild_name"].Condition != "flag:has_guild" { + t.Errorf("guild_name condition wrong: %s", packet.Fields["guild_name"].Condition) + } + + if packet.Fields["enhancement"].Condition != "item_type!=0" { + t.Errorf("enhancement condition wrong: %s", packet.Fields["enhancement"].Condition) + } + + if packet.Fields["aura"].Condition != "special_flags&0x01" { + t.Errorf("aura condition wrong: %s", packet.Fields["aura"].Condition) + } +} + +func TestCommaFieldNames(t *testing.T) { + pml := ` + + + + + ` + + packets, err := Parse(pml) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + packet := packets["CommaTest"] + + expectedFields := []string{"player_id", "account_id", "pos_x", "pos_y", "pos_z"} + if len(packet.Fields) != len(expectedFields) { + t.Errorf("Expected %d fields, got %d", len(expectedFields), len(packet.Fields)) + } + + for _, field := range expectedFields { + if _, exists := packet.Fields[field]; !exists { + t.Errorf("Field %s not found", field) + } + } +} + +func TestSubstructReference(t *testing.T) { + pml := ` + + + + + + + + + + ` + + parser := NewParser(pml) + packets, err := parser.Parse() + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + packet := packets["SubstructTest"] + itemsField := packet.Fields["items"] + + if itemsField.SubDef == nil { + t.Fatal("SubDef should not be nil for referenced substruct") + } + + if len(itemsField.SubDef.Fields) != 2 { + t.Errorf("Expected 2 substruct fields, got %d", len(itemsField.SubDef.Fields)) + } +} + +func TestFieldAttributes(t *testing.T) { + pml := ` + + + + + ` + + packets, err := Parse(pml) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + packet := packets["AttributeTest"] + + if packet.Fields["data_array"].Length != 10 { + t.Errorf("Expected size 10, got %d", packet.Fields["data_array"].Length) + } + + if packet.Fields["optional_text"].Condition != "var:has_text" { + t.Errorf("Expected condition 'var:has_text', got '%s'", packet.Fields["optional_text"].Condition) + } +} + +func TestComments(t *testing.T) { + pml := ` + + + + + + + ` + + packets, err := Parse(pml) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + packet := packets["CommentTest"] + if len(packet.Fields) != 2 { + t.Errorf("Comments should not affect parsing, expected 2 fields, got %d", len(packet.Fields)) + } +} + +func TestErrorHandling(t *testing.T) { + testCases := []struct { + name string + pml string + }{ + {"Unclosed tag", ""}, + {"Invalid XML", ""}, + {"Missing quotes", ""}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := Parse(tc.pml) + if err == nil { + t.Error("Expected error but got none") + } + }) + } +} + +func BenchmarkSimplePacket(b *testing.B) { + pml := ` + + + + + + + ` + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Parse(pml) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkComplexPacket(b *testing.B) { + pml := ` + + + + + + + + + + + + + + + + + + + + + + + + + ` + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Parse(pml) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkLargePacket(b *testing.B) { + // Generate a large packet definition + pmlBuilder := `` + for i := 0; i < 100; i++ { + pmlBuilder += `` + } + pmlBuilder += `` + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Parse(pmlBuilder) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkWithSubstructs(b *testing.B) { + pml := ` + + + + + + + + + + + ` + + b.ResetTimer() + for i := 0; i < b.N; i++ { + parser := NewParser(pml) + _, err := parser.Parse() + if err != nil { + b.Fatal(err) + } + } +} + +// Helper function to compare slices +func equalSlices(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} diff --git a/internal/packets/parser/structs.go b/internal/packets/parser/structs.go new file mode 100644 index 0000000..78ee506 --- /dev/null +++ b/internal/packets/parser/structs.go @@ -0,0 +1,20 @@ +package parser + +import "eq2emu/internal/common" + +// PacketDef defines a complete packet structure with versioned field ordering +type PacketDef struct { + Fields map[string]FieldDesc // Field definitions by name + Orders map[uint32][]string // Field order by version number +} + +// FieldDesc describes a single packet field +type FieldDesc struct { + Type common.EQ2DataType // Primary data type + Condition string // Conditional parsing expression + Length int // Array length or size for fixed-size fields + SubDef *PacketDef // Nested packet definition for arrays + Type2 common.EQ2DataType // Alternative data type for conditional parsing + Type2Cond string // Condition for using Type2 + Oversized int // Threshold for oversized field handling +} diff --git a/internal/packets/parser/tokens.go b/internal/packets/parser/tokens.go new file mode 100644 index 0000000..2e84608 --- /dev/null +++ b/internal/packets/parser/tokens.go @@ -0,0 +1,42 @@ +package parser + +// Token types for PML parsing +type TokenType int + +const ( + TokenError TokenType = iota + TokenOpenTag + TokenCloseTag + TokenSelfCloseTag + TokenText + TokenComment + TokenEOF +) + +// Represents a parsed token with string ranges instead of copies +type Token struct { + Type TokenType + TagStart int // Start index in input for tag name + TagEnd int // End index in input for tag name + TextStart int // Start index for text content + TextEnd int // End index for text content + Attributes map[string]string + Line int + Col int +} + +// Gets tag name from input (avoids allocation until needed) +func (t *Token) Tag(input string) string { + if t.TagStart >= 0 && t.TagEnd > t.TagStart { + return input[t.TagStart:t.TagEnd] + } + return "" +} + +// Gets text content from input +func (t *Token) Text(input string) string { + if t.TextStart >= 0 && t.TextEnd > t.TextStart { + return input[t.TextStart:t.TextEnd] + } + return "" +} diff --git a/internal/packets/parser/types.go b/internal/packets/parser/types.go deleted file mode 100644 index fd5d6ab..0000000 --- a/internal/packets/parser/types.go +++ /dev/null @@ -1,18 +0,0 @@ -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 -}