Merge pull request #12719 from atlimit8/ConditionExpression_with_counting

BooleanExpression => ConditionExpression with integer values for token count comparisons
This commit is contained in:
Paul Chote
2017-02-23 19:16:19 +00:00
committed by GitHub
19 changed files with 1378 additions and 617 deletions

View File

@@ -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)
{

View File

@@ -241,7 +241,7 @@
<Compile Include="Primitives\float3.cs" />
<Compile Include="InstalledMods.cs" />
<Compile Include="CryptoUtil.cs" />
<Compile Include="Support\BooleanExpression.cs" />
<Compile Include="Support\ConditionExpression.cs" />
<Compile Include="ExternalMods.cs" />
</ItemGroup>
<ItemGroup>

View File

@@ -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<string> variables = new HashSet<string>();
public IEnumerable<string> 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<Token>();
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<string, bool> symbols)
{
bool value;
symbols.TryGetValue(t.Symbol, out value);
return value;
}
static void ApplyBinaryOperation(Stack<bool> s, Func<bool, bool, bool> f)
{
var x = s.Pop();
var y = s.Pop();
s.Push(f(x, y));
}
static void ApplyUnaryOperation(Stack<bool> s, Func<bool, bool> f)
{
var x = s.Pop();
s.Push(f(x));
}
static IEnumerable<Token> ToPostfix(IEnumerable<Token> tokens)
{
var s = new Stack<Token>();
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<string, bool> symbols)
{
var s = new Stack<bool>();
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();
}
}
}

View File

@@ -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<string> variables = new HashSet<string>();
public IEnumerable<string> Variables { get { return variables; } }
readonly Func<IReadOnlyDictionary<string, int>, 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<TokenTypeInfo> CreateTokenTypeInfoEnumeration()
{
for (var i = 0; i <= (int)TokenType.Invalid; i++)
{
switch ((TokenType)i)
{
case TokenType.Invalid:
yield return new TokenTypeInfo("(<INVALID>)", 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("(<number>)", Precedence.Value);
continue;
case TokenType.Variable:
yield return new TokenTypeInfo("(<variable>)", 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<TokenType>.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<Token>();
var currentOpeners = new Stack<Token>();
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<string, int> symbols)
{
int value;
symbols.TryGetValue(symbol, out value);
return value;
}
static IEnumerable<Token> ToPostfix(IEnumerable<Token> tokens)
{
var s = new Stack<Token>();
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<string, int>), "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<Expression> expressions = new List<Expression>();
readonly List<ExpressionType> types = new List<ExpressionType>();
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<ExpressionType>.GetValues()[(int)fromType], Enum<ExpressionType>.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<IReadOnlyDictionary<string, int>, 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<string, IReadOnlyDictionary<string, int>, 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<TokenType>.GetValues()[(int)t.Type]));
}
}
return Expressions.Expression.Lambda<Func<IReadOnlyDictionary<string, int>, int>>(
ast.Pop(ExpressionType.Int), SymbolsParam).Compile();
}
}
public int Evaluate(IReadOnlyDictionary<string, int> symbols)
{
return asFunction(symbols);
}
}
}

View File

@@ -29,9 +29,9 @@ namespace OpenRA.Mods.Common.Lint
if (typeof(IEnumerable<string>).IsAssignableFrom(type))
return fieldInfo.GetValue(ruleInfo) as IEnumerable<string>;
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<string>();
}
@@ -48,9 +48,9 @@ namespace OpenRA.Mods.Common.Lint
if (typeof(IEnumerable).IsAssignableFrom(type))
return (IEnumerable<string>)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<string>();
}

View File

@@ -778,7 +778,6 @@
<Compile Include="Traits\World\CliffBackImpassabilityLayer.cs" />
<Compile Include="Traits\Conditions\GrantCondition.cs" />
<Compile Include="Traits\Conditions\ExternalCondition.cs" />
<Compile Include="Traits\Conditions\StackedCondition.cs" />
<Compile Include="Traits\Buildings\BridgeHut.cs" />
<Compile Include="Traits\Buildings\BridgePlaceholder.cs" />
<Compile Include="Traits\Buildings\GroundLevelBridge.cs" />

View File

@@ -63,24 +63,18 @@ namespace OpenRA.Mods.Common.Traits
/// <summary>Each granted condition receives a unique token that is used when revoking.</summary>
Dictionary<int, string> tokens = new Dictionary<int, string>();
/// <summary>Set of conditions that are monitored for stacked bonuses, and the bonus conditions that they grant.</summary>
readonly Dictionary<string, string[]> stackedConditions = new Dictionary<string, string[]>();
/// <summary>Tokens granted by the stacked condition bonuses defined in stackedConditions.</summary>
readonly Dictionary<string, Stack<int>> stackedTokens = new Dictionary<string, Stack<int>>();
int nextToken = 1;
/// <summary>Cache of condition -> enabled state for quick evaluation of boolean conditions.</summary>
readonly Dictionary<string, bool> conditionCache = new Dictionary<string, bool>();
/// <summary>Cache of condition -> enabled state for quick evaluation of token counter conditions.</summary>
readonly Dictionary<string, int> conditionCache = new Dictionary<string, int>();
/// <summary>Read-only version of conditionCache that is passed to IConditionConsumers.</summary>
IReadOnlyDictionary<string, bool> readOnlyConditionCache;
IReadOnlyDictionary<string, int> readOnlyConditionCache;
void INotifyCreated.Created(Actor self)
{
state = new Dictionary<string, ConditionState>();
readOnlyConditionCache = new ReadOnlyDictionary<string, bool>(conditionCache);
readOnlyConditionCache = new ReadOnlyDictionary<string, int>(conditionCache);
var allConsumers = new HashSet<IConditionConsumer>();
var allWatchers = self.TraitsImplementing<IConditionTimerWatcher>().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<StackedConditionInfo>())
{
stackedConditions[sc.Condition] = sc.StackedConditions;
stackedTokens[sc.Condition] = new Stack<int>();
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);
}
}
}
/// <summary>Grants a specified condition.</summary>

View File

@@ -19,11 +19,11 @@ namespace OpenRA.Mods.Common.Traits
/// <summary>Use as base class for *Info to subclass of UpgradableTrait. (See UpgradableTrait.)</summary>
public abstract class ConditionalTraitInfo : IConditionConsumerInfo, IRulesetLoaded
{
static readonly IReadOnlyDictionary<string, bool> NoConditions = new ReadOnlyDictionary<string, bool>(new Dictionary<string, bool>());
static readonly IReadOnlyDictionary<string, int> NoConditions = new ReadOnlyDictionary<string, int>(new Dictionary<string, int>());
[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<string, bool> conditions)
void IConditionConsumer.ConditionsChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
{
if (Info.RequiresCondition == null)
return;
var wasDisabled = IsTraitDisabled;
IsTraitDisabled = !Info.RequiresCondition.Evaluate(conditions);
IsTraitDisabled = Info.RequiresCondition.Evaluate(conditions) <= 0;
if (IsTraitDisabled != wasDisabled)
{

View File

@@ -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<StackedCondition>
{
[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 { }
}

View File

@@ -110,7 +110,7 @@ namespace OpenRA.Mods.Common.Traits
public interface IConditionConsumer
{
IEnumerable<string> Conditions { get; }
void ConditionsChanged(Actor self, IReadOnlyDictionary<string, bool> conditions);
void ConditionsChanged(Actor self, IReadOnlyDictionary<string, int> conditions);
}
public interface INotifyHarvesterAction

View File

@@ -149,14 +149,31 @@ namespace OpenRA.Mods.Common.UtilityCommands
upgradeMaxAcceptedLevel = FieldLoader.GetValue<int>("", 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;
}

View File

@@ -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<string, bool> testValues = new ReadOnlyDictionary<string, bool>(new Dictionary<string, bool>()
{
{ "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");
}
}
}

View File

@@ -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<string, int> testValues = new ReadOnlyDictionary<string, int>(new Dictionary<string, int>()
{
{ "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);
}
}
}

View File

@@ -55,7 +55,7 @@
<Compile Include="OpenRA.Mods.Common\ShapeTest.cs" />
<Compile Include="OpenRA.Game\OrderTest.cs" />
<Compile Include="OpenRA.Game\PlatformTest.cs" />
<Compile Include="OpenRA.Game\BooleanExpressionTest.cs" />
<Compile Include="OpenRA.Game\ConditionExpressionTest.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj">

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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