diff --git a/godot-mono-decomp/GodotMonoDecomp/CollectionExpressionOutputVisitor.cs b/godot-mono-decomp/GodotMonoDecomp/CollectionExpressionOutputVisitor.cs index 43351cdf9..f67135a59 100644 --- a/godot-mono-decomp/GodotMonoDecomp/CollectionExpressionOutputVisitor.cs +++ b/godot-mono-decomp/GodotMonoDecomp/CollectionExpressionOutputVisitor.cs @@ -1,7 +1,18 @@ +using System.Collections.Generic; using System.IO; -using ICSharpCode.Decompiler.CSharp; +using System.Linq; +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.DebugInfo; using ICSharpCode.Decompiler.CSharp.OutputVisitor; using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.IL; +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Disassembler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.ILSpyX.Extensions; +using SequencePoint = ICSharpCode.Decompiler.DebugInfo.SequencePoint; +using System.Reflection.Metadata; namespace GodotMonoDecomp; @@ -22,12 +33,266 @@ private CollectionExpressionSpreadElementAnnotation() { } } +public class ProxyOutput: ITextOutput +{ + public ITextOutput realOutput; + public ProxyOutput(ITextOutput realOutput) + { + this.realOutput = realOutput; + } + public string IndentationString { + get { return realOutput.IndentationString; } + set { realOutput.IndentationString = value; } + } + public void Indent() + { + realOutput.Indent(); + } + public void Unindent() + { + realOutput.Unindent(); + } + public void Write(char ch) + { + realOutput.Write(ch); + } + public void Write(string text) + { + realOutput.Write(text); + } + public void WriteLine() + { + realOutput.WriteLine(); + } + public void WriteReference(OpCodeInfo opCode, bool omitSuffix = false) + { + realOutput.WriteReference(opCode, omitSuffix); + } + public void WriteReference(MetadataFile metadata, Handle handle, string text, string protocol = "decompile", bool isDefinition = false) + { + realOutput.WriteReference(metadata, handle, text, protocol, isDefinition); + } + public void WriteReference(IType type, string text, bool isDefinition = false) + { + realOutput.WriteReference(type, text, isDefinition); + } + public void WriteReference(IMember member, string text, bool isDefinition = false) + { + realOutput.WriteReference(member, text, isDefinition); + } + public void WriteLocalReference(string text, object reference, bool isDefinition = false) + { + realOutput.WriteLocalReference(text, reference, isDefinition); + } + + public void MarkFoldStart(string collapsedText = "...", bool defaultCollapsed = false, bool isDefinition = false) + { + realOutput.MarkFoldStart(collapsedText, defaultCollapsed, isDefinition); + } + public void MarkFoldEnd() + { + realOutput.MarkFoldEnd(); + } +} + +public class GodotLineDisassembler: MethodBodyDisassembler +{ + + ITextOutput fakeOutput; + ITextOutput realOutput; + + ProxyOutput proxyOutput; + IList sequencePoints; + public GodotLineDisassembler(ITextOutput output, CancellationToken cancellationToken, List sequencePointsToDecompile) : this(new ProxyOutput(new PlainTextOutput()), output, cancellationToken, sequencePointsToDecompile) + { + } + + private GodotLineDisassembler(ProxyOutput proxyOutput, ITextOutput output, CancellationToken cancellationToken, List sequencePointsToDecompile) : base(proxyOutput, cancellationToken){ + this.fakeOutput = proxyOutput.realOutput; + this.proxyOutput = proxyOutput; + this.sequencePoints = sequencePointsToDecompile; + this.realOutput = output; + } + + public override void Disassemble(MetadataFile module, System.Reflection.Metadata.MethodDefinitionHandle handle) + { + base.Disassemble(module, handle); + } + + protected override void WriteInstruction(ITextOutput _, MetadataFile metadataFile, System.Reflection.Metadata.MethodDefinitionHandle methodHandle, ref System.Reflection.Metadata.BlobReader blob, int methodRva) + { + int offset = blob.Offset; + if (sequencePoints.Any(seq => (seq.Offset <= offset && seq.EndOffset > offset))) + { + this.proxyOutput.realOutput = realOutput; + base.WriteInstruction(realOutput, metadataFile, methodHandle, ref blob, methodRva); + } + else + { + this.proxyOutput.realOutput = fakeOutput; + base.WriteInstruction(fakeOutput, metadataFile, methodHandle, ref blob, methodRva); + } + this.proxyOutput.realOutput = fakeOutput; + } +} public class GodotCSharpOutputVisitor : CSharpOutputVisitor { - public GodotCSharpOutputVisitor(TextWriter w, CSharpFormattingOptions formattingOptions) - : base(w, formattingOptions) + private readonly bool emitILAnnotationComments; + private readonly TokenWriter commentWriter; + + private readonly GodotMonoDecompSettings settings; + + private Dictionary> sequencePoints = []; + + private Dictionary>>> startLineToSequencePoints = []; + + private readonly CSharpDecompiler? decompiler; + + private int lastStartLine = 0; + private readonly Dictionary<(MetadataFile Module, MethodDefinitionHandle MethodHandle, string SequenceKey), string[]> disassemblyLineCache = []; + + public GodotCSharpOutputVisitor(TextWriter w, GodotMonoDecompSettings settings, bool emitILAnnotationComments = false, CSharpDecompiler? decompiler = null) + : this(new TextWriterTokenWriter(w), settings, emitILAnnotationComments, decompiler) + { + } + + private GodotCSharpOutputVisitor(TokenWriter w, GodotMonoDecompSettings settings, + bool emitILAnnotationComments, CSharpDecompiler? decompiler) : base(w, settings.CSharpFormattingOptions) + { + this.decompiler = decompiler; + this.emitILAnnotationComments = emitILAnnotationComments; + this.settings = settings; + this.commentWriter = w; + } + + // we have to create another visitor and output to a fake writer in order + // for the output visitor to annotate all the nodes with line/column information + // So that we can use this to generate sequence points for the IL instructions + static void WriteCode(TextWriter output, GodotMonoDecompSettings settings, SyntaxTree syntaxTree, IDecompilerTypeSystem typeSystem) + { + syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true }); + TokenWriter tokenWriter = new TextWriterTokenWriter(output) { IndentationString = settings.CSharpFormattingOptions.IndentationString }; + tokenWriter = TokenWriter.WrapInWriterThatSetsLocationsInAST(tokenWriter); + syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(tokenWriter, settings, false, null)); + } + + public override void VisitSyntaxTree(SyntaxTree syntaxTree) + { + if (emitILAnnotationComments && decompiler != null) { + lastStartLine = 0; + startLineToSequencePoints.Clear(); + sequencePoints.Clear(); + disassemblyLineCache.Clear(); + var fakeWriter = new StringWriter(); + WriteCode(fakeWriter, settings, syntaxTree, decompiler.TypeSystem); + sequencePoints = decompiler.CreateSequencePoints(syntaxTree); + + // create a + var startLines = sequencePoints.Where(kvp => kvp.Value.Count > 0).SelectMany(kvp => kvp.Value.Select(seq => seq.StartLine)).Distinct().OrderBy(l => l).ToList(); + foreach (var startLine in startLines) + { + var sequencePointsForLine = sequencePoints.Select(kvp => { + return new KeyValuePair>(kvp.Key, kvp.Value.Where(seq => seq.StartLine == startLine).ToList()); + }).Where(kvp => kvp.Value.Count > 0).ToList(); + startLineToSequencePoints[startLine] = sequencePointsForLine; + } + } + base.VisitSyntaxTree(syntaxTree); + } + + protected override void StartNode(AstNode node) + { + var startLine = node.StartLocation.Line; + if (emitILAnnotationComments && startLine > lastStartLine && startLineToSequencePoints.ContainsKey(startLine)) { + var sequencePointsForLine = startLineToSequencePoints[startLine]; + // transform it into a list of (methodHandle, sequencePoint) + var sps = sequencePointsForLine + .SelectMany(kvp => kvp.Value.Select(sp => (Method: kvp.Key, SequencePoint: sp))) + .OrderBy(sp => sp.SequencePoint.Offset) + .ToList(); + var methodHandles = new Dictionary(); + foreach (var sp in sps) + { + if (methodHandles.ContainsKey(sp.Method)) + { + continue; + } + + if (TryGetMethodDefinitionHandle(sp.Method, out var metadataFile, out var methodHandle)) + { + methodHandles[sp.Method] = (metadataFile, methodHandle); + } + } + + var emittedSequencePoints = new HashSet<(MetadataFile Module, MethodDefinitionHandle MethodHandle, int Offset, int EndOffset)>(); + foreach (var sp in sps) + { + if (!methodHandles.TryGetValue(sp.Method, out var resolvedMethod)) + { + continue; + } + + var emitKey = (resolvedMethod.Module, resolvedMethod.MethodHandle, sp.SequencePoint.Offset, sp.SequencePoint.EndOffset); + if (!emittedSequencePoints.Add(emitKey)) + { + continue; + } + + var lines = GetDisassemblyLines(resolvedMethod.Module, resolvedMethod.MethodHandle, [sp.SequencePoint]); + if (lines.Length == 0) + { + continue; + } + + commentWriter.NewLine(); + foreach (var line in lines) + { + commentWriter.WriteComment(CommentType.SingleLine, line); + } + } + } + lastStartLine = startLine; + base.StartNode(node); + } + + private string[] GetDisassemblyLines(MetadataFile metadataFile, MethodDefinitionHandle methodHandle, List sequencePointList) + { + var sequenceKey = string.Join(";", sequencePointList.Select(sp => $"{sp.Offset:x4}-{sp.EndOffset:x4}")); + var cacheKey = (metadataFile, methodHandle, sequenceKey); + if (disassemblyLineCache.TryGetValue(cacheKey, out var cachedLines)) + { + return cachedLines; + } + + var output = new PlainTextOutput(); + var methodDisassembler = new GodotLineDisassembler(output, default(CancellationToken), sequencePointList); + methodDisassembler.Disassemble(metadataFile, methodHandle); + var lines = output + .ToString() + .Split('\n') + .Where(line => line.Length > 0) + .ToArray(); + disassemblyLineCache[cacheKey] = lines; + return lines; + } + + private static bool TryGetMethodDefinitionHandle(ILFunction function, out MetadataFile metadataFile, out MethodDefinitionHandle methodHandle) { + metadataFile = null!; + methodHandle = default; + + var method = function.Method; + var token = method?.MetadataToken ?? default; + var module = method?.ParentModule?.MetadataFile; + if (module == null || token.IsNil || token.Kind != HandleKind.MethodDefinition) + { + return false; + } + + metadataFile = module; + methodHandle = (MethodDefinitionHandle)token; + return true; } public override void VisitArrayInitializerExpression(ArrayInitializerExpression arrayInitializerExpression) diff --git a/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs b/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs index e91248dc9..c97c7c3d2 100644 --- a/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs +++ b/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs @@ -8,9 +8,84 @@ namespace GodotMonoDecomp; -public class DotNetCoreDepInfo +public class DotNetCoreDepInfo : IEquatable { + public struct RuntimeComponentInfo : IEquatable, IEquatable + { + public readonly string Name; + + public readonly string Extension; + + public readonly string? Directory; + + public readonly Version? AssemblyVersion; + + public readonly Version? FileVersion; + + public readonly string FileName => Name + Extension; + + public readonly string Path => System.IO.Path.Combine(Directory ?? "", FileName); + + public readonly AssemblyNameReference AssemblyRef => AssemblyNameReference.Parse($"{Name}, Version={AssemblyVersion?.ToString(4) ?? "1.0.0.0"}, Culture=neutral, PublicKeyToken=null"); + + public RuntimeComponentInfo(string name, string extension, string? directory, Version? assemblyVersion, Version? fileVersion) + { + Name = name; + Extension = extension; + Directory = directory; + AssemblyVersion = assemblyVersion; + FileVersion = fileVersion; + } + + public readonly bool Matches(IAssemblyReference? reference) + { + return reference != null && Name == reference.Name && AssemblyVersion == reference.Version; + } + + public readonly bool Equals(RuntimeComponentInfo other) + { + return Name == other.Name && Extension == other.Extension && Directory == other.Directory && AssemblyVersion == other.AssemblyVersion && FileVersion == other.FileVersion; + } + + public readonly bool Equals(RuntimeComponentInfo? other) + { + return other != null && Equals(other.Value); + } + + } + + public struct NativeComponentInfo : IEquatable, IEquatable + { + public readonly string Name; + public readonly string Extension; + public readonly string? Directory; + + public readonly string FileName => Name + Extension; + + public readonly string Path => System.IO.Path.Combine(Directory ?? "", FileName); + + public readonly Version? FileVersion; + + public NativeComponentInfo(string name, string extension, string? directory, Version? fileVersion) + { + Name = name; + Extension = extension; + Directory = directory; + FileVersion = fileVersion; + } + + public readonly bool Equals(NativeComponentInfo other) + { + return Name == other.Name && Extension == other.Extension && Directory == other.Directory && FileVersion == other.FileVersion; + } + + public readonly bool Equals(NativeComponentInfo? other) + { + return other != null && Equals(other.Value); + } + } + public enum HashMatchesNugetOrg { // This enum is used to determine if the SHA512 hash matches the package downloaded from nuget.org. @@ -22,19 +97,41 @@ public enum HashMatchesNugetOrg public readonly string Name; public readonly string Version; public readonly string Type; - public readonly string Path; - public readonly string Sha512; + public readonly string? Sha512; + public readonly string? Path; + public readonly string? HashPath; public readonly bool Serviceable; public readonly DotNetCoreDepInfo[] deps; - public readonly string[] runtimeComponents; + public readonly RuntimeComponentInfo[] runtimeComponents; + public readonly NativeComponentInfo[] nativeComponents; + public readonly RuntimeComponentInfo? ThisRuntimeComponent; public HashMatchesNugetOrg HashMatchesNugetOrgStatus { get; private set; } = HashMatchesNugetOrg.Unknown; - public AssemblyNameReference AssemblyRef => AssemblyNameReference.Parse($"{Name}, Version={GetCorrectVersion(Version)}, Culture=neutral, PublicKeyToken=null"); + public System.Version AssemblyVersion => ThisRuntimeComponent?.AssemblyVersion ?? System.Version.Parse(ConvertToAssemblyVersion(Version)); + public AssemblyNameReference AssemblyRef => ThisRuntimeComponent?.AssemblyRef ?? AssemblyNameReference.Parse($"{Name}, Version={ConvertToAssemblyVersion(Version)}, Culture=neutral, PublicKeyToken=null"); + + public bool IsAvailableOnNuget => Serviceable && HashMatchesNugetOrgStatus != HashMatchesNugetOrg.NoMatch; + + public bool IsRuntimePack => Type == "runtimepack"; + public bool IsProject => Type == "project"; - static string GetCorrectVersion(string ver) + public bool HasNoRuntimeComponent => runtimeComponents == null || runtimeComponents.Length == 0; + + static string ConvertToAssemblyVersion(string ver) { - // if it contains less than 4 parts, add ".0" to the end - var parts = ver.Split('.').ToList(); + var parts = ver.TrimStart('v').Split('-')[0].Split('+')[0].Trim().Split('.').ToList(); + for (int i = 0; i < parts.Count; i++) + { + if (!UInt64.TryParse(parts[i], out _)) + { + parts = parts.Take(i).ToList(); + break; + } + } + if (parts.Count == 0) + { + return "1.0.0.0"; + } while (parts.Count < 4) { parts.Add("0"); @@ -48,9 +145,13 @@ public DotNetCoreDepInfo( string version, string type, bool serviceable, - string path, string sha512, - DotNetCoreDepInfo[] deps, string[] runtimeComponents) + string? path, + string? hashPath, + DotNetCoreDepInfo[] deps, + RuntimeComponentInfo[] runtimeComponents, + NativeComponentInfo[] nativeComponents, + RuntimeComponentInfo? thisRuntimeComponent) { var parts = fullName.Split('/'); this.Name = parts[0]; @@ -66,10 +167,13 @@ public DotNetCoreDepInfo( this.Type = type; this.Serviceable = serviceable; this.Path = path; + this.HashPath = hashPath; this.Sha512 = sha512; this.deps = deps; this.runtimeComponents = runtimeComponents; + this.nativeComponents = nativeComponents; + this.ThisRuntimeComponent = thisRuntimeComponent; } static DotNetCoreDepInfo CreateFromJson(string fullName, string version, string target, JObject blob) @@ -77,6 +181,18 @@ static DotNetCoreDepInfo CreateFromJson(string fullName, string version, string return Create(fullName, version, target, blob, []); } + static System.Version? TryParseVersionOrDefault(string? version, string? defaultVersion) + { + if (string.IsNullOrEmpty(version) || !System.Version.TryParse(version, out var versionResult)) + { + if (string.IsNullOrEmpty(defaultVersion) || !System.Version.TryParse(defaultVersion, out versionResult)) + { + return null; + } + } + return versionResult; + } + static DotNetCoreDepInfo Create(string fullName, string version, string target, JObject blob, Dictionary _deps) { @@ -92,34 +208,68 @@ static DotNetCoreDepInfo Create(string fullName, string version, string target, Version = version; } - var type = "runtimedll"; - var serviceable = false; - var path = ""; - var sha512 = ""; + string type = "runtimedll"; + bool serviceable = false; + string sha512 = ""; + string? path = null; + string? hashPath = null; var libraryBlob = blob["libraries"]?[Name + "/" + Version] as JObject; if (libraryBlob != null) { type = libraryBlob["type"]?.ToString() ?? type; serviceable = libraryBlob["serviceable"]?.Value() ?? serviceable; - path = libraryBlob["path"]?.ToString() ?? ""; sha512 = libraryBlob["sha512"]?.ToString() ?? ""; + path = libraryBlob["path"]?.ToString(); + hashPath = libraryBlob["hashPath"]?.ToString(); } - string[] runtimeComponents = Array.Empty(); + var runtimeComponents = new List(); var runtimeBlob = blob["targets"]?[target]?[Name + "/" + Version]?["runtime"] as JObject; + RuntimeComponentInfo? thisRuntimeComponent = null; if (runtimeBlob != null) { - runtimeComponents = new string[runtimeBlob.Count]; - int i = 0; foreach (var prop in runtimeBlob.Properties()) { - runtimeComponents[i] = System.IO.Path.GetFileNameWithoutExtension(prop.Name); - i++; + var name = System.IO.Path.GetFileNameWithoutExtension(prop.Name); + bool isThisAssembly = name == Name; + var directory = System.IO.Path.GetDirectoryName(prop.Name); + var extension = System.IO.Path.GetExtension(prop.Name); + var assemblyVersion = TryParseVersionOrDefault( + prop.Value["assemblyVersion"]?.ToString(), + isThisAssembly ? ConvertToAssemblyVersion(Version) : null + ); + var fileVersion = TryParseVersionOrDefault( + prop.Value["fileVersion"]?.ToString(), + isThisAssembly ? ConvertToAssemblyVersion(Version) : null + ); + var runtimeComponent = new RuntimeComponentInfo(name, extension, directory, assemblyVersion, fileVersion); + runtimeComponents.Add(runtimeComponent); + if (isThisAssembly) + { + thisRuntimeComponent = runtimeComponent; + } } } + // string[] nativeComponents = Array.Empty(); + var nativeBlob = blob["targets"]?[target]?[Name + "/" + Version]?["native"] as JObject; + var nativeComponents = new List(); + if (nativeBlob != null) + { + foreach (var prop in nativeBlob.Properties()) + { + var name = System.IO.Path.GetFileNameWithoutExtension(prop.Name); + var extension = System.IO.Path.GetExtension(prop.Name); + var directory = System.IO.Path.GetDirectoryName(prop.Name); + var fileVersion = TryParseVersionOrDefault( + prop.Value["fileVersion"]?.ToString(), + null + ); + nativeComponents.Add(new NativeComponentInfo(name, extension, directory, fileVersion)); + } + } var deps = getDeps(Name, Version, target, blob, _deps); - return new DotNetCoreDepInfo(Name, Version, type, serviceable, path, sha512, deps, runtimeComponents); + return new DotNetCoreDepInfo(Name, Version, type, serviceable, sha512, path, hashPath, deps, runtimeComponents.ToArray(), nativeComponents.ToArray(), thisRuntimeComponent); } @@ -167,33 +317,71 @@ static DotNetCoreDepInfo[] getDeps(string Name, string Version, string target, J return result.ToArray(); } - public bool HasDep(string name, string? type, bool serviceableAndNuGetOnly = false) + public bool Equals(DotNetCoreDepInfo? other) { - if (runtimeComponents.Contains(name) && !((!string.IsNullOrEmpty(type) && Type != type) || (serviceableAndNuGetOnly && (!Serviceable || HashMatchesNugetOrgStatus == HashMatchesNugetOrg.NoMatch)))) + return other != null + && Name == other.Name + && Version == other.Version + && Type == other.Type + && Serviceable == other.Serviceable + && Path == other.Path + && HashPath == other.HashPath + && Sha512 == other.Sha512 + && deps.SequenceEqual(other.deps) + && runtimeComponents.SequenceEqual(other.runtimeComponents) + && nativeComponents.SequenceEqual(other.nativeComponents) + && (ThisRuntimeComponent?.Equals(other.ThisRuntimeComponent) ?? other.ThisRuntimeComponent == null); + } + + private List GetAllDeps(bool followProjectReferences, HashSet? seen) + { + var result = new HashSet(); + result.AddRange(deps); + var unseenDeps = deps.Where(d => !seen?.Contains(d) ?? true); + if (seen == null) { - return true; + seen = [this, .. deps]; } - for (int i = 0; i < deps.Length; i++) + foreach (var dep in unseenDeps) { - if ((!string.IsNullOrEmpty(type) && deps[i].Type != type) || - (serviceableAndNuGetOnly && (!deps[i].Serviceable || deps[i].HashMatchesNugetOrgStatus == HashMatchesNugetOrg.NoMatch))) + if (!followProjectReferences && dep.Type == "project") { - // skip non-package dependencies if parent is a package continue; } + var found = dep.GetAllDeps(followProjectReferences, seen); + result.AddRange(found); + seen.AddRange(found); + } + return result.ToList(); + } - if (deps[i].Name == name) - { - return true; - } + public List GetAllDeps(bool followProjectReferences = false) { + return GetAllDeps(followProjectReferences, null); + } - if (deps[i].HasDep(name, null, false)) - { - return true; - } + + public bool Matches(IAssemblyReference? reference) + { + return reference != null && reference.Name == Name && reference.Version == AssemblyVersion; + } + + public bool HasDep(IAssemblyReference reference, string? type, bool serviceableAndNuGetOnly = false, bool dontFollowProjectReferences = false) + { + bool ShouldFilter(DotNetCoreDepInfo dep) => !((string.IsNullOrEmpty(type) || dep.Type == type) && (!serviceableAndNuGetOnly || dep.IsAvailableOnNuget)); + if (runtimeComponents.Any(c => c.Matches(reference) && !c.Equals(ThisRuntimeComponent)) && !ShouldFilter(this)) + { + return true; } + return deps.Any( + d => !ShouldFilter(d) && + (d.Matches(reference) || + ((!dontFollowProjectReferences || d.Type != "project") + && d.HasDep(reference, type, serviceableAndNuGetOnly, dontFollowProjectReferences)))); + } - return false; + public DotNetCoreDepInfo? GetDep(IAssemblyReference reference) + { + return GetAllDeps().FirstOrDefault(d => d.Matches(reference)); } public static string GetDepPath(string assemblyPath) diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs b/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs index cb0276050..76f77c4ab 100644 --- a/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs +++ b/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs @@ -494,7 +494,7 @@ public string DecompileIndividualFile(string file) var decompiler = module.CreateCSharpDecompilerWithPartials(types); var tree = decompiler.DecompileTypes(types); var stringWriter = new StringWriter(); - tree.AcceptVisitor(new GodotCSharpOutputVisitor(stringWriter, Settings.CSharpFormattingOptions)); + tree.AcceptVisitor(new GodotCSharpOutputVisitor(stringWriter, Settings, Settings.EmitILAnnotationComments, decompiler)); return stringWriter.ToString(); } @@ -679,7 +679,7 @@ private string GetPathForType(ITypeDefinition? typeDef){ syntaxTree = decompiler.DecompileTypes(types); } StringWriter stringWriter = new StringWriter(); - syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(stringWriter, Settings.CSharpFormattingOptions)); + syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(stringWriter, Settings, Settings.EmitILAnnotationComments, decompiler)); var scriptText = stringWriter.ToString(); var scriptInfo = new GodotScriptInfo( diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs b/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs index d54d9dbef..bca21b277 100644 --- a/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs +++ b/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs @@ -50,6 +50,12 @@ public class GodotMonoDecompSettings : DecompilerSettings /// public bool EnableCollectionInitializerLifting { get; set; } = true; + /// + /// Emit ILInstruction annotations as comments for statement/expression nodes. + /// Intended for debug verification of annotation propagation. + /// + public bool EmitILAnnotationComments { get; set; } = false; + private void InitializeDefaultSettings() { UseNestedDirectoriesForNamespaces = true; @@ -77,7 +83,9 @@ public GodotMonoDecompSettings(LanguageVersion languageVersion) : base(languageV settings.CreateAdditionalProjectsForProjectReferences = CreateAdditionalProjectsForProjectReferences; settings.OverrideLanguageVersion = OverrideLanguageVersion; settings.GodotVersionOverride = GodotVersionOverride; + settings.RemoveGeneratedJsonContextBody = RemoveGeneratedJsonContextBody; settings.EnableCollectionInitializerLifting = EnableCollectionInitializerLifting; + settings.EmitILAnnotationComments = EmitILAnnotationComments; return settings; } diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs b/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs index 4917c8e99..0366ef0ce 100644 --- a/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs +++ b/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs @@ -28,6 +28,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using GodotMonoDecomp.Transforms; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.OutputVisitor; @@ -306,7 +307,7 @@ public virtual CSharpDecompiler CreateDecompiler(DecompilerTypeSystem ts) decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers()); decompiler.AstTransforms.Add(new RemoveCLSCompliantAttribute()); decompiler.AstTransforms.Add(new RemoveGodotScriptPathAttribute()); - decompiler.AstTransforms.Add(new GodotMonoDecomp.RemoveEmbeddedAttributes()); + decompiler.AstTransforms.Add(new GodotMonoDecomp.Transforms.RemoveEmbeddedAttributes()); decompiler.AstTransforms.Add(new RestoreGeneratedRegexMethods()); decompiler.AstTransforms.Add(new RemoveGeneratedExceptionThrows()); if (Settings.EnableCollectionInitializerLifting) @@ -342,7 +343,7 @@ IEnumerable WriteAssemblyInfo(DecompilerTypeSystem ts, Cancella string assemblyInfo = Path.Combine(prop, "AssemblyInfo.cs"); using (var w = CreateFile(Path.Combine(TargetDirectory, assemblyInfo))) { - syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(w, Settings.CSharpFormattingOptions)); + syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(w, Settings, Settings.EmitILAnnotationComments, decompiler)); } return new[] { new ProjectItemInfo("Compile", assemblyInfo) }; } @@ -468,7 +469,7 @@ void ProcessFiles(List> files) var path = Path.Combine(TargetDirectory, file.Key); using StreamWriter w = new StreamWriter(path); - syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(w, Settings.CSharpFormattingOptions)); + syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(w, Settings, Settings.EmitILAnnotationComments, decompiler)); } catch (Exception innerException) when (!(innerException is OperationCanceledException || innerException is DecompilerException)) { diff --git a/godot-mono-decomp/GodotMonoDecomp/ProjectFileWriterGodotStyle.cs b/godot-mono-decomp/GodotMonoDecomp/ProjectFileWriterGodotStyle.cs index eae6ccf1a..bc35fd143 100644 --- a/godot-mono-decomp/GodotMonoDecomp/ProjectFileWriterGodotStyle.cs +++ b/godot-mono-decomp/GodotMonoDecomp/ProjectFileWriterGodotStyle.cs @@ -17,12 +17,14 @@ // DEALINGS IN THE SOFTWARE. using System.Reflection.PortableExecutable; +using System.Runtime.Loader; using System.Xml; using GodotMonoDecomp; using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Util; +using NuGet.Configuration; public interface IGodotProjectWithSettingsProvider : IProjectInfoProvider @@ -212,7 +214,7 @@ static void WritePackageReferences(XmlTextWriter xml, MetadataFile module, IProj // We do not want to include source generator packages in the project file because they will attempt // to generate code which we already have decompiled and create build errors. // Check if it's a possible source generator package; it will have no runtime components - if (dep.runtimeComponents == null || dep.runtimeComponents.Length == 0) + if (dep.HasNoRuntimeComponent) { // double check to see if the module has a reference to this if (module.AssemblyReferences.Any(r => r.Name == dep.Name)) @@ -615,20 +617,28 @@ static void WriteReferences(XmlTextWriter xml, MetadataFile module, IGodotProjec } } - List godotSharpRefs = new List(); + HashSet godotSharpRefs = new HashSet(); - List packageReferences = new List(); + HashSet packageReferences = new HashSet(); - List projectReferences = new List(); + HashSet projectReferences = new HashSet(); HashSet seenRefs = new HashSet(); + bool NotProjectRef(DotNetCoreDepInfo dep) => !settings.CreateAdditionalProjectsForProjectReferences || !dep.IsProject; + List RefsToWrite = module.AssemblyReferences.Where(r => !ImplicitReferences.Contains(r.Name)).Select(IAssemblyReference (r) => r).ToList(); - foreach (var reference in module.AssemblyReferences.Where(r => !ImplicitReferences.Contains(r.Name))) + var allDeps = deps?.GetAllDeps(!settings.CreateAdditionalProjectsForProjectReferences) ?? []; + var runtimepackComponents = allDeps.Where(d => d.IsRuntimePack).SelectMany(d => d.runtimeComponents).ToHashSet() ?? []; + var nonruntimepackComponents = allDeps.Where(d => !d.IsRuntimePack && NotProjectRef(d)).SelectMany(d => d.runtimeComponents).ToHashSet() ?? []; + runtimepackComponents = [.. runtimepackComponents.Where(c => !nonruntimepackComponents.Contains(c))]; + + + foreach (var reference in RefsToWrite) { - if (isNetCoreApp && + if (isNetCoreApp && (runtimepackComponents.Any(c => c.Matches(reference)) || ( project.AssemblyReferenceClassifier.IsSharedAssembly(reference, out string? runtimePack) && - targetPacks.Contains(runtimePack)) + targetPacks.Contains(runtimePack)))) { continue; } @@ -653,6 +663,11 @@ static void WriteReferences(XmlTextWriter xml, MetadataFile module, IGodotProjec continue; } + var referenceDep = deps?.GetDep(reference); + if (referenceDep?.HasNoRuntimeComponent ?? false) { + xml.WriteComment($"Reference '{reference.Name}' has no runtime components, but the assembly makes a reference to it. This may be a source generator package."); + } + WriteRef(xml, reference, true); // if the reference is GodotSharp and there is no GodotSharpEditor reference, we have to add it manually if (reference.Name.Equals("GodotSharp", StringComparison.OrdinalIgnoreCase) && !module.AssemblyReferences.Any(r => r.Name.Equals("GodotSharpEditor", StringComparison.OrdinalIgnoreCase))) @@ -693,15 +708,48 @@ static void WriteReferences(XmlTextWriter xml, MetadataFile module, IGodotProjec }, "The following references were not added to the project file because they are part of the project references above."); } + HashSet AdditionalRefs = GetAdditionalRefsToWrite(deps); + if (AdditionalRefs.Count > 0) { + writeBlockComment(xml, (newXml) => { + foreach (var reference in AdditionalRefs) + { + // realRef is true because we want to copy the file to the output directory if necessary, but not include it in the assembly references + WriteRef(newXml, reference, true); + } + }, + "The following references were found in the dependency manifest but the assembly makes no reference to them."); + } - bool IsProjectReference(AssemblyReference reference) + + bool IsProjectReference(IAssemblyReference reference) { - return settings.CreateAdditionalProjectsForProjectReferences && deps != null && deps.HasDep(reference.Name, "project", false); + return settings.CreateAdditionalProjectsForProjectReferences && deps != null && deps.HasDep(reference, "project", false); } - bool DepExistsInPackages(AssemblyReference reference) + bool DepExistsInPackages(IAssemblyReference reference) { - return settings.WriteNuGetPackageReferences && deps != null && deps.HasDep(reference.Name, "package", true); + return settings.WriteNuGetPackageReferences && deps != null && deps.HasDep(reference, "package", true); + } + + HashSet GetAdditionalRefsToWrite(DotNetCoreDepInfo? deps){ + if (deps == null){ + return []; + } + var depsToWrite = deps?.deps.Where(d => module.AssemblyReferences.Any(r => d.Matches(r))).ToHashSet() ?? []; + bool ShouldNotFilter(DotNetCoreDepInfo d){ + return !( + DepExistsInPackages(d.AssemblyRef) || + IsProjectReference(d.AssemblyRef) || + IsImplicitReference(d.Name) || + d.HasNoRuntimeComponent || + depsToWrite.Contains(d) || + depsToWrite.Any(d2 => d2.HasDep(d.AssemblyRef, null))); + } + return deps?.deps + .Where(ShouldNotFilter) + .Select(d => d.AssemblyRef as IAssemblyReference) + .Where(ar => project.AssemblyResolver.Resolve(ar) != null) + .ToHashSet() ?? []; } string GetNewRefOutputPath(string path) @@ -787,7 +835,7 @@ void CopyRef(MetadataFile asembly, string outputPath) } } - void WriteRef(XmlTextWriter newXml, AssemblyReference reference, bool realRef, string? nameOverride = null) + void WriteRef(XmlTextWriter newXml, IAssemblyReference reference, bool realRef, string? nameOverride = null) { newXml.WriteStartElement("Reference"); newXml.WriteAttributeString("Include", nameOverride ?? reference.Name); diff --git a/godot-mono-decomp/GodotMonoDecomp/FixSwitchExpressionCasts.cs b/godot-mono-decomp/GodotMonoDecomp/Transforms/FixSwitchExpressionCasts.cs similarity index 98% rename from godot-mono-decomp/GodotMonoDecomp/FixSwitchExpressionCasts.cs rename to godot-mono-decomp/GodotMonoDecomp/Transforms/FixSwitchExpressionCasts.cs index 796c55b51..03d8d5eb1 100644 --- a/godot-mono-decomp/GodotMonoDecomp/FixSwitchExpressionCasts.cs +++ b/godot-mono-decomp/GodotMonoDecomp/Transforms/FixSwitchExpressionCasts.cs @@ -3,7 +3,7 @@ using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.TypeSystem; -namespace GodotMonoDecomp; +namespace GodotMonoDecomp.Transforms; /// /// Intended to fix switch expressions that do not have enough context to determine the best type as a result of a parent member reference expression. diff --git a/godot-mono-decomp/GodotMonoDecomp/LiftCollectionInitializers.cs b/godot-mono-decomp/GodotMonoDecomp/Transforms/LiftCollectionInitializers.cs similarity index 94% rename from godot-mono-decomp/GodotMonoDecomp/LiftCollectionInitializers.cs rename to godot-mono-decomp/GodotMonoDecomp/Transforms/LiftCollectionInitializers.cs index 281f7fff2..2b4f6d08e 100644 --- a/godot-mono-decomp/GodotMonoDecomp/LiftCollectionInitializers.cs +++ b/godot-mono-decomp/GodotMonoDecomp/Transforms/LiftCollectionInitializers.cs @@ -1,7 +1,8 @@ +using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; -namespace GodotMonoDecomp; +namespace GodotMonoDecomp.Transforms; /// /// Lifts a narrow set of constructor prelude initializers back to declaration initializers. @@ -344,20 +345,52 @@ private static bool TryApplyInitializer( { return false; } - variable.Initializer = initializer.Clone(); + variable.Initializer = CopyInstructionAnnotationsFromSource(initializer.Clone(), initializer); return true; case PropertyDeclaration property: if (!property.IsAutomaticProperty) { return false; } - property.Initializer = initializer.Clone(); + property.Initializer = CopyInstructionAnnotationsFromSource(initializer.Clone(), initializer); return true; default: return false; } } + private static T CopyInstructionAnnotationsFromSource(T target, AstNode source) + where T : AstNode + { + target.CopyInstructionsFrom(source); + return target; + } + + private static T CopyInstructionAnnotationsFromSources( + T target, + IEnumerable? statementSources = null, + IEnumerable? expressionSources = null) + where T : AstNode + { + if (statementSources != null) + { + foreach (var statement in statementSources) + { + target.CopyInstructionsFrom(statement); + } + } + + if (expressionSources != null) + { + foreach (var expression in expressionSources) + { + target.CopyInstructionsFrom(expression); + } + } + + return target; + } + private static bool IsStaticMember(EntityDeclaration member) { return (member.Modifiers & Modifiers.Static) == Modifiers.Static; @@ -463,6 +496,10 @@ private static bool TryMatchConditionalTempAssignment( ifElse.Condition.Clone(), trueInitializer.Clone(), falseInitializer.Clone()); + CopyInstructionAnnotationsFromSources( + conditionalInitializer, + new List { statements[startIndex], statements[startIndex + 1], statements[startIndex + 2] }, + new[] { ifElse.Condition, trueInitializer, falseInitializer }); match = new ConditionalTempAssignmentMatch( memberName, conditionalInitializer, @@ -532,7 +569,10 @@ private static bool TryRewriteInitializerWithRecoveredLocals( continue; } - identifier.ReplaceWith(replacement.Clone()); + var replacementClone = CopyInstructionAnnotationsFromSources( + replacement.Clone(), + expressionSources: new[] { identifier, replacement }); + identifier.ReplaceWith(replacementClone); changed = true; } @@ -552,14 +592,19 @@ private static Expression SimplifyRedundantReadOnlyListCollectionWrappers(Expres while (expression is ObjectCreateExpression rootCreate && TryUnwrapReadOnlyListOfListCollection(rootCreate, out var rootReplacement)) { - expression = rootReplacement.Clone(); + expression = CopyInstructionAnnotationsFromSources( + rootReplacement.Clone(), + expressionSources: new[] { rootCreate, rootReplacement }); } foreach (var objectCreate in expression.Descendants.OfType().ToArray()) { if (TryUnwrapReadOnlyListOfListCollection(objectCreate, out var replacement)) { - objectCreate.ReplaceWith(replacement.Clone()); + var replacementClone = CopyInstructionAnnotationsFromSources( + replacement.Clone(), + expressionSources: new[] { objectCreate, replacement }); + objectCreate.ReplaceWith(replacementClone); } } @@ -832,10 +877,15 @@ private static bool TryMatchListPrelude( return false; } + var arrayInitializer = CopyInstructionAnnotationsFromSources( + new ArrayInitializerExpression(values), + matchedStatements, + values); var collectionInitializer = new ObjectCreateExpression(listDecl.Type.Clone()) { - Initializer = new ArrayInitializerExpression(values) + Initializer = arrayInitializer }; + CopyInstructionAnnotationsFromSources(collectionInitializer, matchedStatements, values); match = new ListPreludeMatch( listVarName, targetMemberName, @@ -913,7 +963,7 @@ private static bool TryMatchListAddRangeSpreadPrelude( return false; } - if (!TryBuildCollectionFromSpreadSegments(listDecl.Type, segments, out var collectionInitializer)) + if (!TryBuildCollectionFromSpreadSegments(listDecl.Type, segments, matchedStatements, out var collectionInitializer)) { return false; } @@ -995,7 +1045,7 @@ private static bool TryMatchHashSetForeachSpreadPrelude( return false; } - if (!TryBuildCollectionFromSpreadSegments(setDecl.Type, segments, out var collectionInitializer)) + if (!TryBuildCollectionFromSpreadSegments(setDecl.Type, segments, matchedStatements, out var collectionInitializer)) { return false; } @@ -1071,7 +1121,7 @@ private static bool TryMatchListSpreadBuilderWrapperAssignment( return false; } - if (!TryBuildCollectionFromSpreadSegments(listDecl.Type, segments, out var listBuilderInitializer)) + if (!TryBuildCollectionFromSpreadSegments(listDecl.Type, segments, matchedStatements, out var listBuilderInitializer)) { return false; } @@ -1163,6 +1213,7 @@ private static bool TryMatchHashSetSpreadForeach( private static bool TryBuildCollectionFromSpreadSegments( AstType collectionType, IReadOnlyList segments, + IReadOnlyList matchedStatements, out Expression initializer) { initializer = Expression.Null; @@ -1184,7 +1235,9 @@ private static bool TryBuildCollectionFromSpreadSegments( var collectionExpression = new ArrayInitializerExpression(elements); collectionExpression.AddAnnotation(CollectionExpressionArrayAnnotation.Instance); + CopyInstructionAnnotationsFromSources(collectionExpression, matchedStatements, segments.Select(segment => segment.Expression)); initializer = new ObjectCreateExpression(collectionType.Clone(), collectionExpression); + CopyInstructionAnnotationsFromSources(initializer, matchedStatements, segments.Select(segment => segment.Expression)); return true; } @@ -1434,10 +1487,15 @@ private static bool TryMatchObjectMemberListPrelude( if (TryMatchTerminalObjectMemberAssignment(statement, listVarName, objectVariableName, out memberName)) { matchedStatements.Add(statement); + var arrayInitializer = CopyInstructionAnnotationsFromSources( + new ArrayInitializerExpression(values), + matchedStatements, + values); initializerValue = new ObjectCreateExpression(listDecl.Type.Clone()) { - Initializer = new ArrayInitializerExpression(values) + Initializer = arrayInitializer }; + CopyInstructionAnnotationsFromSources(initializerValue, matchedStatements, values); nextIndex = i + 1; return true; } @@ -1549,7 +1607,9 @@ private static bool TryMatchRefSlotAssignment(Statement statement, string slotVa private static void AddObjectMemberInitializer(ObjectCreateExpression objectValue, string memberName, Expression memberValue) { objectValue.Initializer ??= new ArrayInitializerExpression(); - objectValue.Initializer.Elements.Add(new NamedExpression(memberName, memberValue.Clone())); + var namedInitializer = new NamedExpression(memberName, memberValue.Clone()); + CopyInstructionAnnotationsFromSource(namedInitializer, memberValue); + objectValue.Initializer.Elements.Add(namedInitializer); } private static bool IsAllowedPreludeNoise(Statement statement) @@ -1808,14 +1868,18 @@ private static bool TryMatchCtorArgumentListPrelude( return false; } - initializer = new ObjectCreateExpression(listDecl.Type.Clone()) - { - Initializer = new ArrayInitializerExpression(values) - }; for (int i = startIndex; i < boundaryIndex; i++) { matchedStatements.Add(statements[i]); } + initializer = new ObjectCreateExpression(listDecl.Type.Clone()) + { + Initializer = CopyInstructionAnnotationsFromSources( + new ArrayInitializerExpression(values), + matchedStatements, + values) + }; + CopyInstructionAnnotationsFromSources(initializer, matchedStatements, values); return true; } diff --git a/godot-mono-decomp/GodotMonoDecomp/RemoveBogusBaseConstructorCalls.cs b/godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveBogusBaseConstructorCalls.cs similarity index 97% rename from godot-mono-decomp/GodotMonoDecomp/RemoveBogusBaseConstructorCalls.cs rename to godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveBogusBaseConstructorCalls.cs index c516cae36..ff49efc49 100644 --- a/godot-mono-decomp/GodotMonoDecomp/RemoveBogusBaseConstructorCalls.cs +++ b/godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveBogusBaseConstructorCalls.cs @@ -1,7 +1,7 @@ using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; -namespace GodotMonoDecomp; +namespace GodotMonoDecomp.Transforms; /// /// Removes erroneous base._002Ector() calls that sometimes appear at the end of diff --git a/godot-mono-decomp/GodotMonoDecomp/RemoveEmbeddedAttributes.cs b/godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveEmbeddedAttributes.cs similarity index 94% rename from godot-mono-decomp/GodotMonoDecomp/RemoveEmbeddedAttributes.cs rename to godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveEmbeddedAttributes.cs index db257837e..c269ea932 100644 --- a/godot-mono-decomp/GodotMonoDecomp/RemoveEmbeddedAttributes.cs +++ b/godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveEmbeddedAttributes.cs @@ -1,10 +1,8 @@ -using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.Semantics; -using ICSharpCode.Decompiler.TypeSystem; -namespace GodotMonoDecomp; +namespace GodotMonoDecomp.Transforms; /// diff --git a/godot-mono-decomp/GodotMonoDecomp/RemoveGeneratedExceptionThrows.cs b/godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveGeneratedExceptionThrows.cs similarity index 96% rename from godot-mono-decomp/GodotMonoDecomp/RemoveGeneratedExceptionThrows.cs rename to godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveGeneratedExceptionThrows.cs index cf83b2217..5bce918b6 100644 --- a/godot-mono-decomp/GodotMonoDecomp/RemoveGeneratedExceptionThrows.cs +++ b/godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveGeneratedExceptionThrows.cs @@ -16,12 +16,10 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; -using ICSharpCode.Decompiler.TypeSystem; -namespace GodotMonoDecomp; +namespace GodotMonoDecomp.Transforms; /// /// A hack to remove compiler-generated exception throws caused by switch expressions being compiled as imperative code. diff --git a/godot-mono-decomp/GodotMonoDecomp/RemoveJsonSourceGenerationClassBody.cs b/godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveJsonSourceGenerationClassBody.cs similarity index 98% rename from godot-mono-decomp/GodotMonoDecomp/RemoveJsonSourceGenerationClassBody.cs rename to godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveJsonSourceGenerationClassBody.cs index 0cba4cf89..9ee21cd77 100644 --- a/godot-mono-decomp/GodotMonoDecomp/RemoveJsonSourceGenerationClassBody.cs +++ b/godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveJsonSourceGenerationClassBody.cs @@ -1,10 +1,9 @@ -using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.Semantics; using ICSharpCode.Decompiler.TypeSystem; -namespace GodotMonoDecomp; +namespace GodotMonoDecomp.Transforms; /// /// Restores source-level shape for System.Text.Json source-generation context classes. diff --git a/godot-mono-decomp/GodotMonoDecomp/RemoveMathF3x.cs b/godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveMathF3x.cs similarity index 74% rename from godot-mono-decomp/GodotMonoDecomp/RemoveMathF3x.cs rename to godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveMathF3x.cs index 0df97f996..cc6682be3 100644 --- a/godot-mono-decomp/GodotMonoDecomp/RemoveMathF3x.cs +++ b/godot-mono-decomp/GodotMonoDecomp/Transforms/RemoveMathF3x.cs @@ -19,9 +19,8 @@ using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; -using ICSharpCode.Decompiler.TypeSystem; -namespace GodotMonoDecomp; +namespace GodotMonoDecomp.Transforms; /// ///```c# @@ -34,17 +33,24 @@ public void Run(AstNode rootNode, TransformContext context) { rootNode.AcceptVisitor(this); } - public override void VisitMemberReferenceExpression(MemberReferenceExpression methodInvocation) + + public static void ReplaceAndCopyAnnotations(Expression expression, Expression newExpression) + { + newExpression.CopyAnnotationsFrom(expression); + expression.ReplaceWith(newExpression); + } + public override void VisitMemberReferenceExpression(MemberReferenceExpression mre) { - string name = methodInvocation.ToString(); - bool isPi = name.EndsWith("MathF.PI"); - if (isPi || name.EndsWith("MathF.E")) + if (mre.Target.ToString() == "MathF" && (mre.MemberName == "PI" || mre.MemberName == "E")) { - string newMember = isPi ? "Pi" : "E"; - MemberReferenceExpression newExpression = new MemberReferenceExpression(new IdentifierExpression("Mathf"), newMember); - methodInvocation.ReplaceWith(newExpression); + var mathf = new IdentifierExpression("Mathf"); + ReplaceAndCopyAnnotations(mre.Target, mathf); + if (mre.MemberName == "PI") + { + mre.MemberName = "Pi"; + } return; } - base.VisitMemberReferenceExpression(methodInvocation); + base.VisitMemberReferenceExpression(mre); } } diff --git a/godot-mono-decomp/GodotMonoDecomp/RestoreGeneratedRegexMethods.cs b/godot-mono-decomp/GodotMonoDecomp/Transforms/RestoreGeneratedRegexMethods.cs similarity index 94% rename from godot-mono-decomp/GodotMonoDecomp/RestoreGeneratedRegexMethods.cs rename to godot-mono-decomp/GodotMonoDecomp/Transforms/RestoreGeneratedRegexMethods.cs index fe411abc1..d969ea238 100644 --- a/godot-mono-decomp/GodotMonoDecomp/RestoreGeneratedRegexMethods.cs +++ b/godot-mono-decomp/GodotMonoDecomp/Transforms/RestoreGeneratedRegexMethods.cs @@ -1,10 +1,7 @@ -using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; -using ICSharpCode.Decompiler.Semantics; -using ICSharpCode.Decompiler.TypeSystem; -namespace GodotMonoDecomp; +namespace GodotMonoDecomp.Transforms; /// /// Restores source-level shape for methods generated by the Regex source generator. diff --git a/godot-mono-decomp/GodotMonoDecompCLI/Program.cs b/godot-mono-decomp/GodotMonoDecompCLI/Program.cs index a9e177156..f02c16d2c 100644 --- a/godot-mono-decomp/GodotMonoDecompCLI/Program.cs +++ b/godot-mono-decomp/GodotMonoDecompCLI/Program.cs @@ -44,6 +44,7 @@ int Main(string[] args) settings.VerifyNuGetPackageIsFromNugetOrg = result.Value.VerifyNuGetPackages; settings.GodotVersionOverride = result.Value.GodotVersion == null ? null : GodotStuff.ParseGodotVersionFromString(result.Value.GodotVersion); settings.EnableCollectionInitializerLifting = !result.Value.DisableCollectionInitializerLifting || result.Value.EnableCollectionInitializerLifting; + settings.EmitILAnnotationComments = result.Value.EmitILAnnotationComments; // get the current time var startTime = DateTime.Now; // call the DecompileProject function @@ -113,6 +114,9 @@ public class Options [Option("disable-collection-initializer-lifting", Required = false, HelpText = "Disable LiftCollectionInitializers and run RemoveBogusBaseConstructorCalls instead.")] public bool DisableCollectionInitializerLifting { get; set; } + [Option("emit-il-annotation-comments", Required = false, HelpText = "Emit ILInstruction annotations as C# comments for statement/expression nodes.")] + public bool EmitILAnnotationComments { get; set; } + [Option("write-script-info", Required = false, HelpText = "Write script info to a JSON file in the output directory.")] public bool WriteScriptInfo { get; set; } // dump strings option diff --git a/godot-mono-decomp/GodotMonoDecompNativeAOT/Lib.cs b/godot-mono-decomp/GodotMonoDecompNativeAOT/Lib.cs index 39ed410d2..6aa980132 100644 --- a/godot-mono-decomp/GodotMonoDecompNativeAOT/Lib.cs +++ b/godot-mono-decomp/GodotMonoDecompNativeAOT/Lib.cs @@ -58,6 +58,9 @@ public static IntPtr AOTCreateGodotModuleDecompiler( bool verifyNuGetPackageIsFromNugetOrg, bool copyOutOfTreeReferences, bool createAdditionalProjectsForProjectReferences, + bool removeGeneratedJsonContextBody, + bool enableCollectionInitializerLifting, + bool emitILAnnotationComments, int OverrideLanguageVersion ) { @@ -71,6 +74,9 @@ int OverrideLanguageVersion VerifyNuGetPackageIsFromNugetOrg = verifyNuGetPackageIsFromNugetOrg, CopyOutOfTreeReferences = copyOutOfTreeReferences, CreateAdditionalProjectsForProjectReferences = createAdditionalProjectsForProjectReferences, + RemoveGeneratedJsonContextBody = removeGeneratedJsonContextBody, + EnableCollectionInitializerLifting = enableCollectionInitializerLifting, + EmitILAnnotationComments = emitILAnnotationComments, OverrideLanguageVersion = OverrideLanguageVersion == 0 ? null : (LanguageVersion)OverrideLanguageVersion, GodotVersionOverride = godotVersionOverrideStr == null ? null : GodotStuff.ParseGodotVersionFromString(godotVersionOverrideStr) }; @@ -109,6 +115,8 @@ struct AOTGodotModuleDecompilerProgress : IProgress { private delegate int ProgressFunction(IntPtr userData, int current, int total, IntPtr status); + private event Action? OnProgress = null; + private readonly ProgressFunction? progressFunction; private readonly IntPtr userData; diff --git a/godot-mono-decomp/GodotMonoDecompNativeAOT/include/godot_mono_decomp.h b/godot-mono-decomp/GodotMonoDecompNativeAOT/include/godot_mono_decomp.h index 5f21f06f5..1407d5914 100644 --- a/godot-mono-decomp/GodotMonoDecompNativeAOT/include/godot_mono_decomp.h +++ b/godot-mono-decomp/GodotMonoDecompNativeAOT/include/godot_mono_decomp.h @@ -48,6 +48,9 @@ void* GodotMonoDecomp_CreateGodotModuleDecompiler( bool verifyNuGetPackageIsFromNugetOrg, bool copyOutOfTreeReferences, bool createAdditionalProjectsForProjectReferences, + bool removeGeneratedJsonContextBody, + bool enableCollectionInitializerLifting, + bool emitILAnnotationComments, LanguageVersion OverrideLanguageVersion ); diff --git a/godot-mono-decomp/collection_examples/original/TestStatementAnnotationLiftCoverage.cs b/godot-mono-decomp/collection_examples/original/TestStatementAnnotationLiftCoverage.cs new file mode 100644 index 000000000..0f4ef0aae --- /dev/null +++ b/godot-mono-decomp/collection_examples/original/TestStatementAnnotationLiftCoverage.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace ConsoleApp2; + +public static class TestStatementAnnotationLiftCoverage +{ + public class Nested + { + public List Numbers { get; set; } = []; + + public string Name { get; set; } = string.Empty; + } + + public class CoverageClass + { + public List Values { get; set; } = [1, 2, 3]; + + public Nested nested = new Nested + { + Numbers = [4, 5, 6], + Name = "nested" + }; + + public readonly List CtorArgs; + + public CoverageClass() + : this([7, 8, 9]) + { + } + + public CoverageClass(List ctorArgs) + { + CtorArgs = ctorArgs; + } + } +} diff --git a/godot-mono-decomp/collection_examples/validation/CollectionExamplesValidation.csproj b/godot-mono-decomp/collection_examples/validation/CollectionExamplesValidation.csproj index 3ec47c364..10b1b202c 100644 --- a/godot-mono-decomp/collection_examples/validation/CollectionExamplesValidation.csproj +++ b/godot-mono-decomp/collection_examples/validation/CollectionExamplesValidation.csproj @@ -14,5 +14,6 @@ + diff --git a/godot-mono-decomp/collection_examples/validation/validate_collection_lifting.sh b/godot-mono-decomp/collection_examples/validation/validate_collection_lifting.sh index 0e213634d..4f915a11e 100755 --- a/godot-mono-decomp/collection_examples/validation/validate_collection_lifting.sh +++ b/godot-mono-decomp/collection_examples/validation/validate_collection_lifting.sh @@ -59,6 +59,7 @@ if $UPDATE_FIXTURES; then "$OUTPUT_DIR/TestFuncInitializer.cs" \ "$OUTPUT_DIR/TestCtorBoundaryCoverage.cs" \ "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" \ + "$OUTPUT_DIR/TestStatementAnnotationLiftCoverage.cs" \ "$OUTPUT_DIR/TestNestedCollectionExpressionInitializers.cs" \ "$OUTPUT_DIR/CollectionExamplesValidation.Decompiled.csproj" \ "$OUTPUT_DIR/CollectionExamplesValidation.Decompiled.sln" @@ -77,6 +78,7 @@ test -f "$OUTPUT_DIR/TestFuncInitializer.cs" test -f "$OUTPUT_DIR/TestNestedCollectionExpressionInitializers.cs" test -f "$OUTPUT_DIR/TestCtorBoundaryCoverage.cs" test -f "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" +test -f "$OUTPUT_DIR/TestStatementAnnotationLiftCoverage.cs" test -f "$OUTPUT_DIR/CollectionExamplesValidation.Decompiled.csproj" echo "[5/7] Asserting expected lifted and preserved markers" @@ -97,6 +99,8 @@ grep -q "public static readonly List staticStringListField = new List strings = new List" "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" grep -q "public List ListProp1 { get; set; } = new List" "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" grep -q "private Func filter = (string s) => s.Length > 3;" "$OUTPUT_DIR/TestFuncInitializer.cs" +grep -q "= new List { 1, 2, 3 };" "$OUTPUT_DIR/TestStatementAnnotationLiftCoverage.cs" +grep -q ": this(new List { 7, 8, 9 })" "$OUTPUT_DIR/TestStatementAnnotationLiftCoverage.cs" grep -Fq "public static readonly HashSet strings = new HashSet([..stringListConst1" "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" grep -Fq "public static readonly List strings = new List([..stringListConst1" "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" grep -Fq "public static readonly IReadOnlySet strings = new HashSet([..stringListConst1" "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" @@ -130,7 +134,8 @@ if grep -q "_002Ector(" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" \ || grep -q "_002Ector(" "$OUTPUT_DIR/TestFuncInitializer.cs" \ || grep -q "_002Ector(" "$OUTPUT_DIR/TestNestedCollectionExpressionInitializers.cs" \ || grep -q "_002Ector(" "$OUTPUT_DIR/TestCtorBoundaryCoverage.cs" \ - || grep -q "_002Ector(" "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs"; then + || grep -q "_002Ector(" "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" \ + || grep -q "_002Ector(" "$OUTPUT_DIR/TestStatementAnnotationLiftCoverage.cs"; then echo "Found unexpected _002Ector(...) artifact in decompiled output." exit 1 fi diff --git a/utility/gdre_config.cpp b/utility/gdre_config.cpp index 616b919df..827c25d11 100644 --- a/utility/gdre_config.cpp +++ b/utility/gdre_config.cpp @@ -291,6 +291,21 @@ Vector> GDREConfig::_init_default_settings() { "Create additional projects for project references", "If a project reference is detected, create an additional project and add it to the solution.", true)), + memnew(GDREConfigSetting( + "CSharp/remove_generated_json_context_body", + "Remove generated Json context body", + "Remove the body of generated JsonSourceGeneration context classes.", + false)), + memnew(GDREConfigSetting( + "CSharp/enable_collection_initializer_lifting", + "Enable collection initializer lifting", + "Enable the LiftCollectionInitializers transform.\nIf disabled, the legacy RemoveBogusBaseConstructorCalls transform is used.", + true)), + memnew(GDREConfigSetting( + "CSharp/emit_il_annotation_comments", + "Emit IL annotation comments", + "Emit ILInstruction annotations as comments for statement/expression nodes.\nIntended for debug verification of annotation propagation.", + false)), memnew(GDREConfigSetting_CSharpForceLanguageVersion()), memnew(GDREConfigSetting( "CSharp/compile_after_decompile", diff --git a/utility/godot_mono_decomp_wrapper.cpp b/utility/godot_mono_decomp_wrapper.cpp index e73e097ef..03726098c 100644 --- a/utility/godot_mono_decomp_wrapper.cpp +++ b/utility/godot_mono_decomp_wrapper.cpp @@ -49,6 +49,9 @@ Error GodotMonoDecompWrapper::_load(const String &p_assembly_path, const Vector< p_settings.VerifyNuGetPackageIsFromNugetOrg, p_settings.CopyOutOfTreeReferences, p_settings.CreateAdditionalProjectsForProjectReferences, + p_settings.RemoveGeneratedJsonContextBody, + p_settings.EnableCollectionInitializerLifting, + p_settings.EmitILAnnotationComments, (LanguageVersion)p_settings.OverrideLanguageVersion); delete[] originalProjectFiles_c_array; if (new_decompiler_handle == nullptr) { @@ -304,6 +307,9 @@ GodotMonoDecompWrapper::GodotMonoDecompSettings GodotMonoDecompWrapper::GodotMon settings.VerifyNuGetPackageIsFromNugetOrg = GDREConfig::get_singleton()->get_setting("CSharp/verify_nuget_package_is_from_nuget_org", false); settings.CopyOutOfTreeReferences = GDREConfig::get_singleton()->get_setting("CSharp/copy_out_of_tree_references", true); settings.CreateAdditionalProjectsForProjectReferences = GDREConfig::get_singleton()->get_setting("CSharp/create_additional_projects_for_project_references", true); + settings.RemoveGeneratedJsonContextBody = GDREConfig::get_singleton()->get_setting("CSharp/remove_generated_json_context_body", false); + settings.EnableCollectionInitializerLifting = GDREConfig::get_singleton()->get_setting("CSharp/enable_collection_initializer_lifting", true); + settings.EmitILAnnotationComments = GDREConfig::get_singleton()->get_setting("CSharp/emit_il_annotation_comments", false); settings.OverrideLanguageVersion = GDREConfig::get_singleton()->get_setting("CSharp/force_language_version", 0); return settings; } @@ -313,6 +319,9 @@ bool GodotMonoDecompWrapper::GodotMonoDecompSettings::operator==(const GodotMono VerifyNuGetPackageIsFromNugetOrg == p_other.VerifyNuGetPackageIsFromNugetOrg && CopyOutOfTreeReferences == p_other.CopyOutOfTreeReferences && CreateAdditionalProjectsForProjectReferences == p_other.CreateAdditionalProjectsForProjectReferences && + RemoveGeneratedJsonContextBody == p_other.RemoveGeneratedJsonContextBody && + EnableCollectionInitializerLifting == p_other.EnableCollectionInitializerLifting && + EmitILAnnotationComments == p_other.EmitILAnnotationComments && OverrideLanguageVersion == p_other.OverrideLanguageVersion && GodotVersionOverride == p_other.GodotVersionOverride; } diff --git a/utility/godot_mono_decomp_wrapper.h b/utility/godot_mono_decomp_wrapper.h index c9e237b95..6293d7ba1 100644 --- a/utility/godot_mono_decomp_wrapper.h +++ b/utility/godot_mono_decomp_wrapper.h @@ -29,6 +29,9 @@ class GodotMonoDecompWrapper : public RefCounted { bool VerifyNuGetPackageIsFromNugetOrg = false; bool CopyOutOfTreeReferences = true; bool CreateAdditionalProjectsForProjectReferences = true; + bool RemoveGeneratedJsonContextBody = false; + bool EnableCollectionInitializerLifting = true; + bool EmitILAnnotationComments = false; int OverrideLanguageVersion = 0; String GodotVersionOverride; static GodotMonoDecompSettings get_default_settings();