mirror of
https://github.com/ZuneDev/ZuneUIXTools.git
synced 2026-07-27 13:11:59 -07:00
Implement triggers with chained member accesses
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 ?? [])
|
||||
|
||||
Reference in New Issue
Block a user