From c42485f8748533469419d1f85173c55eb134d1fa Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Mon, 28 Jul 2025 13:02:14 -0500 Subject: [PATCH] implement missing types, conditions --- internal/packets/parser/context.go | 396 +++++++++++++++++++++++++ internal/packets/parser/lexer.go | 6 +- internal/packets/parser/parser.go | 124 ++++++-- internal/packets/parser/parser_test.go | 359 +++++++++++++++++++--- internal/packets/parser/structs.go | 110 ++++++- 5 files changed, 916 insertions(+), 79 deletions(-) create mode 100644 internal/packets/parser/context.go diff --git a/internal/packets/parser/context.go b/internal/packets/parser/context.go new file mode 100644 index 0000000..f7ca851 --- /dev/null +++ b/internal/packets/parser/context.go @@ -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 +} diff --git a/internal/packets/parser/lexer.go b/internal/packets/parser/lexer.go index a22ab01..8a992c7 100644 --- a/internal/packets/parser/lexer.go +++ b/internal/packets/parser/lexer.go @@ -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 } diff --git a/internal/packets/parser/parser.go b/internal/packets/parser/parser.go index ddfd145..0066df4 100644 --- a/internal/packets/parser/parser.go +++ b/internal/packets/parser/parser.go @@ -58,23 +58,24 @@ type Parser struct { // Type mapping for efficient lookup var typeMap = map[string]common.EQ2DataType{ - "i8": common.TypeInt8, - "i16": common.TypeInt16, - "i32": common.TypeInt32, - "i64": common.TypeInt64, - "si8": common.TypeSInt8, - "si16": common.TypeSInt16, - "si32": common.TypeSInt32, - "si64": common.TypeSInt64, - "f32": common.TypeFloat, - "f64": common.TypeDouble, - "str8": common.TypeString8, - "str16": common.TypeString16, - "str32": common.TypeString32, - "char": common.TypeChar, - "color": common.TypeColor, - "equip": common.TypeEquipment, - "array": common.TypeArray, + "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, + "double": common.TypeDouble, // XML compatibility + "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 @@ -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": @@ -527,14 +532,32 @@ func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix s } fieldDesc := FieldDesc{ - Type: common.TypeArray, - Condition: attrs["count"], + 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 { @@ -640,13 +663,70 @@ func (p *Parser) parseField(packetDef *PacketDef, fieldOrder *[]string, prefix s } fieldDesc := FieldDesc{ - Type: dataType, - Condition: attrs["if"], + 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 } } @@ -663,4 +743,4 @@ func Parse(content string) (map[string]*PacketDef, error) { parser := NewParser(content) defer parser.cleanup() return parser.Parse() -} \ No newline at end of file +} diff --git a/internal/packets/parser/parser_test.go b/internal/packets/parser/parser_test.go index 5a08023..54907a7 100644 --- a/internal/packets/parser/parser_test.go +++ b/internal/packets/parser/parser_test.go @@ -49,6 +49,187 @@ func TestBasicParsing(t *testing.T) { } } +func TestFloat64Support(t *testing.T) { + pml := ` + + + + + + ` + + 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 := ` + + + + + + ` + + 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 := ` + + + + + + ` + + 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 := ` + + + + + + ` + + 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 := ` + + + + + + + + + ` + + 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 := ` + + + + + + + + + ` + + 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 := ` @@ -265,6 +446,127 @@ func TestComments(t *testing.T) { } } +func TestBinaryParsingFloat64(t *testing.T) { + // Test binary parsing with float64 + pml := ` + + + + ` + + 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 := ` + + + + + ` + + 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 := ` + + + + + ` + + 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 := ` + + + + + + + + + ` + + 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", ""}, {"Invalid XML", ""}, {"Missing quotes", ""}, + {"Invalid oversized", ""}, } for _, tc := range testCases { @@ -295,8 +598,7 @@ func BenchmarkSimplePacket(b *testing.B) { ` - 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 := ` @@ -313,27 +615,27 @@ func BenchmarkComplexPacket(b *testing.B) { - + + - + - + ` - 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 := `` - for i := 0; i < 100; i++ { - pmlBuilder += `` - } - pmlBuilder += `` - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := Parse(pmlBuilder) - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkWithSubstructs(b *testing.B) { - pml := ` - - - - - - - - - - - ` - - b.ResetTimer() - for i := 0; i < b.N; i++ { - parser := NewParser(pml) - _, err := parser.Parse() - if err != nil { - b.Fatal(err) - } - } -} - // Helper function to compare slices func equalSlices(a, b []string) bool { if len(a) != len(b) { diff --git a/internal/packets/parser/structs.go b/internal/packets/parser/structs.go index 78ee506..de12dba 100644 --- a/internal/packets/parser/structs.go +++ b/internal/packets/parser/structs.go @@ -8,13 +8,109 @@ 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 - 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 + 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 + 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 }