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
-}