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

XnaToFnaUtil.Processor.cs
using Mono.Cecil;
using Mono.Cecil.Cil;
using MonoMod;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using XnaToFna.ProxyForms;
using System.Xml.Serialization;
using System.Xml;
using System.Linq;
using MonoMod.Utils;

namespace XnaToFna {
    public partial clast XnaToFnaUtil : IDisposable {

        public static MethodInfo m_XnaToFnaHelper_PreUpdate = typeof(XnaToFnaHelper).GetMethod("PreUpdate");
        public static MethodInfo m_XnaToFnaHelper_MainHook = typeof(XnaToFnaHelper).GetMethod("MainHook");
        public static MethodInfo m_FileSystemHelper_FixPath = typeof(FileSystemHelper).GetMethod("FixPath");

        public void SetupHooks() {
            // To use XnaToFnaGame properly, the actual game override needs to call XnaToFnaGame::.ctor as "base" instead.
            Modder.RelinkMap["System.Void Microsoft.Xna.Framework.Game::.ctor()"] =
                new RelinkMapEntry("XnaToFna.XnaToFnaGame", "System.Void .ctor()");
            Modder.ForceCallMap["System.Void XnaToFna.XnaToFnaGame::.ctor()"] = OpCodes.Call;
            foreach (MethodInfo method in typeof(XnaToFnaGame).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) {
                Modder.RelinkMap[method.GetFindableID(type: "Microsoft.Xna.Framework.Game")] =
                    new RelinkMapEntry("XnaToFna.XnaToFnaGame", method.GetFindableID(withType: false));
                Modder.ForceCallMap[method.GetFindableID(withType: true)] = OpCodes.Call;
            }

            // XNA games expect a WinForms handle. Give it a "proxy" handle instead.
            if (HookCompat) {
                Modder.RelinkMap["System.IntPtr Microsoft.Xna.Framework.GameWindow::get_Handle()"] =
                    new RelinkMapEntry("XnaToFna.XnaToFnaHelper", "System.IntPtr GetProxyFormHandle(Microsoft.Xna.Framework.GameWindow)");
            }

            // X360 games can be larger than the screen. Allow the user to "fix" this by forcing a display resolution via env vars.
            if (HookHacks) {
                Modder.RelinkMap["System.Void Microsoft.Xna.Framework.GraphicsDeviceManager::ApplyChanges()"] =
                    new RelinkMapEntry("XnaToFna.XnaToFnaHelper", "System.Void ApplyChanges(Microsoft.Xna.Framework.GraphicsDeviceManager)");
            }

            // Let's just completely wreck everything.
            foreach (Type type in typeof(Form).astembly.GetTypes()) {
                string name = type.FullName;

                // Subssatute WinForms for ProxyForms
                if (HookCompat && name.StartsWith("XnaToFna.ProxyForms."))
                    Modder.RelinkMap["System.Windows.Forms." + name.Substring(9 + 11)] = name;
                // Subssatute common Drawing clastes (f.e. Rectangle) with our own for Drawing-less environments (f.e. Android)
                else if (HookCompat && name.StartsWith("XnaToFna.ProxyDrawing."))
                    Modder.RelinkMap["System.Drawing." + name.Substring(9 + 13)] = name;
                // Some XNA games use DInput... let's just subssatute all DInput references with our ProxyDInput.
                else if (name.StartsWith("XnaToFna.ProxyDInput."))
                    Modder.RelinkMap[/* no namespace */ name.Substring(9 + 12)] = name;

                // Some X360 games use Microsoft.Xna.Framework.Xdk; let's just stub whatever's required in XTF.
                else if (name.StartsWith("XnaToFna.StubXDK.")) {
                    string nameXDK = "Microsoft.Xna.Framework." + name.Substring(9 + 8);
                    Modder.RelinkMap[nameXDK] = name;
                    // Unfortunately, the signatures refer to GamerServices, while XnaToFna itself can't refer to that.
                    // Let's abuse the MonoModHook attribtue.
                    foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) {
                        MonoModHook hook = method.GetCustomAttribute();
                        if (hook != null) {
                            Modder.RelinkMap[hook.FindableID] = new RelinkMapEntry(name, method.GetFindableID(withType: false));
                        }
                    }
                    foreach (ConstructorInfo ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) {
                        MonoModHook hook = ctor.GetCustomAttribute();
                        if (hook != null) {
                            Modder.RelinkMap[hook.FindableID] = new RelinkMapEntry(name, ctor.GetFindableID(withType: false));
                        }
                    }
                }
            }

            if (HookHacks)
                Modder.RelinkMap["System.Boolean Microsoft.Xna.Framework.GamerServices.Guide::get_IsTrialMode()"] =
                    new RelinkMapEntry("XnaToFna.XnaToFnaHelper", "System.IntPtr get_IsTrialMode()");

            if (HookBinaryFormatter) {
                Modder.RelinkMap["System.Void System.Runtime.Serialization.Formatters.Binary.BinaryFormatter::.ctor()"] =
                    new RelinkMapEntry("XnaToFna.BinaryFormatterHelper", "System.Runtime.Serialization.Formatters.Binary.BinaryFormatter Create()");

                // The longest relink mapping ever seen...
                Modder.RelinkMap["System.Void System.Runtime.Serialization.Formatters.Binary.BinaryFormatter::.ctor(System.Runtime.Serialization.ISurrogateSelector,System.Runtime.Serialization.StreamingContext)"] =
                    new RelinkMapEntry("XnaToFna.BinaryFormatterHelper", "System.Runtime.Serialization.Formatters.Binary.BinaryFormatter Create(System.Runtime.Serialization.ISurrogateSelector,System.Runtime.Serialization.StreamingContext)");

                Modder.RelinkMap["System.Runtime.Serialization.SerializationBinder System.Runtime.Serialization.Formatters.Binary.BinaryFormatter::get_Binder()"] =
                    new RelinkMapEntry("XnaToFna.BinaryFormatterHelper", "System.Runtime.Serialization.SerializationBinder get_Binder(System.Runtime.Serialization.Formatters.Binary.BinaryFormatter)");

                Modder.RelinkMap["System.Void System.Runtime.Serialization.Formatters.Binary.BinaryFormatter::set_Binder(System.Runtime.Serialization.SerializationBinder)"] =
                    new RelinkMapEntry("XnaToFna.BinaryFormatterHelper", "System.Void set_Binder(System.Runtime.Serialization.Formatters.Binary.BinaryFormatter,System.Runtime.Serialization.SerializationBinder)");
            }

            if (HookReflection) {
                Modder.RelinkMap["System.Reflection.FieldInfo System.Type::GetField(System.String,System.Reflection.BindingFlags)"] =
                    new RelinkMapEntry("XnaToFna.ProxyReflection.FieldInfoHelper", "System.Reflection.FieldInfo GetField(System.Type,System.String,System.Reflection.BindingFlags)");

                Modder.RelinkMap["System.Reflection.FieldInfo System.Type::GetField(System.String)"] =
                    new RelinkMapEntry("XnaToFna.ProxyReflection.FieldInfoHelper", "System.Reflection.FieldInfo GetField(System.Type,System.String)");
            }

            // X360 uses the ".NET Compact Framework", which actually ships with some additional stuff...
            Modder.RelinkMap["System.Void System.Threading.Thread::SetProcessorAffinity(System.Int32[])"] =
                new RelinkMapEntry("XnaToFna.X360Helper", "System.Void SetProcessorAffinity(System.Threading.Thread,System.Int32[])");

        }

        public static void SetupGSRelinkMap(XnaToFnaUtil xtf, XnaToFnaMapping mapping) {
            // Required for some X360 satles.

            SetupDirectRelinkMap(xtf, mapping, (_, type) => {
                // Fix .GamerServices references pointing to .Game by creating direct type mappings.
                xtf.Modder.RelinkMap[type.FullName] = type;
                // Fix .GamerServices references being in the .Net namespace in our replacement astembly.
                if (type.FullName.Contains(".Net.")) {
                    xtf.Modder.RelinkMap[type.FullName.Replace(".Net.", ".GamerServices.")] = type;
                }
            });

        }

        public static void SetupDirectRelinkMap(XnaToFnaUtil xtf, XnaToFnaMapping mapping) {
            SetupDirectRelinkMap(xtf, mapping, null);
        }
        public static void SetupDirectRelinkMap(XnaToFnaUtil xtf, XnaToFnaMapping mapping, Action action) {
            foreach (TypeDefinition type in mapping.Module.Types)
                SetupDirectRelinkMapType(xtf, type, action);
        }
        public static void SetupDirectRelinkMapType(XnaToFnaUtil xtf, TypeDefinition type, Action action) {
            if (action != null)
                action(xtf, type);
            else
                xtf.Modder.RelinkMap[type.FullName] = type;

            foreach (TypeDefinition nested in type.NestedTypes)
                SetupDirectRelinkMapType(xtf, nested, action);
        }

        public void PreProcessType(TypeDefinition type) {
            if (HookCompat) {
                foreach (MethodDefinition method in type.Methods) {
                    if (!method.HasPInvokeInfo)
                        continue;
                    // Just check if PInvokeHooks contains the entry point, ignoring the module name, except for its end. What can go wrong?...
                    if (!method.PInvokeInfo.Module.Name.EndsWith("32.dll") && !method.PInvokeInfo.Module.Name.EndsWith("32"))
                        continue;
                    string entryPoint = method.PInvokeInfo.EntryPoint ?? method.Name;
                    if (typeof(PInvokeHooks).GetMethod(entryPoint) != null) {
                        Log($"[PreProcess] [PInvokeHooks] Remapping call to {entryPoint} ({method.GetFindableID()})");
                        Modder.RelinkMap[method.GetFindableID(simple: true)] =
                            new RelinkMapEntry("XnaToFna.PInvokeHooks", entryPoint);
                    } else {
                        Log($"[PreProcess] [PInvokeHooks] Found unhooked call to {entryPoint} ({method.GetFindableID()})");
                    }
                }
            }

            if (FixOldMonoXML) {
                Stack baseTypes = new Stack();
                try {
                    for (TypeDefinition baseType = type.BaseType?.Resolve(); baseType != null; baseType = baseType.BaseType?.Resolve())
                        baseTypes.Push(baseType);
                } catch {
                    // Unresolved astembly, f.e. XNA itself
                }

                foreach (FieldDefinition field in type.Fields) {
                    string name = field.Name;

                    if (baseTypes.Any(baseType => baseType.FindField(name) != null || baseType.FindProperty(name) != null)) {
                        // Field name collision found. Mono 4.4+ handles them well, while Xamarin.Android still fails.
                        Log($"[PreProcess] Renaming field name collison {name} in {type.FullName}");
                        field.Name = $"{name}_{type.Name}";
                        Modder.RelinkMap[$"{type.FullName}::{name}"] = field.FullName;
                    }
                }
            }

            foreach (TypeDefinition nested in type.NestedTypes)
                PreProcessType(nested);
        }

        public void PostProcessType(TypeDefinition type) {
            // Make all Microsoft.Xna.Framework.Games inherit from XnaToFnaGame instead.
            bool isGame = false;
            if (type.BaseType?.FullName == "Microsoft.Xna.Framework.Game") {
                Log($"[PostProcess] Found type overriding Game: {type.FullName})");
                type.BaseType = type.Module.ImportReference(typeof(XnaToFnaGame));
                isGame = true;
            }

            foreach (MethodDefinition method in type.Methods) {
                if (!method.HasBody) continue;

                string id = method.GetFindableID(withType: false);

                if (isGame && id == "System.Void Update(Microsoft.Xna.Framework.GameTime)") {
                    Log("[PostProcess] Injecting call to XnaToFnaHelper.PreUpdate into game Update");
                    ILProcessor il = method.Body.GetILProcessor();
                    il.InsertBefore(method.Body.Instructions[0], il.Create(OpCodes.Ldarg_1));
                    il.InsertAfter(method.Body.Instructions[0], il.Create(OpCodes.Callvirt,
                        method.Module.ImportReference(m_XnaToFnaHelper_PreUpdate)));
                    method.Body.UpdateOffsets(1, 2);
                }

                for (int i = 0; i < method.Body.Instructions.Count; i++) {
                    Instruction instr = method.Body.Instructions[i];

                    // Fix XnaToFnaHelper calls still being callvirt calls.
                    if (instr.OpCode == OpCodes.Callvirt && ((MethodReference) instr.Operand).DeclaringType.FullName == "XnaToFna.XnaToFnaHelper") {
                        instr.OpCode = OpCodes.Call;
                    }

                    if (HookLocks)
                        CheckAndDestroyLock(method, i);

                    if (FixPathsFor.Count != 0)
                        CheckAndInjectFixPath(method, ref i);

                }

                // Calculate updated offsets required for any further manual fixes.
                // ... could this be useful outside of XnaToFna?
                {
                    int offs = 0;
                    for (int i = 0; i < method.Body.Instructions.Count; i++) {
                        Instruction instr = method.Body.Instructions[i];
                        instr.Offset = offs;
                        offs += instr.GetSize();
                    }
                }


                for (int i = 0; i < method.Body.Instructions.Count; i++) {
                    Instruction instr = method.Body.Instructions[i];

                    // Change short  long operations as the method grows / shrinks.
                    if (instr.Operand is Instruction) {
                        int offs = ((Instruction) instr.Operand).Offset - instr.Offset;
                        // sbyte.MinValue is -128, but -127 is the first "long" value.
                        if (offs = 127)
                            instr.OpCode = ShortToLongOp(instr.OpCode);
                        else
                            instr.OpCode = LongToShortOp(instr.OpCode);
                    }

                }

            }

            foreach (TypeDefinition nested in type.NestedTypes)
                PostProcessType(nested);
        }

        public OpCode ShortToLongOp(OpCode op) {
            string name = Enum.GetName(typeof(Code), op.Code);
            if (!name.EndsWith("_S"))
                return op;
            return (OpCode?) typeof(OpCodes).GetField(name.Substring(0, name.Length - 2))?.GetValue(null) ?? op;
        }

        public OpCode LongToShortOp(OpCode op) {
            string name = Enum.GetName(typeof(Code), op.Code);
            if (name.EndsWith("_S"))
                return op;
            return (OpCode?) typeof(OpCodes).GetField(name + "_S")?.GetValue(null) ?? op;
        }

        public void CheckAndDestroyLock(MethodDefinition method, int instri) {
            Instruction instr = method.Body.Instructions[instri];

            /* sickING UGLY HACK DO NOT I SAY DO NOT USE THIS EVERYWHERE
             * *ahem*
             * """Fix""" some games *cough* DUCK GAME *cough* looping for CONTENT to LOAD.
             * I deserve to be hanged for this.
             * -ade
             */
            if (instri >= 1 &&
                (instr.OpCode == OpCodes.Brfalse || instr.OpCode == OpCodes.Brfalse_S ||
                 instr.OpCode == OpCodes.Brtrue || instr.OpCode == OpCodes.Brtrue_S) &&
                ((Instruction) instr.Operand).Offset < instr.Offset && instr.Previous.Operand != null) {
                // Check if field load / method call contains "load" or "content"
                string name =
                    (instr.Previous.Operand as FieldReference)?.Name ??
                    (instr.Previous.Operand as MethodReference)?.Name ??
                    instr.Previous.Operand.ToString();
                name = name.ToLowerInvariant();
                if (instri - method.Body.Instructions.IndexOf((Instruction) instr.Operand) = 3 && instr.OpCode == OpCodes.Call &&
                ((MethodReference) instr.Operand).GetFindableID() ==
                    "System.Void System.Threading.Monitor::Enter(System.Object,System.Boolean&)") {
                // Check for content / load in context
                if (method.DeclaringType.FullName.ToLowerInvariant().Contains("content") ||
                    method.Name.ToLowerInvariant().Contains("load") || method.Name.ToLowerInvariant().Contains("content")) {
                    // "The input must be false.", MSDN says.
                    Log($"[PostProcess] [HACK!!!] Destroying possible content loading lock in {method.GetFindableID()}");
                    DestroyMonitorLock(method, instri);
                } else {
                    // Check for the previous load field, maximally 4 (dup, st, ld in between) behind.
                    for (int i = instri; 0 < i && instri - 4  0; instri++) {
                instr = method.Body.Instructions[instri];
                if (instr.OpCode == OpCodes.Call) {
                    string id = ((MethodReference) instr.Operand).GetFindableID();
                    if (id == "System.Void System.Threading.Monitor::Enter(System.Object,System.Boolean&)") {
                        // Found another Enter
                        depth++;
                        continue;
                    } else if (id == "System.Void System.Threading.Monitor::Exit(System.Object)") {
                        depth--;
                        continue;
                    }
                }
            }
            if (depth != 0)
                return; // Whoops!... Let's just leave.

            // Replace the Exit call.
            instr.Operand = Modder.Module.ImportReference(
                Modder.FindTypeDeep("XnaToFna.FakeMonitor").Resolve().FindMethod("System.Void Exit(System.Object)")
            );
        }

        public void CheckAndInjectFixPath(MethodDefinition method, ref int instri) {
            Mono.Collections.Generic.Collection instrs = method.Body.Instructions;
            Instruction instr = instrs[instri];

            /* This hack at least is somewhat reasonable...
             * """Fix""" paths being pasted to methods that heavily rely on proper paths.
             * This is for when MONO_IOMAP=all isn't enough because it doesn't affect native libs.
             * -ade
             */

            if (instr.OpCode != OpCodes.Call && instr.OpCode != OpCodes.Callvirt && instr.OpCode != OpCodes.Newobj)
                return;

            MethodReference called = (MethodReference) instr.Operand;
            if (!FixPathsFor.Contains(called.GetFindableID()) &&
                !FixPathsFor.Contains(called.GetFindableID(withType: false)) &&
                !FixPathsFor.Contains(called.GetFindableID(simple: true)) &&
                !FixPathsFor.Contains(called.GetFindableID(withType: false, simple: true))
                )
                return;

            MethodReference mr_Push = method.Module.ImportReference(StackOpHelper.m_Push);
            MethodReference mr_Pop = method.Module.ImportReference(StackOpHelper.m_Pop);
            MethodReference mr_FixPath = method.Module.ImportReference(m_FileSystemHelper_FixPath);

            ILProcessor il = method.Body.GetILProcessor();
            // Go through all parameters in stack, store all pasted parameters in StackOpHelper and handle accordingly.
            for (int i = called.Parameters.Count - 1; i > -1; --i) {
                if (called.Parameters[i].ParameterType.MetadataType == MetadataType.String) {
                    // Before pushing to StackOpHelper, try to fix the path (we're astuming it's a path).
                    il.InsertBefore(instrs[instri], il.Create(OpCodes.Call, mr_FixPath));
                    instri++;
                }

                GenericInstanceMethod push = new GenericInstanceMethod(mr_Push);
                push.GenericArguments.Add(called.Parameters[i].ParameterType);
                il.InsertBefore(instrs[instri], il.Create(OpCodes.Call, push));
                instri++;
            }

            // Pop StackOpHelper.
            for (int i = 0; i <  called.Parameters.Count; i++) {
                GenericInstanceMethod pop = new GenericInstanceMethod(mr_Pop);
                pop.GenericArguments.Add(called.Parameters[i].ParameterType);
                il.InsertBefore(instrs[instri], il.Create(OpCodes.Call, pop));
                instri++;
            }
        }


    }
}