综述
本插件利用Mono.cecil静态注入模块(BepInEx包含的一个dll)实现在Unity游戏预加载(PreLoader)阶段的Dll修补工作,用以达到通过同版本Unity创建AssetBundle时候,无法打包脚本导致的游戏运行过程中利用Harmony等动态注入模块通过Hook函数或其他方式加载外部AssetBundle中的GameObject出现如下图所示的脚本缺失问题(The referenced script on this Behaviour is missing!)。
使用方法
Github源码连接:点击此处查看
目录结构
只给出了与项目中所给例子相匹配的目录结构,具体结构自行结合实际修改。
- 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
- 其他文件
构建
将 PatchMod.dll
放入 BepInEx\patchers
文件夹中,将 BundleLoader.dll
放入 BepInEx\plugins
文件夹中。
对应Mod包的结构参考 PatchMod_Example.zip
进行开发,将解压后的 PatchMod
文件夹放入游戏根目录中。
目录中包含 PatchMod.cfg
与各个Mod的包文件。
PatchMod.cfg
文件内容如下:
[General]
# 是否预先加载进内存,预先加载进去可以防止其他Assembly-csharp加载
preLoad=true
# 是否将修补后的Dll输出到本地,用于调试查看
save2local=false
样板Mod中包含一个排行榜Mod,其打包过程如下:自己根据所要开发插件的游戏的Unity版本,用相同版本开发出组件并编写脚本,将要加入到游戏内的 Object
打包为 AssetBundle
,并记住其名字,然后插件项目整体进行构建,得到插件项目的 Assembly-csharp.dll
,放到文件夹内。
在这里我的Dll文件放到了 Dlls
文件夹下,AssetBundle文件放到了 Resources
文件夹下,并在 mod.yml
(拓展名为 .yml
的文件即可)内编辑Mod相关设置。
# Mod名
name: 排行榜面板
# Dll读取路径
dlls:
- Dlls/Assembly-CSharp.dll
# AssetBundle读取路径
resources:
- AseetBundles/rankpanel.ab
进入游戏后在BepInEx控制台内即可以看到相关插件输出内容以及Mod组件加载列表。此后再根据其他插件Hook某些触发调用 Object
即可。本项目内自带一个测试本用例的插件,亦可以下载完整版测试用例【金庸群侠传X】进行测试。
具体实现
首先是了解BepInEx插件,这是一个用于Unity/XNA游戏的外挂程序。
我们此次主要涉及三个部分:
- 预加载时的Dll修补(Mono.cecil实现)
- 游戏加载完成后的Bundle读取管理(Unity自带的Bundle管理机制)
- 游戏内触发(Harmony2的动态修补Dll)
预加载修补
此部分具体参考的是IL-Repack项目,这是一个利用C#反射机制来进行Dll合并的项目(前身是IL-Merge,现已启用),IL-Repack
的修补工作是通过魔改的一个Mono.cecil
实现,本项目采用原版Mono.cecil
模仿其实现。
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