diff --git a/internal/packets/parser/DOCS.md b/internal/packets/parser/DOCS.md index 8da2d81..ff0e044 100644 --- a/internal/packets/parser/DOCS.md +++ b/internal/packets/parser/DOCS.md @@ -56,6 +56,16 @@ RewardData *RewardInfo `eq2:"substruct,ifvariableset=HasRewards"` // Only parse if we set the "has_equipment" flag Equipment []Equipment `eq2:"equipment,ifflag=has_equipment"` + +// Array index variables - access specific array elements +ModCount uint8 `eq2:"int8"` +Mods []Mod `eq2:"array,arraysize=ModCount"` +// This checks if Mods[0] exists and is truthy +ExtraData []byte `eq2:"char,len=10,ifvariableset=header_info_mod_need_0"` + +// Dynamic array index using %i (replaced with current array index) +StatTypes []uint8 `eq2:"int8,len=5"` +StatValues []any `eq2:"int32,type2=float,type2criteria=stat_type_%i!=6"` ``` ### Type switching (when EQ2 reuses the same bytes for different things) @@ -63,6 +73,21 @@ Equipment []Equipment `eq2:"equipment,ifflag=has_equipment"` // Normally parse as int32, but if StatType != 6, parse as float instead StatType uint8 `eq2:"int8"` StatValue any `eq2:"int32,type2=float,type2criteria=StatType!=6"` + +// String length operators for type switching +NameLength uint8 `eq2:"int8"` +Name string `eq2:"string16,type2=string8,type2criteria=stat_name!>10"` +``` + +### String length operators +Use `!>`, `!<`, `!>=`, `!<=`, `!=` for string length comparisons: +```go +// Switch to string8 if name length > 10 characters +Name string `eq2:"string16,type2=string8,type2criteria=player_name!>10"` + +// Only parse if description is not empty +HasDesc uint8 `eq2:"int8"` +Description string `eq2:"string16,ifvariableset=HasDesc,if=description!>0"` ``` ### Size limits (because EQ2 packets can get weird) @@ -104,12 +129,22 @@ type CharacterData struct { // Nested stuff Stats PlayerStats `eq2:"substruct"` + + // Array index access example + BuffCount uint8 `eq2:"int8"` + Buffs []BuffData `eq2:"array,arraysize=BuffCount"` + // Only parse extended data if first buff exists + ExtendedBuffData []byte `eq2:"char,len=20,ifvariableset=buffs_0"` } type InventoryItem struct { ItemID uint32 `eq2:"int32"` Quantity uint16 `eq2:"int16"` Color common.EQ2Color `eq2:"color"` + + // Type switching based on string length + NameType uint8 `eq2:"int8"` + Name any `eq2:"string16,type2=string8,type2criteria=item_name!<=8"` } type PlayerStats struct { @@ -117,6 +152,34 @@ type PlayerStats struct { Mana uint32 `eq2:"int32"` Stamina uint32 `eq2:"int32"` } + +type BuffData struct { + BuffID uint32 `eq2:"int32"` + Duration uint16 `eq2:"int16"` +} +``` + +## Advanced conditional patterns + +```go +type ComplexPacket struct { + // Array with per-element conditionals using %i + StatCount uint8 `eq2:"int8"` + StatTypes []uint8 `eq2:"int8,len=StatCount"` + StatValues []any `eq2:"int32,type2=float,type2criteria=stat_type_%i!=6"` + + // Array index access for conditionals + ModCount uint8 `eq2:"int8"` + Mods []Mod `eq2:"array,arraysize=ModCount"` + // Parse only if specific array elements exist + Bonus1 uint32 `eq2:"int32,ifvariableset=header_info_mod_need_0"` + Bonus2 uint32 `eq2:"int32,ifvariableset=header_info_mod_need_1"` + + // String length conditionals + PlayerName string `eq2:"string16"` + ShortName string `eq2:"string8,if=player_name!<=8"` + LongDesc string `eq2:"string32,if=player_name!>15"` +} ``` ## Converting from XML @@ -128,4 +191,8 @@ If you've got EQ2's XML packet definitions, the conversion is pretty straightfor | `Type="int32"` | `eq2:"int32"` | | `ArraySizeVariable="count"` | `arraysize=Count` | | `IfVariableSet="flag"` | `ifvariableset=Flag` | -| `Size="5"` | `len=5` | \ No newline at end of file +| `Size="5"` | `len=5` | +| `Type2Criteria="field!=value"` | `type2criteria=Field!=value` | +| `Type2Criteria="name!>10"` | `type2criteria=name!>10` | +| Array index access | `ifvariableset=array_name_0` | +| Dynamic index patterns | `type2criteria=field_%i!=value` | \ No newline at end of file diff --git a/internal/packets/parser/conditions.go b/internal/packets/parser/conditions.go index 556c573..e7f81e6 100644 --- a/internal/packets/parser/conditions.go +++ b/internal/packets/parser/conditions.go @@ -2,6 +2,7 @@ package parser import ( "reflect" + "strconv" "strings" ) @@ -50,17 +51,9 @@ func (p *Parser) evaluateCondition(condition *FieldCondition) bool { return p.isVariableSet(condition.Variable) } - cachedValue, exists := p.fieldCache[condition.Variable] + cachedValue, exists := p.resolveVariable(condition.Variable) if !exists { - if p.currentStruct.IsValid() { - if structField := p.currentStruct.FieldByName(condition.Variable); structField.IsValid() { - cachedValue = structField.Interface() - } else { - return false - } - } else { - return false - } + return false } compareValue, err := p.convertValue(condition.Value, cachedValue) @@ -73,29 +66,16 @@ func (p *Parser) evaluateCondition(condition *FieldCondition) bool { // isVariableSet checks if a variable exists and has a truthy value func (p *Parser) isVariableSet(variable string) bool { - if cachedValue, exists := p.fieldCache[variable]; exists { - return p.isTruthy(cachedValue) + cachedValue, exists := p.resolveVariable(variable) + if !exists { + return false } - - if p.currentStruct.IsValid() { - if field := p.currentStruct.FieldByName(variable); field.IsValid() { - return p.isTruthy(field.Interface()) - } - } - - return false + return p.isTruthy(cachedValue) } // evaluateEqualsCondition checks field equality func (p *Parser) evaluateEqualsCondition(variable, value string) bool { - cachedValue, exists := p.fieldCache[variable] - if !exists && p.currentStruct.IsValid() { - if field := p.currentStruct.FieldByName(variable); field.IsValid() { - cachedValue = field.Interface() - exists = true - } - } - + cachedValue, exists := p.resolveVariable(variable) if !exists { return false } @@ -108,16 +88,34 @@ func (p *Parser) evaluateEqualsCondition(variable, value string) bool { return p.compareValues(cachedValue, compareValue, "==") } -// evaluateType2Criteria evaluates type2 criteria for alternative types +// evaluateType2Criteria evaluates type2 criteria for alternative types with string length support func (p *Parser) evaluateType2Criteria(criteria string) bool { - operators := []string{"!=", "==", ">=", "<=", ">", "<"} + // String length operators: !>, !<, !>=, !<=, != + stringLengthOps := []string{"!>=", "!<=", "!>", "!<", "!="} + + for _, op := range stringLengthOps { + if idx := strings.Index(criteria, op); idx > 0 { + fieldName := strings.TrimSpace(criteria[:idx]) + valueStr := strings.TrimSpace(criteria[idx+len(op):]) + + cachedValue, exists := p.resolveVariable(fieldName) + if !exists { + return false + } + + return p.evaluateStringLengthCondition(cachedValue, valueStr, op) + } + } + + // Standard comparison operators + operators := []string{">=", "<=", ">", "<", "=="} for _, op := range operators { if idx := strings.Index(criteria, op); idx > 0 { fieldName := strings.TrimSpace(criteria[:idx]) valueStr := strings.TrimSpace(criteria[idx+len(op):]) - cachedValue, exists := p.fieldCache[fieldName] + cachedValue, exists := p.resolveVariable(fieldName) if !exists { return false } @@ -133,3 +131,137 @@ func (p *Parser) evaluateType2Criteria(criteria string) bool { return false } + +// evaluateStringLengthCondition evaluates string length conditions +func (p *Parser) evaluateStringLengthCondition(value any, lengthStr, operator string) bool { + var stringVal string + + // Extract string value from various types + switch v := value.(type) { + case string: + stringVal = v + default: + // Try to get string representation from struct fields + if reflect.TypeOf(value).Kind() == reflect.Struct { + val := reflect.ValueOf(value) + if dataField := val.FieldByName("Data"); dataField.IsValid() && dataField.Kind() == reflect.String { + stringVal = dataField.String() + } else { + return false + } + } else { + return false + } + } + + targetLength, err := strconv.Atoi(lengthStr) + if err != nil { + return false + } + + stringLength := len(stringVal) + + switch operator { + case "!>": + return stringLength > targetLength + case "!<": + return stringLength < targetLength + case "!>=": + return stringLength >= targetLength + case "!<=": + return stringLength <= targetLength + case "!=": + return stringLength != targetLength + default: + return false + } +} + +// resolveVariable resolves variables with support for array indices and complex patterns +func (p *Parser) resolveVariable(variable string) (any, bool) { + // Handle %i patterns by replacing with current array index + if strings.Contains(variable, "%i") { + currentIndex := p.GetCurrentArrayIndex() + if currentIndex >= 0 { + variable = strings.ReplaceAll(variable, "%i", strconv.Itoa(currentIndex)) + } + } + + // Check field cache first + if cachedValue, exists := p.fieldCache[variable]; exists { + return cachedValue, true + } + + // Handle array index patterns like "header_info_mod_need_0" + if strings.Contains(variable, "_") { + if value, exists := p.resolveArrayIndexVariable(variable); exists { + return value, true + } + } + + // Check current struct + if p.currentStruct.IsValid() { + if field := p.currentStruct.FieldByName(variable); field.IsValid() { + return field.Interface(), true + } + } + + // Check struct stack for nested resolution + for i := len(p.structStack) - 1; i >= 0; i-- { + if field := p.structStack[i].FieldByName(variable); field.IsValid() { + return field.Interface(), true + } + } + + return nil, false +} + +// resolveArrayIndexVariable handles variables with array index suffixes +func (p *Parser) resolveArrayIndexVariable(variable string) (any, bool) { + parts := strings.Split(variable, "_") + if len(parts) < 2 { + return nil, false + } + + // Try to extract index from last part + lastPart := parts[len(parts)-1] + if index, err := strconv.Atoi(lastPart); err == nil { + // Reconstruct base variable name without index + baseVar := strings.Join(parts[:len(parts)-1], "_") + + // Look for array/slice field with this base name + if cachedValue, exists := p.fieldCache[baseVar]; exists { + return p.getArrayElement(cachedValue, index) + } + + // Check current struct for array field + if p.currentStruct.IsValid() { + if field := p.currentStruct.FieldByName(baseVar); field.IsValid() { + return p.getArrayElement(field.Interface(), index) + } + } + + // Check struct stack + for i := len(p.structStack) - 1; i >= 0; i-- { + if field := p.structStack[i].FieldByName(baseVar); field.IsValid() { + return p.getArrayElement(field.Interface(), index) + } + } + } + + return nil, false +} + +// getArrayElement safely extracts element from array/slice +func (p *Parser) getArrayElement(value any, index int) (any, bool) { + val := reflect.ValueOf(value) + + switch val.Kind() { + case reflect.Slice, reflect.Array: + if index >= 0 && index < val.Len() { + return val.Index(index).Interface(), true + } + } + + return nil, false +} diff --git a/internal/packets/parser/field_tag.go b/internal/packets/parser/field_tag.go index 8f1b52c..2bcceb4 100644 --- a/internal/packets/parser/field_tag.go +++ b/internal/packets/parser/field_tag.go @@ -102,9 +102,23 @@ func (p *Parser) parseTagParameter(fieldTag *FieldTag, part string) { } } -// parseCondition creates FieldCondition from condition string +// parseCondition creates FieldCondition from condition string with enhanced operator support func (p *Parser) parseCondition(condition string) *FieldCondition { - operators := []string{"!=", "==", ">=", "<=", ">", "<", "&", "|"} + // String length operators must be checked first due to overlapping patterns + stringLengthOps := []string{"!>=", "!<=", "!>", "!<", "!="} + + for _, op := range stringLengthOps { + if idx := strings.Index(condition, op); idx > 0 { + return &FieldCondition{ + Variable: strings.TrimSpace(condition[:idx]), + Value: strings.TrimSpace(condition[idx+len(op):]), + Operator: op, + } + } + } + + // Standard comparison operators + operators := []string{">=", "<=", ">", "<", "==", "&", "|"} for _, op := range operators { if idx := strings.Index(condition, op); idx > 0 { diff --git a/internal/packets/parser/helpers.go b/internal/packets/parser/helpers.go index d529f85..324db0a 100644 --- a/internal/packets/parser/helpers.go +++ b/internal/packets/parser/helpers.go @@ -3,20 +3,39 @@ package parser import ( "reflect" "strconv" + "strings" ) -// getDynamicLength gets length from another field with stack support +// getDynamicLength gets length from another field with stack support and array index resolution func (p *Parser) getDynamicLength(fieldName string) int { + // Handle %i patterns by replacing with current array index + if strings.Contains(fieldName, "%i") { + currentIndex := p.GetCurrentArrayIndex() + if currentIndex >= 0 { + fieldName = strings.ReplaceAll(fieldName, "%i", strconv.Itoa(currentIndex)) + } + } + + // Check field cache first if cachedValue, exists := p.fieldCache[fieldName]; exists { return p.valueToInt(cachedValue) } + // Handle array index patterns like "header_info_mod_need_0" + if strings.Contains(fieldName, "_") { + if value, exists := p.resolveArrayIndexVariableForLength(fieldName); exists { + return p.valueToInt(value) + } + } + + // Check current struct if p.currentStruct.IsValid() { if field := p.currentStruct.FieldByName(fieldName); field.IsValid() { return p.valueToInt(field.Interface()) } } + // Check struct stack for nested resolution for i := len(p.structStack) - 1; i >= 0; i-- { if field := p.structStack[i].FieldByName(fieldName); field.IsValid() { return p.valueToInt(field.Interface()) @@ -26,7 +45,57 @@ func (p *Parser) getDynamicLength(fieldName string) int { return 0 } -// convertValue converts string to appropriate type for comparison +// resolveArrayIndexVariableForLength handles array index variable resolution for length calculations +func (p *Parser) resolveArrayIndexVariableForLength(variable string) (any, bool) { + parts := strings.Split(variable, "_") + if len(parts) < 2 { + return nil, false + } + + // Try to extract index from last part + lastPart := parts[len(parts)-1] + if index, err := strconv.Atoi(lastPart); err == nil { + // Reconstruct base variable name without index + baseVar := strings.Join(parts[:len(parts)-1], "_") + + // Look for array/slice field with this base name + if cachedValue, exists := p.fieldCache[baseVar]; exists { + return p.getArrayElementSafe(cachedValue, index) + } + + // Check current struct for array field + if p.currentStruct.IsValid() { + if field := p.currentStruct.FieldByName(baseVar); field.IsValid() { + return p.getArrayElementSafe(field.Interface(), index) + } + } + + // Check struct stack + for i := len(p.structStack) - 1; i >= 0; i-- { + if field := p.structStack[i].FieldByName(baseVar); field.IsValid() { + return p.getArrayElementSafe(field.Interface(), index) + } + } + } + + return nil, false +} + +// getArrayElementSafe safely extracts element from array/slice with bounds checking +func (p *Parser) getArrayElementSafe(value any, index int) (any, bool) { + val := reflect.ValueOf(value) + + switch val.Kind() { + case reflect.Slice, reflect.Array: + if index >= 0 && index < val.Len() { + return val.Index(index).Interface(), true + } + } + + return nil, false +} + +// convertValue converts string to appropriate type for comparison with enhanced string support func (p *Parser) convertValue(valueStr string, reference any) (any, error) { switch reference.(type) { case uint8, int8: @@ -57,6 +126,14 @@ func (p *Parser) convertValue(valueStr string, reference any) (any, error) { return valueStr, nil } + // Handle struct types with Data field (EQ2String types) + if reflect.TypeOf(reference).Kind() == reflect.Struct { + refVal := reflect.ValueOf(reference) + if dataField := refVal.FieldByName("Data"); dataField.IsValid() && dataField.Kind() == reflect.String { + return valueStr, nil + } + } + if val, err := strconv.ParseInt(valueStr, 0, 32); err == nil { return int(val), nil } @@ -64,8 +141,16 @@ func (p *Parser) convertValue(valueStr string, reference any) (any, error) { return valueStr, nil } -// compareValues performs comparison between two values +// compareValues performs comparison between two values with enhanced string support func (p *Parser) compareValues(a, b any, op string) bool { + // Handle string comparisons first + if p.isStringValue(a) && p.isStringValue(b) { + aStr := p.extractStringValue(a) + bStr := p.extractStringValue(b) + return p.compareStrings(aStr, bStr, op) + } + + // Fall back to numeric comparison aVal := p.valueToInt64(a) bVal := p.valueToInt64(b) @@ -91,7 +176,59 @@ func (p *Parser) compareValues(a, b any, op string) bool { return false } -// valueToInt converts various types to int +// isStringValue checks if a value is or contains a string +func (p *Parser) isStringValue(value any) bool { + switch value.(type) { + case string: + return true + default: + if reflect.TypeOf(value).Kind() == reflect.Struct { + val := reflect.ValueOf(value) + if dataField := val.FieldByName("Data"); dataField.IsValid() && dataField.Kind() == reflect.String { + return true + } + } + return false + } +} + +// extractStringValue extracts string from value or struct Data field +func (p *Parser) extractStringValue(value any) string { + switch v := value.(type) { + case string: + return v + default: + if reflect.TypeOf(value).Kind() == reflect.Struct { + val := reflect.ValueOf(value) + if dataField := val.FieldByName("Data"); dataField.IsValid() && dataField.Kind() == reflect.String { + return dataField.String() + } + } + return "" + } +} + +// compareStrings performs string comparison operations +func (p *Parser) compareStrings(a, b, op string) bool { + switch op { + case "==": + return a == b + case "!=": + return a != b + case ">": + return a > b + case ">=": + return a >= b + case "<": + return a < b + case "<=": + return a <= b + default: + return false + } +} + +// valueToInt converts various types to int with enhanced type support func (p *Parser) valueToInt(v any) int { switch val := v.(type) { case uint8: @@ -112,11 +249,27 @@ func (p *Parser) valueToInt(v any) int { return int(val) case int: return val + case bool: + if val { + return 1 + } + return 0 + default: + // Handle struct types that might have numeric fields + if reflect.TypeOf(v).Kind() == reflect.Struct { + refVal := reflect.ValueOf(v) + // Try common numeric field names + for _, fieldName := range []string{"Value", "Size", "Length", "Count"} { + if field := refVal.FieldByName(fieldName); field.IsValid() { + return p.valueToInt(field.Interface()) + } + } + } } return 0 } -// valueToInt64 converts various types to int64 for comparison +// valueToInt64 converts various types to int64 for comparison with enhanced support func (p *Parser) valueToInt64(v any) int64 { switch val := v.(type) { case uint8: @@ -141,11 +294,27 @@ func (p *Parser) valueToInt64(v any) int64 { return int64(val) case float64: return int64(val) + case bool: + if val { + return 1 + } + return 0 + default: + // Handle struct types that might have numeric fields + if reflect.TypeOf(v).Kind() == reflect.Struct { + refVal := reflect.ValueOf(v) + // Try common numeric field names + for _, fieldName := range []string{"Value", "Size", "Length", "Count"} { + if field := refVal.FieldByName(fieldName); field.IsValid() { + return p.valueToInt64(field.Interface()) + } + } + } } return 0 } -// isTruthy checks if a value is truthy +// isTruthy checks if a value is truthy with enhanced type support func (p *Parser) isTruthy(v any) bool { switch val := v.(type) { case bool: @@ -154,6 +323,21 @@ func (p *Parser) isTruthy(v any) bool { return p.valueToInt64(val) != 0 case string: return val != "" + default: + // Handle struct types + if reflect.TypeOf(v).Kind() == reflect.Struct { + refVal := reflect.ValueOf(v) + // Check for Data field (string types) + if dataField := refVal.FieldByName("Data"); dataField.IsValid() && dataField.Kind() == reflect.String { + return dataField.String() != "" + } + // Check for numeric fields + for _, fieldName := range []string{"Value", "Size", "Length", "Count"} { + if field := refVal.FieldByName(fieldName); field.IsValid() { + return p.valueToInt64(field.Interface()) != 0 + } + } + } } return false }