diff --git a/Source/Data/Field.cs b/Source/Data/Field.cs index 53a01b2..125074b 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 85ed93d..ca77339 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 28cac8c..c1b4917 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 4547aad..7ed0a74 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 0000000..c2be9ce --- /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 7de5dc8..c67ee3c 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 0000000..e6019ba --- /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")); + } + } +}