From b2a2e9366be0b4e718106c05318913a0a7f9b797 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Mon, 28 Jul 2025 21:55:15 -0500 Subject: [PATCH] Make substructs implicit in arrays --- internal/packets/PARSER.md | 12 +- internal/packets/parser/context.go | 37 +- internal/packets/parser/parser.go | 46 +- internal/packets/parser/parser_test.go | 178 ++++++- internal/packets/parser/structs.go | 2 +- internal/packets/substructs/AASpellInfo.xml | 39 ++ .../substructs/BaseItemDescription.xml | 484 ++++++++++++++++++ 7 files changed, 718 insertions(+), 80 deletions(-) create mode 100644 internal/packets/substructs/AASpellInfo.xml create mode 100644 internal/packets/substructs/BaseItemDescription.xml diff --git a/internal/packets/PARSER.md b/internal/packets/PARSER.md index d80270c..9a8db77 100644 --- a/internal/packets/PARSER.md +++ b/internal/packets/PARSER.md @@ -82,12 +82,6 @@ Fast XML-like parser for binary packet structures with versioning and conditiona Organize related fields with automatic prefixing: ```xml - - - - - - @@ -101,10 +95,8 @@ Organize related fields with automatic prefixing: ```xml - - - - + + ``` diff --git a/internal/packets/parser/context.go b/internal/packets/parser/context.go index f7ca851..e063575 100644 --- a/internal/packets/parser/context.go +++ b/internal/packets/parser/context.go @@ -207,13 +207,13 @@ func (ctx *ParseContext) evaluateCondition(condition string) bool { return (ctx.flags & ctx.getFlagValue(flagName)) == 0 } - // Variable conditions: var:name or !var:name + // Variable conditions: var:name or !var:name (with %i support) if strings.HasPrefix(condition, "var:") { - varName := condition[4:] + varName := ctx.resolveVariableName(condition[4:]) return ctx.hasVar(varName) } if strings.HasPrefix(condition, "!var:") { - varName := condition[5:] + varName := ctx.resolveVariableName(condition[5:]) return !ctx.hasVar(varName) } @@ -222,27 +222,36 @@ func (ctx *ParseContext) evaluateCondition(condition string) bool { return ctx.evaluateVersionCondition(condition) } - // String length operators: name!>5, name!<=10 + // Bitwise AND: header_flag&0x01 (with %i support) + if strings.Contains(condition, "&0x") { + parts := strings.SplitN(condition, "&", 2) + varName := ctx.resolveVariableName(parts[0]) + hexValue, _ := strconv.ParseUint(parts[1], 0, 64) + varValue := ctx.getVarValue(varName) + return (varValue & hexValue) != 0 + } + + // String length operators: name!>5, name!<=10 (with %i support) stringOps := []string{"!>=", "!<=", "!>", "!<", "!="} for _, op := range stringOps { if idx := strings.Index(condition, op); idx > 0 { - varName := condition[:idx] + varName := ctx.resolveVariableName(condition[:idx]) valueStr := condition[idx+len(op):] return ctx.evaluateStringLength(varName, valueStr, op) } } - // Comparison operators: >=, <=, >, <, ==, != + // Comparison operators: >=, <=, >, <, ==, != (with %i support) compOps := []string{">=", "<=", ">", "<", "==", "!="} for _, op := range compOps { if idx := strings.Index(condition, op); idx > 0 { - varName := condition[:idx] + varName := ctx.resolveVariableName(condition[:idx]) valueStr := condition[idx+len(op):] return ctx.evaluateComparison(varName, valueStr, op) } } - // Simple variable existence + // Simple variable existence (with %i support) resolvedName := ctx.resolveVariableName(condition) return ctx.hasVar(resolvedName) } @@ -394,3 +403,15 @@ func (ctx *ParseContext) resolveVariableName(name string) string { } return name } + +func (ctx *ParseContext) setVarWithArrayIndex(name string, value any) { + // Always set the base variable name + ctx.vars[name] = value + + // If we're in an array context, also set the indexed variable + if len(ctx.arrayStack) > 0 { + currentIndex := ctx.arrayStack[len(ctx.arrayStack)-1] + indexedName := name + "_" + strconv.Itoa(currentIndex) + ctx.vars[indexedName] = value + } +} diff --git a/internal/packets/parser/parser.go b/internal/packets/parser/parser.go index 0066df4..3c0efde 100644 --- a/internal/packets/parser/parser.go +++ b/internal/packets/parser/parser.go @@ -520,7 +520,7 @@ func (p *Parser) parseGroup(packetDef *PacketDef, fieldOrder *[]string, prefix s return nil } -// Handles array elements +// Handles array elements - FIXED: Remove redundant substruct wrapper func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix string) error { attrs := p.current.Attributes @@ -565,7 +565,8 @@ func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix s } } - if p.current.Type == TokenSelfCloseTag { + // Arrays with substruct references or explicit self-closing syntax are self-closing + if p.current.Type == TokenSelfCloseTag || fieldDesc.SubDef != nil { p.advance() packetDef.Fields[arrayName] = fieldDesc *fieldOrder = append(*fieldOrder, arrayName) @@ -575,30 +576,9 @@ func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix s 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" { + // Handle direct child elements as substruct fields (no wrapper needed) + if fieldDesc.SubDef == nil { subDef := NewPacketDef(16) - - p.pushTag("substruct") - p.advance() - subOrder := fieldOrderPool.Get().(*[]string) *subOrder = (*subOrder)[:0] defer fieldOrderPool.Put(subOrder) @@ -608,18 +588,12 @@ func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix s 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) + // Only create substruct if we actually have fields + if len(*subOrder) > 0 { + subDef.Orders[1] = make([]string, len(*subOrder)) + copy(subDef.Orders[1], *subOrder) + fieldDesc.SubDef = subDef } - - 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" { diff --git a/internal/packets/parser/parser_test.go b/internal/packets/parser/parser_test.go index 54907a7..e1afb25 100644 --- a/internal/packets/parser/parser_test.go +++ b/internal/packets/parser/parser_test.go @@ -182,9 +182,7 @@ func TestArrayMaxSize(t *testing.T) { - - - + ` @@ -207,9 +205,7 @@ func TestArrayOptionalAttributes(t *testing.T) { - - - + ` @@ -275,10 +271,8 @@ func TestArrayParsing(t *testing.T) { - - - - + + ` @@ -541,9 +535,7 @@ func TestBinaryParsingArrayMaxSize(t *testing.T) { - - - + ` @@ -567,6 +559,146 @@ func TestBinaryParsingArrayMaxSize(t *testing.T) { } } +func TestArrayIndexConditions(t *testing.T) { + // Test array index substitution in conditions + pml := ` + + + + + + + + + + + ` + + packets, err := Parse(pml) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + packet := packets["ArrayConditionTest"] + + // Verify fields were parsed correctly + statsField := packet.Fields["stats"] + if statsField.Type != common.TypeArray { + t.Error("stats should be TypeArray") + } + + if statsField.SubDef == nil { + t.Fatal("SubDef should not be nil") + } + + // Check that conditions were preserved + modifiedValue := statsField.SubDef.Fields["modified_value"] + if modifiedValue.Condition != "stat_type_%i>=1&stat_type_%i<=5" { + t.Errorf("Expected 'stat_type_%%i>=1&stat_type_%%i<=5', got '%s'", modifiedValue.Condition) + } + + percentage := statsField.SubDef.Fields["percentage"] + if percentage.Condition != "stat_type_%i==6" { + t.Errorf("Expected 'stat_type_%%i==6', got '%s'", percentage.Condition) + } + + description := statsField.SubDef.Fields["description"] + if description.Condition != "!var:stat_type_%i" { + t.Errorf("Expected '!var:stat_type_%%i', got '%s'", description.Condition) + } +} + +func TestArrayIndexBinaryParsing(t *testing.T) { + // Test that array index conditions work during binary parsing + pml := ` + + + + + + + + + ` + + packets, err := Parse(pml) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Test data: 2 stats, first with type=5, second with type=6 + // stat_count=2, stat1: type=5, base=100, stat2: type=6, base=200, percentage=1.5 + testData := []byte{ + 0x02, // stat_count = 2 + 0x05, 0x64, 0x00, 0x00, 0x00, // stat 1: type=5, base=100 (no percentage) + 0x06, 0xC8, 0x00, 0x00, 0x00, // stat 2: type=6, base=200 + 0x00, 0x00, 0xC0, 0x3F, // percentage=1.5 (float32) + } + + result, err := packets["ArrayConditionBinary"].Parse(testData, 1, 0) + if err != nil { + t.Fatalf("Binary parse failed: %v", err) + } + + stats := result["stats"].([]map[string]any) + if len(stats) != 2 { + t.Fatalf("Expected 2 stats, got %d", len(stats)) + } + + // First stat (type=5) should not have percentage field + stat1 := stats[0] + if stat1["stat_type"].(uint8) != 5 { + t.Errorf("Expected stat_type 5, got %d", stat1["stat_type"]) + } + if _, hasPercentage := stat1["percentage"]; hasPercentage { + t.Error("Stat type 5 should not have percentage field") + } + + // Second stat (type=6) should have percentage field + stat2 := stats[1] + if stat2["stat_type"].(uint8) != 6 { + t.Errorf("Expected stat_type 6, got %d", stat2["stat_type"]) + } + if percentage, hasPercentage := stat2["percentage"]; !hasPercentage { + t.Error("Stat type 6 should have percentage field") + } else if percentage.(float32) < 1.4 || percentage.(float32) > 1.6 { + t.Errorf("Expected percentage around 1.5, got %f", percentage) + } +} + +func TestComplexArrayConditions(t *testing.T) { + // Test complex conditions with array indices + pml := ` + + + + + + + + + + ` + + packets, err := Parse(pml) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + packet := packets["ComplexArrayTest"] + itemsField := packet.Fields["items"] + + enhancement := itemsField.SubDef.Fields["enhancement"] + if enhancement.Condition != "item_type_%i!=0&item_flags_%i&0x01" { + t.Errorf("Enhancement condition wrong: %s", enhancement.Condition) + } + + specialColor := itemsField.SubDef.Fields["special_color"] + if specialColor.Condition != "item_type_%i>=100,item_flags_%i&0x02" { + t.Errorf("Special color condition wrong: %s", specialColor.Condition) + } +} + func TestErrorHandling(t *testing.T) { testCases := []struct { name string @@ -616,21 +748,17 @@ func BenchmarkComplexPacketWithNewFeatures(b *testing.B) { - - - - - - + + + + - - - - - - + + + + ` diff --git a/internal/packets/parser/structs.go b/internal/packets/parser/structs.go index de12dba..356db6c 100644 --- a/internal/packets/parser/structs.go +++ b/internal/packets/parser/structs.go @@ -31,7 +31,7 @@ func (def *PacketDef) parseStruct(ctx *ParseContext) (map[string]any, error) { value := def.parseField(ctx, field, fieldType, fieldName) result[fieldName] = value - ctx.setVar(fieldName, value) + ctx.setVarWithArrayIndex(fieldName, value) } return result, nil diff --git a/internal/packets/substructs/AASpellInfo.xml b/internal/packets/substructs/AASpellInfo.xml new file mode 100644 index 0000000..257d8b3 --- /dev/null +++ b/internal/packets/substructs/AASpellInfo.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/internal/packets/substructs/BaseItemDescription.xml b/internal/packets/substructs/BaseItemDescription.xml new file mode 100644 index 0000000..e1ccf37 --- /dev/null +++ b/internal/packets/substructs/BaseItemDescription.xml @@ -0,0 +1,484 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file