parser v3
This commit is contained in:
parent
ca46c5617d
commit
e310437c1b
@ -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.
|
|
@ -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
|
|
||||||
}
|
|
@ -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]
|
|
||||||
}
|
|
||||||
}
|
|
354
internal/packets/parser/lexer.go
Normal file
354
internal/packets/parser/lexer.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
396
internal/packets/parser/parser_test.go
Normal file
396
internal/packets/parser/parser_test.go
Normal 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
|
||||||
|
}
|
20
internal/packets/parser/structs.go
Normal file
20
internal/packets/parser/structs.go
Normal 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
|
||||||
|
}
|
42
internal/packets/parser/tokens.go
Normal file
42
internal/packets/parser/tokens.go
Normal 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 ""
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user