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);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
else if (fieldType == typeof(BooleanExpression))
|
else if (fieldType == typeof(ConditionExpression))
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return new BooleanExpression(value);
|
return new ConditionExpression(value);
|
||||||
}
|
}
|
||||||
catch (InvalidDataException e)
|
catch (InvalidDataException e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -241,7 +241,7 @@
|
|||||||
<Compile Include="Primitives\float3.cs" />
|
<Compile Include="Primitives\float3.cs" />
|
||||||
<Compile Include="InstalledMods.cs" />
|
<Compile Include="InstalledMods.cs" />
|
||||||
<Compile Include="CryptoUtil.cs" />
|
<Compile Include="CryptoUtil.cs" />
|
||||||
<Compile Include="Support\BooleanExpression.cs" />
|
<Compile Include="Support\ConditionExpression.cs" />
|
||||||
<Compile Include="ExternalMods.cs" />
|
<Compile Include="ExternalMods.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<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))
|
if (typeof(IEnumerable<string>).IsAssignableFrom(type))
|
||||||
return fieldInfo.GetValue(ruleInfo) as IEnumerable<string>;
|
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>();
|
return expr != null ? expr.Variables : Enumerable.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,9 +48,9 @@ namespace OpenRA.Mods.Common.Lint
|
|||||||
if (typeof(IEnumerable).IsAssignableFrom(type))
|
if (typeof(IEnumerable).IsAssignableFrom(type))
|
||||||
return (IEnumerable<string>)propertyInfo.GetValue(ruleInfo);
|
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>();
|
return expr != null ? expr.Variables : Enumerable.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -778,7 +778,6 @@
|
|||||||
<Compile Include="Traits\World\CliffBackImpassabilityLayer.cs" />
|
<Compile Include="Traits\World\CliffBackImpassabilityLayer.cs" />
|
||||||
<Compile Include="Traits\Conditions\GrantCondition.cs" />
|
<Compile Include="Traits\Conditions\GrantCondition.cs" />
|
||||||
<Compile Include="Traits\Conditions\ExternalCondition.cs" />
|
<Compile Include="Traits\Conditions\ExternalCondition.cs" />
|
||||||
<Compile Include="Traits\Conditions\StackedCondition.cs" />
|
|
||||||
<Compile Include="Traits\Buildings\BridgeHut.cs" />
|
<Compile Include="Traits\Buildings\BridgeHut.cs" />
|
||||||
<Compile Include="Traits\Buildings\BridgePlaceholder.cs" />
|
<Compile Include="Traits\Buildings\BridgePlaceholder.cs" />
|
||||||
<Compile Include="Traits\Buildings\GroundLevelBridge.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>
|
/// <summary>Each granted condition receives a unique token that is used when revoking.</summary>
|
||||||
Dictionary<int, string> tokens = new Dictionary<int, string>();
|
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;
|
int nextToken = 1;
|
||||||
|
|
||||||
/// <summary>Cache of condition -> enabled state for quick evaluation of boolean conditions.</summary>
|
/// <summary>Cache of condition -> enabled state for quick evaluation of token counter conditions.</summary>
|
||||||
readonly Dictionary<string, bool> conditionCache = new Dictionary<string, bool>();
|
readonly Dictionary<string, int> conditionCache = new Dictionary<string, int>();
|
||||||
|
|
||||||
/// <summary>Read-only version of conditionCache that is passed to IConditionConsumers.</summary>
|
/// <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)
|
void INotifyCreated.Created(Actor self)
|
||||||
{
|
{
|
||||||
state = new Dictionary<string, ConditionState>();
|
state = new Dictionary<string, ConditionState>();
|
||||||
readOnlyConditionCache = new ReadOnlyDictionary<string, bool>(conditionCache);
|
readOnlyConditionCache = new ReadOnlyDictionary<string, int>(conditionCache);
|
||||||
|
|
||||||
var allConsumers = new HashSet<IConditionConsumer>();
|
var allConsumers = new HashSet<IConditionConsumer>();
|
||||||
var allWatchers = self.TraitsImplementing<IConditionTimerWatcher>().ToList();
|
var allWatchers = self.TraitsImplementing<IConditionTimerWatcher>().ToList();
|
||||||
@@ -96,7 +90,7 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
if (w.Condition == condition)
|
if (w.Condition == condition)
|
||||||
cs.Watchers.Add(w);
|
cs.Watchers.Add(w);
|
||||||
|
|
||||||
conditionCache[condition] = false;
|
conditionCache[condition] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,13 +102,7 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
conditionState.Tokens.Add(kv.Key);
|
conditionState.Tokens.Add(kv.Key);
|
||||||
conditionCache[kv.Value] = conditionState.Tokens.Count > 0;
|
conditionCache[kv.Value] = conditionState.Tokens.Count;
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var sc in self.Info.TraitInfos<StackedConditionInfo>())
|
|
||||||
{
|
|
||||||
stackedConditions[sc.Condition] = sc.StackedConditions;
|
|
||||||
stackedTokens[sc.Condition] = new Stack<int>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update all traits with their initial condition state
|
// Update all traits with their initial condition state
|
||||||
@@ -133,30 +121,10 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
else
|
else
|
||||||
conditionState.Tokens.Add(token);
|
conditionState.Tokens.Add(token);
|
||||||
|
|
||||||
conditionCache[condition] = conditionState.Tokens.Count > 0;
|
conditionCache[condition] = conditionState.Tokens.Count;
|
||||||
|
|
||||||
foreach (var t in conditionState.Consumers)
|
foreach (var t in conditionState.Consumers)
|
||||||
t.ConditionsChanged(self, readOnlyConditionCache);
|
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>
|
/// <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>
|
/// <summary>Use as base class for *Info to subclass of UpgradableTrait. (See UpgradableTrait.)</summary>
|
||||||
public abstract class ConditionalTraitInfo : IConditionConsumerInfo, IRulesetLoaded
|
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]
|
[ConsumedConditionReference]
|
||||||
[Desc("Boolean expression defining the condition to enable this trait.")]
|
[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);
|
public abstract object Create(ActorInitializer init);
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ namespace OpenRA.Mods.Common.Traits
|
|||||||
|
|
||||||
public virtual void RulesetLoaded(Ruleset rules, ActorInfo ai)
|
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 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)
|
if (Info.RequiresCondition == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var wasDisabled = IsTraitDisabled;
|
var wasDisabled = IsTraitDisabled;
|
||||||
IsTraitDisabled = !Info.RequiresCondition.Evaluate(conditions);
|
IsTraitDisabled = Info.RequiresCondition.Evaluate(conditions) <= 0;
|
||||||
|
|
||||||
if (IsTraitDisabled != wasDisabled)
|
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
|
public interface IConditionConsumer
|
||||||
{
|
{
|
||||||
IEnumerable<string> Conditions { get; }
|
IEnumerable<string> Conditions { get; }
|
||||||
void ConditionsChanged(Actor self, IReadOnlyDictionary<string, bool> conditions);
|
void ConditionsChanged(Actor self, IReadOnlyDictionary<string, int> conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface INotifyHarvesterAction
|
public interface INotifyHarvesterAction
|
||||||
|
|||||||
@@ -149,14 +149,31 @@ namespace OpenRA.Mods.Common.UtilityCommands
|
|||||||
upgradeMaxAcceptedLevel = FieldLoader.GetValue<int>("", maxAcceptedNode.Value.Value);
|
upgradeMaxAcceptedLevel = FieldLoader.GetValue<int>("", maxAcceptedNode.Value.Value);
|
||||||
|
|
||||||
var processed = false;
|
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;
|
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;
|
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.Mods.Common\ShapeTest.cs" />
|
||||||
<Compile Include="OpenRA.Game\OrderTest.cs" />
|
<Compile Include="OpenRA.Game\OrderTest.cs" />
|
||||||
<Compile Include="OpenRA.Game\PlatformTest.cs" />
|
<Compile Include="OpenRA.Game\PlatformTest.cs" />
|
||||||
<Compile Include="OpenRA.Game\BooleanExpressionTest.cs" />
|
<Compile Include="OpenRA.Game\ConditionExpressionTest.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj">
|
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj">
|
||||||
|
|||||||
@@ -17,66 +17,69 @@
|
|||||||
^GainsExperience:
|
^GainsExperience:
|
||||||
GainsExperience:
|
GainsExperience:
|
||||||
Conditions:
|
Conditions:
|
||||||
200: rank-veteran-1
|
200: rank-veteran
|
||||||
400: rank-veteran-2
|
400: rank-veteran
|
||||||
800: rank-veteran-3
|
800: rank-veteran
|
||||||
1600: rank-elite
|
1600: rank-veteran
|
||||||
|
GrantCondition@RANK-ELITE:
|
||||||
|
RequiresCondition: rank-veteran >= 4
|
||||||
|
Condition: rank-elite
|
||||||
DamageMultiplier@RANK-1:
|
DamageMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 95
|
Modifier: 95
|
||||||
DamageMultiplier@RANK-2:
|
DamageMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 90
|
Modifier: 90
|
||||||
DamageMultiplier@RANK-3:
|
DamageMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 85
|
Modifier: 85
|
||||||
DamageMultiplier@RANK-ELITE:
|
DamageMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 75
|
Modifier: 75
|
||||||
FirepowerMultiplier@RANK-1:
|
FirepowerMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 105
|
Modifier: 105
|
||||||
FirepowerMultiplier@RANK-2:
|
FirepowerMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 110
|
Modifier: 110
|
||||||
FirepowerMultiplier@RANK-3:
|
FirepowerMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 120
|
Modifier: 120
|
||||||
FirepowerMultiplier@RANK-ELITE:
|
FirepowerMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 130
|
Modifier: 130
|
||||||
SpeedMultiplier@RANK-1:
|
SpeedMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 105
|
Modifier: 105
|
||||||
SpeedMultiplier@RANK-2:
|
SpeedMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 110
|
Modifier: 110
|
||||||
SpeedMultiplier@RANK-3:
|
SpeedMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 120
|
Modifier: 120
|
||||||
SpeedMultiplier@RANK-ELITE:
|
SpeedMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 140
|
Modifier: 140
|
||||||
ReloadDelayMultiplier@RANK-1:
|
ReloadDelayMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 95
|
Modifier: 95
|
||||||
ReloadDelayMultiplier@RANK-2:
|
ReloadDelayMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 90
|
Modifier: 90
|
||||||
ReloadDelayMultiplier@RANK-3:
|
ReloadDelayMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 85
|
Modifier: 85
|
||||||
ReloadDelayMultiplier@RANK-ELITE:
|
ReloadDelayMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 75
|
Modifier: 75
|
||||||
InaccuracyMultiplier@RANK-1:
|
InaccuracyMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 90
|
Modifier: 90
|
||||||
InaccuracyMultiplier@RANK-2:
|
InaccuracyMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 80
|
Modifier: 80
|
||||||
InaccuracyMultiplier@RANK-3:
|
InaccuracyMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 70
|
Modifier: 70
|
||||||
InaccuracyMultiplier@RANK-ELITE:
|
InaccuracyMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
@@ -92,21 +95,21 @@
|
|||||||
Sequence: rank-veteran-1
|
Sequence: rank-veteran-1
|
||||||
Palette: effect
|
Palette: effect
|
||||||
ReferencePoint: Bottom, Right
|
ReferencePoint: Bottom, Right
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
ZOffset: 256
|
ZOffset: 256
|
||||||
WithDecoration@RANK-2:
|
WithDecoration@RANK-2:
|
||||||
Image: rank
|
Image: rank
|
||||||
Sequence: rank-veteran-2
|
Sequence: rank-veteran-2
|
||||||
Palette: effect
|
Palette: effect
|
||||||
ReferencePoint: Bottom, Right
|
ReferencePoint: Bottom, Right
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
ZOffset: 256
|
ZOffset: 256
|
||||||
WithDecoration@RANK-3:
|
WithDecoration@RANK-3:
|
||||||
Image: rank
|
Image: rank
|
||||||
Sequence: rank-veteran-3
|
Sequence: rank-veteran-3
|
||||||
Palette: effect
|
Palette: effect
|
||||||
ReferencePoint: Bottom, Right
|
ReferencePoint: Bottom, Right
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
ZOffset: 256
|
ZOffset: 256
|
||||||
WithDecoration@RANK-ELITE:
|
WithDecoration@RANK-ELITE:
|
||||||
Image: rank
|
Image: rank
|
||||||
|
|||||||
@@ -17,66 +17,69 @@
|
|||||||
^GainsExperience:
|
^GainsExperience:
|
||||||
GainsExperience:
|
GainsExperience:
|
||||||
Conditions:
|
Conditions:
|
||||||
200: rank-veteran-1
|
200: rank-veteran
|
||||||
400: rank-veteran-2
|
400: rank-veteran
|
||||||
800: rank-veteran-3
|
800: rank-veteran
|
||||||
1600: rank-elite
|
1600: rank-veteran
|
||||||
|
GrantCondition@RANK-ELITE:
|
||||||
|
RequiresCondition: rank-veteran >= 4
|
||||||
|
Condition: rank-elite
|
||||||
DamageMultiplier@RANK-1:
|
DamageMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 96
|
Modifier: 96
|
||||||
DamageMultiplier@RANK-2:
|
DamageMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 92
|
Modifier: 92
|
||||||
DamageMultiplier@RANK-3:
|
DamageMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 88
|
Modifier: 88
|
||||||
DamageMultiplier@RANK-ELITE:
|
DamageMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 80
|
Modifier: 80
|
||||||
FirepowerMultiplier@RANK-1:
|
FirepowerMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 105
|
Modifier: 105
|
||||||
FirepowerMultiplier@RANK-2:
|
FirepowerMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 110
|
Modifier: 110
|
||||||
FirepowerMultiplier@RANK-3:
|
FirepowerMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 115
|
Modifier: 115
|
||||||
FirepowerMultiplier@RANK-ELITE:
|
FirepowerMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 125
|
Modifier: 125
|
||||||
SpeedMultiplier@RANK-1:
|
SpeedMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 105
|
Modifier: 105
|
||||||
SpeedMultiplier@RANK-2:
|
SpeedMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 110
|
Modifier: 110
|
||||||
SpeedMultiplier@RANK-3:
|
SpeedMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 115
|
Modifier: 115
|
||||||
SpeedMultiplier@RANK-ELITE:
|
SpeedMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 125
|
Modifier: 125
|
||||||
ReloadDelayMultiplier@RANK-1:
|
ReloadDelayMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 96
|
Modifier: 96
|
||||||
ReloadDelayMultiplier@RANK-2:
|
ReloadDelayMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 92
|
Modifier: 92
|
||||||
ReloadDelayMultiplier@RANK-3:
|
ReloadDelayMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 88
|
Modifier: 88
|
||||||
ReloadDelayMultiplier@RANK-ELITE:
|
ReloadDelayMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 80
|
Modifier: 80
|
||||||
InaccuracyMultiplier@RANK-1:
|
InaccuracyMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 90
|
Modifier: 90
|
||||||
InaccuracyMultiplier@RANK-2:
|
InaccuracyMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 80
|
Modifier: 80
|
||||||
InaccuracyMultiplier@RANK-3:
|
InaccuracyMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 70
|
Modifier: 70
|
||||||
InaccuracyMultiplier@RANK-ELITE:
|
InaccuracyMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
@@ -93,21 +96,21 @@
|
|||||||
Sequence: rank-veteran-1
|
Sequence: rank-veteran-1
|
||||||
Palette: effect
|
Palette: effect
|
||||||
ReferencePoint: Bottom, Right
|
ReferencePoint: Bottom, Right
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
ZOffset: 256
|
ZOffset: 256
|
||||||
WithDecoration@RANK-2:
|
WithDecoration@RANK-2:
|
||||||
Image: rank
|
Image: rank
|
||||||
Sequence: rank-veteran-2
|
Sequence: rank-veteran-2
|
||||||
Palette: effect
|
Palette: effect
|
||||||
ReferencePoint: Bottom, Right
|
ReferencePoint: Bottom, Right
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
ZOffset: 256
|
ZOffset: 256
|
||||||
WithDecoration@RANK-3:
|
WithDecoration@RANK-3:
|
||||||
Image: rank
|
Image: rank
|
||||||
Sequence: rank-veteran-3
|
Sequence: rank-veteran-3
|
||||||
Palette: effect
|
Palette: effect
|
||||||
ReferencePoint: Bottom, Right
|
ReferencePoint: Bottom, Right
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
ZOffset: 256
|
ZOffset: 256
|
||||||
WithDecoration@RANK-ELITE:
|
WithDecoration@RANK-ELITE:
|
||||||
Image: rank
|
Image: rank
|
||||||
|
|||||||
@@ -59,14 +59,11 @@ V05:
|
|||||||
DOG:
|
DOG:
|
||||||
# HACK: Disable experience without killing the linter
|
# HACK: Disable experience without killing the linter
|
||||||
-GainsExperience:
|
-GainsExperience:
|
||||||
ExternalCondition@RANK-VETERAN-1:
|
ExternalCondition@RANK-VETERAN:
|
||||||
Condition: rank-veteran-1
|
Condition: rank-veteran
|
||||||
ExternalCondition@RANK-VETERAN-2:
|
|
||||||
Condition: rank-veteran-2
|
|
||||||
ExternalCondition@RANK-VETERAN-3:
|
|
||||||
Condition: rank-veteran-3
|
|
||||||
ExternalCondition@RANK-ELITE:
|
ExternalCondition@RANK-ELITE:
|
||||||
Condition: rank-elite
|
Condition: rank-elite
|
||||||
|
-GrantCondition@RANK-ELITE:
|
||||||
|
|
||||||
SPY:
|
SPY:
|
||||||
Mobile:
|
Mobile:
|
||||||
|
|||||||
@@ -16,66 +16,69 @@
|
|||||||
^GainsExperience:
|
^GainsExperience:
|
||||||
GainsExperience:
|
GainsExperience:
|
||||||
Conditions:
|
Conditions:
|
||||||
200: rank-veteran-1
|
200: rank-veteran
|
||||||
400: rank-veteran-2
|
400: rank-veteran
|
||||||
800: rank-veteran-3
|
800: rank-veteran
|
||||||
1600: rank-elite
|
1600: rank-veteran
|
||||||
|
GrantCondition@RANK-ELITE:
|
||||||
|
RequiresCondition: rank-veteran >= 4
|
||||||
|
Condition: rank-elite
|
||||||
DamageMultiplier@RANK-1:
|
DamageMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 95
|
Modifier: 95
|
||||||
DamageMultiplier@RANK-2:
|
DamageMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 90
|
Modifier: 90
|
||||||
DamageMultiplier@RANK-3:
|
DamageMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 85
|
Modifier: 85
|
||||||
DamageMultiplier@RANK-ELITE:
|
DamageMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 75
|
Modifier: 75
|
||||||
FirepowerMultiplier@RANK-1:
|
FirepowerMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 105
|
Modifier: 105
|
||||||
FirepowerMultiplier@RANK-2:
|
FirepowerMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 110
|
Modifier: 110
|
||||||
FirepowerMultiplier@RANK-3:
|
FirepowerMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 120
|
Modifier: 120
|
||||||
FirepowerMultiplier@RANK-ELITE:
|
FirepowerMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 130
|
Modifier: 130
|
||||||
SpeedMultiplier@RANK-1:
|
SpeedMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 105
|
Modifier: 105
|
||||||
SpeedMultiplier@RANK-2:
|
SpeedMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 110
|
Modifier: 110
|
||||||
SpeedMultiplier@RANK-3:
|
SpeedMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 120
|
Modifier: 120
|
||||||
SpeedMultiplier@RANK-ELITE:
|
SpeedMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 140
|
Modifier: 140
|
||||||
ReloadDelayMultiplier@RANK-1:
|
ReloadDelayMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 95
|
Modifier: 95
|
||||||
ReloadDelayMultiplier@RANK-2:
|
ReloadDelayMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 90
|
Modifier: 90
|
||||||
ReloadDelayMultiplier@RANK-3:
|
ReloadDelayMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 85
|
Modifier: 85
|
||||||
ReloadDelayMultiplier@RANK-ELITE:
|
ReloadDelayMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 75
|
Modifier: 75
|
||||||
InaccuracyMultiplier@RANK-1:
|
InaccuracyMultiplier@RANK-1:
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
Modifier: 90
|
Modifier: 90
|
||||||
InaccuracyMultiplier@RANK-2:
|
InaccuracyMultiplier@RANK-2:
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
Modifier: 80
|
Modifier: 80
|
||||||
InaccuracyMultiplier@RANK-3:
|
InaccuracyMultiplier@RANK-3:
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
Modifier: 70
|
Modifier: 70
|
||||||
InaccuracyMultiplier@RANK-ELITE:
|
InaccuracyMultiplier@RANK-ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
@@ -91,21 +94,21 @@
|
|||||||
Sequence: rank-veteran-1
|
Sequence: rank-veteran-1
|
||||||
Palette: effect
|
Palette: effect
|
||||||
ReferencePoint: Bottom, Right
|
ReferencePoint: Bottom, Right
|
||||||
RequiresCondition: rank-veteran-1 && !rank-veteran-2
|
RequiresCondition: rank-veteran == 1
|
||||||
ZOffset: 256
|
ZOffset: 256
|
||||||
WithDecoration@RANK-2:
|
WithDecoration@RANK-2:
|
||||||
Image: rank
|
Image: rank
|
||||||
Sequence: rank-veteran-2
|
Sequence: rank-veteran-2
|
||||||
Palette: effect
|
Palette: effect
|
||||||
ReferencePoint: Bottom, Right
|
ReferencePoint: Bottom, Right
|
||||||
RequiresCondition: rank-veteran-2 && !rank-veteran-3
|
RequiresCondition: rank-veteran == 2
|
||||||
ZOffset: 256
|
ZOffset: 256
|
||||||
WithDecoration@RANK-3:
|
WithDecoration@RANK-3:
|
||||||
Image: rank
|
Image: rank
|
||||||
Sequence: rank-veteran-3
|
Sequence: rank-veteran-3
|
||||||
Palette: effect
|
Palette: effect
|
||||||
ReferencePoint: Bottom, Right
|
ReferencePoint: Bottom, Right
|
||||||
RequiresCondition: rank-veteran-3 && !rank-elite
|
RequiresCondition: rank-veteran == 3
|
||||||
ZOffset: 256
|
ZOffset: 256
|
||||||
WithDecoration@RANK-ELITE:
|
WithDecoration@RANK-ELITE:
|
||||||
Image: rank
|
Image: rank
|
||||||
|
|||||||
@@ -18,28 +18,34 @@
|
|||||||
^GainsExperience:
|
^GainsExperience:
|
||||||
GainsExperience:
|
GainsExperience:
|
||||||
Conditions:
|
Conditions:
|
||||||
500: rank-veteran
|
500: rank
|
||||||
1000: rank-elite
|
1000: rank
|
||||||
|
GrantCondition@RANK-VETERAN:
|
||||||
|
RequiresCondition: rank == 1
|
||||||
|
Condition: rank-veteran
|
||||||
|
GrantCondition@RANK-ELITE:
|
||||||
|
RequiresCondition: rank >= 2
|
||||||
|
Condition: rank-elite
|
||||||
FirepowerMultiplier@VETERAN:
|
FirepowerMultiplier@VETERAN:
|
||||||
RequiresCondition: rank-veteran && !rank-elite
|
RequiresCondition: rank-veteran
|
||||||
Modifier: 110
|
Modifier: 110
|
||||||
FirepowerMultiplier@ELITE:
|
FirepowerMultiplier@ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 130
|
Modifier: 130
|
||||||
DamageMultiplier@VETERAN:
|
DamageMultiplier@VETERAN:
|
||||||
RequiresCondition: rank-veteran && !rank-elite
|
RequiresCondition: rank-veteran
|
||||||
Modifier: 90
|
Modifier: 90
|
||||||
DamageMultiplier@ELITE:
|
DamageMultiplier@ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 75
|
Modifier: 75
|
||||||
SpeedMultiplier@VETERAN:
|
SpeedMultiplier@VETERAN:
|
||||||
RequiresCondition: rank-veteran && !rank-elite
|
RequiresCondition: rank-veteran
|
||||||
Modifier: 120
|
Modifier: 120
|
||||||
SpeedMultiplier@ELITE:
|
SpeedMultiplier@ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
Modifier: 140
|
Modifier: 140
|
||||||
ReloadDelayMultiplier@VETERAN:
|
ReloadDelayMultiplier@VETERAN:
|
||||||
RequiresCondition: rank-veteran && !rank-elite
|
RequiresCondition: rank-veteran
|
||||||
Modifier: 90
|
Modifier: 90
|
||||||
ReloadDelayMultiplier@ELITE:
|
ReloadDelayMultiplier@ELITE:
|
||||||
RequiresCondition: rank-elite
|
RequiresCondition: rank-elite
|
||||||
@@ -55,7 +61,7 @@
|
|||||||
Sequence: veteran
|
Sequence: veteran
|
||||||
Palette: ra
|
Palette: ra
|
||||||
ReferencePoint: Bottom, Right
|
ReferencePoint: Bottom, Right
|
||||||
RequiresCondition: rank-veteran && !rank-elite
|
RequiresCondition: rank-veteran
|
||||||
ZOffset: 256
|
ZOffset: 256
|
||||||
WithDecoration@ELITE:
|
WithDecoration@ELITE:
|
||||||
Image: rank
|
Image: rank
|
||||||
|
|||||||
Reference in New Issue
Block a user