diff --git a/pkg/nip/rule.go b/pkg/nip/rule.go index 994cbbf..79375f2 100644 --- a/pkg/nip/rule.go +++ b/pkg/nip/rule.go @@ -25,6 +25,7 @@ var ( maxQtyRegexp = regexp.MustCompile(`(\[maxquantity]\s*(<=|<|>|>=|!=|==)\s*([0-9]+))`) tierRegexp = regexp.MustCompile(`(\[tier]\s*(<=|<|>|>=|!=|==)\s*([0-9]+))`) mercTierRegexp = regexp.MustCompile(`(\[merctier]\s*(<=|<|>|>=|!=|==)\s*([0-9]+))`) + whitespaceRegexp = regexp.MustCompile(`\s+`) ) type Rule struct { @@ -38,6 +39,10 @@ type Rule struct { stage1 *vm.Program stage2 *vm.Program requiredStats []string + skillTabSum bool + classSkillSum bool + resistSum bool + nonResistStat bool } type RuleResult int @@ -188,7 +193,7 @@ func NewRule(rawRule string, filename string, lineNumber int) (Rule, error) { mercTier: mercTier, } - parts := strings.Split(rule, "#") + parts := strings.SplitN(rule, "#", 3) if len(parts) > 0 { stage1 := strings.TrimSpace(parts[0]) @@ -213,10 +218,24 @@ func NewRule(rawRule string, filename string, lineNumber int) (Rule, error) { if stage2 != "" { // Extract stats before removing brackets for compilation r.requiredStats = getRequiredStatsForRule(stage2) + stage2Source := "" + rawParts := strings.SplitN(rawRule, "#", 3) + if len(rawParts) > 1 { + stage2Source = strings.ToLower(rawParts[1]) + } + r.skillTabSum = strings.Contains(stage2Source, "[itemaddskilltab]") + r.classSkillSum = strings.Contains(stage2Source, "[itemaddclassskills]") + if strings.Contains(stage2Source, "resist") { + r.resistSum = strings.Contains(stage2Source, "+") || strings.Contains(stage2Source, "-") || + (strings.Contains(stage2Source, "(") && strings.Contains(stage2Source, ")")) + } statsMap := make(map[string]int) for _, prop := range r.requiredStats { statsMap[prop] = 0 + if !strings.Contains(prop, "resist") { + r.nonResistStat = true + } } // Normalize whitespace around operators in parenthesized expressions @@ -247,7 +266,7 @@ func normalizeParenthesizedExpressions(expr string) string { expr = strings.ReplaceAll(expr, "<=", " <= ") // Fix extra spaces - expr = regexp.MustCompile(`\s+`).ReplaceAllString(expr, " ") + expr = whitespaceRegexp.ReplaceAllString(expr, " ") // Normalize parentheses spacing expr = strings.ReplaceAll(expr, "( ", "(") @@ -258,56 +277,45 @@ func normalizeParenthesizedExpressions(expr string) string { func (r Rule) Evaluate(it data.Item) (RuleResult, error) { // Stage 1: Basic properties evaluation - stage1Props := make(map[string]int) - for prop := range fixedPropsList { - switch prop { - case "type": - stage1Props["type"] = it.Type().ID - case "quality": - stage1Props["quality"] = int(it.Quality) - case "class": - stage1Props["class"] = int(it.Desc().Tier()) - case "name": - stage1Props["name"] = it.ID - case "flag": - // 0x400000 (eth) | 0x4000000 (runeword) kolbot style. Bit values are - // shared with the rewriter (replaceStringPropertiesInStage1) via the - // package-level flagBit* constants so the rewriter's enumeration of - // possible flag-states stays in lock-step with the values written - // here — adding a third bit requires editing both this writer AND - // the rewriter, but the constants make the dependency discoverable. - currentFlag := 0 - - if it.Ethereal { - currentFlag |= flagBitEthereal - } - if it.IsRuneword { - currentFlag |= flagBitRuneword - } - - stage1Props["flag"] = currentFlag - case "prefix": - if it.Affixes.Rare.Prefix != 0 { - stage1Props["prefix"] = int(it.Affixes.Rare.Prefix) - } - for _, prefix := range it.Affixes.Magic.Prefixes { - if prefix != 0 { - stage1Props["prefix"] = int(prefix) - break - } - } - case "suffix": - if it.Affixes.Rare.Suffix != 0 { - stage1Props["suffix"] = int(it.Affixes.Rare.Suffix) - } - for _, suffix := range it.Affixes.Magic.Suffixes { - if suffix != 0 { - stage1Props["suffix"] = int(suffix) - break - } - } - case "color": - // TODO: Not supported yet + desc := it.Desc() + /* + // 0x400000 (eth) | 0x4000000 (runeword) kolbot style. Bit values are + // shared with the rewriter (replaceStringPropertiesInStage1) via the + // package-level flagBit* constants so the rewriter's enumeration of + // possible flag-states stays in lock-step with the values written + // here — adding a third bit requires editing both this writer AND + // the rewriter, but the constants make the dependency discoverable. + */ + currentFlag := 0 + if it.Ethereal { + currentFlag |= flagBitEthereal + } + if it.IsRuneword { + currentFlag |= flagBitRuneword + } + stage1Props := map[string]int{ + "type": desc.GetType().ID, + "quality": int(it.Quality), + "class": int(desc.Tier()), + "name": it.ID, + "flag": currentFlag, + } + if it.Affixes.Rare.Prefix != 0 { + stage1Props["prefix"] = int(it.Affixes.Rare.Prefix) + } + for _, prefix := range it.Affixes.Magic.Prefixes { + if prefix != 0 { + stage1Props["prefix"] = int(prefix) + break + } + } + if it.Affixes.Rare.Suffix != 0 { + stage1Props["suffix"] = int(it.Affixes.Rare.Suffix) + } + for _, suffix := range it.Affixes.Magic.Suffixes { + if suffix != 0 { + stage1Props["suffix"] = int(suffix) + break } } @@ -337,51 +345,22 @@ func (r Rule) Evaluate(it data.Item) (RuleResult, error) { return RuleResultPartial, nil } - stage2Props := make(map[string]int) - stage2 := "" - if len(strings.Split(r.RawLine, "#")) > 1 { - stage2 = strings.ToLower(strings.Split(r.RawLine, "#")[1]) - } + stage2Props := make(map[string]int, len(r.requiredStats)) // Special handling for skill tabs - if strings.Contains(stage2, "[itemaddskilltab]") { + if r.skillTabSum { stage2Props["itemaddskilltab"] = evaluateSkillTabSum(it) } // Special handling for class skills - if strings.Contains(stage2, "[itemaddclassskills]") { + if r.classSkillSum { stage2Props["itemaddclassskills"] = evaluateClassSkillsSum(it) } - // Preprocess stage2 to see if certain stats are being compared to zero - zeroCheckStats := make(map[string]bool) - for _, statName := range r.requiredStats { - if strings.Contains(stage2, "["+statName+"] == 0") || - strings.Contains(stage2, "["+statName+"]==0") { - zeroCheckStats[statName] = true - } - } - // Handle resist sums - check if item has any resist stats (including negative values) hasAnyResist := false - // Detect if this is a rule with a resist sum expression - isResistSum := false - if strings.Contains(stage2, "resist") { - isResistSum = strings.Contains(stage2, "+") || strings.Contains(stage2, "-") || - (strings.Contains(stage2, "(") && strings.Contains(stage2, ")")) - } - - // Check if rule ONLY checks resist sums (no other OR conditions) - hasNonResistConditions := false - for _, statName := range r.requiredStats { - if !strings.Contains(statName, "resist") { - hasNonResistConditions = true - break - } - } - - if isResistSum { + if r.resistSum { // Check if the item has any resist stats at all (including negative values like sunders) for _, statName := range r.requiredStats { if !strings.Contains(statName, "resist") { @@ -474,7 +453,7 @@ func (r Rule) Evaluate(it data.Item) (RuleResult, error) { isResistStat := strings.Contains(statName, "resist") // Special case: Pure resist-sum rules (like sunders) should fail early if no resists exist // But mixed rules (resists OR other stats) should continue to check other conditions - if isResistStat && isResistSum && !hasAnyResist && !hasNonResistConditions { + if isResistStat && r.resistSum && !hasAnyResist && !r.nonResistStat { // This is a pure resist rule and item has no resists - can't match return RuleResultNoMatch, nil }