From ede45e4aa44800b8f821c714127d5231b2a52b83 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Mon, 1 Jun 2026 21:56:09 -0600 Subject: [PATCH] add remembered() function --- Source/Parser/AchievementScriptInterpreter.cs | 1 + .../Trigger/MemoryValueExpression.cs | 18 +++ .../Trigger/RememberRecallExpression.cs | 12 +- .../Functions/MemoryAccessorFunction.cs | 25 ++-- Source/Parser/Functions/RememberedFunction.cs | 33 ++++++ .../Parser/Internal/TriggerBuilderContext.cs | 6 + .../Trigger/TriggerExpressionTests.cs | 9 +- .../Functions/RememberedFunctionTests.cs | 107 ++++++++++++++++++ 8 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 Source/Parser/Functions/RememberedFunction.cs create mode 100644 Tests/Parser/Functions/RememberedFunctionTests.cs diff --git a/Source/Parser/AchievementScriptInterpreter.cs b/Source/Parser/AchievementScriptInterpreter.cs index 8ad8a267..c965ac1b 100644 --- a/Source/Parser/AchievementScriptInterpreter.cs +++ b/Source/Parser/AchievementScriptInterpreter.cs @@ -159,6 +159,7 @@ internal static InterpreterScope GetGlobalScope() _globalScope.AddFunction(new FlagConditionFunction("trigger_when", RequirementType.Trigger)); _globalScope.AddFunction(new MeasuredFunction()); _globalScope.AddFunction(new DisableWhenFunction()); + _globalScope.AddFunction(new RememberedFunction()); _globalScope.AddFunction(new AchievementSetFunction()); _globalScope.AddFunction(new AchievementFunction()); diff --git a/Source/Parser/Expressions/Trigger/MemoryValueExpression.cs b/Source/Parser/Expressions/Trigger/MemoryValueExpression.cs index a88546e4..2de16094 100644 --- a/Source/Parser/Expressions/Trigger/MemoryValueExpression.cs +++ b/Source/Parser/Expressions/Trigger/MemoryValueExpression.cs @@ -1621,6 +1621,18 @@ public ErrorExpression BuildTrigger(TriggerBuilderContext context, ExpressionBas memoryAccessors.Add(last); } + // if there's a constant modifier that can be moved to the final clause, do so. + appendConstantAccessor = false; + if (constantAccessor != null && context.CanModifyOperator && context.MinimumVersion >= Data.Version._1_3_1) + { + var last = memoryAccessors.Last(); + if (last.ModifyingOperator == RequirementOperator.None) + { + memoryAccessors.Remove(constantAccessor); + appendConstantAccessor = true; + } + } + // output the accessor chain var constantAccessorIndex = -1; foreach (var accessor in memoryAccessors) @@ -1651,6 +1663,12 @@ public ErrorExpression BuildTrigger(TriggerBuilderContext context, ExpressionBas } } + if (appendConstantAccessor) + { + context.LastRequirement.Operator = (constantAccessor.CombiningOperator == RequirementType.SubSource) ? RequirementOperator.Subtract : RequirementOperator.Add; + context.LastRequirement.Right = constantAccessor.MemoryAccessor.Field; + } + // the last item will be flagged as an AddSource (or None if 0 was appended) // make sure it's None before leaving. context.LastRequirement.Type = RequirementType.None; diff --git a/Source/Parser/Expressions/Trigger/RememberRecallExpression.cs b/Source/Parser/Expressions/Trigger/RememberRecallExpression.cs index 04a51318..c234c8e7 100644 --- a/Source/Parser/Expressions/Trigger/RememberRecallExpression.cs +++ b/Source/Parser/Expressions/Trigger/RememberRecallExpression.cs @@ -46,6 +46,13 @@ public static RememberRecallExpression WrapInRemember(ExpressionBase expression) if (modifiedMemoryAccessor != null) return new RememberRecallExpression(modifiedMemoryAccessor); + var memoryAccessor = expression as MemoryAccessorExpression; + if (memoryAccessor != null && memoryAccessor.HasPointerChain) + { + memoryValue = new MemoryValueExpression(new ModifiedMemoryAccessorExpression(memoryAccessor)); + return new RememberRecallExpression(memoryValue); + } + return null; } @@ -114,7 +121,10 @@ public override ErrorExpression BuildTrigger(TriggerBuilderContext context) private ErrorExpression AppendRemember(TriggerBuilderContext context) { - RememberedValue.BuildTrigger(context); + var rememberContext = context.Clone(); + rememberContext.CanModifyOperator = true; + + RememberedValue.BuildTrigger(rememberContext); context.RememberedValue = RememberedValue; // if the RememberedValue only contains ModifiedMemoryReferences, BuildTrigger diff --git a/Source/Parser/Functions/MemoryAccessorFunction.cs b/Source/Parser/Functions/MemoryAccessorFunction.cs index 7982f1d6..119d3914 100644 --- a/Source/Parser/Functions/MemoryAccessorFunction.cs +++ b/Source/Parser/Functions/MemoryAccessorFunction.cs @@ -168,11 +168,23 @@ private static MemoryAccessorExpression CreateMemoryAccessorExpression(ModifiedM return null; } - var result = new MemoryAccessorExpression(); + var result = new MemoryAccessorExpression + { + Field = new Field { Type = FieldType.MemoryAddress, Size = size, Value = offset }, + }; + + if (modifiedMemoryAccessor.ModifyingOperator == RequirementOperator.None) + { + var rememberedPointer = modifiedMemoryAccessor.MemoryAccessor as RememberRecallExpression; + if (rememberedPointer != null) + { + result.RememberPointer = rememberedPointer; + return result; + } + } var requirements = new List(); - var context = new TriggerBuilderContext(); - context.Trigger = requirements; + var context = new TriggerBuilderContext { Trigger = requirements }; modifiedMemoryAccessor.BuildTrigger(context); requirements.Last().Type = RequirementType.AddAddress; @@ -207,13 +219,6 @@ private static MemoryAccessorExpression CreateMemoryAccessorExpression(ModifiedM foreach (var requirement in requirements) result.AddPointer(requirement); - result.Field = new Field - { - Type = FieldType.MemoryAddress, - Size = size, - Value = offset - }; - return result; } } diff --git a/Source/Parser/Functions/RememberedFunction.cs b/Source/Parser/Functions/RememberedFunction.cs new file mode 100644 index 00000000..dcdf0420 --- /dev/null +++ b/Source/Parser/Functions/RememberedFunction.cs @@ -0,0 +1,33 @@ +using RATools.Parser.Expressions; +using RATools.Parser.Expressions.Trigger; +using RATools.Parser.Internal; + +namespace RATools.Parser.Functions +{ + internal class RememberedFunction : FunctionDefinitionExpression + { + public RememberedFunction() + : base("remembered") + { + Parameters.Add(new VariableDefinitionExpression("accessor")); + } + + public override bool Evaluate(InterpreterScope scope, out ExpressionBase result) + { + var accessor = GetParameter(scope, "accessor", out result); + if (accessor == null) + return false; + + if (!accessor.ReplaceVariables(scope, out result)) + return false; + + var remembered = RememberRecallExpression.WrapInRemember(result); + if (remembered != null) + result = remembered; + + CopyLocation(result, scope); + result.MakeReadOnly(); + return true; + } + } +} diff --git a/Source/Parser/Internal/TriggerBuilderContext.cs b/Source/Parser/Internal/TriggerBuilderContext.cs index 90cde583..c14424bd 100644 --- a/Source/Parser/Internal/TriggerBuilderContext.cs +++ b/Source/Parser/Internal/TriggerBuilderContext.cs @@ -45,6 +45,12 @@ public Requirement LastRequirement /// public bool CanModifyComparison { get; set; } + /// + /// If set to true, the operator and right operand can be set. + /// Otherwise, they should be left alone so the caller can set them. + /// + public bool CanModifyOperator { get; set; } + public virtual TriggerBuilderContext Clone() { var clone = new TriggerBuilderContext(); diff --git a/Tests/Parser/Expressions/Trigger/TriggerExpressionTests.cs b/Tests/Parser/Expressions/Trigger/TriggerExpressionTests.cs index 07889f73..598a31af 100644 --- a/Tests/Parser/Expressions/Trigger/TriggerExpressionTests.cs +++ b/Tests/Parser/Expressions/Trigger/TriggerExpressionTests.cs @@ -95,10 +95,11 @@ public static void AssertBuildValueError(string input, string message) ExpressionTests.AssertError(error, message); } - public static string Serialize(ITriggerExpression expression) + public static string Serialize(ITriggerExpression expression, TriggerBuilderContext context = null) { var requirements = new List(); - var context = new TriggerBuilderContext() { Trigger = requirements }; + context ??= new TriggerBuilderContext(); + context.Trigger = requirements; var result = expression.BuildTrigger(context); Assert.That(result, Is.Null); @@ -110,9 +111,9 @@ public static string Serialize(ITriggerExpression expression) return builder.SerializeRequirements(new SerializationContext()); } - public static void AssertSerialize(ITriggerExpression expression, string expected) + public static void AssertSerialize(ITriggerExpression expression, string expected, TriggerBuilderContext context = null) { - Assert.That(Serialize(expression), Is.EqualTo(expected)); + Assert.That(Serialize(expression, context), Is.EqualTo(expected)); } public static string SerializeValue(ITriggerExpression expression) diff --git a/Tests/Parser/Functions/RememberedFunctionTests.cs b/Tests/Parser/Functions/RememberedFunctionTests.cs new file mode 100644 index 00000000..f9bbfe42 --- /dev/null +++ b/Tests/Parser/Functions/RememberedFunctionTests.cs @@ -0,0 +1,107 @@ +using NUnit.Framework; +using RATools.Data; +using RATools.Parser.Expressions; +using RATools.Parser.Functions; +using RATools.Parser.Internal; +using RATools.Parser.Tests.Expressions; +using RATools.Parser.Tests.Expressions.Trigger; +using System.Collections.Generic; +using System.Linq; + +namespace RATools.Parser.Tests.Functions +{ + [TestFixture] + class RememberedFunctionTests + { + [Test] + public void TestDefinition() + { + var def = new RememberedFunction(); + Assert.That(def.Name.Name, Is.EqualTo("remembered")); + Assert.That(def.Parameters.Count, Is.EqualTo(1)); + Assert.That(def.Parameters.ElementAt(0).Name, Is.EqualTo("accessor")); + } + + [Test] + // Constants and comparisons can't be remembered + [TestCase("1", "1")] + [TestCase("always_false()", "0=1")] + [TestCase("byte(0x1234) == 6", "0xH001234=6")] + // Unmodified memory accessors doesn't need to be remembered + [TestCase("byte(0x1234)", "0xH001234")] + // Modified memory accessors and pointed-at values can be remembered. + // NOTE: The output of the serializer is the {recall} field, but the remember chain + // will also be output if it hasn't already. + [TestCase("byte(0x1234) * 2", "K:0xH001234*2_{recall}")] + [TestCase("word(0x1234) + 2", "A:2_K:0x 001234_{recall}")] + [TestCase("word(0x1234) - 2", "B:2_K:0x 001234_{recall}")] + [TestCase("dword(dword(0x1234))", "I:0xX001234_K:0xX000000_{recall}")] + [TestCase("remembered(dword(0x1234)+8)*2", "A:8_K:0xX001234_K:{recall}*2_{recall}")] + public void TestRemember(string input, string expected) + { + var expr = TriggerExpressionTests.Parse("remembered(" + input + ")"); + + var error = expr as ErrorExpression; + if (error != null) + { + ExpressionTests.AssertError(error, expected); + } + else + { + var triggerExpr = expr as ITriggerExpression; + if (triggerExpr != null) + TriggerExpressionTests.AssertSerialize(triggerExpr, expected); + else + ExpressionTests.AssertAppendString(expr, expected); + } + } + + [Test] + [TestCase("word(0x1234) + 2", "K:0x 001234+2_{recall}")] + [TestCase("word(0x1234) - 2", "K:0x 001234-2_{recall}")] + public void TestRememberSimplifiedAddition(string input, string expected) + { + var expr = TriggerExpressionTests.Parse("remembered(" + input + ")"); + + var error = expr as ErrorExpression; + if (error != null) + { + ExpressionTests.AssertError(error, expected); + } + else + { + var triggerExpr = expr as ITriggerExpression; + if (triggerExpr != null) + TriggerExpressionTests.AssertSerialize(triggerExpr, expected, new TriggerBuilderContext { MinimumVersion = Data.Version._1_3_1 }); + else + ExpressionTests.AssertAppendString(expr, expected); + } + } + + [Test] + public void TestScriptInline() + { + var parser = AchievementScriptTests.Parse( + "achievement(\"t\", \"d\", 5, byte(remembered(dword(dword(0x1234) + 8)) + 4) > 5 && " + + "word(remembered(dword(dword(0x1234) + 8)) + 8) == 0)"); + Assert.That(parser.Achievements.Count(), Is.EqualTo(1)); + + var achievement = parser.Achievements.First(); + var serialized = achievement.Trigger.Serialize(new SerializationContext()); + Assert.That(serialized, Is.EqualTo("I:0xX001234_K:0xX000008_I:{recall}_0xH000004>5_I:{recall}_0x 000008=0")); + } + + [Test] + public void TestScriptVariable() + { + var parser = AchievementScriptTests.Parse( + "ptr = remembered(dword(dword(0x1234) + 8))\n" + + "achievement(\"t\", \"d\", 5, byte(ptr + 4) > 5 && word(ptr + 8) == 0)"); + Assert.That(parser.Achievements.Count(), Is.EqualTo(1)); + + var achievement = parser.Achievements.First(); + var serialized = achievement.Trigger.Serialize(new SerializationContext()); + Assert.That(serialized, Is.EqualTo("I:0xX001234_K:0xX000008_I:{recall}_0xH000004>5_I:{recall}_0x 000008=0")); + } + } +}