Implement triggers with chained member accesses

This commit is contained in:
Yoshi Askharoun
2025-09-21 21:25:27 -05:00
parent d29a7ab839
commit 69c543a234
3 changed files with 125 additions and 45 deletions
+9
View File
@@ -107,6 +107,15 @@ internal class DecompileContext
public SyntaxTree GetScriptContent(TypeSchema type, uint startOffset) => _scriptMap[(type.UniqueId, startOffset)];
public bool TryGetScriptContent(TypeSchema type, uint startOffset, [NotNullWhen(true)] out SyntaxTree tree)
{
if (_scriptMap.TryGetValue((type.UniqueId, startOffset), out tree))
return true;
tree = null;
return false;
}
public IEnumerable<SyntaxTree> GetScriptContents(TypeSchema type) => _scriptMap.Where(k => k.Key.Item1 == type.UniqueId).Select(k => k.Value);
public IEnumerable<KeyValuePair<string, XNamespace>> GetUsedNamespaces()
+101 -31
View File
@@ -20,13 +20,13 @@ partial class Decompiler
private static readonly TypeSchema _listType = UIXTypes.MapIDToType(UIXTypeID.List);
private static readonly TypeSchema _dictionaryType = UIXTypes.MapIDToType(UIXTypeID.Dictionary);
private SyntaxTree DecompileScript(uint startOffset, MarkupTypeSchema export, string? attributeName = null)
private SyntaxTree DecompileScript(uint startOffset, MarkupTypeSchema export)
{
var statements = DecompileMethod(startOffset, export, attributeName);
var statements = DecompileMethod(startOffset, export);
return CreateTree(statements);
}
public List<StatementSyntax> DecompileMethod(uint startOffset, MarkupTypeSchema export, string? attributeName = null)
public List<StatementSyntax> DecompileMethod(uint startOffset, MarkupTypeSchema export)
{
var methodBody = _context.GetMethodBody(startOffset).ToArray();
@@ -80,9 +80,13 @@ partial class Decompiler
var value = stack.Pop();
if (value is ExpressionSyntax expr)
{
if (expr is ParenthesizedExpressionSyntax parenExpr)
expr = parenExpr.Expression;
var lastStatement = blockStack.Peek().Statements[^1];
if (lastStatement.DescendantNodes().Any(n => n.IsEquivalentTo(expr)))
break;
if (!lastStatement.DescendantNodes().Any(n => n.IsEquivalentTo(expr)))
blockStack.Peek().Statements.Add(ExpressionStatement(expr));
}
break;
@@ -244,16 +248,7 @@ partial class Decompiler
throw new InvalidOperationException($"Failed to decompile script for {export.Name}, no top-level code blocks");
// Unwrap top-most block to avoid extra curly braces around entire script
var statements = blockStack.Pop().Statements;
if (attributeName is not null)
{
var scriptAttribute = Attribute(IdentifierName(attributeName));
statements[0] = statements[0]
.WithAttributeLists(SingletonList(AttributeList([scriptAttribute])));
}
return statements;
return blockStack.Pop().Statements;
}
private MethodDeclarationSyntax DecompileMethodDeclaration(MarkupMethodSchema method, MarkupTypeSchema export)
@@ -283,6 +278,13 @@ partial class Decompiler
.WithModifiers(modifiers);
}
private void AddMethodAttribute(List<StatementSyntax> statements, string attributeName)
{
var attribute = Attribute(IdentifierName(attributeName));
statements[0] = statements[0]
.AddAttributeLists(AttributeList([attribute]));
}
private void AnalyzeRefreshMethod(uint startOffset, MarkupTypeSchema initType, string methodName = "")
{
var methodBody = _context.GetMethodBody(startOffset).ToArray();
@@ -303,6 +305,25 @@ partial class Decompiler
stack.Push(initType.SymbolReferenceTable[symbolIndex]);
break;
case OpCode.PropertyGet:
case OpCode.PropertyGetStatic:
var propToGet = _context.GetImportedProperty(instruction.Operands.First());
var propGetTarget = instruction.OpCode switch
{
OpCode.PropertyGet => stack.Pop(),
OpCode.PropertyGetPeek => stack.Peek(),
_ => propToGet.Owner,
};
var propertyGetExpression = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
IrisExpression.ToSyntax(propGetTarget, _context),
IdentifierName(propToGet.Name)
);
stack.Push(propertyGetExpression);
break;
case OpCode.Listen:
case OpCode.DestructiveListen:
var listenerIndex = (ushort)instruction.Operands.ElementAt(0).Value;
@@ -336,20 +357,66 @@ partial class Decompiler
object handlerObj = stack.Peek();
try
{
var markupTypeSchema = initType.ResolveScriptId(scriptId, out var scriptOffset);
var scriptContent = _context.GetScriptContent(initType, scriptOffset);
if (!_context.TryGetScriptContent(initType, scriptOffset, out var scriptContent))
{
var statements = DecompileMethod(scriptOffset, initType);
scriptContent = CreateTree(statements);
_context.SetScriptContent(initType, scriptOffset, scriptContent);
}
var scriptRoot = scriptContent.GetRoot();
var nodesDbg = scriptRoot.DescendantNodes()
if (listenerType is not ListenerType.Symbol)
{
var memberAccessExpr = (MemberAccessExpressionSyntax)ParseExpression($"{handlerObj}.{watch}");
var attributeArgument = AttributeArgument(memberAccessExpr);
var attributeName = IdentifierName("DeclareTrigger");
var firstStatement = scriptRoot.DescendantNodes().OfType<StatementSyntax>().First();
// Avoid adding duplicate triggers
var existingTriggers = firstStatement.AttributeLists
.SelectMany(al => al.Attributes)
.Where(a => a.Name.IsEquivalentTo(attributeName))
.SelectMany(a => a.ArgumentList.Arguments)
.ToList();
var hasDuplicateTrigger = existingTriggers
.Any(arg => arg.IsEquivalentTo(attributeArgument)
|| (arg.Expression is MemberAccessExpressionSyntax argExpr && argExpr.Expression.IsEquivalentTo(memberAccessExpr)));
if (!hasDuplicateTrigger)
{
// Remove redundant triggers. For example, if we're adding `Management.ScreenGraphicsSlider.ChosenValue`
// we don't need to trigger on `Management.ScreenGraphicsSlider`.
var precursorTriggers = existingTriggers
.Select(argExpr => argExpr.Expression)
.OfType<MemberAccessExpressionSyntax>()
.Select(m => $"{m.Expression}.{m.Name}")
.ToArray();
.Where(memberAccessExpr.Expression.IsEquivalentTo)
.Select(e => (AttributeArgumentSyntax)e.Parent)
.ToList();
foreach (var precursorTrigger in precursorTriggers)
existingTriggers.Remove(precursorTrigger);
existingTriggers.Add(attributeArgument);
// Reconstruct existing and new attributes
var attributes = existingTriggers
.Select(arg => Attribute(attributeName, AttributeArgumentList(SingletonSeparatedList(arg))));
var newFirstStatement = firstStatement
.WithAttributeLists(SingletonList(AttributeList(SeparatedList(attributes))));
scriptRoot = RecursiveReplaceNode(firstStatement, newFirstStatement);
}
}
SyntaxNode node = scriptRoot
.DescendantNodes()
.OfType<MemberAccessExpressionSyntax>()
.Where(expr => expr.Parent is not AttributeArgumentSyntax)
.FirstOrDefault(n => n.Expression.ToString() == $"{handlerObj}" && n.Name.ToString() == watch);
if (node is not null)
@@ -361,19 +428,10 @@ partial class Decompiler
.WithLeadingTrivia(TriviaList(Trivia(octothorpeTrivia)))
.WithTrailingTrivia(TriviaList(Trivia(octothorpeTrivia)));
SyntaxNode? parent = node.Parent;
while (parent is not null)
{
newNode = node.Parent.ReplaceNode(node, newNode);
node = node.Parent;
parent = node.Parent;
scriptRoot = RecursiveReplaceNode(node, newNode);
}
_context.SetScriptContent(initType, scriptOffset, newNode.SyntaxTree);
}
}
catch { }
_context.SetScriptContent(initType, scriptOffset, scriptRoot.SyntaxTree);
break;
}
}
@@ -384,6 +442,18 @@ partial class Decompiler
}
}
private static SyntaxNode RecursiveReplaceNode(SyntaxNode oldNode, SyntaxNode newNode)
{
var parent = oldNode.Parent;
while (parent is not null)
{
newNode = oldNode.Parent.ReplaceNode(oldNode, newNode);
oldNode = oldNode.Parent;
parent = oldNode.Parent;
}
return newNode;
}
public static SyntaxTree CreateTree(IEnumerable<StatementSyntax> statements)
{
return SyntaxTree(
+3 -2
View File
@@ -73,8 +73,9 @@ public partial class Decompiler
foreach (var offset in export.FinalEvaluateOffsets ?? [])
{
var syntaxTree = DecompileScript(offset, export, "FinalEvaluate");
_context.SetScriptContent(export, offset, syntaxTree);
var statements = DecompileMethod(offset, export);
AddMethodAttribute(statements, "FinalEvaluate");
_context.SetScriptContent(export, offset, CreateTree(statements));
}
foreach (var offset in export.RefreshGroupOffsets ?? [])