[Unity] Mod form Dll and AssetBundle external loading plugin

review

This plug-in uses the Mono.cecil static injection module ( a dll included in BepInEx) to realize the Dll patching work in the Unity game preload (PreLoader) stage, to achieve the game running process caused by the inability to package scripts when creating an AssetBundle through the same version of Unity Using Harmony and other dynamic injection modules to load the GameObject in the external AssetBundle through the Hook function or other methods, the script missing problem ( The referenced script on this Behaviour is missing! ) as shown in the figure below occurs .
The referenced script on this Behaviour is missing!

Instructions

Github source link: Click here to view

Directory Structure

Only the directory structure matching the examples given in the project is given , and the specific structure can be modified in combination with the actual situation.

  • BepInEx
    • config

    • core

    • patches

      • PatchMod.dll
      • PatchModInfo.dll
      • YamlDotNet.dll
    • plugins

      • RankPanel_Trigger.dll
      • BundleLoader
        • BundleLoader.dll
        • PatchModInfo.dll
        • YamlDotNet.dll
  • doorstop_config.ini
  • winhttp.dll
  • PatchMod
    • PatchMod.cfg
    • RankPanel
      • mods.yml
      • Dlls
        • Assembly-CSharp.dll
      • AseetBundles
        • rankpanel.ab
  • Other files

Construct

PatchMod.dllPut in BepInEx\patchersthe folder and BundleLoader.dllput in BepInEx\pluginsthe folder.

Refer to the structure of the Mod package PatchMod_Example.zipfor development, and put the decompressed PatchModfolder into the game root directory.

PatchMod placement location

The directory PatchMod.cfgcontains package files for each mod.

PatchMod.cfgThe content of the file is as follows:

[General]
# 是否预先加载进内存,预先加载进去可以防止其他Assembly-csharp加载
preLoad=true
# 是否将修补后的Dll输出到本地,用于调试查看
save2local=false

The sample Mod contains a leaderboard Mod, and its packaging process is as follows: According to the Unity version of the game to be developed, use the same version to develop components and write scripts, Objectpackage the ones AssetBundle, Then build the plug-in project as a whole, get the plug-in project Assembly-csharp.dll, and put it in the folder.

Mod folder structure

Here I put the Dll file in Dllsthe folder , the AssetBundle file Resourcesin the folder, and edit the Mod related settings in mod.yml(the file .ymlwith is enough).

# Mod名
name: 排行榜面板
# Dll读取路径
dlls:
    - Dlls/Assembly-CSharp.dll
# AssetBundle读取路径
resources:
    - AseetBundles/rankpanel.ab

After entering the game, you can see the output content of relevant plug-ins and the loading list of Mod components in the BepInEx console. After that, some trigger calls Objectcan be . This project comes with a plug-in for testing this use case , and you can also download the full version of the test case [Jin Yong Heroes X] for testing.

Implementation

The first is to learn about the BepInEx plugin , which is a plugin for Unity/XNA games.
We are mainly involved in three parts this time:

  • Dll patching on preload (Mono.cecil implementation)
  • Bundle reading management after the game is loaded (Unity's own Bundle management mechanism)
  • In-Game Trigger (Dynamic Patching Dll for Harmony2)

Preload Patches

This part specifically refers to the IL-Repack project , which is a project that uses the C # reflection mechanism to merge Dlls (the former is IL-Merge, which is now enabled) IL-Repack. Mono.cecilThe original Mono.cecilimitates its implementation.

using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PatchMod
{
    
    
    internal class MergeDll
    {
    
    
        //修复总函数
        internal static void Fix(string repairDllPath,ref AssemblyDefinition patchAssembly)
        {
    
    
            //修复用的文件(包含添加进去的内容)
            AssemblyDefinition repairAssembly = AssemblyDefinition.ReadAssembly(repairDllPath);
            //TODO:下面所有方法只修补二者MainModule.
            MergeDll.FixModuleReference(patchAssembly.MainModule, repairAssembly.MainModule);//修复引用
            foreach (TypeDefinition typeDef in repairAssembly.MainModule.Types)
            {
    
    
                //修复类型
                MergeDll.FixType(patchAssembly.MainModule, typeDef, (module, belongTypeDef, fieldDef) =>
                {
    
    
                    MergeDll.FixField(module, belongTypeDef, fieldDef);
                }, (module, belongTypeDef, methodDef) =>
                {
    
    
                    MergeDll.FixMethod(module, belongTypeDef, methodDef);
                });
            }
        }

        //修复Dll引用,将source添加到target中
        internal static void FixModuleReference(ModuleDefinition target, ModuleDefinition source)
        {
    
    
            foreach (ModuleReference modRef in source.ModuleReferences)
            {
    
    
                string name = modRef.Name;
                //如果存在重名则跳过修补
                if (!target.ModuleReferences.Any(y => y.Name == name))
                {
    
    
                    target.ModuleReferences.Add(modRef);
                }
            }
            foreach (AssemblyNameReference asmRef in source.AssemblyReferences)
            {
    
    
                string name = asmRef.FullName;
                //如果存在重名则跳过修补
                if (!target.AssemblyReferences.Any(y => y.FullName == name))
                {
    
    
                    target.AssemblyReferences.Add(asmRef);
                }
            }
        }

        //修复自定义类型,将source添加到target中
        //TODO:目前只能添加不同命名空间的类型
        internal static void FixType(ModuleDefinition target, TypeDefinition source, Action<ModuleDefinition, TypeDefinition, FieldDefinition> func_FixFeild, Action<ModuleDefinition, TypeDefinition, MethodDefinition> func_FixMethod)
        {
    
    
            //不合并同名Type
            //TODO:是否添加合并同名Type判断?
            if (!target.Types.Any(x => x.Name == source.Name))
            {
    
    
                //新建Type
                //如果是自定义Type直接Add会导致报错,因为属于不同的模块,
                //TODO:暂时没用unity工程的Assembly-csharp测试,不知道直接添加可否成功?
                //只向模块添加类型
                TypeDefinition importTypeDefinition = new(source.Namespace, source.Name, source.Attributes) {
    
     };
                //修复基类引用关系
                //例如 Component : MonoBehaviour
                if (source.BaseType != null)
                {
    
    
                    importTypeDefinition.BaseType = source.BaseType;
                }
                target.Types.Add(importTypeDefinition);


                //添加类型下的字段
                foreach (FieldDefinition fieldDef in source.Fields)
                {
    
    
                    func_FixFeild.Invoke(target, importTypeDefinition, fieldDef);
                }

                //添加类型下的方法
                foreach (MethodDefinition methodDef in source.Methods)
                {
    
    
                    func_FixMethod.Invoke(target, importTypeDefinition, methodDef);
                }
            }
        }

        //修复类型中的Field
        internal static void FixField(ModuleDefinition target, TypeDefinition typeDef, FieldDefinition fieldDef)
        {
    
    
            FieldDefinition importFieldDef = new(fieldDef.Name, fieldDef.Attributes, target.ImportReference(fieldDef.FieldType, typeDef));
            typeDef.Fields.Add(importFieldDef);
            importFieldDef.Constant = fieldDef.HasConstant ? fieldDef.Constant : importFieldDef.Constant;
            importFieldDef.MarshalInfo = fieldDef.HasMarshalInfo ? fieldDef.MarshalInfo : importFieldDef.MarshalInfo;
            importFieldDef.InitialValue = (fieldDef.InitialValue != null && fieldDef.InitialValue.Length > 0) ? fieldDef.InitialValue : importFieldDef.InitialValue;
            importFieldDef.Offset = fieldDef.HasLayoutInfo ? fieldDef.Offset : importFieldDef.Offset;
#if DEBUG
            Log($"Add {importFieldDef.FullName} to {typeDef.FullName}");
#endif
        }

        //修复类型中的Method
        internal static void FixMethod(ModuleDefinition target, TypeDefinition typeDef, MethodDefinition methDef)
        {
    
    
            MethodDefinition importMethodDef = new(methDef.Name, methDef.Attributes, methDef.ReturnType);
            importMethodDef.ImplAttributes = methDef.ImplAttributes;
            typeDef.Methods.Add(importMethodDef);
#if DEBUG
            Log($"Add {importMethodDef.FullName} to {typeDef.FullName}");
#endif

            //复制参数
            foreach (ParameterDefinition gp in methDef.Parameters)
            {
    
    
                ParameterDefinition importPara = new(gp.Name, gp.Attributes, gp.ParameterType);
                importMethodDef.Parameters.Add(importPara);
#if DEBUG
                Log($"Add Parameter {importPara.Name} to {importMethodDef.FullName}");
#endif
            }

            //修复Method函数体
            ILProcessor ilEditor = importMethodDef.Body.GetILProcessor();
            if (methDef.HasBody)
            {
    
    
                importMethodDef.Body = new Mono.Cecil.Cil.MethodBody(importMethodDef);
            }

            //TODO:没看懂,照搬
            if (methDef.HasPInvokeInfo)
            {
    
    
                if (methDef.PInvokeInfo == null)
                {
    
    
                    // Even if this was allowed, I'm not sure it'd work out
                    //nm.RVA = meth.RVA;
                }
                else
                {
    
    
                    importMethodDef.PInvokeInfo = new PInvokeInfo(methDef.PInvokeInfo.Attributes, methDef.PInvokeInfo.EntryPoint, methDef.PInvokeInfo.Module);
                }
            }

            //函数体参数
            foreach (VariableDefinition var in methDef.Body.Variables)
            {
    
    
                importMethodDef.Body.Variables.Add(new VariableDefinition(target.ImportReference(var.VariableType, importMethodDef)));
            }
            importMethodDef.Body.MaxStackSize = methDef.Body.MaxStackSize;
            importMethodDef.Body.InitLocals = methDef.Body.InitLocals;
            importMethodDef.Body.LocalVarToken = methDef.Body.LocalVarToken;

            //修复函数覆写
            foreach (MethodReference ov in methDef.Overrides)
                importMethodDef.Overrides.Add(target.ImportReference(ov, importMethodDef));

            //修改函数返回
            importMethodDef.ReturnType = target.ImportReference(methDef.ReturnType, importMethodDef);
            importMethodDef.MethodReturnType.Attributes = methDef.MethodReturnType.Attributes;
            importMethodDef.MethodReturnType.Constant = methDef.MethodReturnType.HasConstant ? methDef.MethodReturnType.Constant : importMethodDef.MethodReturnType.Constant;
            importMethodDef.MethodReturnType.MarshalInfo = methDef.MethodReturnType.HasMarshalInfo ? methDef.MethodReturnType.MarshalInfo : importMethodDef.MethodReturnType.MarshalInfo;

            //TODO:CustomAttribute还就那个不会
            foreach (var il in methDef.Body.Instructions)
            {
    
    
#if DEBUG
                Log($"Add IL {il.OpCode.OperandType} - {il.ToString()}");
#endif
                Instruction insertIL;

                if (il.OpCode.Code == Code.Calli)
                {
    
    
                    var callSite = (CallSite)il.Operand;
                    CallSite ncs = new(target.ImportReference(callSite.ReturnType, importMethodDef))
                    {
    
    
                        HasThis = callSite.HasThis,
                        ExplicitThis = callSite.ExplicitThis,
                        CallingConvention = callSite.CallingConvention
                    };
                    foreach (ParameterDefinition param in callSite.Parameters)
                    {
    
    
                        ParameterDefinition pd = new(param.Name, param.Attributes, target.ImportReference(param.ParameterType, importMethodDef));
                        if (param.HasConstant)
                            pd.Constant = param.Constant;
                        if (param.HasMarshalInfo)
                            pd.MarshalInfo = param.MarshalInfo;
                        ncs.Parameters.Add(pd);
                    }
                    insertIL = Instruction.Create(il.OpCode, ncs);
                }
                else switch (il.OpCode.OperandType)
                    {
    
    
                        case OperandType.InlineArg:
                        case OperandType.ShortInlineArg:
                            if (il.Operand == methDef.Body.ThisParameter)
                            {
    
    
                                insertIL = Instruction.Create(il.OpCode, importMethodDef.Body.ThisParameter);
                            }
                            else
                            {
    
    
                                int param = methDef.Body.Method.Parameters.IndexOf((ParameterDefinition)il.Operand);
                                insertIL = Instruction.Create(il.OpCode, importMethodDef.Parameters[param]);
                            }
                            break;
                        case OperandType.InlineVar:
                        case OperandType.ShortInlineVar:
                            int var = methDef.Body.Variables.IndexOf((VariableDefinition)il.Operand);
                            insertIL = Instruction.Create(il.OpCode, importMethodDef.Body.Variables[var]);
                            break;
                        case OperandType.InlineField:
                            insertIL = Instruction.Create(il.OpCode, target.ImportReference((FieldReference)il.Operand, importMethodDef));
                            break;
                        case OperandType.InlineMethod:
                            insertIL = Instruction.Create(il.OpCode, target.ImportReference((MethodReference)il.Operand, importMethodDef));
                            //FixAspNetOffset(nb.Instructions, (MethodReference)il.Operand, parent);
                            break;
                        case OperandType.InlineType:
                            insertIL = Instruction.Create(il.OpCode, target.ImportReference((TypeReference)il.Operand, importMethodDef));
                            break;
                        case OperandType.InlineTok:
                            if (il.Operand is TypeReference reference)
                                insertIL = Instruction.Create(il.OpCode, target.ImportReference(reference, importMethodDef));
                            else if (il.Operand is FieldReference reference1)
                                insertIL = Instruction.Create(il.OpCode, target.ImportReference(reference1, importMethodDef));
                            else if (il.Operand is MethodReference reference2)
                                insertIL = Instruction.Create(il.OpCode, target.ImportReference(reference2, importMethodDef));
                            else
                                throw new InvalidOperationException();
                            break;
                        case OperandType.ShortInlineBrTarget:
                        case OperandType.InlineBrTarget:
                            insertIL = Instruction.Create(il.OpCode, (Instruction)il.Operand);
                            break;
                        case OperandType.InlineSwitch:
                            insertIL = Instruction.Create(il.OpCode, (Instruction[])il.Operand);
                            break;
                        case OperandType.InlineR:
                            insertIL = Instruction.Create(il.OpCode, (double)il.Operand);
                            break;
                        case OperandType.ShortInlineR:
                            insertIL = Instruction.Create(il.OpCode, (float)il.Operand);
                            break;
                        case OperandType.InlineNone:
                            insertIL = Instruction.Create(il.OpCode);
                            break;
                        case OperandType.InlineString:
                            insertIL = Instruction.Create(il.OpCode, (string)il.Operand);
                            break;
                        case OperandType.ShortInlineI:
                            if (il.OpCode == OpCodes.Ldc_I4_S)
                                insertIL = Instruction.Create(il.OpCode, (sbyte)il.Operand);
                            else
                                insertIL = Instruction.Create(il.OpCode, (byte)il.Operand);
                            break;
                        case OperandType.InlineI8:
                            insertIL = Instruction.Create(il.OpCode, (long)il.Operand);
                            break;
                        case OperandType.InlineI:
                            insertIL = Instruction.Create(il.OpCode, (int)il.Operand);
                            break;
                        default:
                            throw new InvalidOperationException();
                    }
                //ilEditor.InsertAfter(importMethodDef.Body.Instructions.Last(),ilEditor.Create(OpCodes.Nop));
                importMethodDef.Body.Instructions.Add(insertIL);
#if DEBUG
                Log($"Add IL {il.OpCode.OperandType} - {insertIL.ToString()}");
#endif

            }
            importMethodDef.IsAddOn = methDef.IsAddOn;
            importMethodDef.IsRemoveOn = methDef.IsRemoveOn;
            importMethodDef.IsGetter = methDef.IsGetter;
            importMethodDef.IsSetter = methDef.IsSetter;
            importMethodDef.CallingConvention = methDef.CallingConvention;
        }
    }
}

Please check the source code for the specific calling process, only the implementation process is given here. First, we merge references and namespaces (here only merge different external namespaces without merging namespaces with the same name, so it is much simpler), then use the C# reflection mechanism to generate the same class, method, and variable name, and then for the class The specific implementation is copied through the intermediate code IL, which is also copied with the help of reflection (this paragraph is copied directly IL-Repack, because I don’t know ILthe intermediate code very well).

Bundle read management

First of all, what we should do is to find a package with the same version as the game we need to modify Unity. BundleHere is a package implementation. If it is more perfect, please Baidu.


    [MenuItem("AssetsBundle/Build AssetBundles")]
    static void BuildAllAssetBundles()//进行打包
    {
    
    
    	// Bundle输出目录
        string dir = "../AssetBundles_Generate";
        //判断该目录是否存在
        if (Directory.Exists(dir) == false) Directory.CreateDirectory(dir);//在工程下创建AssetBundles目录
        //参数一为打包到哪个路径,参数二压缩选项  参数三 平台的目标
        BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.UncompressedAssetBundle, BuildTarget.StandaloneWindows);
    }

After it is generated Bundle, we also need to read it. AssetBundleThere are many ways to read it. Because this is local loading, I did not write asynchronous loading processing (maybe there may be some problems). The specific implementation is as follows:

using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using PatchModInfo;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace BundleLoader
{
    
    
    [BepInPlugin("com.EasternDay.BundleLoader", "Mod的Bundle加载器示例", "0.0.1")]
    public class BundleLoader : BaseUnityPlugin
    {
    
    
        // 日志记录
        private readonly static new ManualLogSource Logger = new("BundleLoader");

        // MOD文件读取路径
        private static readonly string modIndexPath = Path.Combine(Paths.GameRootPath, "PatchMod");

        //插件配置
        private static readonly List<ModInfo> mods = new();                 //Mod目录
        public static readonly Dictionary<string, Object> objects = new(); //游戏物体列表

        private void Awake()
        {
    
    
            // BepInEx将自定义日志注册
            BepInEx.Logging.Logger.Sources.Add(Logger);
        }

        private void Start()
        {
    
    
            //读取mods
            foreach (string dir in Directory.GetDirectories(modIndexPath))
            {
    
    
                ModInfo curInfo = ModInfo.GetModInfo(dir);
                mods.Add(curInfo);
                foreach (string bundlePath in curInfo.Resources)
                {
    
    
                    // 提示:Bundle打包请使用同版本Unity进行
                    foreach (Object obj in AssetBundle.CreateFromFile(bundlePath).LoadAllAssets())
                    {
    
    
                        Logger.LogInfo($"加载MOD资源:{dir}-{obj.name}");
                        objects.Add(obj.name, obj);
                    }
                }
            }
        }
    }
}

A self-defined ModInfoclass is introduced here, and all objects are read at the beginning of the game Bundleand stored in the dictionary to be indexed according to the object name, and then loaded and called in other ways later.

in-game trigger

Runtime patching is the process of modifying methods without patching them permanently. Runtime patching happens while the game is running, and can be done very extensively .NETon .
BepInExComes with HarmonyX and MonoMod.RuntimeDetourto perform runtime patching. I choose to use HarmonyXfor patching, which is Harmonya branch of , if you are interested, of course, it is best to look at the latest version.

using BepInEx;
using HarmonyLib;
using JX_Plugin;
using JyGame;
using UnityEngine;
using UnityEngine.UI;

namespace RankPanel_Trigger
{
    
    
    [HarmonyPatch(typeof(RoleStatePanelUI), "Refresh")]
    class RoleStatePanelUI_Refresh_Patch
    {
    
    
        public static bool Prefix(RoleStatePanelUI __instance)
        {
    
    
            if (!__instance.transform.FindChild("HardIcon").GetComponent<Button>())
            {
    
    
                __instance.transform.FindChild("HardIcon").gameObject.AddComponent<Button>();
            }
            __instance.transform.FindChild("HardIcon").GetComponent<Button>().onClick.RemoveAllListeners();
            __instance.transform.FindChild("HardIcon").GetComponent<Button>().onClick.AddListener(() =>
            {
    
    
                GameObject go = (GameObject)GameObject.Instantiate(BundleLoader.BundleLoader.objects["RankPanel"]); //生成面板
                go.transform.SetParent(GameObject.Find("MapRoot/Canvas").transform);
                go.name = "RankPanel";
                go.SetActive(true);
                go.GetComponent<RankPanel>().SetContent("排行榜", LuaManager.Call<string>("RankPanel_Content", new object[0]));
            });
            return true;
        }
    }
    [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]
    [BepInDependency("com.EasternDay.BundleLoader")]
    public class Plugin : BaseUnityPlugin
    {
    
    
        private static Harmony harmony = new("JX_Decode_Patch");
        private void Awake()
        {
    
    
            //Hook所有代码
            harmony.PatchAll();
            // 控制台提示语
            Logger.LogInfo($"插件 {PluginInfo.PLUGIN_GUID} 成功Hook代码!");
        }
    }
}

The automatic patch is used here, automatically PatchAll(), and then we perform a RoleStatePanelUIprefix (Prefix) patch, and continue to execute the original function after the patch is completed ( Prefixthe function returns a boolvalue, if it returns true, it means that the original function can continue to execute, if it returns false, which means to intercept the original function). Only a small example is given here. For more specific operations, you can refer to the official documentation or the decryption plug-in in my open source code to see more usage.

Kind tips

When using this plugin to develop, please note Nugetthat the package and reference library version must Unitymatch your own version.

reference project

name probably reference documents
IL-Repack ILRepack/ILRepack.csand its associated files
dnSpy Decompiler, use the following dnlib to complete IL merger, it was not selected in the end, but it has certain reference significance
dnlib It was not selected in the end, but it has certain reference significance

Guess you like

Origin blog.csdn.net/qq_19577209/article/details/121046004
Recommended