diff --git a/OpenRA.Game/FieldLoader.cs b/OpenRA.Game/FieldLoader.cs
index fad93c4462..6a725c02c0 100644
--- a/OpenRA.Game/FieldLoader.cs
+++ b/OpenRA.Game/FieldLoader.cs
@@ -398,13 +398,13 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
- else if (fieldType == typeof(BooleanExpression))
+ else if (fieldType == typeof(ConditionExpression))
{
if (value != null)
{
try
{
- return new BooleanExpression(value);
+ return new ConditionExpression(value);
}
catch (InvalidDataException e)
{
diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj
index 54e5e3518d..30a49ca89d 100644
--- a/OpenRA.Game/OpenRA.Game.csproj
+++ b/OpenRA.Game/OpenRA.Game.csproj
@@ -241,7 +241,7 @@
-
+
diff --git a/OpenRA.Game/Support/BooleanExpression.cs b/OpenRA.Game/Support/BooleanExpression.cs
deleted file mode 100644
index bdd4ca785c..0000000000
--- a/OpenRA.Game/Support/BooleanExpression.cs
+++ /dev/null
@@ -1,296 +0,0 @@
-#region Copyright & License Information
-/*
- * Copyright 2007-2017 The OpenRA Developers (see AUTHORS)
- * This file is part of OpenRA, which is free software. It is made
- * available to you under the terms of the GNU General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version. For more
- * information, see COPYING.
- */
-#endregion
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-
-namespace OpenRA.Support
-{
- public class BooleanExpression
- {
- public readonly string Expression;
- readonly HashSet variables = new HashSet();
- public IEnumerable Variables { get { return variables; } }
-
- readonly Token[] postfix;
-
- enum Associativity { Left, Right }
- class Token
- {
- public readonly string Symbol;
- public readonly int Index;
- public readonly int Precedence;
- public readonly Associativity Associativity;
-
- public Token(string symbol, int index, Associativity associativity, int precedence)
- {
- Symbol = symbol;
- Index = index;
- Associativity = associativity;
- Precedence = precedence;
- }
- }
-
- class BinaryOperationToken : Token
- {
- public BinaryOperationToken(string symbol, int index, Associativity associativity = Associativity.Left, int precedence = 0)
- : base(symbol, index, associativity, precedence) { }
- }
-
- class UnaryOperationToken : Token
- {
- public UnaryOperationToken(string symbol, int index, Associativity associativity = Associativity.Right, int precedence = 1)
- : base(symbol, index, associativity, precedence) { }
- }
-
- class OpenParenToken : Token { public OpenParenToken(int index) : base("(", index, Associativity.Left, -1) { } }
- class CloseParenToken : Token { public CloseParenToken(int index) : base(")", index, Associativity.Left, -1) { } }
- class VariableToken : Token
- {
- public VariableToken(int index, string symbol)
- : base(symbol, index, Associativity.Left, 0) { }
- }
-
- class AndToken : BinaryOperationToken { public AndToken(int index) : base("&&", index) { } }
- class OrToken : BinaryOperationToken { public OrToken(int index) : base("||", index) { } }
- class EqualsToken : BinaryOperationToken { public EqualsToken(int index) : base("==", index) { } }
- class NotEqualsToken : BinaryOperationToken { public NotEqualsToken(int index) : base("!=", index) { } }
- class NotToken : UnaryOperationToken { public NotToken(int index) : base("!", index) { } }
-
- public BooleanExpression(string expression)
- {
- Expression = expression;
- var openParens = 0;
- var closeParens = 0;
- var tokens = new List();
- for (var i = 0; i < expression.Length; i++)
- {
- switch (expression[i])
- {
- case '(':
- {
- tokens.Add(new OpenParenToken(i));
- openParens++;
- break;
- }
-
- case ')':
- {
- tokens.Add(new CloseParenToken(i));
- if (++closeParens > openParens)
- throw new InvalidDataException("Unmatched closing parenthesis at index {0}".F(i));
-
- break;
- }
-
- default:
- {
- // Ignore whitespace
- if (char.IsWhiteSpace(expression[i]))
- break;
-
- var token = ParseSymbol(expression, ref i);
- tokens.Add(token);
-
- var variable = token as VariableToken;
- if (variable != null)
- variables.Add(variable.Symbol);
-
- break;
- }
- }
- }
-
- // Sanity check parsed tree
- if (!tokens.Any())
- throw new InvalidDataException("Empty expression");
-
- if (closeParens != openParens)
- throw new InvalidDataException("Mismatched opening and closing parentheses");
-
- for (var i = 0; i < tokens.Count - 1; i++)
- {
- // Unary tokens must be followed by a variable, another unary token, or an opening parenthesis
- if (tokens[i] is UnaryOperationToken && !(tokens[i + 1] is VariableToken || tokens[i + 1] is UnaryOperationToken
- || tokens[i + 1] is OpenParenToken))
- throw new InvalidDataException("Unexpected token `{0}` at index {1}".F(tokens[i].Symbol, tokens[i].Index));
-
- // Disallow empty parentheses
- if (tokens[i] is OpenParenToken && tokens[i + 1] is CloseParenToken)
- throw new InvalidDataException("Empty parenthesis at index {0}".F(tokens[i].Index));
-
- // A variable must be followed by a binary operation or by a closing parenthesis
- if (tokens[i] is VariableToken && !(tokens[i + 1] is BinaryOperationToken || tokens[i + 1] is CloseParenToken))
- throw new InvalidDataException("Missing binary operation at index {0}".F(tokens[i + 1].Index));
- }
-
- // Expressions can't start with an operation
- if (tokens[0] is BinaryOperationToken)
- throw new InvalidDataException("Unexpected token `{0}` at index `{1}`".F(tokens[0].Symbol, tokens[0].Index));
-
- // Expressions can't end with a binary or unary operation
- if (tokens[tokens.Count - 1] is BinaryOperationToken || tokens[tokens.Count - 1] is UnaryOperationToken)
- throw new InvalidDataException("Unexpected token `{0}` at index `{1}`".F(tokens[tokens.Count - 1].Symbol, tokens[tokens.Count - 1].Index));
-
- // Binary operations must be preceeded by a closing paren or a variable
- // Binary operations must be followed by an opening paren, a variable, or a unary operation
- for (var i = 1; i < tokens.Count - 1; i++)
- {
- if (tokens[i] is BinaryOperationToken && (
- !(tokens[i - 1] is CloseParenToken || tokens[i - 1] is VariableToken) ||
- !(tokens[i + 1] is OpenParenToken || tokens[i + 1] is VariableToken || tokens[i + 1] is UnaryOperationToken)))
- throw new InvalidDataException("Unexpected token `{0}` at index `{1}`".F(tokens[i].Symbol, tokens[i].Index));
- }
-
- // Convert to postfix (discarding parentheses) ready for evaluation
- postfix = ToPostfix(tokens).ToArray();
- }
-
- static Token ParseSymbol(string expression, ref int i)
- {
- var start = i;
- var c = expression[start];
-
- // Parse operators
- if (c == '!')
- {
- if (i < expression.Length - 1 && expression[start + 1] == '=')
- {
- i++;
- return new NotEqualsToken(start);
- }
-
- return new NotToken(start);
- }
-
- if (c == '=')
- {
- if (i < expression.Length - 1 && expression[start + 1] == '=')
- {
- i++;
- return new EqualsToken(start);
- }
-
- throw new InvalidDataException("Unexpected character '=' at index {0}".F(start));
- }
-
- if (c == '&')
- {
- if (i < expression.Length - 1 && expression[start + 1] == '&')
- {
- i++;
- return new AndToken(start);
- }
-
- throw new InvalidDataException("Unexpected character '&' at index {0}".F(start));
- }
-
- if (c == '|')
- {
- if (i < expression.Length - 1 && expression[start + 1] == '|')
- {
- i++;
- return new OrToken(start);
- }
-
- throw new InvalidDataException("Unexpected character '|' at index {0}".F(start));
- }
-
- // Scan forwards until we find an invalid name character
- for (; i < expression.Length; i++)
- {
- c = expression[i];
- if (char.IsWhiteSpace(c) || c == '(' || c == ')' || c == '!' || c == '&' || c == '|' || c == '=')
- {
- // Put the bad character back for the next parse attempt
- i--;
- return new VariableToken(start, expression.Substring(start, i - start + 1));
- }
- }
-
- // Take the rest of the string
- return new VariableToken(start, expression.Substring(start));
- }
-
- static bool ParseSymbol(VariableToken t, IReadOnlyDictionary symbols)
- {
- bool value;
- symbols.TryGetValue(t.Symbol, out value);
- return value;
- }
-
- static void ApplyBinaryOperation(Stack s, Func f)
- {
- var x = s.Pop();
- var y = s.Pop();
- s.Push(f(x, y));
- }
-
- static void ApplyUnaryOperation(Stack s, Func f)
- {
- var x = s.Pop();
- s.Push(f(x));
- }
-
- static IEnumerable ToPostfix(IEnumerable tokens)
- {
- var s = new Stack();
- foreach (var t in tokens)
- {
- if (t is OpenParenToken)
- s.Push(t);
- else if (t is CloseParenToken)
- {
- Token temp;
- while (!((temp = s.Pop()) is OpenParenToken))
- yield return temp;
- }
- else if (t is VariableToken)
- yield return t;
- else
- {
- while (s.Count > 0 && ((t.Associativity == Associativity.Right && t.Precedence < s.Peek().Precedence)
- || (t.Associativity == Associativity.Left && t.Precedence <= s.Peek().Precedence)))
- yield return s.Pop();
-
- s.Push(t);
- }
- }
-
- while (s.Count > 0)
- yield return s.Pop();
- }
-
- public bool Evaluate(IReadOnlyDictionary symbols)
- {
- var s = new Stack();
- foreach (var t in postfix)
- {
- if (t is AndToken)
- ApplyBinaryOperation(s, (x, y) => y & x);
- else if (t is NotEqualsToken)
- ApplyBinaryOperation(s, (x, y) => y ^ x);
- else if (t is OrToken)
- ApplyBinaryOperation(s, (x, y) => y | x);
- else if (t is EqualsToken)
- ApplyBinaryOperation(s, (x, y) => y == x);
- else if (t is NotToken)
- ApplyUnaryOperation(s, x => !x);
- else if (t is VariableToken)
- s.Push(ParseSymbol((VariableToken)t, symbols));
- }
-
- return s.Pop();
- }
- }
-}
diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs
new file mode 100644
index 0000000000..9ffd48ab29
--- /dev/null
+++ b/OpenRA.Game/Support/ConditionExpression.cs
@@ -0,0 +1,898 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2017 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version. For more
+ * information, see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using Expressions = System.Linq.Expressions;
+
+namespace OpenRA.Support
+{
+ public class ConditionExpression
+ {
+ public readonly string Expression;
+ readonly HashSet variables = new HashSet();
+ public IEnumerable Variables { get { return variables; } }
+
+ readonly Func, int> asFunction;
+
+ enum CharClass { Whitespace, Operator, Mixed, Id, Digit }
+
+ static CharClass CharClassOf(char c)
+ {
+ switch (c)
+ {
+ case '~':
+ case '!':
+ case '%':
+ case '^':
+ case '&':
+ case '*':
+ case '(':
+ case ')':
+ case '+':
+ case '=':
+ case '[':
+ case ']':
+ case '{':
+ case '}':
+ case '|':
+ case ':':
+ case ';':
+ case '\'':
+ case '"':
+ case '<':
+ case '>':
+ case '?':
+ case ',':
+ case '/':
+ return CharClass.Operator;
+
+ case '.':
+ case '$':
+ case '-':
+ case '@':
+ return CharClass.Mixed;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ return CharClass.Digit;
+
+ // Fast-track normal whitespace
+ case ' ':
+ case '\t':
+ case '\n':
+ case '\r':
+ return CharClass.Whitespace;
+
+ // Should other whitespace be tested?
+ default:
+ return char.IsWhiteSpace(c) ? CharClass.Whitespace : CharClass.Id;
+ }
+ }
+
+ enum Associativity { Left, Right }
+
+ [Flags]
+ enum OperandSides
+ {
+ // Value type
+ None = 0,
+
+ // Postfix unary operator and/or group closer
+ Left = 1,
+
+ // Prefix unary operator and/or group opener
+ Right = 2,
+
+ // Binary+ operator
+ Both = Left | Right
+ }
+
+ enum Grouping { None, Parens }
+
+ enum TokenType
+ {
+ // fixed values
+ False,
+ True,
+
+ // varying values
+ Number,
+ Variable,
+
+ // operators
+ OpenParen,
+ CloseParen,
+ Not,
+ Negate,
+ OnesComplement,
+ And,
+ Or,
+ Equals,
+ NotEquals,
+ LessThan,
+ LessThanOrEqual,
+ GreaterThan,
+ GreaterThanOrEqual,
+ Add,
+ Subtract,
+ Multiply,
+ Divide,
+ Modulo,
+
+ Invalid
+ }
+
+ enum Precedence
+ {
+ Unary = 16,
+ Multiplication = 12,
+ Addition = 11,
+ Relation = 9,
+ Equality = 8,
+ And = 4,
+ Or = 3,
+ Binary = 0,
+ Value = 0,
+ Parens = -1,
+ Invalid = ~0
+ }
+
+ struct TokenTypeInfo
+ {
+ public readonly string Symbol;
+ public readonly Precedence Precedence;
+ public readonly OperandSides OperandSides;
+ public readonly Associativity Associativity;
+ public readonly Grouping Opens;
+ public readonly Grouping Closes;
+
+ public TokenTypeInfo(string symbol, Precedence precedence, OperandSides operandSides = OperandSides.None,
+ Associativity associativity = Associativity.Left,
+ Grouping opens = Grouping.None, Grouping closes = Grouping.None)
+ {
+ Symbol = symbol;
+ Precedence = precedence;
+ OperandSides = operandSides;
+ Associativity = associativity;
+ Opens = opens;
+ Closes = closes;
+ }
+
+ public TokenTypeInfo(string symbol, Precedence precedence, Grouping opens, Grouping closes = Grouping.None,
+ Associativity associativity = Associativity.Left)
+ {
+ Symbol = symbol;
+ Precedence = precedence;
+ OperandSides = opens == Grouping.None ?
+ (closes == Grouping.None ? OperandSides.None : OperandSides.Left)
+ :
+ (closes == Grouping.None ? OperandSides.Right : OperandSides.Both);
+ Associativity = associativity;
+ Opens = opens;
+ Closes = closes;
+ }
+ }
+
+ static IEnumerable CreateTokenTypeInfoEnumeration()
+ {
+ for (var i = 0; i <= (int)TokenType.Invalid; i++)
+ {
+ switch ((TokenType)i)
+ {
+ case TokenType.Invalid:
+ yield return new TokenTypeInfo("()", Precedence.Invalid);
+ continue;
+ case TokenType.False:
+ yield return new TokenTypeInfo("false", Precedence.Value);
+ continue;
+ case TokenType.True:
+ yield return new TokenTypeInfo("true", Precedence.Value);
+ continue;
+ case TokenType.Number:
+ yield return new TokenTypeInfo("()", Precedence.Value);
+ continue;
+ case TokenType.Variable:
+ yield return new TokenTypeInfo("()", Precedence.Value);
+ continue;
+ case TokenType.OpenParen:
+ yield return new TokenTypeInfo("(", Precedence.Parens, Grouping.Parens);
+ continue;
+ case TokenType.CloseParen:
+ yield return new TokenTypeInfo(")", Precedence.Parens, Grouping.None, Grouping.Parens);
+ continue;
+ case TokenType.Not:
+ yield return new TokenTypeInfo("!", Precedence.Unary, OperandSides.Right, Associativity.Right);
+ continue;
+ case TokenType.OnesComplement:
+ yield return new TokenTypeInfo("~", Precedence.Unary, OperandSides.Right, Associativity.Right);
+ continue;
+ case TokenType.Negate:
+ yield return new TokenTypeInfo("-", Precedence.Unary, OperandSides.Right, Associativity.Right);
+ continue;
+ case TokenType.And:
+ yield return new TokenTypeInfo("&&", Precedence.And, OperandSides.Both);
+ continue;
+ case TokenType.Or:
+ yield return new TokenTypeInfo("||", Precedence.Or, OperandSides.Both);
+ continue;
+ case TokenType.Equals:
+ yield return new TokenTypeInfo("==", Precedence.Equality, OperandSides.Both);
+ continue;
+ case TokenType.NotEquals:
+ yield return new TokenTypeInfo("!=", Precedence.Equality, OperandSides.Both);
+ continue;
+ case TokenType.LessThan:
+ yield return new TokenTypeInfo("<", Precedence.Relation, OperandSides.Both);
+ continue;
+ case TokenType.LessThanOrEqual:
+ yield return new TokenTypeInfo("<=", Precedence.Relation, OperandSides.Both);
+ continue;
+ case TokenType.GreaterThan:
+ yield return new TokenTypeInfo(">", Precedence.Relation, OperandSides.Both);
+ continue;
+ case TokenType.GreaterThanOrEqual:
+ yield return new TokenTypeInfo(">=", Precedence.Relation, OperandSides.Both);
+ continue;
+ case TokenType.Add:
+ yield return new TokenTypeInfo("+", Precedence.Addition, OperandSides.Both);
+ continue;
+ case TokenType.Subtract:
+ yield return new TokenTypeInfo("-", Precedence.Addition, OperandSides.Both);
+ continue;
+ case TokenType.Multiply:
+ yield return new TokenTypeInfo("*", Precedence.Multiplication, OperandSides.Both);
+ continue;
+ case TokenType.Divide:
+ yield return new TokenTypeInfo("/", Precedence.Multiplication, OperandSides.Both);
+ continue;
+ case TokenType.Modulo:
+ yield return new TokenTypeInfo("%", Precedence.Multiplication, OperandSides.Both);
+ continue;
+ }
+
+ throw new InvalidProgramException("CreateTokenTypeInfoEnumeration is missing a TokenTypeInfo entry for TokenType.{0}".F(
+ Enum.GetValues()[i]));
+ }
+ }
+
+ static readonly TokenTypeInfo[] TokenTypeInfos = CreateTokenTypeInfoEnumeration().ToArray();
+
+ static bool HasRightOperand(TokenType type)
+ {
+ return ((int)TokenTypeInfos[(int)type].OperandSides & (int)OperandSides.Right) != 0;
+ }
+
+ static bool IsLeftOperandOrNone(TokenType type)
+ {
+ return type == TokenType.Invalid || HasRightOperand(type);
+ }
+
+ class Token
+ {
+ public readonly TokenType Type;
+ public readonly int Index;
+
+ public virtual string Symbol { get { return TokenTypeInfos[(int)Type].Symbol; } }
+
+ public int Precedence { get { return (int)TokenTypeInfos[(int)Type].Precedence; } }
+ public OperandSides OperandSides { get { return TokenTypeInfos[(int)Type].OperandSides; } }
+ public Associativity Associativity { get { return TokenTypeInfos[(int)Type].Associativity; } }
+ public bool LeftOperand { get { return ((int)TokenTypeInfos[(int)Type].OperandSides & (int)OperandSides.Left) != 0; } }
+ public bool RightOperand { get { return ((int)TokenTypeInfos[(int)Type].OperandSides & (int)OperandSides.Right) != 0; } }
+
+ public Grouping Opens { get { return TokenTypeInfos[(int)Type].Opens; } }
+ public Grouping Closes { get { return TokenTypeInfos[(int)Type].Closes; } }
+
+ public Token(TokenType type, int index)
+ {
+ Type = type;
+ Index = index;
+ }
+
+ static bool ScanIsNumber(string expression, int start, ref int i)
+ {
+ var cc = CharClassOf(expression[i]);
+
+ // Scan forwards until we find an non-digit character
+ if (cc == CharClass.Digit)
+ {
+ i++;
+ for (; i < expression.Length; i++)
+ {
+ cc = CharClassOf(expression[i]);
+ if (cc != CharClass.Digit)
+ {
+ if (cc != CharClass.Whitespace && cc != CharClass.Operator)
+ throw new InvalidDataException("Number {0} and variable merged at index {1}".F(
+ int.Parse(expression.Substring(start, i - start)), start));
+
+ return true;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ static TokenType VariableOrKeyword(string expression, int start, int length)
+ {
+ var i = start;
+ if (length == 4 && expression[i++] == 't' && expression[i++] == 'r' && expression[i++] == 'u'
+ && expression[i] == 'e')
+ return TokenType.True;
+
+ if (length == 5 && expression[i++] == 'f' && expression[i++] == 'a' && expression[i++] == 'l'
+ && expression[i++] == 's' && expression[i] == 'e')
+ return TokenType.False;
+
+ return TokenType.Variable;
+ }
+
+ public static TokenType GetNextType(string expression, ref int i, TokenType lastType = TokenType.Invalid)
+ {
+ var start = i;
+
+ switch (expression[i])
+ {
+ case '!':
+ i++;
+ if (i < expression.Length && expression[i] == '=')
+ {
+ i++;
+ return TokenType.NotEquals;
+ }
+
+ return TokenType.Not;
+
+ case '<':
+ i++;
+ if (i < expression.Length && expression[i] == '=')
+ {
+ i++;
+ return TokenType.LessThanOrEqual;
+ }
+
+ return TokenType.LessThan;
+
+ case '>':
+ i++;
+ if (i < expression.Length && expression[i] == '=')
+ {
+ i++;
+ return TokenType.GreaterThanOrEqual;
+ }
+
+ return TokenType.GreaterThan;
+
+ case '=':
+ i++;
+ if (i < expression.Length && expression[i] == '=')
+ {
+ i++;
+ return TokenType.Equals;
+ }
+
+ throw new InvalidDataException("Unexpected character '=' at index {0} - should it be `==`?".F(start));
+
+ case '&':
+ i++;
+ if (i < expression.Length && expression[i] == '&')
+ {
+ i++;
+ return TokenType.And;
+ }
+
+ throw new InvalidDataException("Unexpected character '&' at index {0} - should it be `&&`?".F(start));
+
+ case '|':
+ i++;
+ if (i < expression.Length && expression[i] == '|')
+ {
+ i++;
+ return TokenType.Or;
+ }
+
+ throw new InvalidDataException("Unexpected character '|' at index {0} - should it be `||`?".F(start));
+
+ case '(':
+ i++;
+ return TokenType.OpenParen;
+
+ case ')':
+ i++;
+ return TokenType.CloseParen;
+
+ case '~':
+ i++;
+ return TokenType.OnesComplement;
+ case '+':
+ i++;
+ return TokenType.Add;
+
+ case '-':
+ if (++i < expression.Length && ScanIsNumber(expression, start, ref i))
+ return TokenType.Number;
+
+ i = start + 1;
+ if (IsLeftOperandOrNone(lastType))
+ return TokenType.Negate;
+ return TokenType.Subtract;
+
+ case '*':
+ i++;
+ return TokenType.Multiply;
+
+ case '/':
+ i++;
+ return TokenType.Divide;
+
+ case '%':
+ i++;
+ return TokenType.Modulo;
+ }
+
+ if (ScanIsNumber(expression, start, ref i))
+ return TokenType.Number;
+
+ var cc = CharClassOf(expression[start]);
+
+ if (cc != CharClass.Id)
+ throw new InvalidDataException("Invalid character '{0}' at index {1}".F(expression[i], start));
+
+ // Scan forwards until we find an invalid name character
+ for (i = start; i < expression.Length; i++)
+ {
+ cc = CharClassOf(expression[i]);
+ if (cc == CharClass.Whitespace || cc == CharClass.Operator)
+ return VariableOrKeyword(expression, start, i - start);
+ }
+
+ return VariableOrKeyword(expression, start, i - start);
+ }
+
+ public static Token GetNext(string expression, ref int i, TokenType lastType = TokenType.Invalid)
+ {
+ if (i == expression.Length)
+ return null;
+
+ // Ignore whitespace
+ while (CharClassOf(expression[i]) == CharClass.Whitespace)
+ {
+ if (++i == expression.Length)
+ return null;
+ }
+
+ var start = i;
+
+ var type = GetNextType(expression, ref i, lastType);
+ switch (type)
+ {
+ case TokenType.Number:
+ return new NumberToken(start, expression.Substring(start, i - start));
+
+ case TokenType.Variable:
+ return new VariableToken(start, expression.Substring(start, i - start));
+
+ default:
+ return new Token(type, start);
+ }
+ }
+ }
+
+ class VariableToken : Token
+ {
+ public readonly string Name;
+
+ public override string Symbol { get { return Name; } }
+
+ public VariableToken(int index, string symbol) : base(TokenType.Variable, index) { Name = symbol; }
+ }
+
+ class NumberToken : Token
+ {
+ public readonly int Value;
+ readonly string symbol;
+
+ public override string Symbol { get { return symbol; } }
+
+ public NumberToken(int index, string symbol)
+ : base(TokenType.Number, index)
+ {
+ Value = int.Parse(symbol);
+ this.symbol = symbol;
+ }
+ }
+
+ public ConditionExpression(string expression)
+ {
+ Expression = expression;
+ var tokens = new List();
+ var currentOpeners = new Stack();
+ Token lastToken = null;
+ for (var i = 0;;)
+ {
+ var token = Token.GetNext(expression, ref i, lastToken != null ? lastToken.Type : TokenType.Invalid);
+ if (token == null)
+ {
+ // Sanity check parsed tree
+ if (lastToken == null)
+ throw new InvalidDataException("Empty expression");
+
+ // Expressions can't end with a binary or unary prefix operation
+ if (lastToken.RightOperand)
+ throw new InvalidDataException("Missing value or sub-expression at end for `{0}` operator".F(lastToken.Symbol));
+
+ break;
+ }
+
+ if (token.Closes != Grouping.None)
+ {
+ if (currentOpeners.Count == 0)
+ throw new InvalidDataException("Unmatched closing parenthesis at index {0}".F(token.Index));
+
+ currentOpeners.Pop();
+ }
+
+ if (token.Opens != Grouping.None)
+ currentOpeners.Push(token);
+
+ if (lastToken == null)
+ {
+ // Expressions can't start with a binary or unary postfix operation or closer
+ if (token.LeftOperand)
+ throw new InvalidDataException("Missing value or sub-expression at beginning for `{0}` operator".F(token.Symbol));
+ }
+ else
+ {
+ // Disallow empty parentheses
+ if (lastToken.Opens != Grouping.None && token.Closes != Grouping.None)
+ throw new InvalidDataException("Empty parenthesis at index {0}".F(lastToken.Index));
+
+ // Exactly one of two consective tokens must take the other's sub-expression evaluation as an operand
+ if (lastToken.RightOperand == token.LeftOperand)
+ {
+ if (lastToken.RightOperand)
+ throw new InvalidDataException(
+ "Missing value or sub-expression or there is an extra operator `{0}` at index {1} or `{2}` at index {3}".F(
+ lastToken.Symbol, lastToken.Index, token.Symbol, token.Index));
+ throw new InvalidDataException("Missing binary operation before `{0}` at index {1}".F(token.Symbol, token.Index));
+ }
+ }
+
+ if (token.Type == TokenType.Variable)
+ variables.Add(token.Symbol);
+
+ tokens.Add(token);
+ lastToken = token;
+ }
+
+ if (currentOpeners.Count > 0)
+ throw new InvalidDataException("Unclosed opening parenthesis at index {0}".F(currentOpeners.Peek().Index));
+
+ asFunction = new Compiler().Compile(ToPostfix(tokens).ToArray());
+ }
+
+ static int ParseSymbol(string symbol, IReadOnlyDictionary symbols)
+ {
+ int value;
+ symbols.TryGetValue(symbol, out value);
+ return value;
+ }
+
+ static IEnumerable ToPostfix(IEnumerable tokens)
+ {
+ var s = new Stack();
+ foreach (var t in tokens)
+ {
+ if (t.Opens != Grouping.None)
+ s.Push(t);
+ else if (t.Closes != Grouping.None)
+ {
+ Token temp;
+ while (!((temp = s.Pop()).Opens != Grouping.None))
+ yield return temp;
+ }
+ else if (t.OperandSides == OperandSides.None)
+ yield return t;
+ else
+ {
+ while (s.Count > 0 && ((t.Associativity == Associativity.Right && t.Precedence < s.Peek().Precedence)
+ || (t.Associativity == Associativity.Left && t.Precedence <= s.Peek().Precedence)))
+ yield return s.Pop();
+
+ s.Push(t);
+ }
+ }
+
+ while (s.Count > 0)
+ yield return s.Pop();
+ }
+
+ enum ExpressionType { Int, Bool }
+
+ static readonly ParameterExpression SymbolsParam =
+ Expressions.Expression.Parameter(typeof(IReadOnlyDictionary), "symbols");
+ static readonly ConstantExpression Zero = Expressions.Expression.Constant(0);
+ static readonly ConstantExpression One = Expressions.Expression.Constant(1);
+ static readonly ConstantExpression False = Expressions.Expression.Constant(false);
+ static readonly ConstantExpression True = Expressions.Expression.Constant(true);
+
+ static Expression AsBool(Expression expression)
+ {
+ return Expressions.Expression.GreaterThan(expression, Zero);
+ }
+
+ static Expression AsNegBool(Expression expression)
+ {
+ return Expressions.Expression.LessThanOrEqual(expression, Zero);
+ }
+
+ static Expression IfThenElse(Expression test, Expression ifTrue, Expression ifFalse)
+ {
+ return Expressions.Expression.Condition(test, ifTrue, ifFalse);
+ }
+
+ class AstStack
+ {
+ readonly List expressions = new List();
+ readonly List types = new List();
+
+ public ExpressionType PeekType() { return types[types.Count - 1]; }
+
+ public Expression Peek(ExpressionType toType)
+ {
+ var fromType = types[types.Count - 1];
+ var expression = expressions[expressions.Count - 1];
+ if (toType == fromType)
+ return expression;
+
+ switch (toType)
+ {
+ case ExpressionType.Bool:
+ return IfThenElse(AsBool(expression), True, False);
+ case ExpressionType.Int:
+ return IfThenElse(expression, One, Zero);
+ }
+
+ throw new InvalidProgramException("Unable to convert ExpressionType.{0} to ExpressionType.{1}".F(
+ Enum.GetValues()[(int)fromType], Enum.GetValues()[(int)toType]));
+ }
+
+ public Expression Pop(ExpressionType type)
+ {
+ var expression = Peek(type);
+ expressions.RemoveAt(expressions.Count - 1);
+ types.RemoveAt(types.Count - 1);
+ return expression;
+ }
+
+ public void Push(Expression expression, ExpressionType type)
+ {
+ expressions.Add(expression);
+ if (type == ExpressionType.Int)
+ if (expression.Type != typeof(int))
+ throw new InvalidOperationException("Expected System.Int type instead of {0} for {1}".F(expression.Type, expression));
+
+ if (type == ExpressionType.Bool)
+ if (expression.Type != typeof(bool))
+ throw new InvalidOperationException("Expected System.Boolean type instead of {0} for {1}".F(expression.Type, expression));
+ types.Add(type);
+ }
+
+ public void Push(Expression expression)
+ {
+ expressions.Add(expression);
+ if (expression.Type == typeof(int))
+ types.Add(ExpressionType.Int);
+ else if (expression.Type == typeof(bool))
+ types.Add(ExpressionType.Bool);
+ else
+ throw new InvalidOperationException("Unhandled result type {0} for {1}".F(expression.Type, expression));
+ }
+ }
+
+ class Compiler
+ {
+ readonly AstStack ast = new AstStack();
+
+ public Func, int> Compile(Token[] postfix)
+ {
+ foreach (var t in postfix)
+ {
+ switch (t.Type)
+ {
+ case TokenType.And:
+ {
+ var y = ast.Pop(ExpressionType.Bool);
+ var x = ast.Pop(ExpressionType.Bool);
+ ast.Push(Expressions.Expression.And(x, y));
+ continue;
+ }
+
+ case TokenType.Or:
+ {
+ var y = ast.Pop(ExpressionType.Bool);
+ var x = ast.Pop(ExpressionType.Bool);
+ ast.Push(Expressions.Expression.Or(x, y));
+ continue;
+ }
+
+ case TokenType.NotEquals:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ ast.Push(Expressions.Expression.NotEqual(x, y));
+ continue;
+ }
+
+ case TokenType.Equals:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ ast.Push(Expressions.Expression.Equal(x, y));
+ continue;
+ }
+
+ case TokenType.Not:
+ {
+ if (ast.PeekType() == ExpressionType.Bool)
+ ast.Push(Expressions.Expression.Not(ast.Pop(ExpressionType.Bool)));
+ else
+ ast.Push(AsNegBool(ast.Pop(ExpressionType.Int)));
+ continue;
+ }
+
+ case TokenType.Negate:
+ {
+ ast.Push(Expressions.Expression.Negate(ast.Pop(ExpressionType.Int)));
+ continue;
+ }
+
+ case TokenType.OnesComplement:
+ {
+ ast.Push(Expressions.Expression.OnesComplement(ast.Pop(ExpressionType.Int)));
+ continue;
+ }
+
+ case TokenType.LessThan:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ ast.Push(Expressions.Expression.LessThan(x, y));
+ continue;
+ }
+
+ case TokenType.LessThanOrEqual:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ ast.Push(Expressions.Expression.LessThanOrEqual(x, y));
+ continue;
+ }
+
+ case TokenType.GreaterThan:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ ast.Push(Expressions.Expression.GreaterThan(x, y));
+ continue;
+ }
+
+ case TokenType.GreaterThanOrEqual:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ ast.Push(Expressions.Expression.GreaterThanOrEqual(x, y));
+ continue;
+ }
+
+ case TokenType.Add:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ ast.Push(Expressions.Expression.Add(x, y));
+ continue;
+ }
+
+ case TokenType.Subtract:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ ast.Push(Expressions.Expression.Subtract(x, y));
+ continue;
+ }
+
+ case TokenType.Multiply:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ ast.Push(Expressions.Expression.Multiply(x, y));
+ continue;
+ }
+
+ case TokenType.Divide:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ var isNotZero = Expressions.Expression.NotEqual(y, Zero);
+ var divide = Expressions.Expression.Divide(x, y);
+ ast.Push(IfThenElse(isNotZero, divide, Zero));
+ continue;
+ }
+
+ case TokenType.Modulo:
+ {
+ var y = ast.Pop(ExpressionType.Int);
+ var x = ast.Pop(ExpressionType.Int);
+ var isNotZero = Expressions.Expression.NotEqual(y, Zero);
+ var modulo = Expressions.Expression.Modulo(x, y);
+ ast.Push(IfThenElse(isNotZero, modulo, Zero));
+ continue;
+ }
+
+ case TokenType.False:
+ {
+ ast.Push(False);
+ continue;
+ }
+
+ case TokenType.True:
+ {
+ ast.Push(True);
+ continue;
+ }
+
+ case TokenType.Number:
+ {
+ ast.Push(Expressions.Expression.Constant(((NumberToken)t).Value));
+ continue;
+ }
+
+ case TokenType.Variable:
+ {
+ var symbol = Expressions.Expression.Constant(((VariableToken)t).Symbol);
+ Func, int> parseSymbol = ParseSymbol;
+ ast.Push(Expressions.Expression.Call(parseSymbol.Method, symbol, SymbolsParam));
+ continue;
+ }
+
+ default:
+ throw new InvalidProgramException(
+ "ConditionExpression.Compiler.Compile() is missing an expression builder for TokenType.{0}".F(
+ Enum.GetValues()[(int)t.Type]));
+ }
+ }
+
+ return Expressions.Expression.Lambda, int>>(
+ ast.Pop(ExpressionType.Int), SymbolsParam).Compile();
+ }
+ }
+
+ public int Evaluate(IReadOnlyDictionary symbols)
+ {
+ return asFunction(symbols);
+ }
+ }
+}
diff --git a/OpenRA.Mods.Common/Lint/LintExts.cs b/OpenRA.Mods.Common/Lint/LintExts.cs
index 7267c7f539..a8b1ee57fa 100644
--- a/OpenRA.Mods.Common/Lint/LintExts.cs
+++ b/OpenRA.Mods.Common/Lint/LintExts.cs
@@ -29,9 +29,9 @@ namespace OpenRA.Mods.Common.Lint
if (typeof(IEnumerable).IsAssignableFrom(type))
return fieldInfo.GetValue(ruleInfo) as IEnumerable;
- if (type == typeof(BooleanExpression))
+ if (type == typeof(ConditionExpression))
{
- var expr = (BooleanExpression)fieldInfo.GetValue(ruleInfo);
+ var expr = (ConditionExpression)fieldInfo.GetValue(ruleInfo);
return expr != null ? expr.Variables : Enumerable.Empty();
}
@@ -48,9 +48,9 @@ namespace OpenRA.Mods.Common.Lint
if (typeof(IEnumerable).IsAssignableFrom(type))
return (IEnumerable)propertyInfo.GetValue(ruleInfo);
- if (type == typeof(BooleanExpression))
+ if (type == typeof(ConditionExpression))
{
- var expr = (BooleanExpression)propertyInfo.GetValue(ruleInfo);
+ var expr = (ConditionExpression)propertyInfo.GetValue(ruleInfo);
return expr != null ? expr.Variables : Enumerable.Empty();
}
diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index b9e1fe0516..fc98f2f131 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -778,7 +778,6 @@
-
diff --git a/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs b/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs
index b70f5460ae..2123c8896e 100644
--- a/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs
+++ b/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs
@@ -63,24 +63,18 @@ namespace OpenRA.Mods.Common.Traits
/// Each granted condition receives a unique token that is used when revoking.
Dictionary tokens = new Dictionary();
- /// Set of conditions that are monitored for stacked bonuses, and the bonus conditions that they grant.
- readonly Dictionary stackedConditions = new Dictionary();
-
- /// Tokens granted by the stacked condition bonuses defined in stackedConditions.
- readonly Dictionary> stackedTokens = new Dictionary>();
-
int nextToken = 1;
- /// Cache of condition -> enabled state for quick evaluation of boolean conditions.
- readonly Dictionary conditionCache = new Dictionary();
+ /// Cache of condition -> enabled state for quick evaluation of token counter conditions.
+ readonly Dictionary conditionCache = new Dictionary();
/// Read-only version of conditionCache that is passed to IConditionConsumers.
- IReadOnlyDictionary readOnlyConditionCache;
+ IReadOnlyDictionary readOnlyConditionCache;
void INotifyCreated.Created(Actor self)
{
state = new Dictionary();
- readOnlyConditionCache = new ReadOnlyDictionary(conditionCache);
+ readOnlyConditionCache = new ReadOnlyDictionary(conditionCache);
var allConsumers = new HashSet();
var allWatchers = self.TraitsImplementing().ToList();
@@ -96,7 +90,7 @@ namespace OpenRA.Mods.Common.Traits
if (w.Condition == condition)
cs.Watchers.Add(w);
- conditionCache[condition] = false;
+ conditionCache[condition] = 0;
}
}
@@ -108,13 +102,7 @@ namespace OpenRA.Mods.Common.Traits
continue;
conditionState.Tokens.Add(kv.Key);
- conditionCache[kv.Value] = conditionState.Tokens.Count > 0;
- }
-
- foreach (var sc in self.Info.TraitInfos())
- {
- stackedConditions[sc.Condition] = sc.StackedConditions;
- stackedTokens[sc.Condition] = new Stack();
+ conditionCache[kv.Value] = conditionState.Tokens.Count;
}
// Update all traits with their initial condition state
@@ -133,30 +121,10 @@ namespace OpenRA.Mods.Common.Traits
else
conditionState.Tokens.Add(token);
- conditionCache[condition] = conditionState.Tokens.Count > 0;
+ conditionCache[condition] = conditionState.Tokens.Count;
foreach (var t in conditionState.Consumers)
t.ConditionsChanged(self, readOnlyConditionCache);
-
- string[] sc;
- if (stackedConditions.TryGetValue(condition, out sc))
- {
- var target = (conditionState.Tokens.Count - 1).Clamp(0, sc.Length);
- var st = stackedTokens[condition];
- for (var i = st.Count; i < target; i++)
- {
- // Empty strings are used to skip unwanted levels
- var t = !string.IsNullOrEmpty(sc[i]) ? GrantCondition(self, sc[i]) : InvalidConditionToken;
- st.Push(t);
- }
-
- for (var i = st.Count; i > target; i--)
- {
- var t = st.Pop();
- if (t != InvalidConditionToken)
- RevokeCondition(self, t);
- }
- }
}
/// Grants a specified condition.
diff --git a/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs b/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs
index 7a848d3991..3f197345b0 100644
--- a/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs
+++ b/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs
@@ -19,11 +19,11 @@ namespace OpenRA.Mods.Common.Traits
/// Use as base class for *Info to subclass of UpgradableTrait. (See UpgradableTrait.)
public abstract class ConditionalTraitInfo : IConditionConsumerInfo, IRulesetLoaded
{
- static readonly IReadOnlyDictionary NoConditions = new ReadOnlyDictionary(new Dictionary());
+ static readonly IReadOnlyDictionary NoConditions = new ReadOnlyDictionary(new Dictionary());
[ConsumedConditionReference]
[Desc("Boolean expression defining the condition to enable this trait.")]
- public readonly BooleanExpression RequiresCondition = null;
+ public readonly ConditionExpression RequiresCondition = null;
public abstract object Create(ActorInitializer init);
@@ -34,7 +34,7 @@ namespace OpenRA.Mods.Common.Traits
public virtual void RulesetLoaded(Ruleset rules, ActorInfo ai)
{
- EnabledByDefault = RequiresCondition != null ? RequiresCondition.Evaluate(NoConditions) : true;
+ EnabledByDefault = RequiresCondition != null ? RequiresCondition.Evaluate(NoConditions) > 0 : true;
}
}
@@ -77,13 +77,13 @@ namespace OpenRA.Mods.Common.Traits
void INotifyCreated.Created(Actor self) { Created(self); }
- void IConditionConsumer.ConditionsChanged(Actor self, IReadOnlyDictionary conditions)
+ void IConditionConsumer.ConditionsChanged(Actor self, IReadOnlyDictionary conditions)
{
if (Info.RequiresCondition == null)
return;
var wasDisabled = IsTraitDisabled;
- IsTraitDisabled = !Info.RequiresCondition.Evaluate(conditions);
+ IsTraitDisabled = Info.RequiresCondition.Evaluate(conditions) <= 0;
if (IsTraitDisabled != wasDisabled)
{
diff --git a/OpenRA.Mods.Common/Traits/Conditions/StackedCondition.cs b/OpenRA.Mods.Common/Traits/Conditions/StackedCondition.cs
deleted file mode 100644
index c2cd3574a8..0000000000
--- a/OpenRA.Mods.Common/Traits/Conditions/StackedCondition.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-#region Copyright & License Information
-/*
- * Copyright 2007-2017 The OpenRA Developers (see AUTHORS)
- * This file is part of OpenRA, which is free software. It is made
- * available to you under the terms of the GNU General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version. For more
- * information, see COPYING.
- */
-#endregion
-
-using OpenRA.Traits;
-
-namespace OpenRA.Mods.Common.Traits
-{
- [Desc("Grant additional conditions when a specified condition has been granted multiple times.")]
- public class StackedConditionInfo : TraitInfo
- {
- [FieldLoader.Require]
- [ConsumedConditionReference]
- [Desc("Condition to monitor.")]
- public readonly string Condition = null;
-
- [FieldLoader.Require]
- [FieldLoader.AllowEmptyEntries]
- [GrantedConditionReference]
- [Desc("Conditions to grant when the monitored condition is granted multiple times.",
- "The first entry is activated at 2x grants, second entry at 3x grants, and so on.",
- "Use empty entries to skip levels.")]
- public readonly string[] StackedConditions = { };
- }
-
- public class StackedCondition { }
-}
diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs
index 14cc4bfb93..175dedf20c 100644
--- a/OpenRA.Mods.Common/TraitsInterfaces.cs
+++ b/OpenRA.Mods.Common/TraitsInterfaces.cs
@@ -110,7 +110,7 @@ namespace OpenRA.Mods.Common.Traits
public interface IConditionConsumer
{
IEnumerable Conditions { get; }
- void ConditionsChanged(Actor self, IReadOnlyDictionary conditions);
+ void ConditionsChanged(Actor self, IReadOnlyDictionary conditions);
}
public interface INotifyHarvesterAction
diff --git a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs
index 2a53959552..450c009eae 100644
--- a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs
+++ b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs
@@ -149,14 +149,31 @@ namespace OpenRA.Mods.Common.UtilityCommands
upgradeMaxAcceptedLevel = FieldLoader.GetValue("", maxAcceptedNode.Value.Value);
var processed = false;
- if (upgradeTypes.Length == 1 && upgradeMinEnabledLevel == 0 && upgradeMaxEnabledLevel == 0 && upgradeMaxAcceptedLevel == 1)
+ if (upgradeMinEnabledLevel == 0 && upgradeMaxEnabledLevel == 0 && upgradeMaxAcceptedLevel == 1)
{
- node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", "!" + upgradeTypes.First()));
+ node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", upgradeTypes.Select(u => "!" + u).JoinWith(" && ")));
processed = true;
}
- else if (upgradeTypes.Length == 1 && upgradeMinEnabledLevel == 1 && upgradeMaxEnabledLevel == int.MaxValue && upgradeMaxAcceptedLevel == 1)
+ else if (upgradeMinEnabledLevel == 1 && upgradeMaxEnabledLevel == int.MaxValue && upgradeMaxAcceptedLevel == 1)
{
- node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", upgradeTypes.First()));
+ node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", upgradeTypes.JoinWith(" || ")));
+ processed = true;
+ }
+ else if (upgradeMinEnabledLevel == 0 && upgradeMaxEnabledLevel < int.MaxValue)
+ {
+ node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", upgradeTypes.JoinWith(" + ") + " <= " + upgradeMaxEnabledLevel));
+ processed = true;
+ }
+ else if (upgradeMaxEnabledLevel == int.MaxValue && upgradeMinEnabledLevel > 1)
+ {
+ node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", upgradeTypes.JoinWith(" + ") + " >= " + upgradeMinEnabledLevel));
+ processed = true;
+ }
+ else if (upgradeMaxEnabledLevel < int.MaxValue && upgradeMinEnabledLevel > 0)
+ {
+ var lowerBound = upgradeMinEnabledLevel + " <= " + upgradeTypes.JoinWith(" + ");
+ var upperBound = upgradeTypes.JoinWith(" + ") + " <= " + upgradeMaxEnabledLevel;
+ node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", lowerBound + " && " + upperBound));
processed = true;
}
diff --git a/OpenRA.Test/OpenRA.Game/BooleanExpressionTest.cs b/OpenRA.Test/OpenRA.Game/BooleanExpressionTest.cs
deleted file mode 100644
index 25fb284f7b..0000000000
--- a/OpenRA.Test/OpenRA.Game/BooleanExpressionTest.cs
+++ /dev/null
@@ -1,150 +0,0 @@
-#region Copyright & License Information
-/*
- * Copyright 2007-2017 The OpenRA Developers (see AUTHORS)
- * This file is part of OpenRA, which is free software. It is made
- * available to you under the terms of the GNU General Public License
- * as published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version. For more
- * information, see COPYING.
- */
-#endregion
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using NUnit.Framework;
-using OpenRA.Support;
-
-namespace OpenRA.Test
-{
- [TestFixture]
- public class BooleanExpressionTest
- {
- IReadOnlyDictionary testValues = new ReadOnlyDictionary(new Dictionary()
- {
- { "true", true },
- { "false", false }
- });
-
- void AssertFalse(string expression)
- {
- Assert.False(new BooleanExpression(expression).Evaluate(testValues), expression);
- }
-
- void AssertTrue(string expression)
- {
- Assert.True(new BooleanExpression(expression).Evaluate(testValues), expression);
- }
-
- void AssertParseFailure(string expression)
- {
- Assert.Throws(typeof(InvalidDataException), () => new BooleanExpression(expression).Evaluate(testValues), expression);
- }
-
- [TestCase(TestName = "AND operation")]
- public void TestAnd()
- {
- AssertTrue("true && true");
- AssertFalse("false && false");
- AssertFalse("true && false");
- AssertFalse("false && true");
- }
-
- [TestCase(TestName = "OR operation")]
- public void TestOR()
- {
- AssertTrue("true || true");
- AssertFalse("false || false");
- AssertTrue("true || false");
- AssertTrue("false || true");
- }
-
- [TestCase(TestName = "Equals operation")]
- public void TestEquals()
- {
- AssertTrue("true == true");
- AssertTrue("false == false");
- AssertFalse("true == false");
- AssertFalse("false == true");
- }
-
- [TestCase(TestName = "Not-equals (XOR) operation")]
- public void TestNotEquals()
- {
- AssertFalse("true != true");
- AssertFalse("false != false");
- AssertTrue("true != false");
- AssertTrue("false != true");
- }
-
- [TestCase(TestName = "NOT operation")]
- public void TestNOT()
- {
- AssertFalse("!true");
- AssertTrue("!false");
- AssertTrue("!!true");
- AssertFalse("!!false");
- }
-
- [TestCase(TestName = "Precedence")]
- public void TestPrecedence()
- {
- AssertTrue("true && false || true");
- AssertFalse("false || false && true");
- AssertTrue("true && !true || !false");
- AssertFalse("false || !true && !false");
- }
-
- [TestCase(TestName = "Parenthesis")]
- public void TestParens()
- {
- AssertTrue("(true)");
- AssertTrue("((true))");
- AssertFalse("(false)");
- AssertFalse("((false))");
- }
-
- [TestCase(TestName = "Parenthesis and mixed operations")]
- public void TestMixedParens()
- {
- AssertTrue("(!false)");
- AssertTrue("!(false)");
- AssertFalse("!(!false)");
- AssertTrue("(true) || (false)");
- AssertTrue("true && (false || true)");
- AssertTrue("(true && false) || true");
- AssertTrue("!(true && false) || false");
- AssertTrue("((true != true) == false) && true");
- AssertFalse("(true != false) == false && true");
- AssertTrue("true || ((true != false) != !(false && true))");
- AssertFalse("((true != false) != !(false && true))");
- }
-
- [TestCase(TestName = "Test parser errors")]
- public void TestParseErrors()
- {
- AssertParseFailure("()");
- AssertParseFailure("! && true");
- AssertParseFailure("(true");
- AssertParseFailure(")true");
- AssertParseFailure("false)");
- AssertParseFailure("false(");
- AssertParseFailure("false!");
- AssertParseFailure("true false");
- AssertParseFailure("true & false");
- AssertParseFailure("true | false");
- AssertParseFailure("true / false");
- AssertParseFailure("true & false && !");
- AssertParseFailure("(true && !)");
- AssertParseFailure("&& false");
- AssertParseFailure("false ||");
- }
-
- [TestCase(TestName = "Undefined symbols are treated as `false` values")]
- public void TestUndefinedSymbols()
- {
- AssertFalse("undef1 || undef2");
- }
- }
-}
diff --git a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs
new file mode 100644
index 0000000000..66cc5faea7
--- /dev/null
+++ b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs
@@ -0,0 +1,347 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2017 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version. For more
+ * information, see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using NUnit.Framework;
+using OpenRA.Support;
+
+namespace OpenRA.Test
+{
+ [TestFixture]
+ public class ConditionExpressionTest
+ {
+ IReadOnlyDictionary testValues = new ReadOnlyDictionary(new Dictionary()
+ {
+ { "one", 1 },
+ { "five", 5 }
+ });
+
+ void AssertFalse(string expression)
+ {
+ Assert.False(new ConditionExpression(expression).Evaluate(testValues) > 0, expression);
+ }
+
+ void AssertTrue(string expression)
+ {
+ Assert.True(new ConditionExpression(expression).Evaluate(testValues) > 0, expression);
+ }
+
+ void AssertValue(string expression, int value)
+ {
+ Assert.AreEqual(value, new ConditionExpression(expression).Evaluate(testValues), expression);
+ }
+
+ void AssertParseFailure(string expression)
+ {
+ Assert.Throws(typeof(InvalidDataException), () => new ConditionExpression(expression).Evaluate(testValues), expression);
+ }
+
+ void AssertParseFailure(string expression, string errorMessage)
+ {
+ var actualErrorMessage = Assert.Throws(typeof(InvalidDataException),
+ () => new ConditionExpression(expression).Evaluate(testValues),
+ expression).Message;
+ Assert.AreEqual(errorMessage, actualErrorMessage, expression + " ===> " + actualErrorMessage);
+ }
+
+ [TestCase(TestName = "Numbers")]
+ public void TestNumbers()
+ {
+ AssertParseFailure("1a", "Number 1 and variable merged at index 0");
+ AssertValue("0", 0);
+ AssertValue("1", 1);
+ AssertValue("12", 12);
+ AssertValue("-1", -1);
+ AssertValue("-12", -12);
+ }
+
+ [TestCase(TestName = "Variables")]
+ public void TestVariables()
+ {
+ AssertValue("one", 1);
+ AssertValue("five", 5);
+ }
+
+ [TestCase(TestName = "Boolean Constants")]
+ public void TestBoolConsts()
+ {
+ AssertValue(" true", 1);
+ AssertValue(" true ", 1);
+ AssertValue("true", 1);
+ AssertValue("false", 0);
+ AssertValue("tru", 0);
+ AssertValue("fals", 0);
+ AssertValue("tr", 0);
+ AssertValue("fal", 0);
+ }
+
+ [TestCase(TestName = "Booleans")]
+ public void TestBooleans()
+ {
+ AssertValue("false", 0);
+ AssertValue("true", 1);
+ }
+
+ [TestCase(TestName = "AND operation")]
+ public void TestAnd()
+ {
+ AssertTrue("true && true");
+ AssertFalse("false && false");
+ AssertFalse("true && false");
+ AssertFalse("false && true");
+ AssertValue("2 && false", 0);
+ AssertValue("false && 2", 0);
+ AssertValue("3 && 2", 1);
+ AssertValue("2 && 3", 1);
+ }
+
+ [TestCase(TestName = "OR operation")]
+ public void TestOR()
+ {
+ AssertTrue("true || true");
+ AssertFalse("false || false");
+ AssertTrue("true || false");
+ AssertTrue("false || true");
+ AssertValue("2 || false", 1);
+ AssertValue("false || 2", 1);
+ AssertValue("3 || 2", 1);
+ AssertValue("2 || 3", 1);
+ }
+
+ [TestCase(TestName = "Equals operation")]
+ public void TestEquals()
+ {
+ AssertTrue("true == true");
+ AssertTrue("false == false");
+ AssertFalse("true == false");
+ AssertFalse("false == true");
+ AssertTrue("1 == 1");
+ AssertTrue("0 == 0");
+ AssertFalse("1 == 0");
+ AssertTrue("1 == true");
+ AssertFalse("1 == false");
+ AssertTrue("0 == false");
+ AssertFalse("0 == true");
+ AssertValue("12 == 12", 1);
+ AssertValue("1 == 12", 0);
+ }
+
+ [TestCase(TestName = "Not-equals operation")]
+ public void TestNotEquals()
+ {
+ AssertFalse("true != true");
+ AssertFalse("false != false");
+ AssertTrue("true != false");
+ AssertTrue("false != true");
+ AssertValue("1 != 2", 1);
+ AssertValue("1 != 1", 0);
+ AssertFalse("1 != true");
+ AssertFalse("0 != false");
+ AssertTrue("1 != false");
+ AssertTrue("0 != true");
+ }
+
+ [TestCase(TestName = "NOT operation")]
+ public void TestNOT()
+ {
+ AssertValue("!true", 0);
+ AssertValue("!false", 1);
+ AssertValue("!!true", 1);
+ AssertValue("!!false", 0);
+ AssertValue("!0", 1);
+ AssertValue("!1", 0);
+ AssertValue("!5", 0);
+ AssertValue("!!5", 1);
+ AssertValue("!-5", 1);
+ }
+
+ [TestCase(TestName = "Relation operations")]
+ public void TestRelations()
+ {
+ AssertValue("2 < 5", 1);
+ AssertValue("0 < 5", 1);
+ AssertValue("5 < 2", 0);
+ AssertValue("5 < 5", 0);
+ AssertValue("-5 < 0", 1);
+ AssertValue("-2 < -5", 0);
+ AssertValue("-5 < -2", 1);
+ AssertValue("-5 < -5", 0);
+ AssertValue("-7 < 5", 1);
+ AssertValue("0 <= 5", 1);
+ AssertValue("2 <= 5", 1);
+ AssertValue("5 <= 2", 0);
+ AssertValue("5 <= 5", 1);
+ AssertValue("5 <= 0", 0);
+ AssertValue("-2 <= -5", 0);
+ AssertValue("-5 <= -2", 1);
+ AssertValue("-5 <= -5", 1);
+ AssertValue("-7 <= 5", 1);
+ AssertValue("0 <= -5", 0);
+ AssertValue("-5 <= 0", 1);
+ AssertValue("5 > 2", 1);
+ AssertValue("0 > 5", 0);
+ AssertValue("2 > 5", 0);
+ AssertValue("5 > 5", 0);
+ AssertValue("5 > 0", 1);
+ AssertValue("-2 > -5", 1);
+ AssertValue("-7 > -5", 0);
+ AssertValue("-5 > -5", 0);
+ AssertValue("-4 > -5", 1);
+ AssertValue("5 >= 0", 1);
+ AssertValue("0 >= 5", 0);
+ AssertValue("5 >= 2", 1);
+ AssertValue("2 >= 5", 0);
+ AssertValue("5 >= 5", 1);
+ AssertValue("-5 >= 0", 0);
+ AssertValue("0 >= -5", 1);
+ AssertValue("-7 >= 5", 0);
+ AssertValue("-5 >= -5", 1);
+ AssertValue("-4 >= -5", 1);
+ }
+
+ [TestCase(TestName = "Relation Mixed Precedence")]
+ public void TestRelationMixedPrecedence()
+ {
+ AssertValue("5 <= 5 && 2 > 1", 1);
+ AssertValue("5 > 5 || 2 > 1", 1);
+ AssertValue("5 > 5 || 1 > 1", 0);
+ AssertValue("5 <= 5 == 2 > 1", 1);
+ AssertValue("5 > 5 == 2 > 1", 0);
+ AssertValue("5 > 5 == 1 > 1", 1);
+ AssertValue("5 <= 5 != 2 > 1", 0);
+ AssertValue("5 > 5 != 2 > 1", 1);
+ AssertValue("5 > 5 != 1 > 1", 0);
+ AssertValue("5 > 5 != 1 >= 1", 1);
+ }
+
+ [TestCase(TestName = "AND-OR Precedence")]
+ public void TestAndOrPrecedence()
+ {
+ AssertTrue("true && false || true");
+ AssertFalse("false || false && true");
+ AssertTrue("true && !true || !false");
+ AssertFalse("false || !true && !false");
+ }
+
+ [TestCase(TestName = "Parenthesis")]
+ public void TestParens()
+ {
+ AssertTrue("(true)");
+ AssertTrue("((true))");
+ AssertFalse("(false)");
+ AssertFalse("((false))");
+ }
+
+ [TestCase(TestName = "Arithmetic")]
+ public void TestArithmetic()
+ {
+ AssertValue("~0", ~0);
+ AssertValue("-0", 0);
+ AssertValue("-a", 0);
+ AssertValue("-true", -1);
+ AssertValue("~-0", -1);
+ AssertValue("2 + 3", 5);
+ AssertValue("2 + 0", 2);
+ AssertValue("2 + 3", 5);
+ AssertValue("5 - 3", 2);
+ AssertValue("5 - -3", 8);
+ AssertValue("5 - 0", 5);
+ AssertValue("2 * 3", 6);
+ AssertValue("2 * 0", 0);
+ AssertValue("2 * -3", -6);
+ AssertValue("-2 * 3", -6);
+ AssertValue("-2 * -3", 6);
+ AssertValue("6 / 3", 2);
+ AssertValue("7 / 3", 2);
+ AssertValue("-6 / 3", -2);
+ AssertValue("6 / -3", -2);
+ AssertValue("-6 / -3", 2);
+ AssertValue("8 / 3", 2);
+ AssertValue("6 % 3", 0);
+ AssertValue("7 % 3", 1);
+ AssertValue("8 % 3", 2);
+ AssertValue("7 % 0", 0);
+ AssertValue("-7 % 3", -1);
+ AssertValue("7 % -3", 1);
+ AssertValue("-7 % -3", -1);
+ AssertValue("8 / 0", 0);
+ }
+
+ [TestCase(TestName = "Arithmetic Mixed")]
+ public void TestArithmeticMixed()
+ {
+ AssertValue("~~0", 0);
+ AssertValue("-~0", 1);
+ AssertValue("~- 0", -1);
+ AssertValue("2 * 3 + 4", 10);
+ AssertValue("2 * 3 - 4", 2);
+ AssertValue("2 + 3 * 4", 14);
+ AssertValue("2 + 3 % 4", 5);
+ AssertValue("2 + 3 / 4", 2);
+ AssertValue("2 * 3 / 4", 1);
+ AssertValue("8 / 2 == 4", 1);
+ AssertValue("~2 + ~3", -7);
+ AssertValue("~(~2 + ~3)", 6);
+ }
+
+ [TestCase(TestName = "Parenthesis and mixed operations")]
+ public void TestMixedParens()
+ {
+ AssertTrue("(!false)");
+ AssertTrue("!(false)");
+ AssertFalse("!(!false)");
+ AssertTrue("(true) || (false)");
+ AssertTrue("true && (false || true)");
+ AssertTrue("(true && false) || true");
+ AssertTrue("!(true && false) || false");
+ AssertTrue("((true != true) == false) && true");
+ AssertFalse("(true != false) == false && true");
+ AssertTrue("true || ((true != false) != !(false && true))");
+ AssertFalse("((true != false) != !(false && true))");
+ }
+
+ [TestCase(TestName = "Test parser errors")]
+ public void TestParseErrors()
+ {
+ AssertParseFailure("()", "Empty parenthesis at index 0");
+ AssertParseFailure("! && true", "Missing value or sub-expression or there is an extra operator `!` at index 0 or `&&` at index 2");
+ AssertParseFailure("(true", "Unclosed opening parenthesis at index 0");
+ AssertParseFailure(")true", "Unmatched closing parenthesis at index 0");
+ AssertParseFailure("false)", "Unmatched closing parenthesis at index 5");
+ AssertParseFailure("false(", "Missing binary operation before `(` at index 5");
+ AssertParseFailure("(", "Missing value or sub-expression at end for `(` operator");
+ AssertParseFailure(")", "Unmatched closing parenthesis at index 0");
+ AssertParseFailure("false!", "Missing binary operation before `!` at index 5");
+ AssertParseFailure("true false", "Missing binary operation before `false` at index 5");
+ AssertParseFailure("true & false", "Unexpected character '&' at index 5 - should it be `&&`?");
+ AssertParseFailure("true | false", "Unexpected character '|' at index 5 - should it be `||`?");
+ AssertParseFailure("true : false", "Invalid character ':' at index 5");
+ AssertParseFailure("true & false && !", "Unexpected character '&' at index 5 - should it be `&&`?");
+ AssertParseFailure("(true && !)", "Missing value or sub-expression or there is an extra operator `!` at index 9 or `)` at index 10");
+ AssertParseFailure("&& false", "Missing value or sub-expression at beginning for `&&` operator");
+ AssertParseFailure("false ||", "Missing value or sub-expression at end for `||` operator");
+ AssertParseFailure("1 <", "Missing value or sub-expression at end for `<` operator");
+ AssertParseFailure("-1a", "Number -1 and variable merged at index 0");
+ AssertParseFailure("-", "Missing value or sub-expression at end for `-` operator");
+ }
+
+ [TestCase(TestName = "Undefined symbols are treated as `false` (0) values")]
+ public void TestUndefinedSymbols()
+ {
+ AssertFalse("undef1 || undef2");
+ AssertValue("undef1", 0);
+ AssertValue("undef1 + undef2", 0);
+ }
+ }
+}
diff --git a/OpenRA.Test/OpenRA.Test.csproj b/OpenRA.Test/OpenRA.Test.csproj
index 4caa55258f..b9df93b6ab 100644
--- a/OpenRA.Test/OpenRA.Test.csproj
+++ b/OpenRA.Test/OpenRA.Test.csproj
@@ -55,7 +55,7 @@
-
+
diff --git a/mods/cnc/rules/defaults.yaml b/mods/cnc/rules/defaults.yaml
index 60eee515ef..cf38e6798c 100644
--- a/mods/cnc/rules/defaults.yaml
+++ b/mods/cnc/rules/defaults.yaml
@@ -17,66 +17,69 @@
^GainsExperience:
GainsExperience:
Conditions:
- 200: rank-veteran-1
- 400: rank-veteran-2
- 800: rank-veteran-3
- 1600: rank-elite
+ 200: rank-veteran
+ 400: rank-veteran
+ 800: rank-veteran
+ 1600: rank-veteran
+ GrantCondition@RANK-ELITE:
+ RequiresCondition: rank-veteran >= 4
+ Condition: rank-elite
DamageMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 95
DamageMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 90
DamageMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 85
DamageMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 75
FirepowerMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 105
FirepowerMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 110
FirepowerMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 120
FirepowerMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 130
SpeedMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 105
SpeedMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 110
SpeedMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 120
SpeedMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 140
ReloadDelayMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 95
ReloadDelayMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 90
ReloadDelayMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 85
ReloadDelayMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 75
InaccuracyMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 90
InaccuracyMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 80
InaccuracyMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 70
InaccuracyMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
@@ -92,21 +95,21 @@
Sequence: rank-veteran-1
Palette: effect
ReferencePoint: Bottom, Right
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
ZOffset: 256
WithDecoration@RANK-2:
Image: rank
Sequence: rank-veteran-2
Palette: effect
ReferencePoint: Bottom, Right
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
ZOffset: 256
WithDecoration@RANK-3:
Image: rank
Sequence: rank-veteran-3
Palette: effect
ReferencePoint: Bottom, Right
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
ZOffset: 256
WithDecoration@RANK-ELITE:
Image: rank
diff --git a/mods/d2k/rules/defaults.yaml b/mods/d2k/rules/defaults.yaml
index 01ce63a580..51ac9d51de 100644
--- a/mods/d2k/rules/defaults.yaml
+++ b/mods/d2k/rules/defaults.yaml
@@ -17,66 +17,69 @@
^GainsExperience:
GainsExperience:
Conditions:
- 200: rank-veteran-1
- 400: rank-veteran-2
- 800: rank-veteran-3
- 1600: rank-elite
+ 200: rank-veteran
+ 400: rank-veteran
+ 800: rank-veteran
+ 1600: rank-veteran
+ GrantCondition@RANK-ELITE:
+ RequiresCondition: rank-veteran >= 4
+ Condition: rank-elite
DamageMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 96
DamageMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 92
DamageMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 88
DamageMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 80
FirepowerMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 105
FirepowerMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 110
FirepowerMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 115
FirepowerMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 125
SpeedMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 105
SpeedMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 110
SpeedMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 115
SpeedMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 125
ReloadDelayMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 96
ReloadDelayMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 92
ReloadDelayMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 88
ReloadDelayMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 80
InaccuracyMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 90
InaccuracyMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 80
InaccuracyMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 70
InaccuracyMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
@@ -93,21 +96,21 @@
Sequence: rank-veteran-1
Palette: effect
ReferencePoint: Bottom, Right
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
ZOffset: 256
WithDecoration@RANK-2:
Image: rank
Sequence: rank-veteran-2
Palette: effect
ReferencePoint: Bottom, Right
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
ZOffset: 256
WithDecoration@RANK-3:
Image: rank
Sequence: rank-veteran-3
Palette: effect
ReferencePoint: Bottom, Right
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
ZOffset: 256
WithDecoration@RANK-ELITE:
Image: rank
diff --git a/mods/ra/maps/soviet-03/rules.yaml b/mods/ra/maps/soviet-03/rules.yaml
index 13514e9f62..84073817b3 100644
--- a/mods/ra/maps/soviet-03/rules.yaml
+++ b/mods/ra/maps/soviet-03/rules.yaml
@@ -59,14 +59,11 @@ V05:
DOG:
# HACK: Disable experience without killing the linter
-GainsExperience:
- ExternalCondition@RANK-VETERAN-1:
- Condition: rank-veteran-1
- ExternalCondition@RANK-VETERAN-2:
- Condition: rank-veteran-2
- ExternalCondition@RANK-VETERAN-3:
- Condition: rank-veteran-3
+ ExternalCondition@RANK-VETERAN:
+ Condition: rank-veteran
ExternalCondition@RANK-ELITE:
Condition: rank-elite
+ -GrantCondition@RANK-ELITE:
SPY:
Mobile:
diff --git a/mods/ra/rules/defaults.yaml b/mods/ra/rules/defaults.yaml
index 42ff6e71ec..91bf77b37e 100644
--- a/mods/ra/rules/defaults.yaml
+++ b/mods/ra/rules/defaults.yaml
@@ -16,66 +16,69 @@
^GainsExperience:
GainsExperience:
Conditions:
- 200: rank-veteran-1
- 400: rank-veteran-2
- 800: rank-veteran-3
- 1600: rank-elite
+ 200: rank-veteran
+ 400: rank-veteran
+ 800: rank-veteran
+ 1600: rank-veteran
+ GrantCondition@RANK-ELITE:
+ RequiresCondition: rank-veteran >= 4
+ Condition: rank-elite
DamageMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 95
DamageMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 90
DamageMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 85
DamageMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 75
FirepowerMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 105
FirepowerMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 110
FirepowerMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 120
FirepowerMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 130
SpeedMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 105
SpeedMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 110
SpeedMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 120
SpeedMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 140
ReloadDelayMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 95
ReloadDelayMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 90
ReloadDelayMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 85
ReloadDelayMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
Modifier: 75
InaccuracyMultiplier@RANK-1:
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
Modifier: 90
InaccuracyMultiplier@RANK-2:
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
Modifier: 80
InaccuracyMultiplier@RANK-3:
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
Modifier: 70
InaccuracyMultiplier@RANK-ELITE:
RequiresCondition: rank-elite
@@ -91,21 +94,21 @@
Sequence: rank-veteran-1
Palette: effect
ReferencePoint: Bottom, Right
- RequiresCondition: rank-veteran-1 && !rank-veteran-2
+ RequiresCondition: rank-veteran == 1
ZOffset: 256
WithDecoration@RANK-2:
Image: rank
Sequence: rank-veteran-2
Palette: effect
ReferencePoint: Bottom, Right
- RequiresCondition: rank-veteran-2 && !rank-veteran-3
+ RequiresCondition: rank-veteran == 2
ZOffset: 256
WithDecoration@RANK-3:
Image: rank
Sequence: rank-veteran-3
Palette: effect
ReferencePoint: Bottom, Right
- RequiresCondition: rank-veteran-3 && !rank-elite
+ RequiresCondition: rank-veteran == 3
ZOffset: 256
WithDecoration@RANK-ELITE:
Image: rank
diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml
index f9c218adf4..92bdfbaea9 100644
--- a/mods/ts/rules/defaults.yaml
+++ b/mods/ts/rules/defaults.yaml
@@ -18,28 +18,34 @@
^GainsExperience:
GainsExperience:
Conditions:
- 500: rank-veteran
- 1000: rank-elite
+ 500: rank
+ 1000: rank
+ GrantCondition@RANK-VETERAN:
+ RequiresCondition: rank == 1
+ Condition: rank-veteran
+ GrantCondition@RANK-ELITE:
+ RequiresCondition: rank >= 2
+ Condition: rank-elite
FirepowerMultiplier@VETERAN:
- RequiresCondition: rank-veteran && !rank-elite
+ RequiresCondition: rank-veteran
Modifier: 110
FirepowerMultiplier@ELITE:
RequiresCondition: rank-elite
Modifier: 130
DamageMultiplier@VETERAN:
- RequiresCondition: rank-veteran && !rank-elite
+ RequiresCondition: rank-veteran
Modifier: 90
DamageMultiplier@ELITE:
RequiresCondition: rank-elite
Modifier: 75
SpeedMultiplier@VETERAN:
- RequiresCondition: rank-veteran && !rank-elite
+ RequiresCondition: rank-veteran
Modifier: 120
SpeedMultiplier@ELITE:
RequiresCondition: rank-elite
Modifier: 140
ReloadDelayMultiplier@VETERAN:
- RequiresCondition: rank-veteran && !rank-elite
+ RequiresCondition: rank-veteran
Modifier: 90
ReloadDelayMultiplier@ELITE:
RequiresCondition: rank-elite
@@ -55,7 +61,7 @@
Sequence: veteran
Palette: ra
ReferencePoint: Bottom, Right
- RequiresCondition: rank-veteran && !rank-elite
+ RequiresCondition: rank-veteran
ZOffset: 256
WithDecoration@ELITE:
Image: rank