Merge pull request #12719 from atlimit8/ConditionExpression_with_counting
BooleanExpression => ConditionExpression with integer values for token count comparisons
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
898
OpenRA.Game/Support/ConditionExpression.cs
Normal file
898
OpenRA.Game/Support/ConditionExpression.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 { }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
347
OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs
Normal file
347
OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user