行业资讯
📅 2026/7/19 7:44:19
Unity游戏Mod开发实战:BepInEx插件框架原理、安装与Harmony补丁教程
1. 项目概述为什么BepInEx能让你的Unity游戏“活”起来如果你是一名Unity游戏开发者或者是一位热衷于修改、增强PC端Unity游戏的玩家那么你一定遇到过这样的困境面对一个已经发布的游戏你想添加一个新功能、修复一个烦人的Bug或者仅仅是调整一下游戏参数却发现无从下手。直接修改游戏原文件风险极高一次更新就可能让你的心血付诸东流。自己从头写一个Mod加载器技术门槛和时间成本又让人望而却步。这时BepInExBepis Injector Extensible就登场了。它不是一个具体的Mod而是一个强大、通用且开源的插件框架。简单来说它就是为Unity游戏尤其是使用Mono或IL2CPP后端编译的游戏量身打造的“Mod运行环境”。你可以把它想象成游戏的一个“扩展坞”所有第三方插件Mod都通过这个“扩展坞”安全、有序地加载到游戏中而不会直接“污染”游戏本体。我最初接触BepInEx是为了给一款喜欢的独立游戏添加中文翻译和UI优化。当时游戏本身不支持社区Mod又需要一个稳定的运行基础。在尝试了多种注入方案后BepInEx以其近乎完美的兼容性、清晰的日志系统和活跃的社区支持脱颖而出。它彻底改变了我和许多Mod开发者与Unity游戏的互动方式——从被动的玩家变成了能主动塑造游戏体验的创造者。本指南将带你从零开始彻底掌握BepInEx。无论你是想为自己喜欢的游戏制作第一个Mod的爱好者还是寻求为自家产品建立Mod生态的开发者这篇文章都将提供从原理到实践、从安装到排错的全套“工具箱”。我们会避开那些晦涩难懂的理论堆砌聚焦于“如何让它工作”以及“为什么这么操作”并结合我踩过的无数个坑分享那些官方文档里不会写的实战经验。2. BepInEx核心原理与架构拆解在动手之前理解BepInEx是如何“嵌入”并“运作”于一个Unity游戏中的至关重要。这不仅能帮助你在遇到问题时快速定位也能让你在设计自己的插件时做出更明智的选择。2.1 Unity游戏Mod的难点与BepInEx的解决方案一个编译好的Unity游戏通常是.exe和Data文件夹其核心代码和逻辑已经被打包成托管程序集如Assembly-CSharp.dll。传统修改方式是直接反编译、修改、再重新编译这个DLL俗称“打补丁”。这种方法有几个致命缺点易失效游戏每次更新对应的DLL可能变化补丁立即失效。冲突多个Mod修改同一文件时后安装的会覆盖前者无法共存。高风险操作失误极易导致游戏崩溃且难以恢复。门槛高需要较强的逆向工程和汇编知识。BepInEx采用了更优雅的“运行时注入”方案。它不直接修改游戏原文件而是在游戏启动时将自己“插入”到游戏进程和Unity引擎之间。其核心流程可以概括为以下几步引导启动通过修改游戏启动参数或利用Unity自身的插件机制让游戏首先加载BepInEx的核心引导程序BepInEx.dll。环境准备引导程序初始化一个独立的、受控的.NET运行时环境对于Mono游戏或准备好Hook框架对于IL2CPP游戏。程序集修补这是BepInEx的魔法所在。它会在游戏原有的程序集如Assembly-CSharp.dll被加载到内存后、但尚未被执行前通过Harmony等库对其中特定的方法Method进行“打补丁”Patching。这些补丁可以在方法执行前、后或完全替换原方法从而实现功能修改。插件加载在稳定的修补环境建立后BepInEx会扫描BepInEx/plugins目录加载所有符合规范的插件DLL。每个插件都可以独立声明自己的补丁、配置和依赖关系。注意BepInEx 5.x版本对IL2CPP的支持已经非常成熟其原理是通过BepInEx.IL2CPP组件和MonoMod.RuntimeDetour等工具在本地/本地模拟的运行时中实现类似的钩子功能架构上与Mono版本有所不同但最终效果一致。2.2 BepInEx核心目录结构解析安装完BepInEx后游戏根目录下会生成一个BepInEx文件夹它的结构是你管理所有Mod的司令部游戏根目录/ ├── BepInEx/ │ ├── core/ # BepInEx核心运行库切勿随意修改或删除 │ ├── plugins/ # 【核心】所有插件的存放目录。每个插件一个子文件夹 │ │ └── YourPlugin/ │ │ ├── YourPlugin.dll │ │ └── manifest.json # 插件元数据文件 │ ├── patchers/ # 高级用法用于放置“修补器”Patcher在插件加载前对游戏程序集进行更底层的修改 │ ├── config/ # 【重要】所有插件的配置文件.cfg默认存放于此 │ └── LogOutput.log # 【排错神器】BepInEx的运行日志遇到问题首先查看它plugins目录这是你接触最多的地方。一个合格的插件应该将自己的所有文件主要是DLL和依赖项放在一个以插件名命名的独立子文件夹内。这样做的好处是清晰、易于管理卸载时直接删除整个文件夹即可。config目录BepInEx内置了强大的配置管理功能。插件开发者可以通过代码定义配置项用户则可以在游戏内通过按F1键默认调出的配置管理器实时修改所有设置会自动保存到config目录下的.cfg文件。这实现了Mod设置的“热重载”无需重启游戏。LogOutput.log文件这是诊断一切问题的起点。BepInEx的日志系统非常详细从加载进度、遇到的异常到每个插件的初始化信息都会记录在此。任何情况下游戏崩溃或Mod不生效请第一时间打开这个文件。2.3 与同类框架的对比为什么是BepInEx在Unity游戏Mod领域除了BepInEx你可能还听说过UnityModManagerUMM或MelonLoader。它们各有优劣特性BepInExUnityModManager (UMM)MelonLoader核心架构通用注入器侧重底层Hook和运行时修补针对特定游戏如模拟经营类的Mod管理器提供GUI新兴的通用加载器设计现代对IL2CPP支持早兼容性极广支持Mono和IL2CPP适用于绝大多数Unity游戏较窄通常需要为每个游戏适配较广尤其早期在IL2CPP游戏上表现突出配置管理内置强大配置系统支持热重载依赖Mod自身实现不统一内置配置系统功能类似BepInEx社区与生态最大、最活跃插件、教程资源海量集中在特定游戏社群增长迅速尤其在特定游戏社区上手难度中等概念较多但文档齐全简单图形化安装中等概念清晰适用场景通用性Mod开发、深度游戏修改、建立Mod生态特定游戏的轻量级Mod管理追求新特性的Mod开发尤其是IL2CPP游戏选择BepInEx的理由通用性与稳定性它的设计目标就是成为Unity游戏的“标准模组接口”经过多年迭代稳定性和兼容性久经考验。强大的基础设施内置的配置、日志、依赖管理、补丁系统让开发者能专注于功能本身而不是重复造轮子。庞大的社区遇到问题几乎总能找到解决方案或讨论插件共享格式也基本成为社区标准。对开发者友好清晰的API和丰富的示例降低了开发门槛。实操心得对于一款全新的、不确定支持哪种框架的游戏我通常会先尝试BepInEx。因为即使游戏使用IL2CPPBepInEx 5.x的兼容层也大概率能工作。它的“非侵入式”设计理念意味着即使安装失败也几乎不会对游戏原文件造成损害安全系数最高。3. 手把手安装与配置BepInEx理论说得再多不如动手一试。我们以一款假设的、使用Mono后端编译的Windows PC版Unity游戏《MyDemoGame》为例演示完整的安装流程。IL2CPP版本的安装流程在核心步骤上一致只是需要下载对应的BepInEx_il2cpp_版本号.zip包。3.1 环境准备与版本选择定位游戏根目录找到你的游戏安装位置。通常是通过Steam等平台安装后在库中右键游戏 - “管理” - “浏览本地文件”。这个包含游戏名.exe和游戏名_Data文件夹的路径就是根目录。下载BepInEx访问BepInEx的GitHub发布页。对于大多数Mono游戏下载BepInEx_x64_版本号.zip64位系统或BepInEx_x86_版本号.zip32位游戏。如果不确定可以查看游戏主exe文件的属性。关键点务必下载与游戏位数匹配的版本。备份游戏可选但强烈建议复制整个游戏文件夹到另一个位置。这是避免意外发生的最佳保险。3.2 标准安装流程详解安装过程其实就是解压与替换但细节决定成败。解压压缩包将下载的ZIP文件中的所有内容解压出来。复制文件将解压出的所有文件和文件夹主要是BepInEx文件夹、doorstop_config.ini、winhttp.dll等直接复制到游戏根目录。处理冲突如果提示有文件已存在通常不会除非是重装选择覆盖。winhttp.dll和doorstop_config.ini是注入的关键必须存在。首次运行与验证双击游戏主exe启动游戏。此时游戏窗口可能会黑屏或停留一段时间控制台窗口可能出现这是BepInEx在初始化、扫描插件和打补丁属于正常现象。进入游戏主菜单后观察游戏根目录下是否生成了完整的BepInEx文件夹结构特别是LogOutput.log文件。按F1键默认快捷键屏幕左上角或中央应该会弹出一个半透明的配置管理器窗口。如果能弹出恭喜你BepInEx安装成功注意有些游戏的反作弊或加密可能会阻止BepInEx注入。如果游戏完全无法启动或启动后无任何BepInEx痕迹无日志文件就需要尝试“兼容性安装”或查阅该游戏特定的Mod社区寻求帮助。3.3 关键配置文件解析doorstop_config.ini与BepInEx.cfg安装后有两个配置文件至关重要理解它们能解决很多疑难杂症。doorstop_config.ini- 注入器门神这个文件控制BepInEx如何被加载。用记事本打开它你会看到类似以下内容[General] # 是否启用Doorstop。如果设为falseBepInEx将不会被加载。 enabledtrue # 目标程序集BepInEx核心的路径相对于游戏根目录。 targetAssemblyBepInEx/core/BepInEx.Preloader.dll # 要重定向的DLL名称。当游戏尝试加载这个DLL时会被重定向到BepInEx。 doorstopTypeDOORSTOP_DLL_NAMEenabledtrue这是总开关。如果你暂时想以纯净模式运行游戏比如联机时可以将其改为false。targetAssembly一般不需要改动除非你自定义了BepInEx的安装路径。doorstopType这个值默认是winhttp.dll必须与游戏根目录下那个用于“劫持”的DLL文件名一致。这是注入的核心原理游戏会尝试加载一个系统DLLDoorstop通过同名DLL“冒充”它从而获得控制权。BepInEx.cfg- 框架核心配置位于BepInEx/config目录下。它控制BepInEx自身的行为[Logging] # 控制台窗口是否启用。开启后可以看到实时日志对调试非常有帮助。 Enabled true # 日志输出级别。Debug信息最全但日志文件会很大。 LogLevel Info [Chainloader] # 加载插件时是否检查其依赖项。保持启用可以避免因缺失依赖导致的崩溃。 LoadDependencies true [Preloader] # 控制台窗口的标题。你可以改成自己喜欢的名字。 ConsoleTitle MyDemoGame - BepInExEnabled true强烈建议在调试Mod时开启控制台窗口所有日志信息会实时打印比查看日志文件更直观。LogLevel默认Info即可。如果遇到诡异问题可以临时改为Debug来获取海量信息但记得问题解决后改回去否则日志文件会迅速膨胀。实操心得我习惯在安装完BepInEx后第一时间将doorstop_config.ini中的enabled设为true并打开BepInEx.cfg中的控制台输出。这样从第一次运行开始我就能亲眼看到整个加载过程心里有底。如果游戏启动失败控制台最后几行红色的错误信息就是最好的线索。4. 插件Mod的开发、安装与管理实战BepInEx安装好了现在它是空的我们需要用插件来填充它赋予游戏新的能力。4.1 如何为你的游戏寻找和安装现成插件对于大多数玩家来说开发插件不是第一步安装他人制作好的插件才是。社区插件主要发布在GitHub、游戏对应的Nexus Mods、ModDB页面或专门的Discord频道。标准安装步骤下载插件通常是一个.zip或.rar压缩包。解压查看解压后查看文件结构。一个规范的BepInEx插件包应该包含插件名.dll(核心文件)manifest.json(描述文件包含插件名、版本、作者、依赖等)可能还有README.md说明文档、图标、或其他资源文件。放置到正确位置将整个插件文件夹或者至少是.dll和manifest.json复制到BepInEx/plugins/目录下。正确示例BepInEx/plugins/MyAwesomeMod/MyAwesomeMod.dll错误示例BepInEx/plugins/MyAwesomeMod.dll(直接扔在plugins根目录不推荐难以管理)处理依赖有些插件需要其他库才能运行例如BepInEx.Harmony用于打补丁或UnityEngine.UI用于修改UI。这些依赖通常需要你手动下载并放入BepInEx/plugins目录或者放在BepInEx/core目录如果是核心扩展。仔细阅读插件的安装说明。启动游戏验证启动游戏进入主菜单后按F1打开配置管理器。你应该能在插件列表里看到新安装的插件并且可以对其进行配置如果开发者提供了配置项。4.2 开发你的第一个BepInEx插件从“Hello World”开始如果你想从消费者变为创造者那么亲手写一个插件是最好的开始。你需要准备开发环境Visual Studio 2022 或 JetBrains Rider。.NET SDK根据游戏和BepInEx版本通常需要.NET Framework 4.7.2或.NET 6.0/8.0。在Visual Studio安装器中勾选对应版本即可。引用库你需要引用游戏本身的程序集如Assembly-CSharp.dll位于游戏名_Data/Managed目录和BepInEx的核心库位于你安装的BepInEx/core目录。步骤一创建项目并设置引用在Visual Studio中新建一个“类库(.NET Framework)”项目命名为MyFirstPlugin。在解决方案资源管理器中右键“引用” - “添加引用”。点击“浏览”找到游戏目录下的游戏名_Data/Managed文件夹添加Assembly-CSharp.dll这是游戏逻辑的核心。同时添加UnityEngine.dll、UnityEngine.CoreModule.dll等你可能需要的Unity基础库。再次“浏览”找到BepInEx/core目录添加BepInEx.dll和0Harmony.dll如果你打算使用Harmony打补丁。步骤二编写插件基础代码一个最简单的BepInEx插件包含以下要素using BepInEx; using BepInEx.Logging; using HarmonyLib; // 1. 定义插件元数据 [BepInPlugin(PluginGUID, PluginName, PluginVersion)] public class MyFirstPlugin : BaseUnityPlugin // 2. 继承BaseUnityPlugin { // 3. 定义插件的唯一标识符、名称和版本 public const string PluginGUID com.yourname.mydemogame.myfirstplugin; public const string PluginName My First Plugin; public const string PluginVersion 1.0.0; // 4. 内部日志记录器用于输出信息到BepInEx控制台和日志文件 internal static ManualLogSource Log; // 5. Awake方法是插件的入口点在插件加载时自动调用 private void Awake() { // 初始化日志记录器 Log Logger; // 输出一条日志证明插件已加载 Log.LogInfo($Plugin {PluginName} is loaded!); // 6. 应用Harmony补丁示例下一节详细讲 Harmony.CreateAndPatchAll(typeof(MyFirstPlugin).Assembly); } }[BepInPlugin]属性这是插件的“身份证”BepInEx通过它来识别和管理插件。PluginGUID必须是全局唯一的通常使用“作者.游戏.插件名”的格式。BaseUnityPlugin所有BepInEx插件的基类提供了日志记录器(Logger)、配置(Config)等实用工具。Awake()方法相当于Unity脚本的Start是插件逻辑开始的地方。步骤三编译与部署在Visual Studio中按F6生成项目。成功后在项目的bin/Debug或bin/Release目录下找到MyFirstPlugin.dll。在游戏目录的BepInEx/plugins下创建一个新文件夹例如MyFirstPlugin。将编译好的MyFirstPlugin.dll复制到这个新文件夹里。启动游戏。如果一切正常你将在BepInEx的控制台或LogOutput.log中看到“Plugin My First Plugin is loaded!”这条信息。恭喜你的第一个插件工作了4.3 使用Harmony库进行游戏代码“打补丁”仅仅加载插件还不够我们要修改游戏行为。Harmony库是实现这一点的利器。它允许你在运行时修改其他方法包括游戏本身的方法的IL代码。场景假设《MyDemoGame》中有一个Player类里面有一个Heal方法我们想每次治疗时额外多回复5点生命。找到目标方法你需要使用dnSpy、ILSpy或JetBrains dotPeek等反编译工具打开游戏的Assembly-CSharp.dll找到Player类和Heal方法记下它的完整签名包括命名空间、参数和返回类型。假设它是public void Heal(int amount)。创建补丁类using HarmonyLib; [HarmonyPatch(typeof(Player))] // 指定要修补的类 [HarmonyPatch(nameof(Player.Heal))] // 指定要修补的方法 class PlayerHealPatch { // Prefix补丁在原方法执行前运行 static void Prefix(ref int amount) { // 修改传入的amount参数额外增加5点治疗量 amount 5; MyFirstPlugin.Log.LogInfo($前缀补丁生效治疗量被修改为{amount}); } // Postfix补丁在原方法执行后运行 static void Postfix(Player __instance, int amount) { // __instance是原方法的this对象即当前的Player实例 // 可以在这里做一些后续操作例如播放一个特效音 MyFirstPlugin.Log.LogInfo(${__instance.name} 接受了 {amount} 点治疗。); } }[HarmonyPatch]属性用于标记这个类是一个补丁类并指定目标类和方法。Prefix前缀补丁。在原方法之前执行。你可以修改传入的参数ref关键字甚至通过返回false来阻止原方法执行。Postfix后缀补丁。在原方法之后执行。你可以访问方法的返回值如果有用__result参数、this实例__instance和所有参数。Transpiler更高级的补丁直接修改方法的IL指令用于实现更复杂的修改初学者可暂不深入。应用补丁在你的插件主类Awake方法中确保调用了Harmony.CreateAndPatchAll。这行代码会扫描当前程序集你的插件DLL中所有带有[HarmonyPatch]属性的类并自动应用补丁。测试编译并部署插件进入游戏触发治疗行为。你会在日志中看到补丁输出的信息并且玩家的治疗量确实增加了5点。实操心得使用Harmony时最大的挑战是准确找到你要修改的方法。反编译工具是你的眼睛。务必仔细核对方法的签名包括参数类型和返回类型一个字母的错误都会导致补丁失败。另外慎用Prefix返回false来完全阻止原方法除非你完全清楚原方法的作用否则可能导致游戏状态异常。在Postfix中读取和修改__instance的状态通常是更安全的选择。5. 高级技巧、配置管理与性能优化当你能熟练制作基础插件后以下高级技巧将让你的Mod更加专业和强大。5.1 为你的插件添加可配置项一个优秀的插件应该允许用户自定义其行为。BepInEx内置的ConfigurationManager通常按F1唤出让这变得非常简单。在你的插件类中private void Awake() { Log Logger; // 1. 绑定配置项 // 创建一个新的配置绑定当配置文件中没有对应项时使用默认值。 EnableExtraHealing Config.Bind( General, // 配置节(Section)名称在配置管理器中会分组显示 EnableExtraHealing, // 配置项的键(Key) true, // 默认值 是否启用额外治疗效果 // 描述信息会显示在配置管理器中 ); ExtraHealingAmount Config.Bind( General, ExtraHealingAmount, 5, new ConfigDescription(额外治疗量, new AcceptableValueRangeint(1, 20)) // 定义可接受的值范围1-20 ); // 2. 在补丁中使用配置项 Harmony.CreateAndPatchAll(typeof(MyFirstPlugin).Assembly); } // 3. 声明配置绑定对象 private static ConfigEntrybool EnableExtraHealing; private static ConfigEntryint ExtraHealingAmount; // 4. 修改Harmony补丁使用配置值 [HarmonyPatch(typeof(Player))] [HarmonyPatch(nameof(Player.Heal))] class PlayerHealPatch { static void Prefix(ref int amount) { if (EnableExtraHealing.Value) // 读取配置值 { amount ExtraHealingAmount.Value; Log.LogInfo($前缀补丁生效启用额外治疗治疗量{ExtraHealingAmount.Value}); } } }现在用户可以在游戏中按F1找到你的插件“My First Plugin”在“General”分组下看到两个滑块/复选框并可以实时修改它们。修改会立即生效热重载无需重启游戏。配置文件会自动保存在BepInEx/config/com.yourname.mydemogame.myfirstplugin.cfg。5.2 插件依赖管理与版本控制当你的插件需要其他插件或库才能运行时需要声明依赖。在插件主类上添加[BepInDependency]属性[BepInPlugin(PluginGUID, PluginName, PluginVersion)] [BepInDependency(com.someauthor.anotherplugin, BepInDependency.DependencyFlags.SoftDependency)] // 软依赖没有也能运行但有则功能增强 [BepInDependency(BepInEx.Harmony, BepInDependency.DependencyFlags.HardDependency)] // 硬依赖没有则插件加载失败 public class MyFirstPlugin : BaseUnityPlugin { // ... }HardDependency必须依赖。如果指定的插件通过其GUID识别未加载你的插件将不会加载。SoftDependency可选依赖。你的插件会检查该插件是否存在并据此调整自身行为例如如果某个UI框架存在则使用更高级的UI。版本控制在[BepInPlugin]中定义的PluginVersion字符串以及通过[BepInProcess]属性指定兼容的游戏进程名和版本都是管理兼容性的好习惯。5.3 性能考量与最佳实践避免昂贵的操作在Update、FixedUpdate或频繁调用的Harmony补丁中避免进行复杂的计算、字符串拼接特别是Log.LogInfo、或实例化大量Unity对象GameObject.Instantiate。这些操作每帧执行会迅速拖慢游戏。善用缓存如果需要反复访问某个组件或计算结果将其缓存到字段中。谨慎使用GameObject.Find这是一个相对昂贵的操作。尽量在Start或Awake中查找一次并缓存引用。及时清理如果你创建了新的GameObject或订阅了事件记得在适当的时机如OnDestroy销毁对象或取消订阅防止内存泄漏。日志分级使用Log.LogDebug输出调试信息在发布版本中通过配置文件将日志级别设为Info或更高可以减少日志输出对性能的影响。6. 故障排除与调试指南即使按照指南操作也难免会遇到问题。这里是一份常见问题速查表。问题现象可能原因排查步骤与解决方案游戏无法启动或启动后立即崩溃1. BepInEx版本与游戏不兼容如x86/x64错误。2. 游戏有反作弊或加密。3. 某个插件或依赖损坏。1. 检查LogOutput.log的最后几行错误信息。2. 暂时移除doorstop_config.ini或将其enabled设为false确认游戏本身能运行。3. 清空plugins文件夹逐一重新安装插件定位问题插件。游戏能运行但按F1没反应1. 配置管理器快捷键被修改或禁用。2. BepInEx未成功加载插件。1. 检查BepInEx/config/BepInEx.cfg中[Keyboard]下的ToggleConfigWindow键位设置。2. 查看LogOutput.log确认你的插件是否出现在加载列表中。插件已安装但游戏内无效果1. 插件DLL放置位置错误未放在独立文件夹。2. 插件依赖缺失。3. Harmony补丁的目标方法签名错误。4. 插件代码有逻辑错误或异常被捕获。1. 确认DLL在BepInEx/plugins/插件名/下。2. 查看日志中是否有“Missing dependency”错误。3. 使用反编译工具仔细核对目标方法签名命名空间、类名、方法名、参数类型。4. 查看日志中是否有来自你插件的异常堆栈跟踪。在代码中增加更多Log.LogDebug语句进行跟踪。配置管理器中看不到我的插件选项1. 插件没有使用Config.Bind创建配置项。2. 配置项的Section和Key名称有误。1. 确保在Awake方法中正确调用了Config.Bind。2. 检查配置文件BepInEx/config/你的插件GUID.cfg是否生成。游戏运行一段时间后卡顿或崩溃1. 插件存在内存泄漏未销毁对象、未取消事件订阅。2. 某个补丁或循环逻辑性能开销过大。1. 使用性能分析工具如Unity Profiler如果游戏支持或任务管理器观察内存增长。2. 禁用部分插件定位问题插件。检查插件中是否有在Update中执行重负载操作。调试利器日志系统再次强调LogOutput.log是你最好的朋友。学会阅读它[Info]一般信息如插件加载成功。[Warning]警告可能有问题但不致命。[Error]错误需要关注。[Fatal]致命错误通常导致崩溃。搜索你的插件名或GUID快速定位相关日志。开启Debug级别日志可以获得最详细的信息流。一个真实的排错案例我曾为一个游戏制作Mod补丁始终不生效。日志显示插件已加载但无错误。最后发现我反编译看到的方法签名是public void Heal(int)但实际游戏更新后方法内部实现变了虽然签名没变但我补丁中访问的某个私有字段名已更改导致补丁被静默跳过。解决方案是使用Harmony的Debug模式编译我的插件并在日志中看到“未找到方法”的警告从而意识到问题所在。教训是游戏更新后即使Mod还能加载也需重新验证补丁的目标。