[WIP] First pass at script decompilation

This commit is contained in:
Yoshi Askharoun
2025-07-18 00:23:23 -05:00
parent 059e143980
commit f18920fd33
8 changed files with 302 additions and 49 deletions
+199
View File
@@ -0,0 +1,199 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Iris.DecompXml.Mock;
using Microsoft.Iris.Markup;
using Microsoft.Iris.Markup.UIX;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace Microsoft.Iris.DecompXml;
partial class Decompiler
{
private SyntaxTree DecompileScript(uint startOffset, MarkupTypeSchema export)
{
var methodBody = _context.GetMethodBody(startOffset);
List<StatementSyntax> topStatements = [];
Stack<object> stack = new();
for (int i = 0; i < methodBody.Length; i++)
{
var instruction = methodBody[i];
var opCode = instruction.OpCode;
try
{
switch (opCode)
{
case OpCode.PushConstant:
var constant = _context.GetConstant(instruction.Operands.First());
stack.Push(IrisExpression.ToSyntax(constant, _context));
break;
case OpCode.PushNull:
stack.Push(LiteralExpression(SyntaxKind.NullLiteralExpression));
break;
case OpCode.DiscardValue:
stack.Pop();
break;
case OpCode.ConstructObject:
var typeToCtor = _context.GetImportedType(instruction.Operands.First());
stack.Push(ObjectCreationExpression(
IdentifierName(_context.GetQualifiedName(typeToCtor).ToString()),
ArgumentList(),
null
));
break;
case OpCode.LookupSymbol:
var symbolIndex = (ushort)instruction.Operands.First().Value;
stack.Push(IdentifierName(export.SymbolReferenceTable[symbolIndex].Symbol));
break;
case OpCode.WriteSymbol:
case OpCode.WriteSymbolPeek:
var writeSymbolIndex = (ushort)instruction.Operands.First().Value;
var writeSymbolExpr = IdentifierName(export.SymbolReferenceTable[writeSymbolIndex].Symbol);
var newSymbolValue = opCode is OpCode.WriteSymbolPeek
? stack.Peek() : stack.Pop();
var symbolAssignmentExpr = AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
writeSymbolExpr, IrisExpression.ToSyntax(newSymbolValue, _context));
topStatements.Add(ExpressionStatement(symbolAssignmentExpr));
break;
case OpCode.MethodInvoke:
case OpCode.MethodInvokePeek:
case OpCode.MethodInvokeStatic:
case OpCode.MethodInvokePushLastParam:
case OpCode.MethodInvokeStaticPushLastParam:
var methodSchema = _context.GetImportedMethod(instruction.Operands.First());
int parameterCount = methodSchema.ParameterTypes.Length;
var parameters = new ArgumentSyntax[parameterCount];
for (parameterCount--; parameterCount >= 0; parameterCount--)
{
var parameter = IrisExpression.ToSyntax(stack.Pop(), _context);
parameters[parameterCount] = Argument(parameter);
}
bool isStatic = opCode is OpCode.MethodInvokeStatic or OpCode.MethodInvokeStaticPushLastParam;
bool peek = opCode is OpCode.MethodInvokePeek;
bool pushLastParam = opCode is OpCode.MethodInvokePushLastParam or OpCode.MethodInvokeStaticPushLastParam;
var targetObj = opCode switch
{
OpCode.MethodInvokeStatic or
OpCode.MethodInvokeStaticPushLastParam => methodSchema.Owner,
_ when peek => stack.Peek(),
_ => stack.Pop(),
};
var methodExpression = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
IrisExpression.ToSyntax(targetObj, _context),
IdentifierName(methodSchema.Name));
var methodResult = InvocationExpression(methodExpression, ArgumentList([.. parameters]));
if (methodSchema.ReturnType != VoidSchema.Type)
{
stack.Push(methodResult);
}
else
{
topStatements.Add(ExpressionStatement(methodResult));
}
if (pushLastParam)
{
stack.Push(parameters[^1].Expression);
}
break;
case OpCode.PropertyGet:
case OpCode.PropertyGetPeek:
case OpCode.PropertyGetStatic:
var propToGet = _context.GetImportedProperty(instruction.Operands.First());
var propTarget = instruction.OpCode switch
{
OpCode.PropertyGet => stack.Pop(),
OpCode.PropertyGetPeek => stack.Peek(),
_ => propToGet.Owner,
};
var propertyGetExpression = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
IrisExpression.ToSyntax(propTarget, _context),
IdentifierName(propToGet.Name));
stack.Push(propertyGetExpression);
break;
case OpCode.PropertyInitialize:
var propertyToInit = _context.GetImportedProperty(instruction.Operands.First());
var newPropValue = stack.Pop();
var target = stack.Pop();
var xTarget = (XElement)ToXmlFriendlyObject(target);
PropertyAssignOnXElement(xTarget, propertyToInit, IrisObject.Create(newPropValue, propertyToInit.PropertyType, _context));
stack.Push(new IrisObject(xTarget, propertyToInit.Owner));
break;
case OpCode.PropertyDictionaryAdd:
var targetDictProperty = _context.GetImportedProperty(instruction.Operands.ElementAt(0));
var keyReference = instruction.Operands.ElementAt(1);
var key = _context.GetConstant(keyReference).Value.ToString();
var dictValue = stack.Pop();
var targetInstance = (XElement)stack.Peek();
PropertyDictionaryAddOnXElement(targetInstance, targetDictProperty, IrisObject.Create(dictValue, null, _context), key);
break;
case OpCode.PropertyListAdd:
var targetListProperty = _context.GetImportedProperty(instruction.Operands.First());
var valueToAdd = stack.Pop();
var targetInstance2 = (XElement)ToXmlFriendlyObject(stack.Peek());
PropertyListAddOnXElement(targetInstance2, targetListProperty, IrisObject.Create(valueToAdd, null, _context));
break;
}
}
catch (Exception ex)
{
throw new Exception($"Failed to decompile instruction `{instruction}` @ 0x{instruction.Offset:X} in script for {export.Name}", ex);
}
}
return SyntaxTree(
CompilationUnit().WithMembers(
[..topStatements.Select(GlobalStatement)]
)
);
}
public static string FormatScript(SyntaxTree tree, CancellationToken token = default)
{
return tree
.GetRoot(token)
.NormalizeWhitespace()
.SyntaxTree
.GetText(token)
.ToString();
}
}
+17 -1
View File
@@ -11,7 +11,7 @@ using System.Xml.Linq;
namespace Microsoft.Iris.DecompXml; namespace Microsoft.Iris.DecompXml;
public class Decompiler public partial class Decompiler
{ {
private static readonly XNamespace _nsUix = XNamespace.Get("http://schemas.microsoft.com/2007/uix"); private static readonly XNamespace _nsUix = XNamespace.Get("http://schemas.microsoft.com/2007/uix");
private readonly DecompileContext _context; private readonly DecompileContext _context;
@@ -51,6 +51,22 @@ public class Decompiler
if (export.InitializePropertiesOffset is not uint.MaxValue) if (export.InitializePropertiesOffset is not uint.MaxValue)
AnalyzeMethodForInit(export.InitializePropertiesOffset, xExport, export, name + "_prop"); AnalyzeMethodForInit(export.InitializePropertiesOffset, xExport, export, name + "_prop");
if (export.InitialEvaluateOffsets is { Length: > 0 })
{
XElement xScripts = new(_nsUix + "Scripts");
foreach (var offset in export.InitialEvaluateOffsets)
{
var syntaxTree = DecompileScript(offset, export);
var scriptText = FormatScript(syntaxTree);
XElement xScript = new(_nsUix + "Script", scriptText);
xScripts.Add(xScript);
}
xExport.Add(xScripts);
}
if (export.InitializeContentOffset is not uint.MaxValue) if (export.InitializeContentOffset is not uint.MaxValue)
AnalyzeMethodForInit(export.InitializeContentOffset, xExport, export, name + "_cont"); AnalyzeMethodForInit(export.InitializeContentOffset, xExport, export, name + "_cont");
@@ -1,6 +1,11 @@
using Microsoft.Iris.Markup; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Iris.Markup;
using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace Microsoft.Iris.DecompXml.Mock; namespace Microsoft.Iris.DecompXml.Mock;
internal class IrisConstantExpression : IrisExpression, IReturnValueProvider internal class IrisConstantExpression : IrisExpression, IReturnValueProvider
@@ -19,16 +24,17 @@ internal class IrisConstantExpression : IrisExpression, IReturnValueProvider
public TypeSchema ReturnType => TypeSchema; public TypeSchema ReturnType => TypeSchema;
public override string Decompile(DecompileContext context) public override ExpressionSyntax ToSyntax(DecompileContext context)
{ {
if (TypeSchema.IsEnum)
return $"{context.GetQualifiedName(TypeSchema)}.{Value}";
return Value switch return Value switch
{ {
string str => str, Enum enumValue => MemberAccessExpression(
IStringEncodable strEnc => strEnc.EncodeString(), SyntaxKind.SimpleMemberAccessExpression,
_ => Value?.ToString() ?? "null", IdentifierName(context.GetQualifiedName(TypeSchema).ToString()),
IdentifierName(Value.ToString())
),
_ => ToSyntax(Value, context)
}; };
} }
} }
+33 -4
View File
@@ -1,12 +1,20 @@
using Microsoft.Iris.Asm; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Iris.Asm;
using Microsoft.Iris.Markup;
using Newtonsoft.Json.Linq;
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace Microsoft.Iris.DecompXml.Mock; namespace Microsoft.Iris.DecompXml.Mock;
internal class IrisExpression : Expression internal abstract class IrisExpression : Expression
{ {
public virtual string Decompile(DecompileContext context) => ToString(); public virtual string Decompile(DecompileContext context) => ToSyntax(context).ToString();
public abstract ExpressionSyntax ToSyntax(DecompileContext context);
public static Expression Wrap(object p) public static Expression Wrap(object p)
{ {
@@ -15,7 +23,7 @@ internal class IrisExpression : Expression
null => Constant(null), null => Constant(null),
Expression expr => expr, Expression expr => expr,
Disassembler.RawConstantInfo constantInfo => new IrisConstantExpression(constantInfo.Value, constantInfo.Type), Disassembler.RawConstantInfo constantInfo => new IrisConstantExpression(constantInfo.Value, constantInfo.Type),
Markup.SymbolReference symbolRef => Constant(symbolRef), SymbolReference symbolRef => Constant(symbolRef),
_ => throw new NotImplementedException($"Unable to wrap '{p}' in an expression") _ => throw new NotImplementedException($"Unable to wrap '{p}' in an expression")
}; };
@@ -27,4 +35,25 @@ internal class IrisExpression : Expression
? irisExpr.Decompile(context) ? irisExpr.Decompile(context)
: expr.ToString(); : expr.ToString();
} }
public static ExpressionSyntax ToSyntax(object obj, DecompileContext context)
{
return obj switch
{
null => LiteralExpression(SyntaxKind.NullLiteralExpression),
int intValue => LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(intValue)),
string strValue => LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(strValue)),
IStringEncodable strEnc => ParseExpression(strEnc.EncodeString()),
Disassembler.RawConstantInfo constantInfo => new IrisConstantExpression(constantInfo.Value, constantInfo.Type).ToSyntax(context),
SymbolReference symbolRef => IdentifierName(symbolRef.Symbol),
TypeSchema typeSchema => IdentifierName(context.GetQualifiedName(typeSchema).ToString()),
IrisExpression irisExpr => irisExpr.ToSyntax(context),
Expression expr => ParseExpression(expr.ToString()),
ExpressionSyntax exprSyn => exprSyn,
_ => IdentifierName(obj.ToString())
};
}
} }
@@ -1,8 +1,10 @@
using Microsoft.Iris.Markup; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Iris.Markup;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace Microsoft.Iris.DecompXml.Mock; namespace Microsoft.Iris.DecompXml.Mock;
@@ -29,27 +31,22 @@ internal class IrisMethodCallExpression : IrisExpression, IArgumentProvider, IRe
public Expression GetArgument(int index) => _arguments[index]; public Expression GetArgument(int index) => _arguments[index];
public override string Decompile(DecompileContext context) public override ExpressionSyntax ToSyntax(DecompileContext context)
{ {
StringBuilder sb = new(); var targetExpression = Target switch
if (Target is null)
{ {
var qfn = context.GetQualifiedName(Method.Owner); null => IdentifierName(context.GetQualifiedName(Method.Owner).ToString()),
sb.Append(qfn); IrisExpression irisExpr => irisExpr.ToSyntax(context),
} _ => IdentifierName(Target.ToString())
else };
{
sb.Append(Decompile(Target, context));
}
sb.Append('.'); var methodExpression = MemberAccessExpression(CodeAnalysis.CSharp.SyntaxKind.SimpleMemberAccessExpression,
sb.Append(Method.Name); targetExpression, IdentifierName(Method.Name));
sb.Append('('); var argumentExpressions = _arguments
sb.Append(string.Join(", ", _arguments.Select(x => Decompile(x, context)))); .Select(expr => ToSyntax(expr, context))
sb.Append(')'); .Select(Argument);
return sb.ToString(); return InvocationExpression(methodExpression, ArgumentList([..argumentExpressions]));
} }
} }
@@ -1,6 +1,8 @@
using Microsoft.Iris.Markup; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Iris.Markup;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace Microsoft.Iris.DecompXml.Mock; namespace Microsoft.Iris.DecompXml.Mock;
@@ -20,24 +22,18 @@ internal class IrisPropertyExpression : IrisExpression, IReturnValueProvider
public TypeSchema ReturnType => Property.PropertyType; public TypeSchema ReturnType => Property.PropertyType;
public override ExpressionSyntax ToSyntax(DecompileContext context)
public override string Decompile(DecompileContext context)
{ {
StringBuilder sb = new(); var targetExpression = Target switch
if (Target is null)
{ {
var qfn = context.GetQualifiedName(Property.Owner); null => IdentifierName(context.GetQualifiedName(Property.Owner).ToString()),
sb.Append(qfn); IrisExpression irisExpr => irisExpr.ToSyntax(context),
} _ => IdentifierName(Target.ToString())
else };
{
sb.Append(Decompile(Target, context));
}
sb.Append('.'); var propertyAccessExpression = MemberAccessExpression(CodeAnalysis.CSharp.SyntaxKind.SimpleMemberAccessExpression,
sb.Append(Property.Name); targetExpression, IdentifierName(Property.Name));
return sb.ToString(); return propertyAccessExpression;
} }
} }
+2
View File
@@ -9,6 +9,8 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\UIX.Asm\UIX.Asm.csproj" /> <ProjectReference Include="..\UIX.Asm\UIX.Asm.csproj" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="PolySharp" Version="1.14.1"> <PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+8
View File
@@ -9,6 +9,14 @@
</Properties> </Properties>
</Class> </Class>
<UI Name="AboutDialogContentUI" Base="dialog:DialogContentUI"> <UI Name="AboutDialogContentUI" Base="dialog:DialogContentUI">
<Scripts>
<Script>DialogTitle = zuneUI:Shell.LoadString(zuneUI:StringId.IDS_ABOUTDIALOG_TITLE);</Script>
<Script>ButtonCommands = new List();
ButtonCommands.Add(Dialog.Cancel);
DefaultButtonModel = Dialog.Cancel;
ButtonModelToFocus = Dialog.Cancel;</Script>
<Script></Script>
</Scripts>
<Content> <Content>
<Panel MaximumSize="360, 0"> <Panel MaximumSize="360, 0">
<Layout> <Layout>