中级玩家
 
- 贡献度
- 12
- 金元
- 1605
- 积分
- 219
- 精华
- 1
- 注册时间
- 2017-6-17
|
今古群侠传 Mod 技术分析报告零、快速总结TL;DR: 经过大量测试,该游戏 Unity 2021.3.15f1 的 Mono 运行时做了高强度托管代码裁剪,导致 BepInEx、MelonLoader、Harmony、MonoMod 等所有主流动态 Mod 框架全部无法使用。最终方案为自建静态 IL 注入框架(JinguMod),利用 Assembly.Load + Mono.Cecil 在游戏启动前修改 Assembly-CSharp.dll,已验证可成功修改游戏数值。
一、游戏基本信息项值
游戏名今古群侠传 / JinGu
开发商金十四工作室
平台Steam
Unity 版本2021.3.15f1c1
运行时Mono (MonoBleedingEdge) 6.12
架构x64 (64-bit)
C# 后端Mono(非 IL2CPP),存在 Assembly-CSharp.dll 二、主流 Mod 框架实验结论BepInEx — 全部失败版本代理 DLL结果
5.3.0 / 5.4.19 / 5.4.20 / 5.4.21 / 5.4.23winhttp.dll游戏正常启动,BepInEx 完全不加载,无 LogOutput.log
同上全部版本version.dll同上,完全不加载
所有版本x86 变体不兼容(游戏为 x64)失败原因: 该游戏的 UnityPlayer.dll 对 winhttp.dll 和 version.dll 使用了延迟加载(delay-load),实际运行时从不调用这些 DLL 的函数,导致 Windows 不会加载本地拷贝。Doorstop 代理 DLL 根本没有执行机会。
⚠️ 此问题在其他 Unity 2021 游戏中未复现,系该游戏特有的构建配置。
MelonLoader — 各版本表现不一版本结果
0.4.3闪退:MissingMethodException: AmbiguousMatchException..ctor
0.5.4闪退:MissingMethodException: ServicePointManager.set_Expect100Continue
0.5.7闪退:MissingMethodException: ServicePointManager.get_ServerCertificateValidationCallback
0.6.5安装器失败:Internal Failure: Failed to find MelonLoader Bootstrap
0.6.6手动安装报错找不到 dobby.dll;闪退但生成了 Mods/ 目录
0.7.3代理注入成功,但 Bootstrap 初始化失败:CustomAttributeFormatException: Could not find a field with name CharSet关键发现: MelonLoader 0.7.x 的 version.dll 代理确实能成功注入(与 BepInEx 不同),说明注入通道存在。失败发生在注入后的托管代码初始化阶段,被 Unity Mono 的 API 缺失所阻断。
尝试修复 MelonLoader 的过程针对 MelonLoader 0.7.3,尝试用 Mono.Cecil 修补其 DLL 来绕过 CharSet 报错:
- 清空 NativeLibrary<T>..ctor(导致后续 ldfld 空引用)→ 失败
- 在 BootstrapInterop.Initialize 的 catch 块中返回 null(跳过错误)→ 游戏能进主菜单,但 Core.Initialize 未被调用 → Mods 不加载
- 插入 br 指令直接跳到 Core.Initialize → 闪退(方法元数据损坏)
- 将所有 NativeLibrary 的 newobj 替换为 ldnull → 空引用异常
针对 MelonLoader 0.5.4 和 0.4.3,同样遇到一连串缺失 API:
- ServicePointManager.set_Expect100Continue — 清空 ServerCertificateValidation.Install 方法
- Directory.SetCurrentDirectory — 清空 MelonUtils.SetCurrentDomainBaseDirectory
- AmbiguousMatchException..ctor(string, Exception) — 清空 InvariantCurrentCulture.Install
- FileSystemWatcher..ctor(string, string) — 清空后仍旧失败,链式报错无止境
结论: 每一处补丁修完都会暴露下一个缺失的 API,属于系统性不兼容,无法通过逐个修补解决。
三、深层原因:Mono 托管代码裁剪Unity 在打包游戏时会分析哪些 .NET 类型被实际使用,未引用的都会被自动裁剪以减小包体。该游戏设置了高等级裁剪(High),导致以下关键 API 全部缺失:
缺失 API影响
System.Threading.ReaderWriterLockSlimHarmony / HarmonyX 类型初始化失败
RuntimeHelpers.PrepareMethodMonoMod 动态 Detour 无法执行 JIT 方法准备
ServicePointManager 方法MelonLoader 0.4.x / 0.5.x 网络层崩溃
DllImportAttribute.CharSet 字段MelonLoader 0.7.x 原生方法扫描失败
Directory.SetCurrentDirectoryMelonLoader 0.5.x 工作目录设置失败
AmbiguousMatchException..ctor(string,Exception)MelonLoader 0.4.x 异常处理失败
FileSystemWatcher..ctor(string,string)MelonLoader 0.4.x 配置文件监视失败⚠️ 这些 API 属于 .NET 运行时的内部实现或底层同步原语,无法通过简单的 DLL 文件替换来补齐。PrepareMethod 更是 JIT 编译器层面的功能,仅存在于 C++ 编写的 Mono 运行时引擎中。
四、为何其他方案也不可行?修改 ScriptingAssemblies.jsonScriptingAssemblies.json 仅用于 Unity Editor 构建,游戏运行时忽略此文件。在其中添加 DLL 名称不会导致该 DLL 被加载。
Unity Subsystems游戏日志虽然打印了 Discovering subsystems at path .../UnitySubsystems,但该目录实际不存在,无法利用。
IL2CPP 转换游戏是 Mono 后端而非 IL2CPP,不存在 GameAssembly.dll。Mono 后端的优势是托管代码以原始 IL 字节码存储,可以用 Mono.Cecil 直接读写。
五、最终方案:静态 IL 注入框架核心思路放弃运行时动态 Hook,改为启动前用 Mono.Cecil 直接修改 Assembly-CSharp.dll 的 IL 字节码。Mono.Cecil 是纯 .NET 字节码读写库,不依赖任何游戏运行时 API。
实现架构Mods/YourPatch.dll ← 你写的补丁(C# 代码,命名约定) │JinguPatcher.exe ← IL 注入工具(启动前运行一次) │Assembly-CSharp.dll ← 游戏代码被直接修改 │JinguMod.dll (Bootstrap) ← 运行时引导器(Unity 通过 GameEntry 自动加载) │游戏正常运行,补丁生效工作流程- GameEntry 静态构造函数(自动注入,无需用户操作)
static GameEntry(){ var asm = Assembly.Load("JinguMod"); var type = asm.GetType("JinguMod.Bootstrap"); type.GetField("Loaded").GetValue(null); // 触发 Bootstrap 初始化} - Bootstrap 初始化:扫描 Mods/ 目录,加载补丁 DLL
- JinguPatcher.exe:扫描 Mods 补丁,生成 IL 注入到 Assembly-CSharp.dll
- 补丁编写:使用命名约定 Prefix_类名_方法名 或 Postfix_类名_方法名
补丁编写示例namespace JinguModPatch;public static class PatchEntry{ // 拦截 ChangeItem,阻止扣钱 public static void Prefix_SaveData_ChangeItem(int itemId, ref int changeNum) { if (itemId == 10001 && changeNum < 0) changeNum = 0; }}核心代码Bootstrap 加载器(JinguMod/Bootstrap.cs):
public static class Bootstrap{ public static readonly bool Loaded; public static readonly string Status; static Bootstrap() { try { var modsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Mods"); if (Directory.Exists(modsDir)) { foreach (var dllPath in Directory.GetFiles(modsDir, "*.dll")) { var patchAsm = Assembly.LoadFrom(dllPath); var entryType = patchAsm.GetType("JinguModPatch.PatchEntry"); if (entryType != null) { var loadedField = entryType.GetField("Loaded"); if (loadedField != null) loadedField.GetValue(null); // 触发补丁静态构造函数 } } } Status = "OK"; } catch (Exception ex) { Status = ex.GetType().Name + ": " + ex.Message; } finally { Loaded = true; } }}IL 注入工具(JinguPatcher/Program.cs):
- 使用 Mono.Cecil 读取 Assembly-CSharp.dll
- 扫描 Mods/ 中的补丁 DLL
- 解析 Prefix_类名_方法名 命名约定
- 将补丁方法的 call 指令注入目标方法开头
完整源码见项目仓库。
六、优缺点对比BepInEx / MelonLoader本方案(静态 IL 注入)
安装方式解压到游戏目录解压到游戏目录
是否依赖运行时 API是(被裁剪后失效)否
补丁语法[HarmonyPatch] 特性Prefix_类名_方法名 约定
入参修改支持支持(ref 参数)
返回值修改支持(__result)实验性支持(ref 返回值参数)
动态热重载支持不支持(需重启 + 重新注入)
兼容性取决于 Mono 裁剪等级任意 Mono 游戏通用
需要 dnSpy否仅查找方法签名时需要
玩家使用门槛极低(解压即用)极低(解压即用) 七、对未来的建议如果有开发者想从根本上解决此问题,可以考虑以下方向:
- Unity 重新打包:在用 Unity 2021.3.15f1导出项目时,把 Managed Stripping Level 改为 Disabled,重新 Build 后提取完整的 System 类 DLL(mscorlib.dll、System.Core.dll 等),覆盖到原游戏中。这样做可以补齐所有缺失的 .NET API,使 BepInEx/MelonLoader 全部恢复可用。这是唯一可能让动态框架重回战场的方法。但没有原始项目文件难以复现
- Assembly Publicizing:使用工具将游戏所有程序集的 internal 成员公开化,为 mod 提供最大灵活性。
- Mono Runtime 注入:深入 mono-2.0-bdwgc.dll 层面,通过 C++ 直接向 Mono 注册缺失类型——但这是极其复杂且风险很高的方案。
本报告编写于 2026 年 5 月15日,基于对游戏版本构建号 080beb55ecda(Unity 2021.3.15f1c1) 【v1.0.0】的实际测试。——酸菜鱼真菜
|
评分
-
7
查看全部评分
-
|