implement new features on parser

This commit is contained in:
Sky Johnson 2025-07-26 12:06:44 -05:00
parent ba40814729
commit 15dab24125
4 changed files with 437 additions and 40 deletions

View File

@ -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
@ -129,3 +192,7 @@ If you've got EQ2's XML packet definitions, the conversion is pretty straightfor
| `ArraySizeVariable="count"` | `arraysize=Count` |
| `IfVariableSet="flag"` | `ifvariableset=Flag` |
| `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` |

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}