implement missing types, conditions

This commit is contained in:
Sky Johnson 2025-07-28 13:02:14 -05:00
parent e310437c1b
commit c42485f874
5 changed files with 916 additions and 79 deletions

View File

@ -0,0 +1,396 @@
package parser
import (
"encoding/binary"
"eq2emu/internal/common"
"strconv"
"strings"
"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) readUint64() uint64 {
val := binary.LittleEndian.Uint64(ctx.data[ctx.offset:])
ctx.offset += 8
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) readOversizedUint32(threshold int) uint32 {
if ctx.data[ctx.offset] == byte(threshold) {
ctx.offset++
return ctx.readUint32()
}
return uint32(ctx.readUint16())
}
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) readFloat64() float64 {
val := ctx.readUint64()
return *(*float64)(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]
}
}
// Condition evaluation methods
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)
}
}
// Simple variable existence
resolvedName := ctx.resolveVariableName(condition)
return ctx.hasVar(resolvedName)
}
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 {
resolvedName := ctx.resolveVariableName(varName)
str := ctx.getStringVar(resolvedName)
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 {
resolvedName := ctx.resolveVariableName(varName)
varValue := ctx.getVarValue(resolvedName)
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 {
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
}
func (ctx *ParseContext) resolveVariableName(name string) string {
// Handle %i substitution for array contexts
if strings.Contains(name, "%i") && len(ctx.arrayStack) > 0 {
currentIndex := ctx.arrayStack[len(ctx.arrayStack)-1]
return strings.ReplaceAll(name, "%i", strconv.Itoa(currentIndex))
}
return name
}

View File

@ -63,7 +63,7 @@ func (l *Lexer) next() byte {
// 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 {
if length < 2 || length > 6 {
return false
}
@ -103,6 +103,10 @@ func (l *Lexer) isSelfClosingTag(start, end int) bool {
(l.input[start] == 's' && l.input[start+1] == 't' &&
l.input[start+2] == 'r' && l.input[start+3] == '3' &&
l.input[start+4] == '2')
case 6:
return (l.input[start] == 'd' && l.input[start+1] == 'o' &&
l.input[start+2] == 'u' && l.input[start+3] == 'b' &&
l.input[start+4] == 'l' && l.input[start+5] == 'e')
}
return false
}

View File

@ -68,6 +68,7 @@ var typeMap = map[string]common.EQ2DataType{
"si64": common.TypeSInt64,
"f32": common.TypeFloat,
"f64": common.TypeDouble,
"double": common.TypeDouble, // XML compatibility
"str8": common.TypeString8,
"str16": common.TypeString16,
"str32": common.TypeString32,
@ -183,6 +184,10 @@ func getDataType(tag string) (common.EQ2DataType, bool) {
case "f64":
return common.TypeDouble, true
}
case 'd':
if tag == "double" {
return common.TypeDouble, true
}
case 'c':
switch tag {
case "char":
@ -529,12 +534,30 @@ func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix s
fieldDesc := FieldDesc{
Type: common.TypeArray,
Condition: attrs["count"],
AddToStruct: true, // Default to true
}
if ifCond := attrs["if"]; ifCond != "" {
fieldDesc.Condition = combineConditions(fieldDesc.Condition, ifCond)
}
// Parse additional attributes
if maxSize := attrs["max_size"]; maxSize != "" {
if m, err := strconv.Atoi(maxSize); err == nil {
fieldDesc.MaxArraySize = m
} else {
return fmt.Errorf("invalid max_size value '%s' at line %d: %v", maxSize, p.current.Line, err)
}
}
if optional := attrs["optional"]; optional == "true" {
fieldDesc.Optional = true
}
if addToStruct := attrs["add_to_struct"]; addToStruct == "false" {
fieldDesc.AddToStruct = false
}
// Handle substruct reference
if substruct := attrs["substruct"]; substruct != "" {
if subDef, exists := p.substructs[substruct]; exists {
@ -642,11 +665,68 @@ func (p *Parser) parseField(packetDef *PacketDef, fieldOrder *[]string, prefix s
fieldDesc := FieldDesc{
Type: dataType,
Condition: attrs["if"],
AddToStruct: true, // Default to true
AddType: dataType,
}
// Parse size attribute
if size := attrs["size"]; size != "" {
if s, err := strconv.Atoi(size); err == nil {
fieldDesc.Length = s
} else {
return fmt.Errorf("invalid size value '%s' at line %d: %v", size, p.current.Line, err)
}
}
// Parse oversized attribute
if oversized := attrs["oversized"]; oversized != "" {
if o, err := strconv.Atoi(oversized); err == nil {
fieldDesc.Oversized = o
} else {
return fmt.Errorf("invalid oversized value '%s' at line %d: %v", oversized, p.current.Line, err)
}
}
// Parse type2 attributes
if type2 := attrs["type2"]; type2 != "" {
if t2, exists := getDataType(type2); exists {
fieldDesc.Type2 = t2
fieldDesc.Type2Cond = attrs["type2_if"]
}
}
// Parse default value
if defaultVal := attrs["default"]; defaultVal != "" {
if d, err := strconv.ParseInt(defaultVal, 10, 8); err == nil {
fieldDesc.DefaultValue = int8(d)
} else {
return fmt.Errorf("invalid default value '%s' at line %d: %v", defaultVal, p.current.Line, err)
}
}
// Parse max_size
if maxSize := attrs["max_size"]; maxSize != "" {
if m, err := strconv.Atoi(maxSize); err == nil {
fieldDesc.MaxArraySize = m
} else {
return fmt.Errorf("invalid max_size value '%s' at line %d: %v", maxSize, p.current.Line, err)
}
}
// Parse optional
if optional := attrs["optional"]; optional == "true" {
fieldDesc.Optional = true
}
// Parse add_to_struct
if addToStruct := attrs["add_to_struct"]; addToStruct == "false" {
fieldDesc.AddToStruct = false
}
// Parse add_type
if addType := attrs["add_type"]; addType != "" {
if at, exists := getDataType(addType); exists {
fieldDesc.AddType = at
}
}

View File

@ -49,6 +49,187 @@ func TestBasicParsing(t *testing.T) {
}
}
func TestFloat64Support(t *testing.T) {
pml := `<packet name="FloatTest">
<version number="1">
<f32 name="position_x">
<f64 name="precise_value">
<double name="legacy_double">
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
packet := packets["FloatTest"]
if packet.Fields["position_x"].Type != common.TypeFloat {
t.Error("position_x should be TypeFloat")
}
if packet.Fields["precise_value"].Type != common.TypeDouble {
t.Error("precise_value should be TypeDouble")
}
if packet.Fields["legacy_double"].Type != common.TypeDouble {
t.Error("legacy_double should be TypeDouble")
}
}
func TestOversizedFields(t *testing.T) {
pml := `<packet name="OversizedTest">
<version number="1">
<i8 name="small_count">
<i16 name="num_words" oversized="255">
<i32 name="large_value" oversized="65535">
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
packet := packets["OversizedTest"]
if packet.Fields["small_count"].Oversized != 0 {
t.Error("small_count should not be oversized")
}
if packet.Fields["num_words"].Oversized != 255 {
t.Errorf("num_words oversized should be 255, got %d", packet.Fields["num_words"].Oversized)
}
if packet.Fields["large_value"].Oversized != 65535 {
t.Errorf("large_value oversized should be 65535, got %d", packet.Fields["large_value"].Oversized)
}
}
func TestType2Support(t *testing.T) {
pml := `<packet name="Type2Test">
<version number="1">
<i8 name="stat_type">
<i32 name="stat_value" type2="f32" type2_if="stat_type!=6">
<i16 name="another_field" type2="i32">
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
packet := packets["Type2Test"]
statValue := packet.Fields["stat_value"]
if statValue.Type != common.TypeInt32 {
t.Error("stat_value primary type should be TypeInt32")
}
if statValue.Type2 != common.TypeFloat {
t.Error("stat_value type2 should be TypeFloat")
}
if statValue.Type2Cond != "stat_type!=6" {
t.Errorf("Expected type2_if 'stat_type!=6', got '%s'", statValue.Type2Cond)
}
anotherField := packet.Fields["another_field"]
if anotherField.Type2 != common.TypeInt32 {
t.Error("another_field type2 should be TypeInt32")
}
if anotherField.Type2Cond != "" {
t.Error("another_field should have empty type2_if")
}
}
func TestAdvancedFieldAttributes(t *testing.T) {
pml := `<packet name="AttributeTest">
<version number="1">
<i8 name="data_array" size="10" default="5">
<str16 name="optional_text" if="var:has_text" optional="true">
<i32 name="hidden_field" add_to_struct="false" add_type="i16">
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
packet := packets["AttributeTest"]
dataArray := packet.Fields["data_array"]
if dataArray.Length != 10 {
t.Errorf("Expected size 10, got %d", dataArray.Length)
}
if dataArray.DefaultValue != 5 {
t.Errorf("Expected default 5, got %d", dataArray.DefaultValue)
}
optionalText := packet.Fields["optional_text"]
if optionalText.Condition != "var:has_text" {
t.Errorf("Expected condition 'var:has_text', got '%s'", optionalText.Condition)
}
if !optionalText.Optional {
t.Error("optional_text should be optional")
}
hiddenField := packet.Fields["hidden_field"]
if hiddenField.AddToStruct {
t.Error("hidden_field should not be added to struct")
}
if hiddenField.AddType != common.TypeInt16 {
t.Error("hidden_field add_type should be TypeInt16")
}
}
func TestArrayMaxSize(t *testing.T) {
pml := `<packet name="ArrayMaxTest">
<version number="1">
<i8 name="item_count">
<array name="items" count="var:item_count" max_size="100">
<substruct>
<i32 name="item_id">
</substruct>
</array>
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
packet := packets["ArrayMaxTest"]
itemsField := packet.Fields["items"]
if itemsField.MaxArraySize != 100 {
t.Errorf("Expected max_size 100, got %d", itemsField.MaxArraySize)
}
}
func TestArrayOptionalAttributes(t *testing.T) {
pml := `<packet name="ArrayOptionalTest">
<version number="1">
<i8 name="count">
<array name="optional_items" count="var:count" optional="true" add_to_struct="false">
<substruct>
<i32 name="id">
</substruct>
</array>
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
packet := packets["ArrayOptionalTest"]
itemsField := packet.Fields["optional_items"]
if !itemsField.Optional {
t.Error("optional_items should be optional")
}
if itemsField.AddToStruct {
t.Error("optional_items should not be added to struct")
}
}
func TestMultipleVersions(t *testing.T) {
pml := `<packet name="MultiVersion">
<version number="1">
@ -265,6 +446,127 @@ func TestComments(t *testing.T) {
}
}
func TestBinaryParsingFloat64(t *testing.T) {
// Test binary parsing with float64
pml := `<packet name="BinaryFloat64">
<version number="1">
<f64 name="precise_value">
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
// Create test data: 8 bytes representing float64 value 123.456
testData := []byte{0x77, 0xbe, 0x9f, 0x1a, 0x2f, 0xdd, 0x5e, 0x40} // 123.456 in little-endian
result, err := packets["BinaryFloat64"].Parse(testData, 1, 0)
if err != nil {
t.Fatalf("Binary parse failed: %v", err)
}
if val, ok := result["precise_value"].(float64); !ok {
t.Error("precise_value should be float64")
} else if val < 123.0 || val > 124.0 { // Rough check
t.Errorf("Expected value around 123.456, got %f", val)
}
}
func TestBinaryParsingOversized(t *testing.T) {
// Test oversized field parsing
pml := `<packet name="BinaryOversized">
<version number="1">
<i16 name="normal_value">
<i16 name="oversized_value" oversized="255">
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
// Test data: normal 16-bit value (100), then oversized marker (255) + 16-bit value (1000)
testData := []byte{0x64, 0x00, 0xff, 0xe8, 0x03} // 100, 255, 1000
result, err := packets["BinaryOversized"].Parse(testData, 1, 0)
if err != nil {
t.Fatalf("Binary parse failed: %v", err)
}
if val := result["normal_value"].(uint16); val != 100 {
t.Errorf("Expected normal_value 100, got %d", val)
}
if val := result["oversized_value"].(uint16); val != 1000 {
t.Errorf("Expected oversized_value 1000, got %d", val)
}
}
func TestBinaryParsingType2(t *testing.T) {
// Test type2 switching in binary parsing
pml := `<packet name="BinaryType2">
<version number="1">
<i8 name="stat_type">
<i32 name="stat_value" type2="f32" type2_if="stat_type==6">
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
// Test with stat_type = 6 (should use float)
testData1 := []byte{0x06, 0x00, 0x00, 0x20, 0x41} // stat_type=6, float 10.0
result1, err := packets["BinaryType2"].Parse(testData1, 1, 0)
if err != nil {
t.Fatalf("Binary parse failed: %v", err)
}
if statType := result1["stat_type"].(uint8); statType != 6 {
t.Errorf("Expected stat_type 6, got %d", statType)
}
// Note: The actual type switching logic depends on conditions.go implementation
// This test verifies the parsing structure is correct
}
func TestBinaryParsingArrayMaxSize(t *testing.T) {
// Test array max size limit
pml := `<packet name="BinaryArrayMax">
<version number="1">
<i8 name="item_count">
<array name="items" count="var:item_count" max_size="2">
<substruct>
<i16 name="item_id">
</substruct>
</array>
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
// Test data: count=5, but max_size=2 should limit to 2 items
testData := []byte{0x05, 0x01, 0x00, 0x02, 0x00} // count=5, item1=1, item2=2
result, err := packets["BinaryArrayMax"].Parse(testData, 1, 0)
if err != nil {
t.Fatalf("Binary parse failed: %v", err)
}
items := result["items"].([]map[string]any)
if len(items) != 2 {
t.Errorf("Expected 2 items due to max_size, got %d", len(items))
}
}
func TestErrorHandling(t *testing.T) {
testCases := []struct {
name string
@ -273,6 +575,7 @@ func TestErrorHandling(t *testing.T) {
{"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>"},
{"Invalid oversized", "<packet name=\"Test\"><version number=\"1\"><i32 name=\"id\" oversized=\"abc\"></version></packet>"},
}
for _, tc := range testCases {
@ -295,8 +598,7 @@ func BenchmarkSimplePacket(b *testing.B) {
</version>
</packet>`
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_, err := Parse(pml)
if err != nil {
b.Fatal(err)
@ -304,7 +606,7 @@ func BenchmarkSimplePacket(b *testing.B) {
}
}
func BenchmarkComplexPacket(b *testing.B) {
func BenchmarkComplexPacketWithNewFeatures(b *testing.B) {
pml := `<packet name="Complex">
<version number="562">
<i32 name="player_id,account_id">
@ -313,27 +615,27 @@ func BenchmarkComplexPacket(b *testing.B) {
<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">
<array name="equipment" count="var:equipment_count" max_size="50">
<substruct>
<i16 name="slot_id,item_type">
<color name="primary_color,secondary_color">
<i8 name="enhancement_level" if="item_type!=0">
<i32 name="stat_value" type2="f32" type2_if="item_type==6" oversized="255">
</substruct>
</array>
<i8 name="stat_count">
<array name="stats" count="var:stat_count">
<array name="stats" count="var:stat_count" optional="true">
<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">
<f64 name="percentage" if="stat_type==6">
</substruct>
</array>
</version>
</packet>`
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_, err := Parse(pml)
if err != nil {
b.Fatal(err)
@ -341,47 +643,6 @@ func BenchmarkComplexPacket(b *testing.B) {
}
}
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) {

View File

@ -8,6 +8,97 @@ type PacketDef struct {
Orders map[uint32][]string // Field order by version number
}
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:
if field.Oversized > 0 {
return ctx.readOversizedUint32(field.Oversized)
}
return ctx.readUint32()
case common.TypeInt64, common.TypeSInt64:
return ctx.readUint64()
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.TypeDouble:
return ctx.readFloat64()
case common.TypeColor:
return ctx.readEQ2Color()
case common.TypeEquipment:
return ctx.readEQ2Equipment()
case common.TypeArray:
size := ctx.getArraySize(field.Condition)
if field.MaxArraySize > 0 && size > field.MaxArraySize {
size = field.MaxArraySize
}
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]
}
// FieldDesc describes a single packet field
type FieldDesc struct {
Type common.EQ2DataType // Primary data type
@ -17,4 +108,9 @@ type FieldDesc struct {
Type2 common.EQ2DataType // Alternative data type for conditional parsing
Type2Cond string // Condition for using Type2
Oversized int // Threshold for oversized field handling
DefaultValue int8 // Default value for initialization
MaxArraySize int // Maximum allowed array size
Optional bool // Whether this field is optional
AddToStruct bool // Whether to include in packet structure
AddType common.EQ2DataType // Type to use when adding to packet
}