csharp/0x0ade/XnaToFna/src/XnaToFnaUtil.cs

XnaToFnaUtil.cs
using Mono.Cecil;
using Mono.Cecil.Cil;
using MonoMod;
using MonoMod.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using XnaToFna.XEX;

namespace XnaToFna {
    public partial clast XnaToFnaUtil : IDisposable {

        public readonly static byte[] DotNetFrameworkKeyToken = { 0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89 }; // b77a5c561934e089
        public readonly static Version DotNetFramework4Version = new Version(4, 0, 0, 0);
        public readonly static Version DotNetFramework2Version = new Version(2, 0, 0, 0);

        public readonly static Version DotNetX360Version = new Version(2, 0, 5, 0);

        public readonly static astembly Thisastembly = astembly.GetExecutingastembly();
        public readonly static string ThisastemblyName = Thisastembly.GetName().Name;
        public readonly static Version Version = Thisastembly.GetName().Version;

        public readonly ModuleDefinition ThisModule;

        public List Mappings = new List {
            // X360 satles are weird.
            new XnaToFnaMapping("System", new string[] {
                "System.Net"
            }),

            new XnaToFnaMapping("FNA", new string[] {
                "Microsoft.Xna.Framework",
                "Microsoft.Xna.Framework.Avatar",
                "Microsoft.Xna.Framework.Content.Pipeline",
                "Microsoft.Xna.Framework.Game",
                "Microsoft.Xna.Framework.Graphics",
                "Microsoft.Xna.Framework.Input.Touch",
                "Microsoft.Xna.Framework.Storage",
                "Microsoft.Xna.Framework.Video",
                "Microsoft.Xna.Framework.Xact"
            }),

            new XnaToFnaMapping("FNA.NetStub", new string[] {
                "Microsoft.Xna.Framework.GamerServices",
                "Microsoft.Xna.Framework.Net",
                "Microsoft.Xna.Framework.Xdk"
            }, SetupGSRelinkMap),
        };

        public XnaToFnaModder Modder;

        public DefaultastemblyResolver astemblyResolver = new DefaultastemblyResolver();
        public List Directories = new List();
        public List Modules = new List();
        public Dictionary ModulePaths = new Dictionary();

        public HashSet RemoveDeps = new HashSet() {
            // Some mixed-mode astemblies refer to nameless dependencies..?
            null,
            "",
            "Microsoft.DirectX.DirectInput",
            "Microsoft.VisualC"

        };
        public List ModulesToStub = new List();

        public List ExtractedXEX = new List();

        public bool HookCompat = true;
        public bool HookHacks = true;
        public bool HookEntryPoint = true;
        public bool HookLocks = false;
        public bool FixOldMonoXML = false;
        public bool HookBinaryFormatter = true;
        public bool HookReflection = true;

        public bool AddastemblyReference = true;

        public List DestroyPublicKeyTokens = new List();

        public List FixPathsFor = new List();

        public ILPlatform PreferredPlatform = ILPlatform.Keep;
        public MixedDepAction MixedDeps = MixedDepAction.Stub;

        public XnaToFnaUtil() {
            Modder = new XnaToFnaModder(this);
            Modder.ReadingMode = ReadingMode.Immediate;

            Modder.Strict = false;

            Modder.astemblyResolver = astemblyResolver;
            Modder.DependencyDirs = Directories;

            Modder.MissingDependencyResolver = MissingDependencyResolver;

            using (FileStream xtfStream = new FileStream(astembly.GetExecutingastembly().Location, FileMode.Open, FileAccess.Read))
                ThisModule = MonoModExt.ReadModule(xtfStream, new ReaderParameters(ReadingMode.Immediate));
            Modder.DependencyCache[ThisModule.astembly.Name.Name] = ThisModule;
            Modder.DependencyCache[ThisModule.astembly.Name.FullName] = ThisModule;
        }
        public XnaToFnaUtil(params string[] paths)
            : this() {

            ScanPaths(paths);
        }

        public void Log(string txt) {
            Console.Write("[XnaToFna] ");
            Console.WriteLine(txt);
        }

        public ModuleDefinition MissingDependencyResolver(MonoModder modder, ModuleDefinition main, string name, string fullName) {
            Modder.Log($"Cannot map dependency {main.Name} -> (({fullName}), ({name})) - not found");
            return null;
        }

        public void ScanPaths(params string[] paths) {
            foreach (string path in paths)
                ScanPath(path);
        }

        public void ScanPath(string path) {
            if (Directory.Exists(path)) {
                // Use the directory as "dependency directory" and scan in it.
                if (Directories.Contains(path))
                    // No need to scan the dir if the dir is scanned...
                    return;

                RestoreBackup(path);

                Log($"[ScanPath] Scanning directory {path}");
                Directories.Add(path);
                astemblyResolver.AddSearchDirectory(path); // Needs to be added manually as DependencyDirs was already added

                // Most probably the actual game directory - let's just copy XnaToFna.exe to there to be referenced properly.
                string xtfPath = Path.Combine(path, Path.GetFileName(Thisastembly.Location));
                if (Path.GetDirectoryName(Thisastembly.Location) != path) {
                    Log($"[ScanPath] Found separate game directory - copying XnaToFna.exe and FNA.dll");
                    File.Copy(Thisastembly.Location, xtfPath, true);

                    string dbExt = null;
                    if (File.Exists(Path.ChangeExtension(Thisastembly.Location, "pdb")))
                        dbExt = "pdb";
                    if (File.Exists(Path.ChangeExtension(Thisastembly.Location, "mdb")))
                        dbExt = "mdb";
                    if (dbExt != null)
                        File.Copy(Path.ChangeExtension(Thisastembly.Location, dbExt), Path.ChangeExtension(xtfPath, dbExt), true);

                    if (File.Exists(Path.Combine(Path.GetDirectoryName(Thisastembly.Location), "FNA.dll")))
                        File.Copy(Path.Combine(Path.GetDirectoryName(Thisastembly.Location), "FNA.dll"), Path.Combine(path, "FNA.dll"), true);
                    else if (File.Exists(Path.Combine(Path.GetDirectoryName(Thisastembly.Location), "FNA.dll.tmp")))
                        File.Copy(Path.Combine(Path.GetDirectoryName(Thisastembly.Location), "FNA.dll.tmp"), Path.Combine(path, "FNA.dll"), true);

                }

                ScanPaths(Directory.GetFiles(path));
                return;
            }

            if (File.Exists(path + ".xex")) {
                if (!ExtractedXEX.Contains(path)) {
                    // Remove the original file - let XnaToFna unpack and handle it later.
                    File.Delete(path);
                } else {
                    // XnaToFna will handle the .xex instead.
                }
                return;
            }

            if (path.EndsWith(".xex")) {
                string pathTarget = path.Substring(0, path.Length - 4);
                if (string.IsNullOrEmpty(Path.GetExtension(pathTarget)))
                    return;

                using (Stream streamXEX = File.OpenRead(path))
                using (BinaryReader reader = new BinaryReader(streamXEX))
                using (Stream streamRAW = File.OpenWrite(pathTarget)) {
                    XEXImageData data = new XEXImageData(reader);

                    int offset = 0;
                    int size = data.m_memorySize;

                    // Check if this file is a PE containing an embedded PE.
                    if (data.m_memorySize > 0x10000) { // One default segment alignment.
                        using (MemoryStream streamMEM = new MemoryStream(data.m_memoryData))
                        using (BinaryReader mem = new BinaryReader(streamMEM)) {
                            if (mem.ReadUInt32() != 0x00905A4D) // MZ
                                goto WriteRaw;
                            // This is horrible.
                            streamMEM.Seek(0x00000280, SeekOrigin.Begin);
                            if (mem.ReadUInt64() != 0x000061746164692E) // ".idata\0\0"
                                goto WriteRaw;
                            streamMEM.Seek(0x00000288, SeekOrigin.Begin);
                            mem.ReadInt32(); // Virtual size; It's somewhat incorrect?
                            offset = mem.ReadInt32(); // Virtual offset.
                            // mem.ReadInt32(); // Raw size; Still incorrect.
                            // Let's just write everything...
                            size = data.m_memorySize - offset;
                        }
                    }

                    WriteRaw:
                    streamRAW.Write(data.m_memoryData, offset, size);
                }

                path = pathTarget;
                ExtractedXEX.Add(pathTarget);
            } else if (!path.EndsWith(".dll") && !path.EndsWith(".exe"))
                return;

            // Check if .dll is CLR astembly
            astemblyName name;
            try {
                name = astemblyName.GetastemblyName(path);
            } catch {
                return;
            }

            ReaderParameters modReaderParams = Modder.GenReaderParameters(false);
            // Don't ReadWrite if the module being read is XnaToFna or a relink target.
            bool isReadWrite =
#if !CECIL0_9
            modReaderParams.ReadWrite =
#endif
                path != Thisastembly.Location &&
                !Mappings.Exists(mappings => name.Name == mappings.Target);
            // Only read debug info if it exists
            if (!File.Exists(path + ".mdb") && !File.Exists(Path.ChangeExtension(path, "pdb")))
                modReaderParams.ReadSymbols = false;
            Log($"[ScanPath] Checking astembly {name.Name} ({(isReadWrite ? "rw" : "r-")})");
            ModuleDefinition mod;
            try {
                mod = MonoModExt.ReadModule(path, modReaderParams);
            } catch (Exception e) {
                Log($"[ScanPath] WARNING: Cannot load astembly: {e}");
                return;
            }
            bool add = !isReadWrite || name.Name == ThisastemblyName;

            if ((mod.Attributes & ModuleAttributes.ILOnly) != ModuleAttributes.ILOnly) {
                // Mono.Cecil can't handle mixed mode astemblies.
                Log($"[ScanPath] WARNING: Cannot handle mixed mode astembly {name.Name}");
                if (MixedDeps == MixedDepAction.Stub) {
                    ModulesToStub.Add(mod);
                    add = true;
                } else {
                    if (MixedDeps == MixedDepAction.Remove) {
                        RemoveDeps.Add(name.Name);
                    }
#if !CECIL0_9
                    mod.Dispose();
#endif
                    return;
                }
            }

            if (add && !isReadWrite) { // XNA replacement
                foreach (XnaToFnaMapping mapping in Mappings)
                    if (name.Name == mapping.Target) {
                        mapping.IsActive = true;
                        mapping.Module = mod;
                        foreach (string from in mapping.Sources) {
                            Log($"[ScanPath] Mapping {from} -> {name.Name}");
                            Modder.RelinkModuleMap[from] = mod;
                        }
                    }
            } else if (!add) {
                foreach (XnaToFnaMapping mapping in Mappings)
                    if (mod.astemblyReferences.Any(dep => mapping.Sources.Contains(dep.Name))) {
                        add = true;
                        Log($"[ScanPath] XnaToFna-ing {name.Name}");
                        goto BreakMappings;
                    }
            }
            BreakMappings:

            if (add) {
                Modules.Add(mod);
                ModulePaths[mod] = path;
            } else {
#if !CECIL0_9
                mod.Dispose();
#endif
            }

        }

        public void RestoreBackup(string root) {
            string origRoot = Path.Combine(root, "orig");
            // Check for an "orig" folder to restore any backups from
            if (!Directory.Exists(origRoot))
                return;
            RestoreBackup(root, origRoot);
        }
        public void RestoreBackup(string root, string origRoot) {
            Log($"[RestoreBackup] Restoring from {origRoot} to {root}");
            foreach (string origPath in Directory.EnumerateFiles(origRoot, "*", SearchOption.AllDirectories)) {
                Directory.CreateDirectory(Path.GetDirectoryName(root + origPath.Substring(origRoot.Length)));
                File.Copy(origPath, root + origPath.Substring(origRoot.Length), true);
            }
        }

        public void OrderModules() {
            List ordered = new List(Modules);

            Log("[OrderModules] Unordered: ");
            for (int i = 0; i < Modules.Count; i++)
                Log($"[OrderModules] #{i + 1}: {Modules[i].astembly.Name.Name}");

            ModuleDefinition dep = null;
            foreach (ModuleDefinition mod in Modules)
                foreach (astemblyNameReference depName in mod.astemblyReferences)
                    if (Modules.Exists(other => (dep = other).astembly.Name.Name == depName.Name) &&
                        ordered.IndexOf(dep) > ordered.IndexOf(mod)) {
                        Log($"[OrderModules] Reordering {mod.astembly.Name.Name} dependency {dep.Name}");
                        ordered.Remove(mod);
                        ordered.Insert(ordered.IndexOf(dep) + 1, mod);
                    }

            Modules = ordered;

            Log("[OrderModules] Reordered: ");
            for (int i = 0; i < Modules.Count; i++)
                Log($"[OrderModules] #{i + 1}: {Modules[i].astembly.Name.Name}");
        }

        public void RelinkAll() {
            SetupHooks();

            foreach (XnaToFnaMapping mapping in Mappings)
                if (mapping.IsActive && mapping.Setup != null)
                    mapping.Setup(this, mapping);

            foreach (ModuleDefinition mod in Modules)
                Modder.DependencyCache[mod.astembly.Name.Name] = mod;

            foreach (ModuleDefinition mod in ModulesToStub)
                Stub(mod);

            foreach (ModuleDefinition mod in Modules)
                Relink(mod);
        }

        public void Relink(ModuleDefinition mod) {
            // Don't relink the relink targets!
            if (Mappings.Exists(mappings => mod.astembly.Name.Name == mappings.Target))
                return;

            // Don't relink stubbed targets again!
            if (ModulesToStub.Contains(mod))
                return;

            // Don't relink XnaToFna itself!
            if (mod.astembly.Name.Name == ThisastemblyName)
                return;

            Log($"[Relink] Relinking {mod.astembly.Name.Name}");
            Modder.Module = mod;

            ApplyCommonChanges(mod);

            Log("[Relink] Pre-processing");
            foreach (TypeDefinition type in mod.Types)
                PreProcessType(type);

            Log("[Relink] Relinking (MonoMod PatchRefs past)");
            Modder.PatchRefs();

            Log("[Relink] Post-processing");
            foreach (TypeDefinition type in mod.Types)
                PostProcessType(type);

            if (HookEntryPoint && mod.EntryPoint != null) {
                Log("[Relink] Injecting XnaToFna entry point hook");
                ILProcessor il = mod.EntryPoint.Body.GetILProcessor();
                Instruction call = il.Create(OpCodes.Call, mod.ImportReference(m_XnaToFnaHelper_MainHook));
                il.InsertBefore(mod.EntryPoint.Body.Instructions[0], call);
                il.InsertBefore(call, il.Create(OpCodes.Ldarg_0));
            }

            Log("[Relink] Rewriting and disposing module\n");
#if !CECIL0_9
            Modder.Module.Write(Modder.WriterParameters);
#else
            Modder.Module.Write(ModulePaths[Modder.Module], Modder.WriterParameters);
#endif
            // Dispose the module so other modules can read it as a dependency again.
#if !CECIL0_9
            Modder.Module.Dispose();
#endif
            Modder.Module = null;
            Modder.ClearCaches(moduleSpecific: true);
        }

        public void ApplyCommonChanges(ModuleDefinition mod, string tag = "Relink") {
            if (DestroyPublicKeyTokens.Contains(mod.astembly.Name.Name)) {
                Log($"[{tag}] Destroying public key token for module {mod.astembly.Name.Name}");
                mod.astembly.Name.PublicKeyToken = new byte[0];
            }

            Log($"[{tag}] Updating dependencies");
            for (int i = 0; i < mod.astemblyReferences.Count; i++) {
                astemblyNameReference dep = mod.astemblyReferences[i];

                // Main mapping mast.
                foreach (XnaToFnaMapping mapping in Mappings)
                    if (mapping.Sources.Contains(dep.Name) &&
                        // Check if the target module has been found and cached
                        Modder.DependencyCache.ContainsKey(mapping.Target)) {
                        // Check if module already depends on the remap
                        if (mod.astemblyReferences.Any(existingDep => existingDep.Name == mapping.Target)) {
                            // If so, just remove the dependency.
                            mod.astemblyReferences.RemoveAt(i);
                            i--;
                            goto NextDep;
                        }
                        Log($"[{tag}] Replacing dependency {dep.Name} -> {mapping.Target}");
                        // Replace the dependency.
                        mod.astemblyReferences[i] = Modder.DependencyCache[mapping.Target].astembly.Name;
                        // Only check until first match found.
                        goto NextDep;
                    }

                // Didn't remap; Check for RemoveDeps
                if (RemoveDeps.Contains(dep.Name)) {
                    // Remove any unwanted (f.e. mixed) dependencies.
                    Log($"[{tag}] Removing unwanted dependency {dep.Name}");
                    mod.astemblyReferences.RemoveAt(i);
                    i--;
                    goto NextDep;
                }

                // Didn't remove

                // Check for DestroyPublicKeyTokens
                if (DestroyPublicKeyTokens.Contains(dep.Name)) {
                    Log($"[{tag}] Destroying public key token for dependency {dep.Name}");
                    dep.PublicKeyToken = new byte[0];
                }

                // Check for ModulesToStub (formerly managed references)
                if (ModulesToStub.Any(stub => stub.astembly.Name.Name == dep.Name)) {
                    // Fix stubbed dependencies.
                    Log($"[{tag}] Fixing stubbed dependency {dep.Name}");
                    dep.IsWindowsRuntime = false;
                    dep.HasPublicKey = false;
                }

                // Check for .NET compact (X360) version
                if (dep.Version == DotNetX360Version) {
                    // Replace public key token.
                    dep.PublicKeyToken = DotNetFrameworkKeyToken;
                    // Technically .NET 2(?), but let's just bump the version.
                    dep.Version = DotNetFramework4Version;
                }

                NextDep:
                continue;
            }
            if (AddastemblyReference && !mod.astemblyReferences.Any(dep => dep.Name == ThisastemblyName)) {
                // Add XnaToFna as dependency
                Log($"[{tag}] Adding dependency XnaToFna");
                mod.astemblyReferences.Add(Modder.DependencyCache[ThisastemblyName].astembly.Name);
            }

            if (mod.Runtime < TargetRuntime.Net_4_0) {
                // XNA 3.0 / 3.1 and X360 games depend on a .NET Framework pre-4.0
                mod.Runtime = TargetRuntime.Net_4_0;
                // TODO: What about the System.*.dll dependencies?
            }

            Log($"[{tag}] Updating module attributes");
            mod.Attributes &= ~ModuleAttributes.StrongNameSigned;
            if (PreferredPlatform != ILPlatform.Keep) {
                // "Clear" to AnyCPU.
                mod.Architecture = TargetArchitecture.I386;
                mod.Attributes &= ~ModuleAttributes.Required32Bit & ~ModuleAttributes.Preferred32Bit;

                switch (PreferredPlatform) {
                    case ILPlatform.x86:
                        mod.Architecture = TargetArchitecture.I386;
                        mod.Attributes |= ModuleAttributes.Required32Bit;
                        break;
                    case ILPlatform.x64:
                        mod.Architecture = TargetArchitecture.AMD64;
                        break;
                    case ILPlatform.x86Pref:
                        mod.Architecture = TargetArchitecture.I386;
                        mod.Attributes |= ModuleAttributes.Preferred32Bit;
                        break;
                }
            }

            bool mixed = (mod.Attributes & ModuleAttributes.ILOnly) != ModuleAttributes.ILOnly;
            if (ModulesToStub.Count != 0 || mixed) {
                Log($"[{tag}] Making astembly unsafe");
                mod.Attributes |= ModuleAttributes.ILOnly;
                for (int i = 0; i < mod.astembly.CustomAttributes.Count; i++) {
                    CustomAttribute attrib = mod.astembly.CustomAttributes[i];
                    if (attrib.AttributeType.FullName == "System.CLSCompliantAttribute") {
                        mod.astembly.CustomAttributes.RemoveAt(i);
                        i--;
                    }
                }
                if (!mod.CustomAttributes.Any(ca => ca.AttributeType.FullName == "System.Security.UnverifiableCodeAttribute"))
                    mod.AddAttribute(mod.ImportReference(m_UnverifiableCodeAttribute_ctor));
            }

            // MonoMod needs to relink some types (f.e. XnaToFnaHelper) via FindType, which requires a dependency map.
            Log($"[{tag}] Mapping dependencies for MonoMod");
            Modder.MapDependencies(mod);
        }

        public void Dispose() {
            Modder?.Dispose();

#if !CECIL0_9
            foreach (ModuleDefinition mod in Modules)
                mod.Dispose();
#endif
            Modules.Clear();
            ModulesToStub.Clear();
            Directories.Clear();
        }

    }
}