diff --git a/internal/packets/parser/DOCS.md b/internal/packets/parser/DOCS.md index ff0e044..067e6c5 100644 --- a/internal/packets/parser/DOCS.md +++ b/internal/packets/parser/DOCS.md @@ -68,6 +68,37 @@ StatTypes []uint8 `eq2:"int8,len=5"` StatValues []any `eq2:"int32,type2=float,type2criteria=stat_type_%i!=6"` ``` +### Comma-separated conditions +Multiple variables can be checked in a single condition using comma-separated lists: + +```go +// Parse only if NONE of the listed variables are set +NumEffects uint8 `eq2:"int8,ifvariablenotset=header_info_header_unknown_0_0,header_unknown_0"` + +// Parse if ANY of the listed variables are set +BonusData []byte `eq2:"char,len=20,ifvariableset=has_bonus,has_special,has_extra"` + +// Parse if ANY of the listed flags are set +OptionalField uint32 `eq2:"int32,ifflag=debug_mode,test_mode,dev_mode"` + +// Parse if ALL of the listed flags are NOT set +ProductionData []byte `eq2:"char,len=50,ifflagnotset=debug_mode,test_mode"` + +// Multiple equals conditions (ANY must be true) +TypeData any `eq2:"int32,ifequals=type=1,category=special"` + +// Multiple not-equals conditions (ALL must be true) +Value uint16 `eq2:"int16,ifnotequals=status=disabled,flag=hidden"` +``` + +**Comma-separated logic rules:** +- `ifvariableset` - TRUE if ANY variable is set +- `ifvariablenotset` - TRUE if ALL variables are NOT set +- `ifflag` - TRUE if ANY flag is set +- `ifflagnotset` - TRUE if ALL flags are NOT set +- `ifequals` - TRUE if ANY condition matches +- `ifnotequals` - TRUE if ALL conditions are true (none match) + ### Type switching (when EQ2 reuses the same bytes for different things) ```go // Normally parse as int32, but if StatType != 6, parse as float instead @@ -135,6 +166,12 @@ type CharacterData struct { Buffs []BuffData `eq2:"array,arraysize=BuffCount"` // Only parse extended data if first buff exists ExtendedBuffData []byte `eq2:"char,len=20,ifvariableset=buffs_0"` + + // Comma-separated conditions example + HeaderFlags uint8 `eq2:"int8"` + // Parse effects only if neither unknown field is set + NumEffects uint8 `eq2:"int8,ifvariablenotset=header_info_header_unknown_0_0,header_unknown_0"` + Effects []EffectData `eq2:"array,arraysize=NumEffects"` } type InventoryItem struct { @@ -157,6 +194,11 @@ type BuffData struct { BuffID uint32 `eq2:"int32"` Duration uint16 `eq2:"int16"` } + +type EffectData struct { + Effect common.EQ2String16 `eq2:"string16"` + Percentage uint8 `eq2:"int8"` +} ``` ## Advanced conditional patterns @@ -179,6 +221,14 @@ type ComplexPacket struct { PlayerName string `eq2:"string16"` ShortName string `eq2:"string8,if=player_name!<=8"` LongDesc string `eq2:"string32,if=player_name!>15"` + + // Comma-separated multi-condition examples + DebugInfo []byte `eq2:"char,len=100,ifflag=debug_mode,test_mode,dev_mode"` + ProdData []byte `eq2:"char,len=50,ifflagnotset=debug_mode,test_mode,dev_mode"` + + // Multiple variable checks + OptionalData []byte `eq2:"char,len=20,ifvariableset=has_optional,has_extended"` + CleanupData []byte `eq2:"char,len=10,ifvariablenotset=dirty_flag,temp_flag,cache_flag"` } ``` @@ -191,6 +241,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` | +| `IfVariableNotSet="var1,var2"` | `ifvariablenotset=var1,var2` | +| `IfFlag="flag1,flag2"` | `ifflag=flag1,flag2` | | `Size="5"` | `len=5` | | `Type2Criteria="field!=value"` | `type2criteria=Field!=value` | | `Type2Criteria="name!>10"` | `type2criteria=name!>10` | diff --git a/internal/packets/parser/conditions.go b/internal/packets/parser/conditions.go index e7f81e6..9c2a200 100644 --- a/internal/packets/parser/conditions.go +++ b/internal/packets/parser/conditions.go @@ -12,32 +12,30 @@ func (p *Parser) evaluateAllConditions(fieldTag *FieldTag, _ reflect.Value) bool return false } - if fieldTag.IfVariableSet != "" && !p.isVariableSet(fieldTag.IfVariableSet) { + if fieldTag.IfVariableSet != "" && !p.evaluateVariableSetCondition(fieldTag.IfVariableSet) { return false } - if fieldTag.IfVariableNotSet != "" && p.isVariableSet(fieldTag.IfVariableNotSet) { + if fieldTag.IfVariableNotSet != "" && !p.evaluateVariableNotSetCondition(fieldTag.IfVariableNotSet) { return false } - if fieldTag.IfFlag != "" && !p.flags[fieldTag.IfFlag] { + if fieldTag.IfFlag != "" && !p.evaluateFlagCondition(fieldTag.IfFlag) { return false } - if fieldTag.IfFlagNotSet != "" && p.flags[fieldTag.IfFlagNotSet] { + if fieldTag.IfFlagNotSet != "" && !p.evaluateFlagNotSetCondition(fieldTag.IfFlagNotSet) { return false } if fieldTag.IfEquals != "" { - parts := strings.Split(fieldTag.IfEquals, "=") - if len(parts) == 2 && !p.evaluateEqualsCondition(parts[0], parts[1]) { + if !p.evaluateEqualsConditions(fieldTag.IfEquals) { return false } } if fieldTag.IfNotEquals != "" { - parts := strings.Split(fieldTag.IfNotEquals, "=") - if len(parts) == 2 && p.evaluateEqualsCondition(parts[0], parts[1]) { + if !p.evaluateNotEqualsConditions(fieldTag.IfNotEquals) { return false } } @@ -45,6 +43,96 @@ func (p *Parser) evaluateAllConditions(fieldTag *FieldTag, _ reflect.Value) bool return true } +// evaluateVariableSetCondition handles comma-separated variable set conditions +func (p *Parser) evaluateVariableSetCondition(variables string) bool { + varList := p.splitCommaSeparated(variables) + // Return true if ANY variable is set + for _, variable := range varList { + if p.isVariableSet(variable) { + return true + } + } + return false +} + +// evaluateVariableNotSetCondition handles comma-separated variable not set conditions +func (p *Parser) evaluateVariableNotSetCondition(variables string) bool { + varList := p.splitCommaSeparated(variables) + // Return true if ALL variables are not set + for _, variable := range varList { + if p.isVariableSet(variable) { + return false + } + } + return true +} + +// evaluateFlagCondition handles comma-separated flag conditions +func (p *Parser) evaluateFlagCondition(flags string) bool { + flagList := p.splitCommaSeparated(flags) + // Return true if ANY flag is set + for _, flag := range flagList { + if p.flags[flag] { + return true + } + } + return false +} + +// evaluateFlagNotSetCondition handles comma-separated flag not set conditions +func (p *Parser) evaluateFlagNotSetCondition(flags string) bool { + flagList := p.splitCommaSeparated(flags) + // Return true if ALL flags are not set + for _, flag := range flagList { + if p.flags[flag] { + return false + } + } + return true +} + +// evaluateEqualsConditions handles comma-separated equals conditions +func (p *Parser) evaluateEqualsConditions(conditions string) bool { + conditionList := p.splitCommaSeparated(conditions) + // Return true if ANY condition is true + for _, condition := range conditionList { + parts := strings.Split(condition, "=") + if len(parts) == 2 && p.evaluateEqualsCondition(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) { + return true + } + } + return false +} + +// evaluateNotEqualsConditions handles comma-separated not equals conditions +func (p *Parser) evaluateNotEqualsConditions(conditions string) bool { + conditionList := p.splitCommaSeparated(conditions) + // Return true if ALL conditions are true (all not equal) + for _, condition := range conditionList { + parts := strings.Split(condition, "=") + if len(parts) == 2 { + if p.evaluateEqualsCondition(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) { + return false // This condition is false (they are equal) + } + } + } + return true +} + +// splitCommaSeparated splits a comma-separated string and trims whitespace +func (p *Parser) splitCommaSeparated(input string) []string { + if input == "" { + return []string{} + } + + parts := strings.Split(input, ",") + result := make([]string, len(parts)) + for i, part := range parts { + result[i] = strings.TrimSpace(part) + } + return result +} + // evaluateCondition evaluates conditions for conditional fields func (p *Parser) evaluateCondition(condition *FieldCondition) bool { if condition.Type == "simple" {