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 .
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.dll
Put in BepInEx\patchers
the folder and BundleLoader.dll
put in BepInEx\plugins
the folder.
Refer to the structure of the Mod package PatchMod_Example.zip
for development, and put the decompressed PatchMod
folder into the game root directory.
The directory PatchMod.cfg
contains package files for each mod.
PatchMod.cfg
The 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, Object
package 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.
Here I put the Dll file in Dlls
the folder , the AssetBundle file Resources
in the folder, and edit the Mod related settings in mod.yml
(the file .yml
with 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 Object
can 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.cecil
The original Mono.cecil
imitates 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 IL
the 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
. Bundle
Here 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. AssetBundle
There 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 ModInfo
class is introduced here, and all objects are read at the beginning of the game Bundle
and 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 .NET
on .
BepInEx
Comes with HarmonyX and MonoMod.RuntimeDetour
to perform runtime patching. I choose to use HarmonyX
for patching, which is Harmony
a 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 RoleStatePanelUI
prefix (Prefix) patch, and continue to execute the original function after the patch is completed ( Prefix
the function returns a bool
value, 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 Nuget
that the package and reference library version must Unity
match your own version.
reference project
name | probably reference documents |
---|---|
IL-Repack | ILRepack/ILRepack.cs and 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 |