new parser attempt
This commit is contained in:
parent
c5a5786400
commit
ca46c5617d
273
internal/packets/PARSER.md
Normal file
273
internal/packets/PARSER.md
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
# Packet Parser
|
||||||
|
|
||||||
|
Write packet definitions as data instead of code! This parser handles complex binary protocols with versioning, conditional fields, and nested structures.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "your-project/internal/common"
|
||||||
|
|
||||||
|
var MyPacketDef = &PacketDef{
|
||||||
|
Fields: map[string]FieldDesc{
|
||||||
|
"count": {Type: common.TypeInt16},
|
||||||
|
"items": {Type: common.TypeArray, Condition: "var:count", SubDef: ItemDef},
|
||||||
|
"name": {Type: common.TypeString8},
|
||||||
|
},
|
||||||
|
Orders: map[uint32][]string{
|
||||||
|
1: {"count", "items", "name"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse it
|
||||||
|
result, err := MyPacketDef.Parse(data, version, flags)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field Types
|
||||||
|
|
||||||
|
### Integer Types
|
||||||
|
|
||||||
|
| Type | Description | Size | Example |
|
||||||
|
|------|-------------|------|---------|
|
||||||
|
| `common.TypeInt8` | Unsigned 8-bit integer | 1 byte | `{Type: common.TypeInt8}` |
|
||||||
|
| `common.TypeInt16` | Unsigned 16-bit integer | 2 bytes | `{Type: common.TypeInt16}` |
|
||||||
|
| `common.TypeInt32` | Unsigned 32-bit integer | 4 bytes | `{Type: common.TypeInt32}` |
|
||||||
|
| `common.TypeInt64` | Unsigned 64-bit integer | 8 bytes | `{Type: common.TypeInt64}` |
|
||||||
|
| `common.TypeSInt8` | Signed 8-bit integer | 1 byte | `{Type: common.TypeSInt8}` |
|
||||||
|
| `common.TypeSInt16` | Signed 16-bit integer | 2 bytes | `{Type: common.TypeSInt16}` |
|
||||||
|
| `common.TypeSInt32` | Signed 32-bit integer | 4 bytes | `{Type: common.TypeSInt32}` |
|
||||||
|
| `common.TypeSInt64` | Signed 64-bit integer | 8 bytes | `{Type: common.TypeSInt64}` |
|
||||||
|
|
||||||
|
### String Types
|
||||||
|
|
||||||
|
| Type | Description | Returns | Example |
|
||||||
|
|------|-------------|---------|---------|
|
||||||
|
| `common.TypeString8` | 1-byte length prefix | `EQ2String8{Size, Data}` | `{Type: common.TypeString8}` |
|
||||||
|
| `common.TypeString16` | 2-byte length prefix | `EQ2String16{Size, Data}` | `{Type: common.TypeString16}` |
|
||||||
|
| `common.TypeString32` | 4-byte length prefix | `EQ2String32{Size, Data}` | `{Type: common.TypeString32}` |
|
||||||
|
|
||||||
|
### Other Types
|
||||||
|
|
||||||
|
| Type | Description | Returns | Example |
|
||||||
|
|------|-------------|---------|---------|
|
||||||
|
| `common.TypeChar` | Fixed-size byte array | `[]byte` | `{Type: common.TypeChar, Length: 10}` |
|
||||||
|
| `common.TypeFloat` | 32-bit float | `float32` | `{Type: common.TypeFloat}` |
|
||||||
|
| `common.TypeDouble` | 64-bit float | `float64` | `{Type: common.TypeDouble}` |
|
||||||
|
| `common.TypeColor` | RGB color value | `EQ2Color{Red, Green, Blue}` | `{Type: common.TypeColor}` |
|
||||||
|
| `common.TypeEquipment` | Equipment item | `EQ2EquipmentItem{Type, Color, Highlight}` | `{Type: common.TypeEquipment}` |
|
||||||
|
| `common.TypeArray` | Array of substructures | `[]map[string]any` | `{Type: common.TypeArray, SubDef: ItemDef}` |
|
||||||
|
|
||||||
|
## Conditions
|
||||||
|
|
||||||
|
Control when fields are parsed using simple condition strings:
|
||||||
|
|
||||||
|
### Variable Checks
|
||||||
|
```go
|
||||||
|
"var:item_count" // Parse if item_count exists and is non-zero
|
||||||
|
"!var:item_count" // Parse if item_count doesn't exist or is zero
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flag Checks
|
||||||
|
```go
|
||||||
|
"flag:loot" // Parse if loot flag is set
|
||||||
|
"!flag:loot" // Parse if loot flag is not set
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Checks
|
||||||
|
```go
|
||||||
|
"version>=1188" // Parse if version 1188 or higher
|
||||||
|
"version<562" // Parse if version below 562
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comparisons
|
||||||
|
```go
|
||||||
|
"stat_type!=6" // Parse if stat_type is not 6
|
||||||
|
"level>=10" // Parse if level is 10 or higher
|
||||||
|
```
|
||||||
|
|
||||||
|
### String Length
|
||||||
|
```go
|
||||||
|
"name!>5" // Parse if name is longer than 5 characters
|
||||||
|
"description!<=100" // Parse if description is 100 chars or less
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bitwise Operations
|
||||||
|
```go
|
||||||
|
"header_flags&0x01" // Parse if bit 1 is set in header_flags
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combining Conditions
|
||||||
|
```go
|
||||||
|
"var:count,var:size" // OR: either count or size exists
|
||||||
|
"version>=562&version<1188" // AND: version between 562-1187
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Ordering
|
||||||
|
|
||||||
|
Different versions can have different field orders:
|
||||||
|
|
||||||
|
```go
|
||||||
|
Orders: map[uint32][]string{
|
||||||
|
373: {"basic_field", "name"},
|
||||||
|
1188: {"basic_field", "new_field", "name"},
|
||||||
|
2000: {"basic_field", "new_field", "another_field", "name"},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The parser automatically uses the highest version ≤ your target version.
|
||||||
|
|
||||||
|
## Substructures
|
||||||
|
|
||||||
|
Create reusable packet definitions:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var ItemDef = &PacketDef{
|
||||||
|
Fields: map[string]FieldDesc{
|
||||||
|
"item_id": {Type: common.TypeInt32},
|
||||||
|
"item_name": {Type: common.TypeString16},
|
||||||
|
"quantity": {Type: common.TypeInt8, Condition: "version>=546"},
|
||||||
|
"color": {Type: common.TypeColor, Condition: "flag:has_colors"},
|
||||||
|
},
|
||||||
|
Orders: map[uint32][]string{
|
||||||
|
1: {"item_id", "item_name", "quantity", "color"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var InventoryDef = &PacketDef{
|
||||||
|
Fields: map[string]FieldDesc{
|
||||||
|
"item_count": {Type: common.TypeInt8},
|
||||||
|
"items": {Type: common.TypeArray, Condition: "var:item_count", SubDef: ItemDef},
|
||||||
|
},
|
||||||
|
Orders: map[uint32][]string{
|
||||||
|
1: {"item_count", "items"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Type Switching
|
||||||
|
Use different types based on conditions:
|
||||||
|
```go
|
||||||
|
{
|
||||||
|
Type: common.TypeInt32,
|
||||||
|
Type2: common.TypeFloat,
|
||||||
|
Type2Cond: "stat_type!=6", // Use float if stat_type is not 6
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Oversized Fields
|
||||||
|
Handle fields that can exceed normal size limits:
|
||||||
|
```go
|
||||||
|
{
|
||||||
|
Type: common.TypeInt16,
|
||||||
|
Oversized: 127, // Switch to 16-bit if first byte is 127
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Array Index Substitution
|
||||||
|
Reference array indices in conditions:
|
||||||
|
```go
|
||||||
|
"var:item_type_%i" // Substitutes %i with current array index
|
||||||
|
```
|
||||||
|
|
||||||
|
## Working with Structured Types
|
||||||
|
|
||||||
|
### String Types
|
||||||
|
String types return structured objects with size and data:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// TypeString8 returns: EQ2String8{Size: uint8, Data: string}
|
||||||
|
// TypeString16 returns: EQ2String16{Size: uint16, Data: string}
|
||||||
|
// TypeString32 returns: EQ2String32{Size: uint32, Data: string}
|
||||||
|
|
||||||
|
result, _ := packet.Parse(data, version, flags)
|
||||||
|
if nameField, ok := result["player_name"].(common.EQ2String16); ok {
|
||||||
|
fmt.Printf("Name: %s (length: %d)\n", nameField.Data, nameField.Size)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color Types
|
||||||
|
```go
|
||||||
|
// TypeColor returns: EQ2Color{Red: uint8, Green: uint8, Blue: uint8}
|
||||||
|
|
||||||
|
if colorField, ok := result["shirt_color"].(common.EQ2Color); ok {
|
||||||
|
fmt.Printf("RGB: %d,%d,%d\n", colorField.Red, colorField.Green, colorField.Blue)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Equipment Types
|
||||||
|
```go
|
||||||
|
// TypeEquipment returns: EQ2EquipmentItem{Type: uint16, Color: EQ2Color, Highlight: EQ2Color}
|
||||||
|
|
||||||
|
if equipField, ok := result["helmet"].(common.EQ2EquipmentItem); ok {
|
||||||
|
fmt.Printf("Equipment Type: %d\n", equipField.Type)
|
||||||
|
fmt.Printf("Color: RGB(%d,%d,%d)\n",
|
||||||
|
equipField.Color.Red, equipField.Color.Green, equipField.Color.Blue)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "your-project/internal/common"
|
||||||
|
|
||||||
|
var QuestPacketDef = &PacketDef{
|
||||||
|
Fields: map[string]FieldDesc{
|
||||||
|
"quest_id": {Type: common.TypeInt32},
|
||||||
|
"has_rewards": {Type: common.TypeInt8},
|
||||||
|
"reward_count": {Type: common.TypeInt8, Condition: "var:has_rewards"},
|
||||||
|
"rewards": {Type: common.TypeArray, Condition: "var:reward_count", SubDef: RewardDef},
|
||||||
|
"is_complete": {Type: common.TypeInt8},
|
||||||
|
"complete_text": {Type: common.TypeString16, Condition: "var:is_complete"},
|
||||||
|
"quest_color": {Type: common.TypeColor, Condition: "version>=1200"},
|
||||||
|
"version_field": {Type: common.TypeInt32, Condition: "version>=1200"},
|
||||||
|
},
|
||||||
|
Orders: map[uint32][]string{
|
||||||
|
1: {"quest_id", "has_rewards", "reward_count", "rewards", "is_complete", "complete_text"},
|
||||||
|
1200: {"quest_id", "has_rewards", "reward_count", "rewards", "is_complete", "complete_text", "quest_color", "version_field"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var RewardDef = &PacketDef{
|
||||||
|
Fields: map[string]FieldDesc{
|
||||||
|
"reward_type": {Type: common.TypeInt8},
|
||||||
|
"amount": {Type: common.TypeInt32},
|
||||||
|
"bonus": {Type: common.TypeInt16, Condition: "reward_type==1"},
|
||||||
|
"item_color": {Type: common.TypeColor, Condition: "reward_type==2"},
|
||||||
|
},
|
||||||
|
Orders: map[uint32][]string{
|
||||||
|
1: {"reward_type", "amount", "bonus", "item_color"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
result, err := QuestPacketDef.Parse(packetData, 1200, questFlags)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access structured data
|
||||||
|
if questText, ok := result["complete_text"].(common.EQ2String16); ok {
|
||||||
|
fmt.Printf("Quest completion text: %s\n", questText.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if questColor, ok := result["quest_color"].(common.EQ2Color); ok {
|
||||||
|
fmt.Printf("Quest color: #%02x%02x%02x\n",
|
||||||
|
questColor.Red, questColor.Green, questColor.Blue)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Old Types
|
||||||
|
|
||||||
|
| Old Type | New Type | Breaking Change |
|
||||||
|
|----------|----------|-----------------|
|
||||||
|
| `Uint8` | `common.TypeInt8` | Type name only |
|
||||||
|
| `Uint16` | `common.TypeInt16` | Type name only |
|
||||||
|
| `Uint32` | `common.TypeInt32` | Type name only |
|
||||||
|
| `String8` | `common.TypeString8` | Returns `EQ2String8` struct |
|
||||||
|
| `String16` | `common.TypeString16` | Returns `EQ2String16` struct |
|
||||||
|
| `ByteArray` | `common.TypeChar` | Type name only |
|
||||||
|
| `Float32` | `common.TypeFloat` | Type name only |
|
||||||
|
| `SubArray` | `common.TypeArray` | Type name only |
|
||||||
|
|
||||||
|
**Note**: String types now return structured objects instead of plain strings. Access the string data via the `.Data` field.
|
239
internal/packets/parser/conditions.go
Normal file
239
internal/packets/parser/conditions.go
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"eq2emu/internal/common"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ctx *ParseContext) checkCondition(condition string) bool {
|
||||||
|
if condition == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle comma-separated OR conditions
|
||||||
|
if strings.Contains(condition, ",") {
|
||||||
|
parts := strings.Split(condition, ",")
|
||||||
|
for _, part := range parts {
|
||||||
|
if ctx.evaluateCondition(strings.TrimSpace(part)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle AND conditions with &
|
||||||
|
if strings.Contains(condition, "&") && !strings.Contains(condition, "0x") {
|
||||||
|
parts := strings.Split(condition, "&")
|
||||||
|
for _, part := range parts {
|
||||||
|
if !ctx.evaluateCondition(strings.TrimSpace(part)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.evaluateCondition(condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) evaluateCondition(condition string) bool {
|
||||||
|
// Flag conditions: flag:name or !flag:name
|
||||||
|
if strings.HasPrefix(condition, "flag:") {
|
||||||
|
flagName := condition[5:]
|
||||||
|
return (ctx.flags & ctx.getFlagValue(flagName)) != 0
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(condition, "!flag:") {
|
||||||
|
flagName := condition[6:]
|
||||||
|
return (ctx.flags & ctx.getFlagValue(flagName)) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable conditions: var:name or !var:name
|
||||||
|
if strings.HasPrefix(condition, "var:") {
|
||||||
|
varName := condition[4:]
|
||||||
|
return ctx.hasVar(varName)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(condition, "!var:") {
|
||||||
|
varName := condition[5:]
|
||||||
|
return !ctx.hasVar(varName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version comparisons
|
||||||
|
if strings.HasPrefix(condition, "version") {
|
||||||
|
return ctx.evaluateVersionCondition(condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String length operators: name!>5, name!<=10
|
||||||
|
stringOps := []string{"!>=", "!<=", "!>", "!<", "!="}
|
||||||
|
for _, op := range stringOps {
|
||||||
|
if idx := strings.Index(condition, op); idx > 0 {
|
||||||
|
varName := condition[:idx]
|
||||||
|
valueStr := condition[idx+len(op):]
|
||||||
|
return ctx.evaluateStringLength(varName, valueStr, op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparison operators: >=, <=, >, <, ==, !=
|
||||||
|
compOps := []string{">=", "<=", ">", "<", "==", "!="}
|
||||||
|
for _, op := range compOps {
|
||||||
|
if idx := strings.Index(condition, op); idx > 0 {
|
||||||
|
varName := condition[:idx]
|
||||||
|
valueStr := condition[idx+len(op):]
|
||||||
|
return ctx.evaluateComparison(varName, valueStr, op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bitwise AND: header_flag&0x01
|
||||||
|
if strings.Contains(condition, "&0x") {
|
||||||
|
parts := strings.SplitN(condition, "&", 2)
|
||||||
|
varName := parts[0]
|
||||||
|
hexValue, _ := strconv.ParseUint(parts[1], 0, 64)
|
||||||
|
varValue := ctx.getVarValue(varName)
|
||||||
|
return (varValue & hexValue) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple variable existence
|
||||||
|
return ctx.hasVar(condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) evaluateVersionCondition(condition string) bool {
|
||||||
|
if strings.Contains(condition, ">=") {
|
||||||
|
versionStr := condition[strings.Index(condition, ">=")+2:]
|
||||||
|
targetVersion, _ := strconv.ParseUint(versionStr, 10, 32)
|
||||||
|
return ctx.version >= uint32(targetVersion)
|
||||||
|
}
|
||||||
|
if strings.Contains(condition, "<=") {
|
||||||
|
versionStr := condition[strings.Index(condition, "<=")+2:]
|
||||||
|
targetVersion, _ := strconv.ParseUint(versionStr, 10, 32)
|
||||||
|
return ctx.version <= uint32(targetVersion)
|
||||||
|
}
|
||||||
|
if strings.Contains(condition, ">") {
|
||||||
|
versionStr := condition[strings.Index(condition, ">")+1:]
|
||||||
|
targetVersion, _ := strconv.ParseUint(versionStr, 10, 32)
|
||||||
|
return ctx.version > uint32(targetVersion)
|
||||||
|
}
|
||||||
|
if strings.Contains(condition, "<") {
|
||||||
|
versionStr := condition[strings.Index(condition, "<")+1:]
|
||||||
|
targetVersion, _ := strconv.ParseUint(versionStr, 10, 32)
|
||||||
|
return ctx.version < uint32(targetVersion)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) evaluateStringLength(varName, valueStr, op string) bool {
|
||||||
|
str := ctx.getStringVar(varName)
|
||||||
|
targetLen, _ := strconv.Atoi(valueStr)
|
||||||
|
strLen := len(str)
|
||||||
|
|
||||||
|
switch op {
|
||||||
|
case "!>":
|
||||||
|
return strLen > targetLen
|
||||||
|
case "!<":
|
||||||
|
return strLen < targetLen
|
||||||
|
case "!>=":
|
||||||
|
return strLen >= targetLen
|
||||||
|
case "!<=":
|
||||||
|
return strLen <= targetLen
|
||||||
|
case "!=":
|
||||||
|
return strLen != targetLen
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) evaluateComparison(varName, valueStr, op string) bool {
|
||||||
|
varValue := ctx.getVarValue(varName)
|
||||||
|
targetValue, _ := strconv.ParseUint(valueStr, 0, 64)
|
||||||
|
|
||||||
|
switch op {
|
||||||
|
case ">=":
|
||||||
|
return varValue >= targetValue
|
||||||
|
case "<=":
|
||||||
|
return varValue <= targetValue
|
||||||
|
case ">":
|
||||||
|
return varValue > targetValue
|
||||||
|
case "<":
|
||||||
|
return varValue < targetValue
|
||||||
|
case "==":
|
||||||
|
return varValue == targetValue
|
||||||
|
case "!=":
|
||||||
|
return varValue != targetValue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) hasVar(name string) bool {
|
||||||
|
// Handle %i substitution
|
||||||
|
if strings.Contains(name, "%i") && len(ctx.arrayStack) > 0 {
|
||||||
|
currentIndex := ctx.arrayStack[len(ctx.arrayStack)-1]
|
||||||
|
name = strings.ReplaceAll(name, "%i", strconv.Itoa(currentIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, exists := ctx.vars[name]; exists {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case uint8, uint16, uint32, uint64:
|
||||||
|
return ctx.getVarValue(name) != 0
|
||||||
|
case string:
|
||||||
|
return v != ""
|
||||||
|
case common.EQ2String8:
|
||||||
|
return v.Data != ""
|
||||||
|
case common.EQ2String16:
|
||||||
|
return v.Data != ""
|
||||||
|
case common.EQ2String32:
|
||||||
|
return v.Data != ""
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) getVarValue(name string) uint64 {
|
||||||
|
if val, exists := ctx.vars[name]; exists {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case uint8:
|
||||||
|
return uint64(v)
|
||||||
|
case uint16:
|
||||||
|
return uint64(v)
|
||||||
|
case uint32:
|
||||||
|
return uint64(v)
|
||||||
|
case uint64:
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) getStringVar(name string) string {
|
||||||
|
if val, exists := ctx.vars[name]; exists {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case string:
|
||||||
|
return v
|
||||||
|
case common.EQ2String8:
|
||||||
|
return v.Data
|
||||||
|
case common.EQ2String16:
|
||||||
|
return v.Data
|
||||||
|
case common.EQ2String32:
|
||||||
|
return v.Data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) getFlagValue(flagName string) uint64 {
|
||||||
|
flagMap := map[string]uint64{
|
||||||
|
"loot": 0x01,
|
||||||
|
"has_equipment": 0x02,
|
||||||
|
"no_colors": 0x04,
|
||||||
|
}
|
||||||
|
if val, exists := flagMap[flagName]; exists {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) getArraySize(condition string) int {
|
||||||
|
if strings.HasPrefix(condition, "var:") {
|
||||||
|
varName := condition[4:]
|
||||||
|
return int(ctx.getVarValue(varName))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
145
internal/packets/parser/context.go
Normal file
145
internal/packets/parser/context.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"eq2emu/internal/common"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ParseContext struct {
|
||||||
|
data []byte
|
||||||
|
offset int
|
||||||
|
version uint32
|
||||||
|
flags uint64
|
||||||
|
vars map[string]any
|
||||||
|
arrayStack []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContext(data []byte, version uint32, flags uint64) *ParseContext {
|
||||||
|
return &ParseContext{
|
||||||
|
data: data,
|
||||||
|
version: version,
|
||||||
|
flags: flags,
|
||||||
|
vars: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readUint8() uint8 {
|
||||||
|
val := ctx.data[ctx.offset]
|
||||||
|
ctx.offset++
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readUint16() uint16 {
|
||||||
|
val := binary.LittleEndian.Uint16(ctx.data[ctx.offset:])
|
||||||
|
ctx.offset += 2
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readUint32() uint32 {
|
||||||
|
val := binary.LittleEndian.Uint32(ctx.data[ctx.offset:])
|
||||||
|
ctx.offset += 4
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readOversizedUint8(threshold int) uint8 {
|
||||||
|
if ctx.data[ctx.offset] == byte(threshold) {
|
||||||
|
ctx.offset++
|
||||||
|
return ctx.readUint8()
|
||||||
|
}
|
||||||
|
return ctx.readUint8()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readOversizedUint16(threshold int) uint16 {
|
||||||
|
if ctx.data[ctx.offset] == byte(threshold) {
|
||||||
|
ctx.offset++
|
||||||
|
return ctx.readUint16()
|
||||||
|
}
|
||||||
|
return uint16(ctx.readUint8())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readString8() string {
|
||||||
|
length := ctx.readUint8()
|
||||||
|
str := string(ctx.data[ctx.offset : ctx.offset+int(length)])
|
||||||
|
ctx.offset += int(length)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readString16() string {
|
||||||
|
length := ctx.readUint16()
|
||||||
|
str := string(ctx.data[ctx.offset : ctx.offset+int(length)])
|
||||||
|
ctx.offset += int(length)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readEQ2String8() common.EQ2String8 {
|
||||||
|
size := ctx.readUint8()
|
||||||
|
data := string(ctx.data[ctx.offset : ctx.offset+int(size)])
|
||||||
|
ctx.offset += int(size)
|
||||||
|
return common.EQ2String8{
|
||||||
|
Size: size,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readEQ2String16() common.EQ2String16 {
|
||||||
|
size := ctx.readUint16()
|
||||||
|
data := string(ctx.data[ctx.offset : ctx.offset+int(size)])
|
||||||
|
ctx.offset += int(size)
|
||||||
|
return common.EQ2String16{
|
||||||
|
Size: size,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readEQ2String32() common.EQ2String32 {
|
||||||
|
size := ctx.readUint32()
|
||||||
|
data := string(ctx.data[ctx.offset : ctx.offset+int(size)])
|
||||||
|
ctx.offset += int(size)
|
||||||
|
return common.EQ2String32{
|
||||||
|
Size: size,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readEQ2Color() common.EQ2Color {
|
||||||
|
return common.EQ2Color{
|
||||||
|
Red: ctx.readUint8(),
|
||||||
|
Green: ctx.readUint8(),
|
||||||
|
Blue: ctx.readUint8(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readEQ2Equipment() common.EQ2EquipmentItem {
|
||||||
|
return common.EQ2EquipmentItem{
|
||||||
|
Type: ctx.readUint16(),
|
||||||
|
Color: ctx.readEQ2Color(),
|
||||||
|
Highlight: ctx.readEQ2Color(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readBytes(count int) []byte {
|
||||||
|
val := make([]byte, count)
|
||||||
|
copy(val, ctx.data[ctx.offset:ctx.offset+count])
|
||||||
|
ctx.offset += count
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) readFloat32() float32 {
|
||||||
|
val := ctx.readUint32()
|
||||||
|
return *(*float32)(unsafe.Pointer(&val))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) setVar(name string, value any) {
|
||||||
|
ctx.vars[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) pushArrayIndex(index int) {
|
||||||
|
ctx.arrayStack = append(ctx.arrayStack, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *ParseContext) popArrayIndex() {
|
||||||
|
if len(ctx.arrayStack) > 0 {
|
||||||
|
ctx.arrayStack = ctx.arrayStack[:len(ctx.arrayStack)-1]
|
||||||
|
}
|
||||||
|
}
|
84
internal/packets/parser/parser.go
Normal file
84
internal/packets/parser/parser.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import "eq2emu/internal/common"
|
||||||
|
|
||||||
|
func (def *PacketDef) Parse(data []byte, version uint32, flags uint64) (map[string]any, error) {
|
||||||
|
ctx := NewContext(data, version, flags)
|
||||||
|
return def.parseStruct(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (def *PacketDef) parseStruct(ctx *ParseContext) (map[string]any, error) {
|
||||||
|
result := make(map[string]any)
|
||||||
|
order := def.getVersionOrder(ctx.version)
|
||||||
|
|
||||||
|
for _, fieldName := range order {
|
||||||
|
field := def.Fields[fieldName]
|
||||||
|
|
||||||
|
if !ctx.checkCondition(field.Condition) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldType := field.Type
|
||||||
|
if field.Type2 != 0 && ctx.checkCondition(field.Type2Cond) {
|
||||||
|
fieldType = field.Type2
|
||||||
|
}
|
||||||
|
|
||||||
|
value := def.parseField(ctx, field, fieldType, fieldName)
|
||||||
|
result[fieldName] = value
|
||||||
|
ctx.setVar(fieldName, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (def *PacketDef) parseField(ctx *ParseContext, field FieldDesc, fieldType common.EQ2DataType, fieldName string) any {
|
||||||
|
switch fieldType {
|
||||||
|
case common.TypeInt8, common.TypeSInt8:
|
||||||
|
if field.Oversized > 0 {
|
||||||
|
return ctx.readOversizedUint8(field.Oversized)
|
||||||
|
}
|
||||||
|
return ctx.readUint8()
|
||||||
|
case common.TypeInt16, common.TypeSInt16:
|
||||||
|
if field.Oversized > 0 {
|
||||||
|
return ctx.readOversizedUint16(field.Oversized)
|
||||||
|
}
|
||||||
|
return ctx.readUint16()
|
||||||
|
case common.TypeInt32, common.TypeSInt32:
|
||||||
|
return ctx.readUint32()
|
||||||
|
case common.TypeString8:
|
||||||
|
return ctx.readEQ2String8()
|
||||||
|
case common.TypeString16:
|
||||||
|
return ctx.readEQ2String16()
|
||||||
|
case common.TypeString32:
|
||||||
|
return ctx.readEQ2String32()
|
||||||
|
case common.TypeChar:
|
||||||
|
return ctx.readBytes(field.Length)
|
||||||
|
case common.TypeFloat:
|
||||||
|
return ctx.readFloat32()
|
||||||
|
case common.TypeColor:
|
||||||
|
return ctx.readEQ2Color()
|
||||||
|
case common.TypeEquipment:
|
||||||
|
return ctx.readEQ2Equipment()
|
||||||
|
case common.TypeArray:
|
||||||
|
size := ctx.getArraySize(field.Condition)
|
||||||
|
result := make([]map[string]any, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
ctx.pushArrayIndex(i)
|
||||||
|
item, _ := field.SubDef.parseStruct(ctx)
|
||||||
|
result[i] = item
|
||||||
|
ctx.popArrayIndex()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (def *PacketDef) getVersionOrder(version uint32) []string {
|
||||||
|
var bestVersion uint32
|
||||||
|
for v := range def.Orders {
|
||||||
|
if v <= version && v > bestVersion {
|
||||||
|
bestVersion = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def.Orders[bestVersion]
|
||||||
|
}
|
18
internal/packets/parser/types.go
Normal file
18
internal/packets/parser/types.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import "eq2emu/internal/common"
|
||||||
|
|
||||||
|
type PacketDef struct {
|
||||||
|
Fields map[string]FieldDesc
|
||||||
|
Orders map[uint32][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldDesc struct {
|
||||||
|
Type common.EQ2DataType
|
||||||
|
Condition string
|
||||||
|
Length int
|
||||||
|
SubDef *PacketDef
|
||||||
|
Type2 common.EQ2DataType
|
||||||
|
Type2Cond string
|
||||||
|
Oversized int
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user