/// <summary> /// Reads a rename map from the specified name into the specified instance of options. /// The stream is not closed by this method. /// </summary> public static void ReadRenameMap(Stream fileStream, bool isGzip, UnhollowerOptions options) { if (isGzip) { using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress, true); ReadRenameMap(gzipStream, false, options); return; } using var reader = new StreamReader(fileStream, Encoding.UTF8, false, 65536, true); while (!reader.EndOfStream) { var line = reader.ReadLine(); if (string.IsNullOrEmpty(line) || line.StartsWith("#")) { continue; } var split = line.Split(';'); if (split.Length < 2) { continue; } options.RenameMap[split[0]] = split[1]; } }
public static void AnalyzeDeobfuscationParams(UnhollowerOptions options) { RewriteGlobalContext rewriteContext; IIl2CppMetadataAccess inputAssemblies; using (new TimingCookie("Reading assemblies")) inputAssemblies = new CecilMetadataAccess(options.Source); using (new TimingCookie("Creating assembly contexts")) rewriteContext = new RewriteGlobalContext(options, inputAssemblies, NullMetadataAccess.Instance, NullMetadataAccess.Instance); for (var chars = 1; chars <= 3; chars++) { for (var uniq = 3; uniq <= 15; uniq++) { options.TypeDeobfuscationCharsPerUniquifier = chars; options.TypeDeobfuscationMaxUniquifiers = uniq; rewriteContext.RenamedTypes.Clear(); rewriteContext.RenameGroups.Clear(); Pass05CreateRenameGroups.DoPass(rewriteContext); var uniqueTypes = rewriteContext.RenameGroups.Values.Count(it => it.Count == 1); var nonUniqueTypes = rewriteContext.RenameGroups.Values.Count(it => it.Count > 1); Console.WriteLine($"Chars=\t{chars}\tMaxU=\t{uniq}\tUniq=\t{uniqueTypes}\tNonUniq=\t{nonUniqueTypes}"); } } }
public static void AnalyzeDeobfuscationParams(UnhollowerOptions options) { RewriteGlobalContext rewriteContext; using (new TimingCookie("Reading assemblies")) rewriteContext = new RewriteGlobalContext(options, Directory.EnumerateFiles(options.SourceDir, "*.dll")); for (var chars = 1; chars <= 3; chars++) { for (var uniq = 3; uniq <= 15; uniq++) { options.TypeDeobfuscationCharsPerUniquifier = chars; options.TypeDeobfuscationMaxUniquifiers = uniq; rewriteContext.RenamedTypes.Clear(); rewriteContext.RenameGroups.Clear(); Pass05CreateRenameGroups.DoPass(rewriteContext); var uniqueTypes = rewriteContext.RenameGroups.Values.Count(it => it.Count == 1); var nonUniqueTypes = rewriteContext.RenameGroups.Values.Count(it => it.Count > 1); Console.WriteLine($"Chars=\t{chars}\tMaxU=\t{uniq}\tUniq=\t{uniqueTypes}\tNonUniq=\t{nonUniqueTypes}"); } } }
public static void Main(string[] args) { LogSupport.InstallConsoleHandlers(); var options = new UnhollowerOptions(); var analyze = false; var generateMap = false; foreach (var s in args) { if (s == ParamAnalyze) { analyze = true; } else if (s == ParamGenerateDeobMap) { generateMap = true; } else if (s == ParamHelp || s == ParamHelpShort || s == ParamHelpShortSlash) { PrintUsage(); return; } else if (s == ParamVerbose) { LogSupport.TraceHandler += Console.WriteLine; options.Verbose = true; } else if (s == ParamNoXrefCache) { options.NoXrefCache = true; } else if (s == ParamNoCopyUnhollowerLibs) { options.NoCopyUnhollowerLibs = true; } else if (s.StartsWith(ParamInputDir)) { options.SourceDir = s.Substring(ParamInputDir.Length); } else if (s.StartsWith(ParamOutputDir)) { options.OutputDir = s.Substring(ParamOutputDir.Length); } else if (s.StartsWith(ParamMscorlibPath)) { options.MscorlibPath = s.Substring(ParamMscorlibPath.Length); } else if (s.StartsWith(ParamUnityDir)) { options.UnityBaseLibsDir = s.Substring(ParamUnityDir.Length); } else if (s.StartsWith(ParamGameAssemblyPath)) { options.GameAssemblyPath = s.Substring(ParamGameAssemblyPath.Length); } else if (s.StartsWith(ParamUniqChars)) { options.TypeDeobfuscationCharsPerUniquifier = Int32.Parse(s.Substring(ParamUniqChars.Length)); } else if (s.StartsWith(ParamUniqMax)) { options.TypeDeobfuscationMaxUniquifiers = Int32.Parse(s.Substring(ParamUniqMax.Length)); } else if (s.StartsWith(ParamBlacklistAssembly)) { options.AdditionalAssembliesBlacklist.Add(s.Substring(ParamBlacklistAssembly.Length)); } else if (s.StartsWith(ParamObfRegex)) { options.ObfuscatedNamesRegex = new Regex(s.Substring(ParamObfRegex.Length), RegexOptions.Compiled); } else if (s.StartsWith(ParamRenameMap)) { ReadRenameMap(s.Substring(ParamRenameMap.Length), options); } else if (s.StartsWith(ParamGenerateDeobMapAssembly)) { options.DeobfuscationGenerationAssemblies.Add(s.Substring(ParamGenerateDeobMapAssembly.Length)); } else if (s.StartsWith(ParamGenerateDeobMapNew)) { options.DeobfuscationNewAssembliesPath = s.Substring(ParamGenerateDeobMapNew.Length); } else { LogSupport.Error($"Unrecognized option {s}; use -h for help"); return; } } if (analyze && generateMap) { LogSupport.Error($"Can't use {ParamAnalyze} and {ParamGenerateDeobMap} at the same time"); return; } if (analyze) { AnalyzeDeobfuscationParams(options); } else if (generateMap) { DeobfuscationMapGenerator.GenerateDeobfuscationMap(options); } else { Main(options); } }
/// <summary> /// Reads a rename map from the specified name into the specified instance of options /// </summary> public static void ReadRenameMap(string fileName, UnhollowerOptions options) { using var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read); ReadRenameMap(fileStream, fileName.EndsWith(".gz"), options); }
public static void Main(UnhollowerOptions options) { if (string.IsNullOrEmpty(options.SourceDir)) { Console.WriteLine("No input dir specified; use -h for help"); return; } if (string.IsNullOrEmpty(options.OutputDir)) { Console.WriteLine("No target dir specified; use -h for help"); return; } if (string.IsNullOrEmpty(options.MscorlibPath)) { Console.WriteLine("No mscorlib specified; use -h for help"); return; } if (!Directory.Exists(options.OutputDir)) { Directory.CreateDirectory(options.OutputDir); } RewriteGlobalContext rewriteContext; using (new TimingCookie("Reading assemblies")) rewriteContext = new RewriteGlobalContext(options, Directory.EnumerateFiles(options.SourceDir, "*.dll")); using (new TimingCookie("Computing renames")) Pass05CreateRenameGroups.DoPass(rewriteContext); using (new TimingCookie("Creating typedefs")) Pass10CreateTypedefs.DoPass(rewriteContext); using (new TimingCookie("Computing struct blittability")) Pass11ComputeTypeSpecifics.DoPass(rewriteContext); using (new TimingCookie("Filling typedefs")) Pass12FillTypedefs.DoPass(rewriteContext); using (new TimingCookie("Filling generic constraints")) Pass13FillGenericConstraints.DoPass(rewriteContext); using (new TimingCookie("Creating members")) Pass15GenerateMemberContexts.DoPass(rewriteContext); using (new TimingCookie("Scanning method cross-references")) Pass16ScanMethodRefs.DoPass(rewriteContext, options); using (new TimingCookie("Finalizing method declarations")) Pass18FinalizeMethodContexts.DoPass(rewriteContext); LogSupport.Info($"{Pass18FinalizeMethodContexts.TotalPotentiallyDeadMethods} total potentially dead methods"); using (new TimingCookie("Filling method parameters")) Pass19CopyMethodParameters.DoPass(rewriteContext); using (new TimingCookie("Creating static constructors")) Pass20GenerateStaticConstructors.DoPass(rewriteContext); using (new TimingCookie("Creating value type fields")) Pass21GenerateValueTypeFields.DoPass(rewriteContext); using (new TimingCookie("Creating enums")) Pass22GenerateEnums.DoPass(rewriteContext); using (new TimingCookie("Creating IntPtr constructors")) Pass23GeneratePointerConstructors.DoPass(rewriteContext); using (new TimingCookie("Creating type getters")) Pass24GenerateTypeStaticGetters.DoPass(rewriteContext); using (new TimingCookie("Creating non-blittable struct constructors")) Pass25GenerateNonBlittableValueTypeDefaultCtors.DoPass(rewriteContext); using (new TimingCookie("Creating generic method static constructors")) Pass30GenerateGenericMethodStoreConstructors.DoPass(rewriteContext); using (new TimingCookie("Creating field accessors")) Pass40GenerateFieldAccessors.DoPass(rewriteContext); using (new TimingCookie("Filling methods")) Pass50GenerateMethods.DoPass(rewriteContext); using (new TimingCookie("Generating implicit conversions")) Pass60AddImplicitConversions.DoPass(rewriteContext); using (new TimingCookie("Creating properties")) Pass70GenerateProperties.DoPass(rewriteContext); if (options.UnityBaseLibsDir != null) { using (new TimingCookie("Unstripping types")) Pass79UnstripTypes.DoPass(rewriteContext); using (new TimingCookie("Unstripping fields")) Pass80UnstripFields.DoPass(rewriteContext); using (new TimingCookie("Unstripping methods")) Pass80UnstripMethods.DoPass(rewriteContext); using (new TimingCookie("Unstripping method bodies")) Pass81FillUnstrippedMethodBodies.DoPass(rewriteContext); } else { LogSupport.Warning("Not performing unstripping as unity libs are not specified"); } using (new TimingCookie("Generating forwarded types")) Pass89GenerateForwarders.DoPass(rewriteContext); using (new TimingCookie("Writing xref cache")) Pass89GenerateMethodXrefCache.DoPass(rewriteContext, options); using (new TimingCookie("Writing assemblies")) Pass90WriteToDisk.DoPass(rewriteContext, options); using (new TimingCookie("Writing method pointer map")) Pass91GenerateMethodPointerMap.DoPass(rewriteContext, options); if (!options.NoCopyUnhollowerLibs) { File.Copy(typeof(IL2CPP).Assembly.Location, Path.Combine(options.OutputDir, typeof(IL2CPP).Assembly.GetName().Name + ".dll"), true); File.Copy(typeof(RuntimeLibMarker).Assembly.Location, Path.Combine(options.OutputDir, typeof(RuntimeLibMarker).Assembly.GetName().Name + ".dll"), true); File.Copy(typeof(Decoder).Assembly.Location, Path.Combine(options.OutputDir, typeof(Decoder).Assembly.GetName().Name + ".dll"), true); } LogSupport.Info("Done!"); rewriteContext.Dispose(); }
public static void Main(string[] args) { LogSupport.InstallConsoleHandlers(); var options = new UnhollowerOptions(); var analyze = false; foreach (var s in args) { if (s == ParamAnalyze) { analyze = true; } else if (s == ParamHelp || s == ParamHelpShort || s == ParamHelpShortSlash) { PrintUsage(); return; } else if (s == ParamVerbose) { LogSupport.TraceHandler += Console.WriteLine; options.Verbose = true; } else if (s.StartsWith(ParamInputDir)) { options.SourceDir = s.Substring(ParamInputDir.Length); } else if (s.StartsWith(ParamOutputDir)) { options.OutputDir = s.Substring(ParamOutputDir.Length); } else if (s.StartsWith(ParamMscorlibPath)) { options.MscorlibPath = s.Substring(ParamMscorlibPath.Length); } else if (s.StartsWith(ParamUnityDir)) { options.UnityBaseLibsDir = s.Substring(ParamUnityDir.Length); } else if (s.StartsWith(ParamGameAssemblyPath)) { options.GameAssemblyPath = s.Substring(ParamGameAssemblyPath.Length); } else if (s.StartsWith(ParamUniqChars)) { options.TypeDeobfuscationCharsPerUniquifier = Int32.Parse(s.Substring(ParamUniqChars.Length)); } else if (s.StartsWith(ParamUniqMax)) { options.TypeDeobfuscationMaxUniquifiers = Int32.Parse(s.Substring(ParamUniqMax.Length)); } else if (s.StartsWith(ParamBlacklistAssembly)) { options.AdditionalAssembliesBlacklist.Add(s.Substring(ParamBlacklistAssembly.Length)); } else { Console.WriteLine($"Unrecognized option {s}; use -h for help"); return; } } if (analyze) { AnalyzeDeobfuscationParams(options); } else { Main(options); } }
private static int MethodSignatureMatchWeight(MethodDefinition a, MethodDefinition b, UnhollowerOptions options) { if (a.Parameters.Count != b.Parameters.Count || a.IsStatic != b.IsStatic || (a.Attributes & MethodAttributes.MemberAccessMask) != (b.Attributes & MethodAttributes.MemberAccessMask)) { return(-1); } var runningSum = TypeMatchWeight(a.ReturnType, b.ReturnType, options); if (runningSum == -1) { return(-1); } void Accumulate(int i) { if (i < 0 || runningSum < 0) { runningSum = -1; } else { runningSum += i; } } for (var i = 0; i < a.Parameters.Count; i++) { Accumulate(TypeMatchWeight(a.Parameters[i].ParameterType, b.Parameters[i].ParameterType, options)); } return(runningSum * (a.Parameters.Count + 1)); }
private static int TypeMatchWeight(TypeReference a, TypeReference b, UnhollowerOptions options) { if (a.GetType() != b.GetType()) { return(-1); } var runningSum = 0; void Accumulate(int i) { if (i < 0 || runningSum < 0) { runningSum = -1; } else { runningSum += i; } } switch (a) { case ArrayType arr: if (!(b is ArrayType brr)) { return(-1); } return(TypeMatchWeight(arr.ElementType, brr.ElementType, options) * 5); case ByReferenceType abr: if (!(b is ByReferenceType bbr)) { return(-1); } return(TypeMatchWeight(abr.ElementType, bbr.ElementType, options) * 5); case GenericInstanceType agi: if (!(b is GenericInstanceType bgi)) { return(-1); } if (agi.GenericArguments.Count != bgi.GenericArguments.Count) { return(-1); } Accumulate(TypeMatchWeight(agi.ElementType, bgi.ElementType, options)); for (var i = 0; i < agi.GenericArguments.Count; i++) { Accumulate(TypeMatchWeight(agi.GenericArguments[i], bgi.GenericArguments[i], options)); } return(runningSum * 5); case GenericParameter: if (!(b is GenericParameter)) { return(-1); } return(5); default: if (a.IsNested) { if (!b.IsNested) { return(-1); } if (a.Name.IsObfuscated(options)) { return(0); } var declMatch = TypeMatchWeight(a.DeclaringType, b.DeclaringType, options); if (declMatch == -1 || a.Name != b.Name) { return(-1); } return(1); } if (a.Name.IsObfuscated(options)) { return(0); } return(a.Name == b.Name && a.Namespace == b.Namespace ? 1 : -1); } }
public static void GenerateDeobfuscationMap(UnhollowerOptions options) { if (string.IsNullOrEmpty(options.SourceDir)) { Console.WriteLine("No input dir specified; use -h for help"); return; } if (string.IsNullOrEmpty(options.OutputDir)) { Console.WriteLine("No target dir specified; use -h for help"); return; } if (string.IsNullOrEmpty(options.DeobfuscationNewAssembliesPath)) { Console.WriteLine("No obfuscated assembly path specified; use -h for help"); return; } if (!Directory.Exists(options.OutputDir)) { Directory.CreateDirectory(options.OutputDir); } RewriteGlobalContext rewriteContext; IIl2CppMetadataAccess inputAssemblies; IIl2CppMetadataAccess systemAssemblies; using (new TimingCookie("Reading assemblies")) inputAssemblies = new CecilMetadataAccess(Directory.EnumerateFiles(options.DeobfuscationNewAssembliesPath, "*.dll")); using (new TimingCookie("Reading system assemblies")) { if (!string.IsNullOrEmpty(options.SystemLibrariesPath)) { systemAssemblies = new CecilMetadataAccess(Directory.EnumerateFiles(options.SystemLibrariesPath, "*.dll") .Where(it => Path.GetFileName(it).StartsWith("System.") || Path.GetFileName(it) == "mscorlib.dll" || Path.GetFileName(it) == "netstandard.dll")); } else { systemAssemblies = new CecilMetadataAccess(new[] { options.MscorlibPath }); } } using (new TimingCookie("Creating rewrite assemblies")) rewriteContext = new RewriteGlobalContext(options, inputAssemblies, systemAssemblies, NullMetadataAccess.Instance); using (new TimingCookie("Computing renames")) Pass05CreateRenameGroups.DoPass(rewriteContext); using (new TimingCookie("Creating typedefs")) Pass10CreateTypedefs.DoPass(rewriteContext); using (new TimingCookie("Computing struct blittability")) Pass11ComputeTypeSpecifics.DoPass(rewriteContext); using (new TimingCookie("Filling typedefs")) Pass12FillTypedefs.DoPass(rewriteContext); using (new TimingCookie("Filling generic constraints")) Pass13FillGenericConstraints.DoPass(rewriteContext); using (new TimingCookie("Creating members")) Pass15GenerateMemberContexts.DoPass(rewriteContext); RewriteGlobalContext cleanContext; IIl2CppMetadataAccess cleanAssemblies; using (new TimingCookie("Reading clean assemblies")) cleanAssemblies = new CecilMetadataAccess(Directory.EnumerateFiles(options.SourceDir, "*.dll")); using (new TimingCookie("Creating clean rewrite assemblies")) cleanContext = new RewriteGlobalContext(options, cleanAssemblies, systemAssemblies, NullMetadataAccess.Instance); using (new TimingCookie("Computing clean assembly renames")) Pass05CreateRenameGroups.DoPass(cleanContext); using (new TimingCookie("Creating clean assembly typedefs")) Pass10CreateTypedefs.DoPass(cleanContext); var usedNames = new Dictionary <TypeDefinition, (string OldName, int Penalty, bool ForceNs)>(); using var fileOutput = new FileStream(options.OutputDir + Path.DirectorySeparatorChar + "RenameMap.csv.gz", FileMode.Create, FileAccess.Write); using var gzipStream = new GZipStream(fileOutput, CompressionLevel.Optimal, true); using var writer = new StreamWriter(gzipStream, Encoding.UTF8, 65536, true); void DoEnum(TypeRewriteContext obfuscatedType, TypeRewriteContext cleanType) { foreach (var originalTypeField in obfuscatedType.OriginalType.Fields) { if (!originalTypeField.Name.IsObfuscated(obfuscatedType.AssemblyContext.GlobalContext.Options)) { continue; } var matchedField = cleanType.OriginalType.Fields[obfuscatedType.OriginalType.Fields.IndexOf(originalTypeField)]; writer.WriteLine(obfuscatedType.NewType.GetNamespacePrefix() + "." + obfuscatedType.NewType.Name + "::" + Pass22GenerateEnums.GetUnmangledName(originalTypeField) + ";" + matchedField.Name + ";0"); } } foreach (var assemblyContext in rewriteContext.Assemblies) { if (options.DeobfuscationGenerationAssemblies.Count > 0 && !options.DeobfuscationGenerationAssemblies.Contains(assemblyContext.NewAssembly.Name.Name)) { continue; } var cleanAssembly = cleanContext.GetAssemblyByName(assemblyContext.OriginalAssembly.Name.Name); void DoType(TypeRewriteContext typeContext, TypeRewriteContext?enclosingType) { if (!typeContext.OriginalNameWasObfuscated) { return; } var cleanType = FindBestMatchType(typeContext, cleanAssembly, enclosingType); if (cleanType.Item1 == null) { return; } if (!usedNames.TryGetValue(cleanType.Item1.NewType, out var existing) || existing.Item2 < cleanType.Item2) { usedNames[cleanType.Item1.NewType] = (typeContext.NewType.GetNamespacePrefix() + "." + typeContext.NewType.Name, cleanType.Item2, typeContext.OriginalType.Namespace != cleanType.Item1.OriginalType.Namespace); } else { return; } if (typeContext.OriginalType.IsEnum) { DoEnum(typeContext, cleanType.Item1); } foreach (var originalTypeNestedType in typeContext.OriginalType.NestedTypes) { DoType(typeContext.AssemblyContext.GetContextForOriginalType(originalTypeNestedType), cleanType.Item1); } } foreach (var typeContext in assemblyContext.Types) { if (typeContext.NewType.DeclaringType != null) { continue; } DoType(typeContext, null); } } foreach (var keyValuePair in usedNames) { writer.WriteLine(keyValuePair.Value.Item1 + ";" + (keyValuePair.Value.ForceNs ? keyValuePair.Key.Namespace + "." : "") + keyValuePair.Key.Name + ";" + keyValuePair.Value.Item2); } LogSupport.Info("Done!"); rewriteContext.Dispose(); }
public static void Main(UnhollowerOptions options) { if (string.IsNullOrEmpty(options.SourceDir)) { Console.WriteLine("No input dir specified; use -h for help"); return; } if (string.IsNullOrEmpty(options.OutputDir)) { Console.WriteLine("No target dir specified; use -h for help"); return; } if (string.IsNullOrEmpty(options.MscorlibPath)) { Console.WriteLine("No mscorlib specified; use -h for help"); return; } if (!Directory.Exists(options.OutputDir)) { Directory.CreateDirectory(options.OutputDir); } RewriteGlobalContext rewriteContext; using (new TimingCookie("Reading assemblies")) rewriteContext = new RewriteGlobalContext(options, Directory.EnumerateFiles(options.SourceDir, "*.dll")); using (new TimingCookie("Computing renames")) Pass05CreateRenameGroups.DoPass(rewriteContext); using (new TimingCookie("Creating typedefs")) Pass10CreateTypedefs.DoPass(rewriteContext); using (new TimingCookie("Computing struct blittability")) Pass11ComputeTypeSpecifics.DoPass(rewriteContext); using (new TimingCookie("Filling typedefs")) Pass12FillTypedefs.DoPass(rewriteContext); using (new TimingCookie("Filling generic constraints")) Pass13FillGenericConstraints.DoPass(rewriteContext); using (new TimingCookie("Creating members")) Pass15GenerateMemberContexts.DoPass(rewriteContext); using (new TimingCookie("Creating static constructors")) Pass20GenerateStaticConstructors.DoPass(rewriteContext); using (new TimingCookie("Creating value type fields")) Pass21GenerateValueTypeFields.DoPass(rewriteContext); using (new TimingCookie("Creating enums")) Pass22GenerateEnums.DoPass(rewriteContext); using (new TimingCookie("Creating IntPtr constructors")) Pass23GeneratePointerConstructors.DoPass(rewriteContext); using (new TimingCookie("Creating type getters")) Pass24GenerateTypeStaticGetters.DoPass(rewriteContext); using (new TimingCookie("Creating non-blittable struct constructors")) Pass25GenerateNonBlittableValueTypeDefaultCtors.DoPass(rewriteContext); using (new TimingCookie("Creating generic method static constructors")) Pass30GenerateGenericMethodStoreConstructors.DoPass(rewriteContext); using (new TimingCookie("Creating field accessors")) Pass40GenerateFieldAccessors.DoPass(rewriteContext); using (new TimingCookie("Filling methods")) Pass50GenerateMethods.DoPass(rewriteContext); using (new TimingCookie("Generating implicit conversions")) Pass60AddImplicitConversions.DoPass(rewriteContext); using (new TimingCookie("Creating properties")) Pass70GenerateProperties.DoPass(rewriteContext); if (options.UnityBaseLibsDir != null) { using (new TimingCookie("Unstripping types")) Pass79UnstripTypes.DoPass(rewriteContext); using (new TimingCookie("Unstripping methods")) Pass80UnstripMethods.DoPass(rewriteContext); } else { Console.WriteLine("Not performing unstripping as unity libs are not specified"); } using (new TimingCookie("Writing assemblies")) Pass99WriteToDisk.DoPass(rewriteContext, options); File.Copy(typeof(IL2CPP).Assembly.Location, Path.Combine(options.OutputDir, typeof(IL2CPP).Assembly.GetName().Name + ".dll"), true); File.Copy(typeof(DelegateSupport).Assembly.Location, Path.Combine(options.OutputDir, typeof(DelegateSupport).Assembly.GetName().Name + ".dll"), true); File.Copy(typeof(Decoder).Assembly.Location, Path.Combine(options.OutputDir, typeof(Decoder).Assembly.GetName().Name + ".dll"), true); Console.WriteLine("Done!"); }