Addon: [1.9.12] - Unified expression parser for complex requirements#786
Merged
Addon: [1.9.12] - Unified expression parser for complex requirements#786
Conversation
- Replace InfixToPostfix RPN evaluator with ExpressionParser/Tokenizer supporting full arithmetic expressions (+, -, *, /, %), parentheses, comparison operators (==, !=, <, >, <=, >=), and logical operators (&&, ||) - Support array syntax in IntVariables for aura prefixes (Buff_, Debuff_, etc.) returning max remaining time across all icon IDs - Add SinceDamageTakenMs requirement keyword (PlayerReader) - Add NullAddonDataProvider and NullScreenImageProvider for testing - Add DependencyInjection.AddCoreLoadOnly() for headless/benchmark use - HeadlessServer.LoadClassProfile returns bool; loadall.bat reports summary - Rewrite LoadAllProfiles benchmark to use DI instead of process spawning - Fix DataToColor:PS() to place highest spell rank instead of lowest - Bump addon version 1.9.11 → 1.9.12 - Update class profiles to use new expression syntax - Update README with new features and examples Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closed
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why Replace the Old Expression System?
The previous system had two disconnected layers:
&&,||,()as a string-level converter, producing postfix tokensVariable OP Valueflat comparisonsShunting Yard limitations:
(Health% + 10) > 50 && InCombatwould break becauseInfixToPostfixtreats(as logical grouping, not arithmetic+,-,*,/) — users were limited to flatVariable OP Valuecomparisons%modulo operator was hardcoded to check== 0only — no way to writeDeaths % 2 == 1not,!) was handled via character scanning withSearchValues, separate from the expression evaluationThe new unified Pratt parser handles all operators (arithmetic, comparison, logical) with proper precedence in a single pass, eliminating all of these limitations.
Breaking Change:
notkeyword removedThe
notkeyword prefix has been removed from the expression system. Use the!operator instead.Migration regex (for user JSON profiles):
\bnot\s+!Examples:
"not InCombat""!InCombat""not Stealth && Health% > 50""!Stealth && Health% > 50""not Swimming""!Swimming"All shipped class profiles have already been updated.
New Unified Expression Parser (Pratt Parser)
Replace the two-layer expression system (
InfixToPostfixShunting Yard +CreateArithmeticflat comparisons) with a unified Pratt parser that handles all operators in a single pass with proper precedence.New files:
Core/RPN/ExpressionToken.cs—TokenKindenum andExpressionTokenreadonly structCore/RPN/ExpressionTokenizer.cs—ref structtokenizer operating onReadOnlySpan<char>for zero-allocation scanning. Greedy variable matching (sorted by length desc) resolves ambiguity betweenHealth%(variable) and%(modulo operator)Core/RPN/ExpressionParser.cs— Pratt parser with type-checkedExprValuestruct (Func<int>?/Func<bool>?/Func<string>?). Delegates are composed at parse-time, not runtime — same performance model as the previous systemDeleted:
Core/RPN/InfixToPostfix.cs— fully replaced by the new parserOperator precedence table:
||&&==,!=>,<,>=,<=+,-*,/,%!, unary-New operators:
+,-,*,/(arithmetic),!=(comparison).%now returns int (previously hardcoded to== 0).Backward compatibility: If a top-level expression (or logical operand) evaluates to
intrather thanbool, the parser implicitly wraps it asexpr == 0to preserve the existingDeaths % 2behavior.Example new expressions:
All existing expressions continue to work unchanged (including
Health%>70,Stealth&&InMeleeRangewithout spaces). The tokenizer checks multi-char operators before variable names, and variable matching verifies the next character is a delimiter, so no-space expressions parse correctly.Log message format preserved:
Health% > 50->"Health% 75 > 50"Deaths % 2 == 1->"Deaths 3 % 2 == 1"InCombat && Health% > 50->"InCombat and Health% 75 > 50"(uses existingRequirement.And/Requirement.Orjoin strings)RequirementFactory Simplification
Process()method reduced from a 30-line InfixToPostfix + stack evaluation loop to a singleexpressionParser.Parse(requirement)callrequirementMap(>=,<=,>,<,==,%) — the Pratt parser handles these natively as operatorsCreateRequirement,CreateGreaterThen,CreateLesserThen,CreateGreaterOrEquals,CreateLesserOrEquals,CreateEquals,CreateModulo,CreateArithmeticnegateKeywordsSpanand comparison constant stringsRequirement.cs:RequirementExtclass (And(),Or(),Negate()extensions) — only used by the oldProcess()methodnpcID:,Form:,SpellInRange:, etc.) remain handled by existing factory methods — the parser delegates to them via therequirementMapArray IntVariables for Aura Prefixes
IntVariablestype changed fromDictionary<string, int>toDictionary<string, int[]>with customIntOrIntArrayDictionaryConverter(scalars are normalized to single-element arrays for uniform handling)Buff_,Debuff_,TDebuff_,TBuff_,FBuff_) register a single variable returning the max remaining time across all icon IDsExample JSON config:
This checks if any of the listed buff IDs is active (max remaining time > 0).
SinceDamageTakenMs Keyword
PlayerReaderproperty tracking milliseconds since last damage taken (usingStopwatch.GetTimestamp()for high-resolution timing)int.MaxValueif no damage has been recorded yetHealthPercent()between frames — when current health < previous health, records a new damage timestampExample:
Checks if the player hasn't taken damage for 5 seconds.
Testing Infrastructure
NullAddonDataProvider(Core/AddonDataProvider/NullAddonDataProvider.cs) — implementsIAddonDataProviderwith a zeroedDataarray for headless/benchmark use without WoW runningNullScreenImageProvider(Core/NullScreenImageProvider.cs) — implementsIScreenImageProviderreturning emptyDirectBitmapDependencyInjection.AddCoreLoadOnly()— registers a minimal DI container sufficient for parsing and loading class profiles without a live game connection. Registers null providers, database singletons, and addon componentsHeadlessServer Improvements
HeadlessServer.RunLoadOnly()andBotController.LoadClassProfile()now returnboolsuccess/failure (previouslyvoid)Program.csexits with code 1 on load failure (previously always exited 0)loadall.batrewritten: iterates all JSON profiles, reports[PASS]/[FAIL]per file, prints summary with total passed/failed counts, and exits with non-zero code if any failedBenchmark Rewrite
LoadAllProfilesbenchmark now uses DI directly (AddCoreLoadOnly()) instead of spawning externalHeadlessServerprocessesSetWorkingDirectory()walks up to solution root reliably from any build output path[Params]forUnitRace/UnitClassto inject correct race/class into addon dataAddon Fix: DataToColor:PS() — Highest Spell Rank
PS()now places the highest spell rank instead of the lowest — spells in the spell book are ordered by rank, so the function now tracks the last matching index (best) and breaks when matches end, ensuring the highest rank is placed on the action barClassConfiguration Fix
Merge()method: base action requirements now joined with&&instead of bare space concatenation, preventing malformed combined expressions (e.g.,"InCombat Health% > 50"is now correctly"InCombat && Health% > 50")Class Profile Updates
Updated profiles to use new expression syntax:
Buff_arrays and arithmetic expressionsREADME Updates
+,-,*,/),!=comparison, and full expression syntaxSinceDamageTakenMsTest plan
dotnet build MasterOfPuppets.slncompiles without errorsdotnet testpasses all existing testsloadall.batloads all class profiles successfully with[PASS]statusdotnet run --project Benchmarks -c Release -- --filter *LoadAllProfiles*— all JSON configs parse without error2 + 3 * 4 == 14)Health%>70,Stealth&&InMeleeRange)Deaths % 2still implicitly meansDeaths % 2 == 0SinceDamageTakenMsreturnsint.MaxValuewhen no damage taken, and correct elapsed time after damagePS()places highest spell rank (e.g., Immolate Rank 9 not Rank 1) on action bar!operator works as replacement fornot(e.g.,!InCombat)Generated with Claude Code