From 975b9338908acce676ccd7b2874cd7bc9eaa0a5e Mon Sep 17 00:00:00 2001 From: Jamiras Date: Tue, 2 Apr 2024 20:28:03 -0600 Subject: [PATCH] add runtime expression types for rich presence macros --- Source/Parser/Expressions/ExpressionType.cs | 5 + .../Trigger/RichPresenceLookupExpression.cs | 46 ++++++ .../RichPresenceMacroExpressionBase.cs | 20 +++ .../Trigger/RichPresenceValueExpression.cs | 88 ++++++++++ Source/Parser/Functions/FormatFunction.cs | 23 ++- .../Functions/RichPresenceDisplayFunction.cs | 119 +++++--------- .../Functions/RichPresenceLookupFunction.cs | 32 ++-- .../Functions/RichPresenceMacroFunction.cs | 37 ++--- .../Functions/RichPresenceValueFunction.cs | 71 ++------ Source/Parser/RichPresenceBuilder.cs | 153 ++++++++++-------- Source/Parser/ValueBuilder.cs | 12 +- .../AchievementScriptInterpreterTests.cs | 13 ++ .../RichPresenceLookupFunctionTests.cs | 49 ++---- .../RichPresenceMacroFunctionTests.cs | 43 ++--- .../RichPresenceValueFunctionTests.cs | 56 +++---- 15 files changed, 424 insertions(+), 343 deletions(-) create mode 100644 Source/Parser/Expressions/Trigger/RichPresenceLookupExpression.cs create mode 100644 Source/Parser/Expressions/Trigger/RichPresenceMacroExpressionBase.cs create mode 100644 Source/Parser/Expressions/Trigger/RichPresenceValueExpression.cs diff --git a/Source/Parser/Expressions/ExpressionType.cs b/Source/Parser/Expressions/ExpressionType.cs index 2d14f1b5..319cae1b 100644 --- a/Source/Parser/Expressions/ExpressionType.cs +++ b/Source/Parser/Expressions/ExpressionType.cs @@ -119,6 +119,11 @@ public enum ExpressionType /// A comparison of MemoryValues with a possible hit target. /// Requirement, + + /// + /// A rich presence macro parameter. + /// + RichPresenceMacro, } internal static class ExpressionTypeExtension diff --git a/Source/Parser/Expressions/Trigger/RichPresenceLookupExpression.cs b/Source/Parser/Expressions/Trigger/RichPresenceLookupExpression.cs new file mode 100644 index 00000000..ec127f57 --- /dev/null +++ b/Source/Parser/Expressions/Trigger/RichPresenceLookupExpression.cs @@ -0,0 +1,46 @@ +using System.Text; + +namespace RATools.Parser.Expressions.Trigger +{ + internal class RichPresenceLookupExpression : RichPresenceMacroExpressionBase + { + public RichPresenceLookupExpression(StringConstantExpression name, ExpressionBase parameter) + : base(name, parameter) + { + } + + public override string FunctionName { get { return "rich_presence_lookup"; } } + + public DictionaryExpression Items { get; set; } + + public StringConstantExpression Fallback { get; set; } + + protected override bool Equals(ExpressionBase obj) + { + var that = obj as RichPresenceLookupExpression; + return (that != null && that.Name == Name && that.Parameter == Parameter && that.Fallback == Fallback && that.Items == Items); + } + + internal override void AppendString(StringBuilder builder) + { + builder.Append("rich_presence_lookup("); + Name.AppendString(builder); + builder.Append(", "); + Parameter.AppendString(builder); + builder.Append(", { }"); + + if (Fallback != null && Fallback.Value != "") + { + builder.Append(", "); + Fallback.AppendString(builder); + } + + builder.Append(')'); + } + + public override ErrorExpression Attach(RichPresenceBuilder builder) + { + return builder.AddLookupField(this, Name, Items, Fallback); + } + } +} diff --git a/Source/Parser/Expressions/Trigger/RichPresenceMacroExpressionBase.cs b/Source/Parser/Expressions/Trigger/RichPresenceMacroExpressionBase.cs new file mode 100644 index 00000000..fd8824bc --- /dev/null +++ b/Source/Parser/Expressions/Trigger/RichPresenceMacroExpressionBase.cs @@ -0,0 +1,20 @@ +namespace RATools.Parser.Expressions.Trigger +{ + internal abstract class RichPresenceMacroExpressionBase : ExpressionBase + { + protected RichPresenceMacroExpressionBase(StringConstantExpression name, ExpressionBase parameter) + : base(ExpressionType.RichPresenceMacro) + { + Name = name; + Parameter = parameter; + } + + public abstract string FunctionName { get; } + + public StringConstantExpression Name { get; private set; } + + public ExpressionBase Parameter { get; private set; } + + public abstract ErrorExpression Attach(RichPresenceBuilder builder); + } +} diff --git a/Source/Parser/Expressions/Trigger/RichPresenceValueExpression.cs b/Source/Parser/Expressions/Trigger/RichPresenceValueExpression.cs new file mode 100644 index 00000000..388e0bcc --- /dev/null +++ b/Source/Parser/Expressions/Trigger/RichPresenceValueExpression.cs @@ -0,0 +1,88 @@ +using RATools.Data; +using System.Text; + +namespace RATools.Parser.Expressions.Trigger +{ + internal class RichPresenceValueExpression : RichPresenceMacroExpressionBase + { + public RichPresenceValueExpression(StringConstantExpression name, ExpressionBase parameter) + : base(name, parameter) + { + } + + public override string FunctionName { get { return "rich_presence_value"; } } + + public ValueFormat Format { get; set; } + + public static ValueFormat ParseFormat(string format) + { + var valueFormat = Leaderboard.ParseFormat(format); + if (valueFormat == ValueFormat.None) + { + if (format == "ASCIICHAR") + valueFormat = ValueFormat.ASCIIChar; + else if (format == "UNICODECHAR") + valueFormat = ValueFormat.UnicodeChar; + } + return valueFormat; + } + + public static string GetFormatString(ValueFormat format) + { + switch (format) + { + case ValueFormat.ASCIIChar: + return "ASCIICHAR"; + + case ValueFormat.UnicodeChar: + return "UNICODECHAR"; + + default: + return Leaderboard.GetFormatString(format); + } + } + + protected override bool Equals(ExpressionBase obj) + { + var that = obj as RichPresenceValueExpression; + return (that != null && that.Format == Format && that.Name == Name && that.Parameter == Parameter); + } + + internal override void AppendString(StringBuilder builder) + { + builder.Append("rich_presence_value("); + Name.AppendString(builder); + builder.Append(", "); + Parameter.AppendString(builder); + + if (Format != ValueFormat.Value) + builder.AppendFormat(", \"{0}\"", GetFormatString(Format)); + + builder.Append(')'); + } + + public override ErrorExpression Attach(RichPresenceBuilder builder) + { + return builder.AddValueField(this, Name, Format); + } + } + + internal class RichPresenceMacroExpression : RichPresenceValueExpression + { + public RichPresenceMacroExpression(StringConstantExpression name, ExpressionBase parameter) + : base(name, parameter) + { + } + + public override string FunctionName { get { return "rich_presence_macro"; } } + + internal override void AppendString(StringBuilder builder) + { + builder.Append("rich_presence_macro("); + Name.AppendString(builder); + builder.Append(", "); + Parameter.AppendString(builder); + builder.Append(')'); + } + } +} diff --git a/Source/Parser/Functions/FormatFunction.cs b/Source/Parser/Functions/FormatFunction.cs index 5d4ce8d3..29ab1ffb 100644 --- a/Source/Parser/Functions/FormatFunction.cs +++ b/Source/Parser/Functions/FormatFunction.cs @@ -34,7 +34,7 @@ public override bool Evaluate(InterpreterScope scope, out ExpressionBase result) if (varargs == null) return false; - result = Evaluate(stringExpression, varargs, false); + result = Evaluate(stringExpression, varargs, false, ProcessParameter); return (result is StringConstantExpression); } @@ -60,7 +60,7 @@ private ArrayExpression EvaluateVarArgs(InterpreterScope scope, out ExpressionBa var stringExpression = lastExpression as StringConstantExpression; if (stringExpression != null) { - result = Evaluate(stringExpression, varargs, false); + result = Evaluate(stringExpression, varargs, false, ProcessParameter); if (result is ErrorExpression) return null; } @@ -68,7 +68,17 @@ private ArrayExpression EvaluateVarArgs(InterpreterScope scope, out ExpressionBa return varargs; } - internal static ExpressionBase Evaluate(StringConstantExpression formatString, ArrayExpression parameters, bool ignoreMissing) + private static ErrorExpression ProcessParameter(StringBuilder builder, int index, ExpressionBase parameter) + { + if (!parameter.IsLiteralConstant) + return new ConversionErrorExpression(parameter, ExpressionType.StringConstant); + + parameter.AppendStringLiteral(builder); + return null; + } + + internal static ExpressionBase Evaluate(StringConstantExpression formatString, ArrayExpression parameters, bool ignoreMissing, + Func processParameter) { var builder = new StringBuilder(); @@ -118,10 +128,9 @@ internal static ExpressionBase Evaluate(StringConstantExpression formatString, A var parameter = parameters.Entries[parameterIndex]; if (parameter != null) { - if (!parameter.IsLiteralConstant) - return new ConversionErrorExpression(parameter, ExpressionType.StringConstant); - - parameter.AppendStringLiteral(builder); + var error = processParameter(builder, parameterIndex, parameter); + if (error != null) + return error; } } diff --git a/Source/Parser/Functions/RichPresenceDisplayFunction.cs b/Source/Parser/Functions/RichPresenceDisplayFunction.cs index 71831bf3..53e823f5 100644 --- a/Source/Parser/Functions/RichPresenceDisplayFunction.cs +++ b/Source/Parser/Functions/RichPresenceDisplayFunction.cs @@ -1,7 +1,7 @@ -using RATools.Data; -using RATools.Parser.Expressions; +using RATools.Parser.Expressions; +using RATools.Parser.Expressions.Trigger; using RATools.Parser.Internal; -using System.Collections.Generic; +using System.Text; namespace RATools.Parser.Functions { @@ -69,44 +69,22 @@ protected ArrayExpression EvaluateVarArgs(InterpreterScope scope, out Expression if (varargs == null) return null; - for (int parameterIndex = 0; parameterIndex < varargs.Entries.Count; parameterIndex++) + var stringExpression = lastExpression as StringConstantExpression; + if (stringExpression != null) { - result = varargs.Entries[parameterIndex]; - var functionCall = result as FunctionCallExpression; - if (functionCall != null) - { - if (!functionCall.Evaluate(scope, out result)) - return null; + var richPresenceContext = scope.GetContext(); - varargs.Entries[parameterIndex] = result; - } - else - { - var stringValue = result as StringConstantExpression; - if (stringValue == null) + result = FormatFunction.Evaluate(stringExpression, varargs, false, + (StringBuilder builder, int index, ExpressionBase parameter) => { - var combine = result as IMathematicCombineExpression; - if (combine != null) - { - result = combine.Combine(new StringConstantExpression(""), MathematicOperation.Add); - varargs.Entries[parameterIndex] = result; - - stringValue = result as StringConstantExpression; - } - - if (stringValue == null) - stringValue = new StringConstantExpression("{" + parameterIndex + "}"); - } + // keep the placeholder - we'll need it when we serialize + builder.Append('{'); + builder.Append(index); + builder.Append('}'); - var richPresenceContext = scope.GetContext(); - richPresenceContext.DisplayString.AddParameter(stringValue); - } - } + return ProcessParameter(parameter, index, richPresenceContext); + }); - var stringExpression = lastExpression as StringConstantExpression; - if (stringExpression != null) - { - result = FormatFunction.Evaluate(stringExpression, varargs, false); if (result is ErrorExpression) return null; } @@ -114,58 +92,45 @@ protected ArrayExpression EvaluateVarArgs(InterpreterScope scope, out Expression return varargs; } - internal class RichPresenceDisplayContext - { - public RichPresenceBuilder RichPresence { get; set; } - public RichPresenceBuilder.ConditionalDisplayString DisplayString { get; set; } - } - - internal abstract class FunctionDefinition : FunctionDefinitionExpression + static ErrorExpression ProcessParameter(ExpressionBase parameter, int index, RichPresenceDisplayContext richPresenceContext) { - public FunctionDefinition(string name) - : base(name) + var richPresenceMacro = parameter as RichPresenceMacroExpressionBase; + if (richPresenceMacro != null) { - } + var error = richPresenceMacro.Attach(richPresenceContext.RichPresence); + if (error != null) + return error; - public override bool Evaluate(InterpreterScope scope, out ExpressionBase result) - { - var richPresenceContext = scope.GetContext(); - if (richPresenceContext == null) - { - result = new ErrorExpression(Name.Name + " has no meaning outside of a rich_presence_display call"); - return false; - } + var value = ValueBuilder.BuildValue(richPresenceMacro.Parameter, out error); + if (error != null) + return new ErrorExpression(richPresenceMacro.FunctionName + " call failed", richPresenceMacro) { InnerError = error }; - return BuildMacro(richPresenceContext, scope, out result); + richPresenceContext.DisplayString.AddParameter(index, richPresenceMacro, value); + return null; } - protected abstract bool BuildMacro(RichPresenceDisplayContext context, InterpreterScope scope, out ExpressionBase result); - - protected static Value GetExpressionValue(InterpreterScope scope, out ExpressionBase result) + var stringValue = parameter as StringConstantExpression; + if (stringValue == null) { - var expression = GetParameter(scope, "expression", out result); - if (expression == null) - return null; - - var requirements = new List(); - var context = new ValueBuilderContext { Trigger = requirements }; - var triggerBuilderScope = new InterpreterScope(scope) { Context = context }; - if (!expression.ReplaceVariables(triggerBuilderScope, out expression)) - { - result = expression; - return null; - } - - ErrorExpression error; - var value = ValueBuilder.BuildValue(expression, out error); - if (value == null) + var combine = parameter as IMathematicCombineExpression; + if (combine != null) { - result = error; - return null; + var result = combine.Combine(new StringConstantExpression(""), MathematicOperation.Add); + stringValue = result as StringConstantExpression; } - return value; + if (stringValue == null) + stringValue = new StringConstantExpression("{" + index + "}"); } + + richPresenceContext.DisplayString.AddParameter(index, stringValue); + return null; + } + + internal class RichPresenceDisplayContext + { + public RichPresenceBuilder RichPresence { get; set; } + public RichPresenceBuilder.ConditionalDisplayString DisplayString { get; set; } } } } diff --git a/Source/Parser/Functions/RichPresenceLookupFunction.cs b/Source/Parser/Functions/RichPresenceLookupFunction.cs index fd54a4ed..91f85cfd 100644 --- a/Source/Parser/Functions/RichPresenceLookupFunction.cs +++ b/Source/Parser/Functions/RichPresenceLookupFunction.cs @@ -1,8 +1,9 @@ using RATools.Parser.Expressions; +using RATools.Parser.Expressions.Trigger; namespace RATools.Parser.Functions { - internal class RichPresenceLookupFunction : RichPresenceDisplayFunction.FunctionDefinition + internal class RichPresenceLookupFunction : FunctionDefinitionExpression { public RichPresenceLookupFunction() : base("rich_presence_lookup") @@ -15,7 +16,7 @@ public RichPresenceLookupFunction() DefaultParameters["fallback"] = new StringConstantExpression(""); } - protected override bool BuildMacro(RichPresenceDisplayFunction.RichPresenceDisplayContext context, InterpreterScope scope, out ExpressionBase result) + public override bool ReplaceVariables(InterpreterScope scope, out ExpressionBase result) { var name = GetStringParameter(scope, "name", out result); if (name == null) @@ -30,6 +31,8 @@ protected override bool BuildMacro(RichPresenceDisplayFunction.RichPresenceDispl return false; var expression = GetParameter(scope, "expression", out result); + if (expression == null) + return false; var integer = expression as IntegerConstantExpression; if (integer != null) @@ -39,22 +42,27 @@ protected override bool BuildMacro(RichPresenceDisplayFunction.RichPresenceDispl var stringValue = entry as StringConstantExpression; if (stringValue != null) { - context.DisplayString.AddParameter(stringValue); + result = stringValue; return true; } } - - var value = GetExpressionValue(scope, out result); - if (value == null) - return false; - - var functionCall = scope.GetContext(); - result = context.RichPresence.AddLookupField(functionCall, name, dictionary, fallback); - if (result != null) + else if (!ValueBuilder.IsConvertible(expression)) + { + result = ValueBuilder.InconvertibleError(expression); return false; + } - context.DisplayString.AddParameter(name.Value, value); + result = new RichPresenceLookupExpression(name, expression) { Items = dictionary, Fallback = fallback }; + CopyLocation(result); + result.MakeReadOnly(); return true; } + + public override bool Evaluate(InterpreterScope scope, out ExpressionBase result) + { + var functionCall = scope.GetContext(); + result = new ErrorExpression(Name.Name + " has no meaning outside of a rich_presence_display call", functionCall.FunctionName); + return false; + } } } diff --git a/Source/Parser/Functions/RichPresenceMacroFunction.cs b/Source/Parser/Functions/RichPresenceMacroFunction.cs index fe62b6c2..9d46b3d2 100644 --- a/Source/Parser/Functions/RichPresenceMacroFunction.cs +++ b/Source/Parser/Functions/RichPresenceMacroFunction.cs @@ -1,10 +1,10 @@ using RATools.Data; using RATools.Parser.Expressions; -using System.Diagnostics; +using RATools.Parser.Expressions.Trigger; namespace RATools.Parser.Functions { - internal class RichPresenceMacroFunction : RichPresenceDisplayFunction.FunctionDefinition + internal class RichPresenceMacroFunction : FunctionDefinitionExpression { public RichPresenceMacroFunction() : base("rich_presence_macro") @@ -79,7 +79,8 @@ public override bool ReplaceVariables(InterpreterScope scope, out ExpressionBase if (macro == null) return false; - if (GetValueFormat(macro.Value) == ValueFormat.None) + var valueFormat = GetValueFormat(macro.Value); + if (valueFormat == ValueFormat.None) { result = new ErrorExpression("Unknown rich presence macro: " + macro.Value); return false; @@ -89,31 +90,23 @@ public override bool ReplaceVariables(InterpreterScope scope, out ExpressionBase if (expression == null) return false; - result = new FunctionCallExpression(Name.Name, new ExpressionBase[] { macro, expression }); + if (!ValueBuilder.IsConvertible(expression)) + { + result = ValueBuilder.InconvertibleError(expression); + return false; + } + + result = new RichPresenceMacroExpression(macro, expression) { Format = valueFormat }; CopyLocation(result); + result.MakeReadOnly(); return true; } - protected override bool BuildMacro(RichPresenceDisplayFunction.RichPresenceDisplayContext context, InterpreterScope scope, out ExpressionBase result) + public override bool Evaluate(InterpreterScope scope, out ExpressionBase result) { - var macro = GetStringParameter(scope, "macro", out result); - if (macro == null) - return false; - - var valueFormat = GetValueFormat(macro.Value); - Debug.Assert(valueFormat != ValueFormat.None); // validated in ReplaceVariables - - var value = GetExpressionValue(scope, out result); - if (value == null) - return false; - var functionCall = scope.GetContext(); - result = context.RichPresence.AddValueField(functionCall, macro, valueFormat); - if (result != null) - return false; - - context.DisplayString.AddParameter(macro.Value, value); - return true; + result = new ErrorExpression(Name.Name + " has no meaning outside of a rich_presence_display call", functionCall.FunctionName); + return false; } } } diff --git a/Source/Parser/Functions/RichPresenceValueFunction.cs b/Source/Parser/Functions/RichPresenceValueFunction.cs index b25801ef..10405a12 100644 --- a/Source/Parser/Functions/RichPresenceValueFunction.cs +++ b/Source/Parser/Functions/RichPresenceValueFunction.cs @@ -1,9 +1,10 @@ using RATools.Data; using RATools.Parser.Expressions; +using RATools.Parser.Expressions.Trigger; namespace RATools.Parser.Functions { - internal class RichPresenceValueFunction : RichPresenceDisplayFunction.FunctionDefinition + internal class RichPresenceValueFunction : FunctionDefinitionExpression { public RichPresenceValueFunction() : base("rich_presence_value") @@ -15,34 +16,6 @@ public RichPresenceValueFunction() DefaultParameters["format"] = new StringConstantExpression("value"); } - public static ValueFormat ParseFormat(string format) - { - var valueFormat = Leaderboard.ParseFormat(format); - if (valueFormat == ValueFormat.None) - { - if (format == "ASCIICHAR") - valueFormat = ValueFormat.ASCIIChar; - else if (format == "UNICODECHAR") - valueFormat = ValueFormat.UnicodeChar; - } - return valueFormat; - } - - public static string GetFormatString(ValueFormat format) - { - switch (format) - { - case ValueFormat.ASCIIChar: - return "ASCIICHAR"; - - case ValueFormat.UnicodeChar: - return "UNICODECHAR"; - - default: - return Leaderboard.GetFormatString(format); - } - } - public override bool ReplaceVariables(InterpreterScope scope, out ExpressionBase result) { var name = GetStringParameter(scope, "name", out result); @@ -53,7 +26,7 @@ public override bool ReplaceVariables(InterpreterScope scope, out ExpressionBase if (format == null) return false; - var valueFormat = ParseFormat(format.Value); + var valueFormat = RichPresenceValueExpression.ParseFormat(format.Value); if (valueFormat == ValueFormat.None) { result = new ErrorExpression(format.Value + " is not a supported rich_presence_value format", format); @@ -64,39 +37,23 @@ public override bool ReplaceVariables(InterpreterScope scope, out ExpressionBase if (expression == null) return false; - result = new FunctionCallExpression(Name.Name, new ExpressionBase[] { name, expression, format }); - CopyLocation(result); - return true; - } - - protected override bool BuildMacro(RichPresenceDisplayFunction.RichPresenceDisplayContext context, InterpreterScope scope, out ExpressionBase result) - { - var name = GetStringParameter(scope, "name", out result); - if (name == null) - return false; - - var format = GetStringParameter(scope, "format", out result); - if (format == null) - return false; - - var valueFormat = ParseFormat(format.Value); - if (valueFormat == ValueFormat.None) + if (!ValueBuilder.IsConvertible(expression)) { - result = new ErrorExpression("Unknown format", format); + result = ValueBuilder.InconvertibleError(expression); return false; } - var value = GetExpressionValue(scope, out result); - if (value == null) - return false; + result = new RichPresenceValueExpression(name, expression) { Format = valueFormat }; + CopyLocation(result); + result.MakeReadOnly(); + return true; + } + public override bool Evaluate(InterpreterScope scope, out ExpressionBase result) + { var functionCall = scope.GetContext(); - result = context.RichPresence.AddValueField(functionCall, name, valueFormat); - if (result != null) - return false; - - context.DisplayString.AddParameter(name.Value, value); - return true; + result = new ErrorExpression(Name.Name + " has no meaning outside of a rich_presence_display call", functionCall.FunctionName); + return false; } } } diff --git a/Source/Parser/RichPresenceBuilder.cs b/Source/Parser/RichPresenceBuilder.cs index 94e6160a..4b5499a3 100644 --- a/Source/Parser/RichPresenceBuilder.cs +++ b/Source/Parser/RichPresenceBuilder.cs @@ -1,6 +1,7 @@ using Jamiras.Components; using RATools.Data; using RATools.Parser.Expressions; +using RATools.Parser.Expressions.Trigger; using RATools.Parser.Functions; using System; using System.Collections.Generic; @@ -20,9 +21,9 @@ public RichPresenceBuilder() _displayStrings = new List(); } - private List _displayStrings; - private SortedDictionary _valueFields; - private SortedDictionary _lookupFields; + private readonly List _displayStrings; + private readonly SortedDictionary _valueFields; + private readonly SortedDictionary _lookupFields; /// /// The line associated to the `rich_presence_display` call. @@ -67,23 +68,9 @@ private class ValueField public ValueFormat Format { get; set; } } - [DebuggerDisplay("@{Name}({Value})")] - private class DisplayStringParameter + internal class ConditionalDisplayString { - /// - /// The name of the macro to use for this parameter. - /// - public string Name { get; set; } - - /// - /// The parameter to pass to the macro. - /// - public Value Value { get; set; } - } - - public class ConditionalDisplayString - { - private ICollection _parameters; + private Dictionary _parameters; /// /// The raw string with placeholders. @@ -96,20 +83,27 @@ public class ConditionalDisplayString /// If null, this is the default case. public Trigger Condition { get; set; } - public void AddParameter(string macro, Value parameter) + private class Parameter + { + public RichPresenceMacroExpressionBase Macro { get; set; } + public StringConstantExpression Constant { get; set; } + public Value Value { get; set; } + } + + public void AddParameter(int index, RichPresenceMacroExpressionBase macro, Value value) { if (_parameters == null) - _parameters = new List(); + _parameters = new Dictionary(); - _parameters.Add(new DisplayStringParameter { Name = macro, Value = parameter }); + _parameters[index] = new Parameter { Macro = macro, Value = value }; } - public void AddParameter(StringConstantExpression parameter) + public void AddParameter(int index, StringConstantExpression value) { if (_parameters == null) - _parameters = new List(); + _parameters = new Dictionary(); - _parameters.Add(new DisplayStringParameter { Name = parameter.Value, Value = null }); + _parameters[index] = new Parameter { Constant = value }; } /// @@ -130,62 +124,81 @@ private string Serialize(SerializationContext serializationContext, bool ignoreM var parameters = new ArrayExpression(); if (_parameters != null) { - foreach (var parameter in _parameters) - { - string formatted; - if (parameter.Value == null) - { - // raw string, not a macro - formatted = parameter.Name; - } - else - { - SerializationContext useSerializationContext = serializationContext; - if (serializationContext.MinimumVersion >= Data.Version._0_77) - { - if (parameter.Value.Values.Count() == 1 && - parameter.Value.Values.First().Requirements.Count() == 1 && - !parameter.Value.Values.First().Requirements.First().IsComparison) - { - // single field lookup - force legacy format, even if using sizes only available in 0.77+ - useSerializationContext = serializationContext.WithVersion(Data.Version._0_76); - } - else if (parameter.Value.MinimumVersion() < Data.Version._0_77) - { - // simple AddSource chain, just use legacy format - useSerializationContext = serializationContext.WithVersion(Data.Version._0_76); - } - } + var maxIndex = 0; + foreach (var index in _parameters.Keys) + maxIndex = Math.Max(index, maxIndex); - formatted = String.Format("@{0}({1})", - parameter.Name, parameter.Value.Serialize(useSerializationContext)); - } + for (int i = 0; i <= maxIndex; i++) + parameters.Entries.Add(null); - parameters.Entries.Add(new StringConstantExpression(formatted)); + foreach (var kvp in _parameters) + { + if (kvp.Value.Macro != null) + parameters.Entries[kvp.Key] = kvp.Value.Macro; + else + parameters.Entries[kvp.Key] = kvp.Value.Constant; } } - ExpressionBase result = FormatFunction.Evaluate(Format, parameters, ignoreMissing); + var result = FormatFunction.Evaluate(Format, parameters, ignoreMissing, + (StringBuilder builder, int index, ExpressionBase parameter) => ProcessParameter(builder, index, parameter, serializationContext)); + var stringResult = result as StringConstantExpression; return (stringResult != null) ? stringResult.Value : ""; } - public bool UsesMacro(string macroName) + private ErrorExpression ProcessParameter(StringBuilder builder, int index, ExpressionBase parameter, SerializationContext serializationContext) { - if (_parameters != null) + var str = parameter as StringConstantExpression; + if (str != null) + { + str.AppendStringLiteral(builder); + return null; + } + + var param = _parameters[index]; + if (param.Macro != null) { - int i = 0; - foreach (var parameter in _parameters) + SerializationContext useSerializationContext = serializationContext; + + if (serializationContext.MinimumVersion >= Data.Version._0_77) { - if (parameter.Name == macroName) + if (param.Value.Values.Count() == 1 && + param.Value.Values.First().Requirements.Count() == 1 && + !param.Value.Values.First().Requirements.First().IsComparison) { - var placeholder = "{" + i + "}"; - - if (Format.Value.Contains(placeholder)) - return true; + // single field lookup - force legacy format, even if using sizes only available in 0.77+ + useSerializationContext = serializationContext.WithVersion(Data.Version._0_76); } + else if (param.Value.MinimumVersion() < Data.Version._0_77) + { + // simple AddSource chain, just use legacy format + useSerializationContext = serializationContext.WithVersion(Data.Version._0_76); + } + } - ++i; + builder.Append('@'); + builder.Append(param.Macro.Name.Value); + builder.Append('('); + builder.Append(param.Value.Serialize(useSerializationContext)); + builder.Append(')'); + return null; + } + + builder.Append('{'); + builder.Append(index); + builder.Append('}'); + return null; + } + + public bool UsesMacro(string macroName) + { + if (_parameters != null) + { + foreach (var kvp in _parameters) + { + if (kvp.Value.Macro != null && kvp.Value.Macro.Name.Value == macroName) + return true; } } @@ -197,7 +210,7 @@ public SoftwareVersion MinimumVersion() var minimumVersion = (Condition != null) ? Condition.MinimumVersion() : Data.Version.MinimumVersion; if (_parameters != null) { - foreach (var parameter in _parameters) + foreach (var parameter in _parameters.Values) { if (parameter.Value != null) minimumVersion = minimumVersion.OrNewer(parameter.Value.MinimumVersion()); @@ -212,7 +225,7 @@ public uint MaximumAddress() uint maximumAddress = (Condition != null) ? Condition.MaximumAddress() : 0; if (_parameters != null) { - foreach (var parameter in _parameters) + foreach (var parameter in _parameters.Values) { if (parameter.Value != null) maximumAddress = Math.Max(maximumAddress, parameter.Value.MaximumAddress()); @@ -247,7 +260,7 @@ public static ValueFormat GetValueFormat(string macro) public static string GetFormatString(ValueFormat format) { - return RichPresenceValueFunction.GetFormatString(format); + return RichPresenceValueExpression.GetFormatString(format); } public ErrorExpression AddValueField(ExpressionBase func, StringConstantExpression name, ValueFormat format) @@ -299,7 +312,7 @@ public ErrorExpression AddLookupField(ExpressionBase func, StringConstantExpress return null; } - public ConditionalDisplayString AddDisplayString(Trigger condition, StringConstantExpression formatString) + internal ConditionalDisplayString AddDisplayString(Trigger condition, StringConstantExpression formatString) { var displayString = new ConditionalDisplayString { diff --git a/Source/Parser/ValueBuilder.cs b/Source/Parser/ValueBuilder.cs index 57a51c02..2f1eff2b 100644 --- a/Source/Parser/ValueBuilder.cs +++ b/Source/Parser/ValueBuilder.cs @@ -28,6 +28,16 @@ public ValueBuilder(Value source) _values = values; } + internal static bool IsConvertible(ExpressionBase expression) + { + return (expression is ITriggerExpression || expression is IntegerConstantExpression); + } + + internal static ErrorExpression InconvertibleError(ExpressionBase expression) + { + return new ErrorExpression("Cannot create value from " + expression.Type.ToLowerString(), expression); + } + public static Value BuildValue(ExpressionBase expression, out ErrorExpression error) { var integerConstant = expression as IntegerConstantExpression; @@ -45,7 +55,7 @@ public static Value BuildValue(ExpressionBase expression, out ErrorExpression er var trigger = expression as ITriggerExpression; if (trigger == null) { - error = new ErrorExpression("Cannot create value from " + expression.Type.ToLowerString(), expression); + error = InconvertibleError(expression); return null; } diff --git a/Tests/Parser/AchievementScriptInterpreterTests.cs b/Tests/Parser/AchievementScriptInterpreterTests.cs index 868df9f9..76ea8560 100644 --- a/Tests/Parser/AchievementScriptInterpreterTests.cs +++ b/Tests/Parser/AchievementScriptInterpreterTests.cs @@ -753,6 +753,19 @@ public void TestRichPresenceLookupReusedDifferingDictionary() Assert.That(parser.ErrorMessage, Is.EqualTo("4:41 Multiple rich_presence_lookup calls with the same name must have the same dictionary")); } + [Test] + public void TestRichPresenceSkippedParameter() + { + // {1} does not appear in the format string, so Unused should not be defined as a Format + var parser = Parse("rich_presence_display(\"value {0} here, {2} there\",\n" + + "rich_presence_value(\"Test\", byte(0x1234))," + + "rich_presence_value(\"Unused\", byte(0x2345))," + + "rich_presence_value(\"Third\", byte(0x3456))" + + ")"); + Assert.That(parser.RichPresence, Is.EqualTo("Format:Test\r\nFormatType=VALUE\r\n\r\nFormat:Third\r\nFormatType=VALUE\r\n\r\nDisplay:\r\nvalue @Test(0xH1234) here, @Third(0xH3456) there\r\n")); + } + + [Test] public void TestRichPresenceLookupReusedEquivalentDictionary() { diff --git a/Tests/Parser/Functions/RichPresenceLookupFunctionTests.cs b/Tests/Parser/Functions/RichPresenceLookupFunctionTests.cs index ccca3762..cba7063c 100644 --- a/Tests/Parser/Functions/RichPresenceLookupFunctionTests.cs +++ b/Tests/Parser/Functions/RichPresenceLookupFunctionTests.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using RATools.Parser.Expressions; using RATools.Parser.Functions; +using RATools.Parser.Tests.Expressions; using System.Linq; namespace RATools.Parser.Tests.Functions @@ -18,37 +19,16 @@ public RichPresenceLookupFunctionHarness() public InterpreterScope Scope { get; private set; } - public RichPresenceBuilder Evaluate(string input, string expectedError = null) + public RichPresenceBuilder Evaluate(string input) { - var funcDef = new RichPresenceLookupFunction(); - + input = "rich_presence_display(\"{0}\", " + input + ")"; var expression = ExpressionBase.Parse(new PositionalTokenizer(Tokenizer.CreateTokenizer(input))); Assert.That(expression, Is.InstanceOf()); var funcCall = (FunctionCallExpression)expression; - ExpressionBase error; - var scope = funcCall.GetParameters(funcDef, Scope, out error); - var context = new RichPresenceDisplayFunction.RichPresenceDisplayContext { RichPresence = new RichPresenceBuilder() }; - context.DisplayString = context.RichPresence.AddDisplayString(null, new StringConstantExpression("{0}")); - scope.Context = context; - - ExpressionBase evaluated; - if (expectedError != null && expectedError.EndsWith(" format")) - { - Assert.That(funcDef.ReplaceVariables(scope, out evaluated), Is.False); - var parseError = evaluated as ErrorExpression; - Assert.That(parseError, Is.Not.Null); - Assert.That(parseError.Message, Is.EqualTo(expectedError)); - return context.RichPresence; - } - - ExpressionBase result; - Assert.That(funcDef.Evaluate(scope, out result), Is.True); - if (expectedError != null) - { - Assert.That(result, Is.InstanceOf()); - Assert.That(((ErrorExpression)result).Message, Is.EqualTo(expectedError)); - } + var context = new AchievementScriptContext { RichPresence = new RichPresenceBuilder() }; + Scope.Context = context; + funcCall.Execute(Scope); return context.RichPresence; } @@ -90,23 +70,18 @@ public void TestSimple() [Test] public void TestExplicitCall() { - // not providing a RichPresenceDisplayContext simulates calling the function at a global scope - var funcDef = new RichPresenceLookupFunction(); - var input = "rich_presence_lookup(\"Name\", byte(0x1234), lookup)"; var expression = ExpressionBase.Parse(new PositionalTokenizer(Tokenizer.CreateTokenizer(input))); Assert.That(expression, Is.InstanceOf()); var funcCall = (FunctionCallExpression)expression; - var parentScope = new InterpreterScope(AchievementScriptInterpreter.GetGlobalScope()); - var dict = new DictionaryExpression(); - parentScope.DefineVariable(new VariableDefinitionExpression("lookup"), dict); + var context = new AchievementScriptContext { RichPresence = new RichPresenceBuilder() }; + var scope = new InterpreterScope(AchievementScriptInterpreter.GetGlobalScope()); + scope.DefineVariable(new VariableDefinitionExpression("lookup"), new DictionaryExpression()); + scope.Context = context; + var error = funcCall.Execute(scope); - ExpressionBase error; - var scope = funcCall.GetParameters(funcDef, parentScope, out error); - Assert.That(funcDef.Evaluate(scope, out error), Is.False); - Assert.That(error, Is.InstanceOf()); - Assert.That(((ErrorExpression)error).Message, Is.EqualTo("rich_presence_lookup has no meaning outside of a rich_presence_display call")); + ExpressionTests.AssertError(error, "rich_presence_lookup has no meaning outside of a rich_presence_display call"); } [Test] diff --git a/Tests/Parser/Functions/RichPresenceMacroFunctionTests.cs b/Tests/Parser/Functions/RichPresenceMacroFunctionTests.cs index bc7153f7..13fdef3f 100644 --- a/Tests/Parser/Functions/RichPresenceMacroFunctionTests.cs +++ b/Tests/Parser/Functions/RichPresenceMacroFunctionTests.cs @@ -3,6 +3,7 @@ using RATools.Data; using RATools.Parser.Expressions; using RATools.Parser.Functions; +using RATools.Parser.Tests.Expressions; using System.Linq; namespace RATools.Parser.Tests.Functions @@ -20,37 +21,17 @@ public void TestDefinition() Assert.That(def.Parameters.ElementAt(1).Name, Is.EqualTo("expression")); } - private static RichPresenceBuilder Evaluate(string input, string expectedError = null) + private static RichPresenceBuilder Evaluate(string input) { - var funcDef = new RichPresenceMacroFunction(); - + input = "rich_presence_display(\"{0}\", " + input + ")"; var expression = ExpressionBase.Parse(new PositionalTokenizer(Tokenizer.CreateTokenizer(input))); Assert.That(expression, Is.InstanceOf()); var funcCall = (FunctionCallExpression)expression; - ExpressionBase error; - var scope = funcCall.GetParameters(funcDef, AchievementScriptInterpreter.GetGlobalScope(), out error); - var context = new RichPresenceDisplayFunction.RichPresenceDisplayContext { RichPresence = new RichPresenceBuilder() }; - context.DisplayString = context.RichPresence.AddDisplayString(null, new StringConstantExpression("{0}")); + var context = new AchievementScriptContext { RichPresence = new RichPresenceBuilder() }; + var scope = new InterpreterScope(AchievementScriptInterpreter.GetGlobalScope()); scope.Context = context; - - ExpressionBase evaluated; - if (expectedError != null && expectedError.StartsWith("Unknown rich presence macro")) - { - Assert.That(funcDef.ReplaceVariables(scope, out evaluated), Is.False); - var parseError = evaluated as ErrorExpression; - Assert.That(parseError, Is.Not.Null); - Assert.That(parseError.Message, Is.EqualTo(expectedError)); - return context.RichPresence; - } - - ExpressionBase result; - Assert.That(funcDef.Evaluate(scope, out result), Is.True); - if (expectedError != null) - { - Assert.That(result, Is.InstanceOf()); - Assert.That(((ErrorExpression)result).Message, Is.EqualTo(expectedError)); - } + funcCall.Execute(scope); return context.RichPresence; } @@ -80,7 +61,17 @@ public void TestMacro(string macro) [Test] public void TestMacroInvalid() { - Evaluate("rich_presence_macro(\"unknown\", byte(0x1234))", "Unknown rich presence macro: unknown"); + var input = "rich_presence_display(\"{0}\", rich_presence_macro(\"unknown\", byte(0x1234)))"; + var expression = ExpressionBase.Parse(new PositionalTokenizer(Tokenizer.CreateTokenizer(input))); + Assert.That(expression, Is.InstanceOf()); + var funcCall = (FunctionCallExpression)expression; + + var context = new AchievementScriptContext { RichPresence = new RichPresenceBuilder() }; + var scope = new InterpreterScope(AchievementScriptInterpreter.GetGlobalScope()); + scope.Context = context; + var error = funcCall.Execute(scope); + + ExpressionTests.AssertError(error, "Unknown rich presence macro: unknown"); } } } diff --git a/Tests/Parser/Functions/RichPresenceValueFunctionTests.cs b/Tests/Parser/Functions/RichPresenceValueFunctionTests.cs index f86eb082..9dc6e533 100644 --- a/Tests/Parser/Functions/RichPresenceValueFunctionTests.cs +++ b/Tests/Parser/Functions/RichPresenceValueFunctionTests.cs @@ -23,37 +23,17 @@ public void TestDefinition() Assert.That(def.DefaultParameters["format"], Is.EqualTo(new StringConstantExpression("value"))); } - private static RichPresenceBuilder Evaluate(string input, string expectedError = null) + private static RichPresenceBuilder Evaluate(string input) { - var funcDef = new RichPresenceValueFunction(); - + input = "rich_presence_display(\"{0}\", " + input + ")"; var expression = ExpressionBase.Parse(new PositionalTokenizer(Tokenizer.CreateTokenizer(input))); Assert.That(expression, Is.InstanceOf()); var funcCall = (FunctionCallExpression)expression; - ExpressionBase error; - var scope = funcCall.GetParameters(funcDef, AchievementScriptInterpreter.GetGlobalScope(), out error); - var context = new RichPresenceDisplayFunction.RichPresenceDisplayContext { RichPresence = new RichPresenceBuilder() }; - context.DisplayString = context.RichPresence.AddDisplayString(null, new StringConstantExpression("{0}")); + var context = new AchievementScriptContext { RichPresence = new RichPresenceBuilder() }; + var scope = new InterpreterScope(AchievementScriptInterpreter.GetGlobalScope()); scope.Context = context; - - ExpressionBase evaluated; - if (expectedError != null && expectedError.EndsWith(" format")) - { - Assert.That(funcDef.ReplaceVariables(scope, out evaluated), Is.False); - var parseError = evaluated as ErrorExpression; - Assert.That(parseError, Is.Not.Null); - Assert.That(parseError.Message, Is.EqualTo(expectedError)); - return context.RichPresence; - } - - ExpressionBase result; - Assert.That(funcDef.Evaluate(scope, out result), Is.True); - if (expectedError != null) - { - Assert.That(result, Is.InstanceOf()); - Assert.That(((ErrorExpression)result).Message, Is.EqualTo(expectedError)); - } + funcCall.Execute(scope); return context.RichPresence; } @@ -69,18 +49,17 @@ public void TestSimple() public void TestExplicitCall() { // not providing a RichPresenceDisplayContext simulates calling the function at a global scope - var funcDef = new RichPresenceValueFunction(); - var input = "rich_presence_value(\"Name\", byte(0x1234))"; var expression = ExpressionBase.Parse(new PositionalTokenizer(Tokenizer.CreateTokenizer(input))); Assert.That(expression, Is.InstanceOf()); var funcCall = (FunctionCallExpression)expression; - ExpressionBase error; - var scope = funcCall.GetParameters(funcDef, AchievementScriptInterpreter.GetGlobalScope(), out error); - Assert.That(funcDef.Evaluate(scope, out error), Is.False); - Assert.That(error, Is.InstanceOf()); - Assert.That(((ErrorExpression)error).Message, Is.EqualTo("rich_presence_value has no meaning outside of a rich_presence_display call")); + var context = new AchievementScriptContext { RichPresence = new RichPresenceBuilder() }; + var scope = new InterpreterScope(AchievementScriptInterpreter.GetGlobalScope()); + scope.Context = context; + var error = funcCall.Execute(scope); + + ExpressionTests.AssertError(error, "rich_presence_value has no meaning outside of a rich_presence_display call"); } [Test] @@ -137,8 +116,17 @@ public void TestFormat(string format, string expectedFormat) [Test] public void TestFormatInvalid() { - Evaluate("rich_presence_value(\"Name\", byte(0x1234), format=\"INVALID\")", - "INVALID is not a supported rich_presence_value format"); + var input = "rich_presence_display(\"{0}\", rich_presence_value(\"Name\", byte(0x1234), format=\"INVALID\"))"; + var expression = ExpressionBase.Parse(new PositionalTokenizer(Tokenizer.CreateTokenizer(input))); + Assert.That(expression, Is.InstanceOf()); + var funcCall = (FunctionCallExpression)expression; + + var context = new AchievementScriptContext { RichPresence = new RichPresenceBuilder() }; + var scope = new InterpreterScope(AchievementScriptInterpreter.GetGlobalScope()); + scope.Context = context; + var error = funcCall.Execute(scope); + + ExpressionTests.AssertError(error, "INVALID is not a supported rich_presence_value format"); } } }