From f5cb7b02aa25555465272e22253fbb772c608cce Mon Sep 17 00:00:00 2001
From: Jamiras <32680403+Jamiras@users.noreply.github.com>
Date: Tue, 1 Oct 2024 14:28:05 -0600
Subject: [PATCH] add rich_presence_ascii_string_lookup function (#538)
---
Source/Data/Field.cs | 15 ++
Source/Parser/AchievementScriptInterpreter.cs | 1 +
.../Expressions/Trigger/FieldFactory.cs | 1 -
.../Functions/AsciiStringEqualsFunction.cs | 9 +-
.../RichPresenceAsciiStringLookupFunction.cs | 219 ++++++++++++++++++
Source/Parser/RichPresenceBuilder.cs | 6 +-
...hPresenceAsciiStringLookupFunctionTests.cs | 197 ++++++++++++++++
7 files changed, 436 insertions(+), 12 deletions(-)
create mode 100644 Source/Parser/Functions/RichPresenceAsciiStringLookupFunction.cs
create mode 100644 Tests/Parser/Functions/RichPresenceAsciiStringLookupFunctionTests.cs
diff --git a/Source/Data/Field.cs b/Source/Data/Field.cs
index 53a01b2e..125074b2 100644
--- a/Source/Data/Field.cs
+++ b/Source/Data/Field.cs
@@ -303,6 +303,21 @@ public static uint GetByteSize(FieldSize size)
}
}
+ ///
+ /// Gets the size needed to read the specified number of bytes in little-endian order.
+ ///
+ public static FieldSize GetSizeForBytes(int bytes)
+ {
+ switch (bytes)
+ {
+ case 1: return FieldSize.Byte;
+ case 2: return FieldSize.Word;
+ case 3: return FieldSize.TByte;
+ case 4: return FieldSize.DWord;
+ default: return FieldSize.None;
+ }
+ }
+
///
/// Gets whether or not the field references memory.
///
diff --git a/Source/Parser/AchievementScriptInterpreter.cs b/Source/Parser/AchievementScriptInterpreter.cs
index 85ed93d5..ca773399 100644
--- a/Source/Parser/AchievementScriptInterpreter.cs
+++ b/Source/Parser/AchievementScriptInterpreter.cs
@@ -168,6 +168,7 @@ internal static InterpreterScope GetGlobalScope()
_globalScope.AddFunction(new RichPresenceValueFunction());
_globalScope.AddFunction(new RichPresenceMacroFunction());
_globalScope.AddFunction(new RichPresenceLookupFunction());
+ _globalScope.AddFunction(new RichPresenceAsciiStringLookupFunction());
_globalScope.AddFunction(new AlwaysTrueFunction());
_globalScope.AddFunction(new AlwaysFalseFunction());
diff --git a/Source/Parser/Expressions/Trigger/FieldFactory.cs b/Source/Parser/Expressions/Trigger/FieldFactory.cs
index 28cac8c7..c1b49170 100644
--- a/Source/Parser/Expressions/Trigger/FieldFactory.cs
+++ b/Source/Parser/Expressions/Trigger/FieldFactory.cs
@@ -1,5 +1,4 @@
using RATools.Data;
-using System.Linq;
namespace RATools.Parser.Expressions.Trigger
{
diff --git a/Source/Parser/Functions/AsciiStringEqualsFunction.cs b/Source/Parser/Functions/AsciiStringEqualsFunction.cs
index 4547aadf..7ed0a743 100644
--- a/Source/Parser/Functions/AsciiStringEqualsFunction.cs
+++ b/Source/Parser/Functions/AsciiStringEqualsFunction.cs
@@ -112,14 +112,7 @@ public override bool Evaluate(InterpreterScope scope, out ExpressionBase result)
break;
}
- FieldSize size;
- switch (remaining)
- {
- case 1: size = FieldSize.Byte; break;
- case 2: size = FieldSize.Word; break;
- case 3: size = FieldSize.TByte; break;
- default: size = FieldSize.DWord; break;
- }
+ FieldSize size = Field.GetSizeForBytes(Math.Min(remaining, 4));
var scan = address.Clone();
scan.Field = new Field { Type = address.Field.Type, Size = size, Value = address.Field.Value + (uint)offset };
diff --git a/Source/Parser/Functions/RichPresenceAsciiStringLookupFunction.cs b/Source/Parser/Functions/RichPresenceAsciiStringLookupFunction.cs
new file mode 100644
index 00000000..c2be9ce1
--- /dev/null
+++ b/Source/Parser/Functions/RichPresenceAsciiStringLookupFunction.cs
@@ -0,0 +1,219 @@
+using RATools.Data;
+using RATools.Parser.Expressions;
+using RATools.Parser.Expressions.Trigger;
+using System;
+
+namespace RATools.Parser.Functions
+{
+ internal class RichPresenceAsciiStringLookupFunction : FunctionDefinitionExpression
+ {
+ public RichPresenceAsciiStringLookupFunction()
+ : base("rich_presence_ascii_string_lookup")
+ {
+ Parameters.Add(new VariableDefinitionExpression("name"));
+ Parameters.Add(new VariableDefinitionExpression("address"));
+ Parameters.Add(new VariableDefinitionExpression("dictionary"));
+
+ Parameters.Add(new VariableDefinitionExpression("fallback"));
+ DefaultParameters["fallback"] = new StringConstantExpression("");
+ }
+
+ public override bool Evaluate(InterpreterScope scope, out ExpressionBase result)
+ {
+ var name = GetStringParameter(scope, "name", out result);
+ if (name == null)
+ return false;
+
+ var dictionary = GetDictionaryParameter(scope, "dictionary", out result);
+ if (dictionary == null)
+ return false;
+ if (dictionary.Count == 0)
+ {
+ result = new ErrorExpression("dictionary is empty", dictionary);
+ return false;
+ }
+
+ var fallback = GetStringParameter(scope, "fallback", out result);
+ if (fallback == null)
+ return false;
+
+ var address = GetMemoryAddressParameter(scope, "address", out result);
+ if (address == null)
+ return false;
+
+ var maxLength = 0xFFFFFF;
+ foreach (var pair in dictionary.Entries)
+ {
+ var stringKey = pair.Key as StringConstantExpression;
+ if (stringKey == null)
+ {
+ result = new ConversionErrorExpression(pair.Key, ExpressionType.StringConstant);
+ return false;
+ }
+
+ maxLength = Math.Min(stringKey.Value.Length + 1, maxLength);
+ }
+
+ int offset = 0;
+ int length = 4;
+ DictionaryExpression hashedDictionary = null;
+
+ for (int i = 0; i < maxLength - 3; i += 4)
+ {
+ offset = i;
+ hashedDictionary = BuildHashedDictionary(dictionary, offset, length);
+
+ if (hashedDictionary != null)
+ break;
+ }
+
+ if (hashedDictionary == null)
+ {
+ // could not find key aligned to 4 bytes. try intermediate offsets
+ for (int i = 2; i < maxLength - 3; i += 4)
+ {
+ offset = i;
+ hashedDictionary = BuildHashedDictionary(dictionary, offset, length);
+
+ if (hashedDictionary != null)
+ break;
+ }
+
+ // still no match, try matching the end of the string
+ if (hashedDictionary == null)
+ {
+ length = maxLength & 3;
+ if (length > 0)
+ {
+ offset = maxLength & ~3;
+ hashedDictionary = BuildHashedDictionary(dictionary, offset, length);
+ }
+ }
+ }
+
+ ExpressionBase expression = null;
+
+ if (hashedDictionary == null)
+ {
+ for (length = 8; length < maxLength; length += 4)
+ {
+ hashedDictionary = BuildSummedHashDictionary(dictionary, length);
+ if (hashedDictionary != null)
+ {
+ var summedExpression = new MemoryValueExpression();
+ for (int i = 0; i < length; i += 4)
+ {
+ var clone = address.Clone();
+ clone.Field = new Field
+ {
+ Type = FieldType.MemoryAddress,
+ Size = FieldSize.DWord,
+ Value = address.Field.Value + (uint)i,
+ };
+ summedExpression.ApplyMathematic(clone, MathematicOperation.Add);
+ }
+
+ expression = summedExpression;
+ break;
+ }
+ }
+
+ if (hashedDictionary == null)
+ {
+ result = new ErrorExpression("Could not find a unique sequence of characters within the available keys", dictionary);
+ return false;
+ }
+ }
+ else
+ {
+ // apply the offset and potentially change the size of the read
+ var clone = address.Clone();
+ clone.Field = new Field
+ {
+ Type = FieldType.MemoryAddress,
+ Size = Field.GetSizeForBytes(length),
+ Value = address.Field.Value + (uint)offset,
+ };
+ expression = clone;
+ }
+
+ result = new RichPresenceLookupExpression(name, expression) { Items = hashedDictionary, Fallback = fallback };
+ CopyLocation(result);
+ result.MakeReadOnly();
+ return true;
+ }
+
+ private static DictionaryExpression BuildSummedHashDictionary(DictionaryExpression dictionary, int length)
+ {
+ var hashedDictionary = new DictionaryExpression { Location = dictionary.Location };
+
+ foreach (var pair in dictionary.Entries)
+ {
+ var stringKey = (StringConstantExpression)pair.Key;
+ var hash = CreateHashKey(stringKey, 0, 4).Value;
+ for (int i = 4; i < length; i += 4)
+ hash += CreateHashKey(stringKey, i, 4).Value;
+
+ var hashKey = new IntegerConstantExpression(hash) { Location = stringKey.Location };
+ if (hashedDictionary.GetEntry(hashKey) != null)
+ return null;
+
+ hashedDictionary.Add(hashKey, pair.Value);
+ }
+
+ return hashedDictionary;
+ }
+
+ private static DictionaryExpression BuildHashedDictionary(DictionaryExpression dictionary, int offset, int length)
+ {
+ var hashedDictionary = new DictionaryExpression { Location = dictionary.Location };
+
+ foreach (var pair in dictionary.Entries)
+ {
+ var stringKey = (StringConstantExpression)pair.Key;
+ var hashKey = CreateHashKey(stringKey, offset, length);
+
+ if (hashedDictionary.GetEntry(hashKey) != null)
+ return null;
+
+ hashedDictionary.Add(hashKey, pair.Value);
+ }
+
+ return hashedDictionary;
+ }
+
+ private static IntegerConstantExpression CreateHashKey(StringConstantExpression stringKey, int index, int length)
+ {
+ int value = (index < stringKey.Value.Length) ? stringKey.Value[index] : 0;
+ switch (length)
+ {
+ case 4:
+ if (index + 3 < stringKey.Value.Length)
+ value |= stringKey.Value[index + 3] << 24;
+ goto case 3;
+
+ case 3:
+ if (index + 2 < stringKey.Value.Length)
+ value |= stringKey.Value[index + 2] << 16;
+ goto case 2;
+
+ case 2:
+ if (index + 1 < stringKey.Value.Length)
+ value |= stringKey.Value[index + 1] << 8;
+ goto default;
+
+ default:
+ break;
+ }
+
+ return new IntegerConstantExpression(value) { Location = stringKey.Location };
+ }
+
+ public override bool Invoke(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/RichPresenceBuilder.cs b/Source/Parser/RichPresenceBuilder.cs
index 7de5dc82..c67ee3ca 100644
--- a/Source/Parser/RichPresenceBuilder.cs
+++ b/Source/Parser/RichPresenceBuilder.cs
@@ -293,7 +293,7 @@ public ErrorExpression AddLookupField(ExpressionBase func, StringConstantExpress
if (_valueFields.ContainsKey(name.Value))
return new ErrorExpression("A rich_presence_value already exists for '" + name.Value + "'", name);
- var tinyDict = new TinyDictionary();
+ var lookupDict = new Dictionary();
foreach (var entry in dict.Entries)
{
var key = entry.Key as IntegerConstantExpression;
@@ -304,13 +304,13 @@ public ErrorExpression AddLookupField(ExpressionBase func, StringConstantExpress
if (value == null)
return new ErrorExpression("value is not a string", entry.Value);
- tinyDict[key.Value] = value.Value;
+ lookupDict[key.Value] = value.Value;
}
_lookupFields[name.Value] = new Lookup
{
Func = func,
- Entries = tinyDict,
+ Entries = lookupDict,
Fallback = fallback
};
diff --git a/Tests/Parser/Functions/RichPresenceAsciiStringLookupFunctionTests.cs b/Tests/Parser/Functions/RichPresenceAsciiStringLookupFunctionTests.cs
new file mode 100644
index 00000000..e6019bad
--- /dev/null
+++ b/Tests/Parser/Functions/RichPresenceAsciiStringLookupFunctionTests.cs
@@ -0,0 +1,197 @@
+using Jamiras.Components;
+using NUnit.Framework;
+using RATools.Data;
+using RATools.Parser.Expressions;
+using RATools.Parser.Functions;
+using RATools.Parser.Tests.Expressions;
+using System.Linq;
+
+namespace RATools.Parser.Tests.Functions
+{
+ [TestFixture]
+ class RichPresenceAsciiStringLookupFunctionTests
+ {
+ class RichPresenceAsciiStringLookupFunctionHarness
+ {
+ public RichPresenceAsciiStringLookupFunctionHarness()
+ {
+ Scope = new InterpreterScope(AchievementScriptInterpreter.GetGlobalScope());
+ }
+
+ public InterpreterScope Scope { get; private set; }
+
+ public RichPresenceBuilder Evaluate(string input)
+ {
+ input = "rich_presence_display(\"{0}\", " + input + ")";
+ 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() };
+ Scope.Context = context;
+ funcCall.Execute(Scope);
+
+ return context.RichPresence;
+ }
+
+ public DictionaryExpression DefineLookup(string name)
+ {
+ var dict = new DictionaryExpression();
+ Scope.DefineVariable(new VariableDefinitionExpression(name), dict);
+ return dict;
+ }
+ }
+
+ [Test]
+ public void TestDefinition()
+ {
+ var def = new RichPresenceAsciiStringLookupFunction();
+ Assert.That(def.Name.Name, Is.EqualTo("rich_presence_ascii_string_lookup"));
+ Assert.That(def.Parameters.Count, Is.EqualTo(4));
+ Assert.That(def.Parameters.ElementAt(0).Name, Is.EqualTo("name"));
+ Assert.That(def.Parameters.ElementAt(1).Name, Is.EqualTo("address"));
+ Assert.That(def.Parameters.ElementAt(2).Name, Is.EqualTo("dictionary"));
+ Assert.That(def.Parameters.ElementAt(3).Name, Is.EqualTo("fallback"));
+
+ Assert.That(def.DefaultParameters["fallback"], Is.EqualTo(new StringConstantExpression("")));
+ }
+
+ [Test]
+ public void TestSimple()
+ {
+ var rp = new RichPresenceAsciiStringLookupFunctionHarness();
+ var lookup = rp.DefineLookup("lookup");
+ lookup.Add(new StringConstantExpression("Zero"), new StringConstantExpression("False"));
+ lookup.Add(new StringConstantExpression("One"), new StringConstantExpression("True"));
+
+ var builder = rp.Evaluate("rich_presence_ascii_string_lookup(\"Name\", 0x1234, lookup)");
+ Assert.That(builder.ToString(), Is.EqualTo("Lookup:Name\r\n6647375=True\r\n1869768026=False\r\n\r\nDisplay:\r\n@Name(0xX001234)\r\n"));
+ }
+
+ [Test]
+ public void TestSimplePointer()
+ {
+ var rp = new RichPresenceAsciiStringLookupFunctionHarness();
+ var lookup = rp.DefineLookup("lookup");
+ lookup.Add(new StringConstantExpression("Zero"), new StringConstantExpression("False"));
+ lookup.Add(new StringConstantExpression("One"), new StringConstantExpression("True"));
+
+ var builder = rp.Evaluate("rich_presence_ascii_string_lookup(\"Name\", dword(0x1234), lookup)");
+ var serializationContext = new SerializationContext { MinimumVersion = builder.MinimumVersion() };
+ Assert.That(builder.Serialize(serializationContext), Is.EqualTo("Lookup:Name\r\n6647375=True\r\n1869768026=False\r\n\r\nDisplay:\r\n@Name(I:0xX001234_M:0xX000000)\r\n"));
+ }
+
+ [Test]
+ public void TestIntegerDictionaryKey()
+ {
+ var input = "lookup = {\"Zero\": \"False\", 1: \"True\" }\r\n" +
+ "\r\n" +
+ "rich_presence_display(\"{0}\", rich_presence_ascii_string_lookup(\"Name\", 0x1234, lookup))";
+
+ AchievementScriptTests.Evaluate(input,
+ "3:30 Invalid value for parameter: ...\r\n" +
+ "- 3:30 rich_presence_ascii_string_lookup call failed\r\n" +
+ "- 1:28 Cannot convert integer to string");
+ }
+
+ [Test]
+ public void TestCommonPrefix()
+ {
+ var rp = new RichPresenceAsciiStringLookupFunctionHarness();
+ var lookup = rp.DefineLookup("lookup");
+ lookup.Add(new StringConstantExpression("Test_Zero"), new StringConstantExpression("False"));
+ lookup.Add(new StringConstantExpression("Test_One"), new StringConstantExpression("True"));
+
+ var builder = rp.Evaluate("rich_presence_ascii_string_lookup(\"Name\", 0x1234, lookup)");
+ Assert.That(builder.ToString(), Is.EqualTo("Lookup:Name\r\n1701728095=True\r\n1919244895=False\r\n\r\nDisplay:\r\n@Name(0xX001238)\r\n"));
+ }
+
+ [Test]
+ public void TestMultipleOverlaps()
+ {
+ var rp = new RichPresenceAsciiStringLookupFunctionHarness();
+ var lookup = rp.DefineLookup("lookup");
+ // Test is not unique among the first four characters.
+ // _Zer is not unique among the second four characters.
+ // st_Z, st_O, and ow_Z are unique in the middle.
+ lookup.Add(new StringConstantExpression("Test_Zero"), new StringConstantExpression("False"));
+ lookup.Add(new StringConstantExpression("Test_One"), new StringConstantExpression("True"));
+ lookup.Add(new StringConstantExpression("Slow_Zero"), new StringConstantExpression("Unknown"));
+
+ var builder = rp.Evaluate("rich_presence_ascii_string_lookup(\"Name\", 0x1234, lookup)");
+ Assert.That(builder.ToString(), Is.EqualTo("Lookup:Name\r\n1331655795=True\r\n1516205171=False\r\n1516205935=Unknown\r\n\r\nDisplay:\r\n@Name(0xX001236)\r\n"));
+ }
+
+ [Test]
+ public void TestTailDiffrentiator()
+ {
+ var rp = new RichPresenceAsciiStringLookupFunctionHarness();
+ var lookup = rp.DefineLookup("lookup");
+ lookup.Add(new StringConstantExpression("Value0"), new StringConstantExpression("False"));
+ lookup.Add(new StringConstantExpression("Value1"), new StringConstantExpression("True"));
+
+ var builder = rp.Evaluate("rich_presence_ascii_string_lookup(\"Name\", 0x1234, lookup)");
+ Assert.That(builder.ToString(), Is.EqualTo("Lookup:Name\r\n811955564=False\r\n828732780=True\r\n\r\nDisplay:\r\n@Name(0xX001236)\r\n"));
+ }
+
+ [Test]
+ public void TestShortStrings()
+ {
+ var rp = new RichPresenceAsciiStringLookupFunctionHarness();
+ var lookup = rp.DefineLookup("lookup");
+ lookup.Add(new StringConstantExpression("Off"), new StringConstantExpression("False"));
+ lookup.Add(new StringConstantExpression("On"), new StringConstantExpression("True"));
+
+ var builder = rp.Evaluate("rich_presence_ascii_string_lookup(\"Name\", 0x1234, lookup)");
+ Assert.That(builder.ToString(), Is.EqualTo("Lookup:Name\r\n28239=True\r\n6710863=False\r\n\r\nDisplay:\r\n@Name(0xW001234)\r\n"));
+ }
+
+ [Test]
+ public void TestShortStringsPointer()
+ {
+ var rp = new RichPresenceAsciiStringLookupFunctionHarness();
+ var lookup = rp.DefineLookup("lookup");
+ lookup.Add(new StringConstantExpression("Off"), new StringConstantExpression("False"));
+ lookup.Add(new StringConstantExpression("On"), new StringConstantExpression("True"));
+
+ var builder = rp.Evaluate("rich_presence_ascii_string_lookup(\"Name\", dword(0x1234), lookup)");
+ var serializationContext = new SerializationContext { MinimumVersion = builder.MinimumVersion() };
+ Assert.That(builder.Serialize(serializationContext), Is.EqualTo("Lookup:Name\r\n28239=True\r\n6710863=False\r\n\r\nDisplay:\r\n@Name(I:0xX001234_M:0xW000000)\r\n"));
+ }
+
+ [Test]
+ public void TestSummedKey()
+ {
+ var rp = new RichPresenceAsciiStringLookupFunctionHarness();
+ var lookup = rp.DefineLookup("lookup");
+ // there is no unique set of four characters, so the lookup will need to combine multiple groups of four.
+ lookup.Add(new StringConstantExpression("uppercase"), new StringConstantExpression("uc"));
+ lookup.Add(new StringConstantExpression("upperCase"), new StringConstantExpression("uC"));
+ lookup.Add(new StringConstantExpression("UpperCase"), new StringConstantExpression("UC"));
+ lookup.Add(new StringConstantExpression("lowercase"), new StringConstantExpression("lc"));
+ lookup.Add(new StringConstantExpression("lowerCase"), new StringConstantExpression("lC"));
+ lookup.Add(new StringConstantExpression("LowerCase"), new StringConstantExpression("LC"));
+
+ var builder = rp.Evaluate("rich_presence_ascii_string_lookup(\"Name\", 0x1234, lookup)");
+ Assert.That(builder.ToString(), Is.EqualTo("Lookup:Name\r\n-657345593=UC\r\n-657345561=uC\r\n-657337369=uc\r\n-656887106=LC\r\n-656887074=lC\r\n-656878882=lc\r\n\r\nDisplay:\r\n@Name(0xX001234_0xX001238)\r\n"));
+ }
+
+ [Test]
+ public void TestSummedKeyPointer()
+ {
+ var rp = new RichPresenceAsciiStringLookupFunctionHarness();
+ var lookup = rp.DefineLookup("lookup");
+ // there is no unique set of four characters, so the lookup will need to combine multiple groups of four.
+ lookup.Add(new StringConstantExpression("uppercase"), new StringConstantExpression("uc"));
+ lookup.Add(new StringConstantExpression("upperCase"), new StringConstantExpression("uC"));
+ lookup.Add(new StringConstantExpression("UpperCase"), new StringConstantExpression("UC"));
+ lookup.Add(new StringConstantExpression("lowercase"), new StringConstantExpression("lc"));
+ lookup.Add(new StringConstantExpression("lowerCase"), new StringConstantExpression("lC"));
+ lookup.Add(new StringConstantExpression("LowerCase"), new StringConstantExpression("LC"));
+
+ var builder = rp.Evaluate("rich_presence_ascii_string_lookup(\"Name\", dword(0x1234), lookup)");
+ var serializationContext = new SerializationContext { MinimumVersion = builder.MinimumVersion() };
+ Assert.That(builder.Serialize(serializationContext), Is.EqualTo("Lookup:Name\r\n-657345593=UC\r\n-657345561=uC\r\n-657337369=uc\r\n-656887106=LC\r\n-656887074=lC\r\n-656878882=lc\r\n\r\nDisplay:\r\n@Name(I:0xX001234_A:0xX000000_I:0xX001234_M:0xX000004)\r\n"));
+ }
+ }
+}