parser v3

This commit is contained in:
Sky Johnson 2025-07-28 12:31:44 -05:00
parent ca46c5617d
commit e310437c1b
9 changed files with 1561 additions and 710 deletions

View File

@ -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 ```xml
import "your-project/internal/common" <packet name="PacketName">
<version number="1">
var MyPacketDef = &PacketDef{ <i32 name="player_id">
Fields: map[string]FieldDesc{ <str16 name="player_name">
"count": {Type: common.TypeInt16}, <color name="skin_color">
"items": {Type: common.TypeArray, Condition: "var:count", SubDef: ItemDef}, </version>
"name": {Type: common.TypeString8}, </packet>
},
Orders: map[uint32][]string{
1: {"count", "items", "name"},
},
}
// Parse it
result, err := MyPacketDef.Parse(data, version, flags)
``` ```
## Field Types ## 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 | ## Multiple Field Names
|------|-------------|------|---------|
| `common.TypeInt8` | Unsigned 8-bit integer | 1 byte | `{Type: common.TypeInt8}` |
| `common.TypeInt16` | Unsigned 16-bit integer | 2 bytes | `{Type: common.TypeInt16}` |
| `common.TypeInt32` | Unsigned 32-bit integer | 4 bytes | `{Type: common.TypeInt32}` |
| `common.TypeInt64` | Unsigned 64-bit integer | 8 bytes | `{Type: common.TypeInt64}` |
| `common.TypeSInt8` | Signed 8-bit integer | 1 byte | `{Type: common.TypeSInt8}` |
| `common.TypeSInt16` | Signed 16-bit integer | 2 bytes | `{Type: common.TypeSInt16}` |
| `common.TypeSInt32` | Signed 32-bit integer | 4 bytes | `{Type: common.TypeSInt32}` |
| `common.TypeSInt64` | Signed 64-bit integer | 8 bytes | `{Type: common.TypeSInt64}` |
### String Types ```xml
<i32 name="player_id,account_id">
| Type | Description | Returns | Example | <f32 name="pos_x,pos_y,pos_z">
|------|-------------|---------|---------|
| `common.TypeString8` | 1-byte length prefix | `EQ2String8{Size, Data}` | `{Type: common.TypeString8}` |
| `common.TypeString16` | 2-byte length prefix | `EQ2String16{Size, Data}` | `{Type: common.TypeString16}` |
| `common.TypeString32` | 4-byte length prefix | `EQ2String32{Size, Data}` | `{Type: common.TypeString32}` |
### Other Types
| Type | Description | Returns | Example |
|------|-------------|---------|---------|
| `common.TypeChar` | Fixed-size byte array | `[]byte` | `{Type: common.TypeChar, Length: 10}` |
| `common.TypeFloat` | 32-bit float | `float32` | `{Type: common.TypeFloat}` |
| `common.TypeDouble` | 64-bit float | `float64` | `{Type: common.TypeDouble}` |
| `common.TypeColor` | RGB color value | `EQ2Color{Red, Green, Blue}` | `{Type: common.TypeColor}` |
| `common.TypeEquipment` | Equipment item | `EQ2EquipmentItem{Type, Color, Highlight}` | `{Type: common.TypeEquipment}` |
| `common.TypeArray` | Array of substructures | `[]map[string]any` | `{Type: common.TypeArray, SubDef: ItemDef}` |
## Conditions
Control when fields are parsed using simple condition strings:
### Variable Checks
```go
"var:item_count" // Parse if item_count exists and is non-zero
"!var:item_count" // Parse if item_count doesn't exist or is zero
``` ```
### Flag Checks ## Conditional Fields
```go
"flag:loot" // Parse if loot flag is set ```xml
"!flag:loot" // Parse if loot flag is not set <str16 name="guild_name" if="flag:has_guild">
<i8 name="enhancement" if="item_type!=0">
<color name="aura" if="special_flags&0x01">
``` ```
### Version Checks ### Condition Types
```go - `flag:name` - Flag is set
"version>=1188" // Parse if version 1188 or higher - `!flag:name` - Flag not set
"version<562" // Parse if version below 562 - `var:name` - Variable exists
- `!var:name` - Variable doesn't exist
- `field>=value` - Comparison operators: `>=`, `<=`, `>`, `<`, `==`, `!=`
- `field&0x01` - Bitwise AND
## Arrays
```xml
<i8 name="item_count">
<array name="items" count="var:item_count">
<substruct>
<i32 name="item_id">
<str16 name="item_name">
</substruct>
</array>
``` ```
### Comparisons ## Reusable Substructs
```go
"stat_type!=6" // Parse if stat_type is not 6 ```xml
"level>=10" // Parse if level is 10 or higher <substruct name="ItemInfo">
<i32 name="item_id">
<str16 name="item_name">
</substruct>
<packet name="Inventory">
<version number="1">
<i8 name="count">
<array name="items" count="var:count" substruct="ItemInfo">
</version>
</packet>
``` ```
### String Length ## Field Attributes
```go
"name!>5" // Parse if name is longer than 5 characters - `name="field1,field2"` - Field name(s)
"description!<=100" // Parse if description is 100 chars or less - `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
<packet name="PlayerInfo">
<version number="1">
<i32 name="id">
<str16 name="name">
</version>
<version number="562">
<i32 name="id">
<str16 name="name">
<color name="skin_color">
</version>
</packet>
``` ```
### Bitwise Operations ## Comments
```go
"header_flags&0x01" // Parse if bit 1 is set in header_flags ```xml
<!-- This is a comment -->
<packet name="Test"> <!-- Inline comment -->
<version number="1">
<i32 name="id"> <!-- Field comment -->
</version>
</packet>
``` ```
### Combining Conditions ## Usage
```go
"var:count,var:size" // OR: either count or size exists
"version>=562&version<1188" // AND: version between 562-1187
```
## Version Ordering
Different versions can have different field orders:
```go ```go
Orders: map[uint32][]string{ import "eq2emu/internal/parser"
373: {"basic_field", "name"},
1188: {"basic_field", "new_field", "name"},
2000: {"basic_field", "new_field", "another_field", "name"},
}
```
The parser automatically uses the highest version ≤ your target version. // Parse PML content
packets, err := parser.Parse(pmlContent)
## Substructures
Create reusable packet definitions:
```go
var ItemDef = &PacketDef{
Fields: map[string]FieldDesc{
"item_id": {Type: common.TypeInt32},
"item_name": {Type: common.TypeString16},
"quantity": {Type: common.TypeInt8, Condition: "version>=546"},
"color": {Type: common.TypeColor, Condition: "flag:has_colors"},
},
Orders: map[uint32][]string{
1: {"item_id", "item_name", "quantity", "color"},
},
}
var InventoryDef = &PacketDef{
Fields: map[string]FieldDesc{
"item_count": {Type: common.TypeInt8},
"items": {Type: common.TypeArray, Condition: "var:item_count", SubDef: ItemDef},
},
Orders: map[uint32][]string{
1: {"item_count", "items"},
},
}
```
## Advanced Features
### Type Switching
Use different types based on conditions:
```go
{
Type: common.TypeInt32,
Type2: common.TypeFloat,
Type2Cond: "stat_type!=6", // Use float if stat_type is not 6
}
```
### Oversized Fields
Handle fields that can exceed normal size limits:
```go
{
Type: common.TypeInt16,
Oversized: 127, // Switch to 16-bit if first byte is 127
}
```
### Array Index Substitution
Reference array indices in conditions:
```go
"var:item_type_%i" // Substitutes %i with current array index
```
## Working with Structured Types
### String Types
String types return structured objects with size and data:
```go
// TypeString8 returns: EQ2String8{Size: uint8, Data: string}
// TypeString16 returns: EQ2String16{Size: uint16, Data: string}
// TypeString32 returns: EQ2String32{Size: uint32, Data: string}
result, _ := packet.Parse(data, version, flags)
if nameField, ok := result["player_name"].(common.EQ2String16); ok {
fmt.Printf("Name: %s (length: %d)\n", nameField.Data, nameField.Size)
}
```
### Color Types
```go
// TypeColor returns: EQ2Color{Red: uint8, Green: uint8, Blue: uint8}
if colorField, ok := result["shirt_color"].(common.EQ2Color); ok {
fmt.Printf("RGB: %d,%d,%d\n", colorField.Red, colorField.Green, colorField.Blue)
}
```
### Equipment Types
```go
// TypeEquipment returns: EQ2EquipmentItem{Type: uint16, Color: EQ2Color, Highlight: EQ2Color}
if equipField, ok := result["helmet"].(common.EQ2EquipmentItem); ok {
fmt.Printf("Equipment Type: %d\n", equipField.Type)
fmt.Printf("Color: RGB(%d,%d,%d)\n",
equipField.Color.Red, equipField.Color.Green, equipField.Color.Blue)
}
```
## Complete Example
```go
import "your-project/internal/common"
var QuestPacketDef = &PacketDef{
Fields: map[string]FieldDesc{
"quest_id": {Type: common.TypeInt32},
"has_rewards": {Type: common.TypeInt8},
"reward_count": {Type: common.TypeInt8, Condition: "var:has_rewards"},
"rewards": {Type: common.TypeArray, Condition: "var:reward_count", SubDef: RewardDef},
"is_complete": {Type: common.TypeInt8},
"complete_text": {Type: common.TypeString16, Condition: "var:is_complete"},
"quest_color": {Type: common.TypeColor, Condition: "version>=1200"},
"version_field": {Type: common.TypeInt32, Condition: "version>=1200"},
},
Orders: map[uint32][]string{
1: {"quest_id", "has_rewards", "reward_count", "rewards", "is_complete", "complete_text"},
1200: {"quest_id", "has_rewards", "reward_count", "rewards", "is_complete", "complete_text", "quest_color", "version_field"},
},
}
var RewardDef = &PacketDef{
Fields: map[string]FieldDesc{
"reward_type": {Type: common.TypeInt8},
"amount": {Type: common.TypeInt32},
"bonus": {Type: common.TypeInt16, Condition: "reward_type==1"},
"item_color": {Type: common.TypeColor, Condition: "reward_type==2"},
},
Orders: map[uint32][]string{
1: {"reward_type", "amount", "bonus", "item_color"},
},
}
// Usage
result, err := QuestPacketDef.Parse(packetData, 1200, questFlags)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Access structured data // Get packet definition
if questText, ok := result["complete_text"].(common.EQ2String16); ok { packet := packets["PacketName"]
fmt.Printf("Quest completion text: %s\n", questText.Data)
}
if questColor, ok := result["quest_color"].(common.EQ2Color); ok { // Parse binary data
fmt.Printf("Quest color: #%02x%02x%02x\n", result, err := packet.Parse(data, version, flags)
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.

View File

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

View File

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

View File

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

View File

@ -1,84 +1,666 @@
package parser 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) { // Object pools for reducing allocations
ctx := NewContext(data, version, flags) var (
return def.parseStruct(ctx) 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) { func (sb *stringBuilder) reset() {
result := make(map[string]any) sb.buf = sb.buf[:0]
order := def.getVersionOrder(ctx.version) }
for _, fieldName := range order { func (sb *stringBuilder) writeString(s string) {
field := def.Fields[fieldName] sb.buf = append(sb.buf, s...)
}
if !ctx.checkCondition(field.Condition) { func (sb *stringBuilder) string() string {
continue return string(sb.buf)
} }
fieldType := field.Type // Parses PML into PacketDef structures
if field.Type2 != 0 && ctx.checkCondition(field.Type2Cond) { type Parser struct {
fieldType = field.Type2 lexer *Lexer
} current *Token
input string
substructs map[string]*PacketDef
tagStack []string
fieldNames []string
}
value := def.parseField(ctx, field, fieldType, fieldName) // Type mapping for efficient lookup
result[fieldName] = value var typeMap = map[string]common.EQ2DataType{
ctx.setVar(fieldName, value) "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 { // Checks for unclosed tags
switch fieldType { func (p *Parser) validateAllTagsClosed() error {
case common.TypeInt8, common.TypeSInt8: if len(p.tagStack) > 0 {
if field.Oversized > 0 { return fmt.Errorf("unclosed tag '%s'", p.tagStack[len(p.tagStack)-1])
return ctx.readOversizedUint8(field.Oversized)
}
return ctx.readUint8()
case common.TypeInt16, common.TypeSInt16:
if field.Oversized > 0 {
return ctx.readOversizedUint16(field.Oversized)
}
return ctx.readUint16()
case common.TypeInt32, common.TypeSInt32:
return ctx.readUint32()
case common.TypeString8:
return ctx.readEQ2String8()
case common.TypeString16:
return ctx.readEQ2String16()
case common.TypeString32:
return ctx.readEQ2String32()
case common.TypeChar:
return ctx.readBytes(field.Length)
case common.TypeFloat:
return ctx.readFloat32()
case common.TypeColor:
return ctx.readEQ2Color()
case common.TypeEquipment:
return ctx.readEQ2Equipment()
case common.TypeArray:
size := ctx.getArraySize(field.Condition)
result := make([]map[string]any, size)
for i := 0; i < size; i++ {
ctx.pushArrayIndex(i)
item, _ := field.SubDef.parseStruct(ctx)
result[i] = item
ctx.popArrayIndex()
}
return result
} }
return nil return nil
} }
func (def *PacketDef) getVersionOrder(version uint32) []string { // Fast type lookup using first character
var bestVersion uint32 func getDataType(tag string) (common.EQ2DataType, bool) {
for v := range def.Orders { if len(tag) == 0 {
if v <= version && v > bestVersion { return 0, false
bestVersion = v }
// 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()
} }

View File

@ -0,0 +1,396 @@
package parser
import (
"eq2emu/internal/common"
"testing"
)
func TestBasicParsing(t *testing.T) {
pml := `<packet name="Test">
<version number="1">
<i32 name="player_id">
<str16 name="player_name">
<color name="skin_color">
</version>
</packet>`
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 := `<packet name="MultiVersion">
<version number="1">
<i32 name="id">
<str16 name="name">
</version>
<version number="562">
<i32 name="id">
<str16 name="name">
<color name="color">
</version>
</packet>`
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 := `<packet name="ArrayTest">
<version number="1">
<i8 name="item_count">
<array name="items" count="var:item_count">
<substruct>
<i32 name="item_id">
<str16 name="item_name">
</substruct>
</array>
</version>
</packet>`
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 := `<packet name="ConditionalTest">
<version number="1">
<i32 name="player_id">
<str16 name="guild_name" if="flag:has_guild">
<i8 name="enhancement" if="item_type!=0">
<color name="aura" if="special_flags&0x01">
</version>
</packet>`
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 := `<packet name="CommaTest">
<version number="1">
<i32 name="player_id,account_id">
<f32 name="pos_x,pos_y,pos_z">
</version>
</packet>`
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 := `<substruct name="ItemInfo">
<i32 name="item_id">
<str16 name="item_name">
</substruct>
<packet name="SubstructTest">
<version number="1">
<i8 name="count">
<array name="items" count="var:count" substruct="ItemInfo">
</version>
</packet>`
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 := `<packet name="AttributeTest">
<version number="1">
<i8 name="data_array" size="10">
<str16 name="optional_text" if="var:has_text">
</version>
</packet>`
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 := `<!-- This is a comment -->
<packet name="CommentTest">
<!-- Another comment -->
<version number="1">
<i32 name="id"> <!-- Inline comment -->
<str16 name="name">
</version>
</packet>`
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", "<packet name=\"Test\"><version number=\"1\"><i32 name=\"id\">"},
{"Invalid XML", "<packet><version><i32></packet>"},
{"Missing quotes", "<packet name=Test><version number=1></version></packet>"},
}
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 := `<packet name="Simple">
<version number="1">
<i32 name="id">
<str16 name="name">
<i8 name="level">
<f32 name="x,y,z">
</version>
</packet>`
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Parse(pml)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkComplexPacket(b *testing.B) {
pml := `<packet name="Complex">
<version number="562">
<i32 name="player_id,account_id">
<str16 name="player_name">
<color name="skin_color,hair_color,eye_color">
<str16 name="guild_name" if="flag:has_guild">
<i32 name="guild_id" if="flag:has_guild">
<i8 name="equipment_count">
<array name="equipment" count="var:equipment_count">
<substruct>
<i16 name="slot_id,item_type">
<color name="primary_color,secondary_color">
<i8 name="enhancement_level" if="item_type!=0">
</substruct>
</array>
<i8 name="stat_count">
<array name="stats" count="var:stat_count">
<substruct>
<i8 name="stat_type">
<i32 name="base_value">
<i32 name="modified_value" if="stat_type>=1&stat_type<=5">
<f32 name="percentage" if="stat_type==6">
</substruct>
</array>
</version>
</packet>`
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 := `<packet name="Large"><version number="1">`
for i := 0; i < 100; i++ {
pmlBuilder += `<i32 name="field` + string(rune('A'+i%26)) + `">`
}
pmlBuilder += `</version></packet>`
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Parse(pmlBuilder)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkWithSubstructs(b *testing.B) {
pml := `<substruct name="Item">
<i32 name="id">
<str16 name="name">
<i16 name="quantity">
</substruct>
<packet name="WithSubstruct">
<version number="1">
<i8 name="count">
<array name="items" count="var:count" substruct="Item">
</version>
</packet>`
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
}

View File

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

View File

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

View File

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