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