1. 项目概述:为什么需要Harmony库?
如果你玩过《边缘世界》(Rimworld),并且尝试过自己动手写Mod,那你肯定遇到过这样的困境:游戏的核心代码是编译好的DLL,你没法直接修改。你想给一个原版的工作台添加新的交互选项,或者想改变小人(Pawn)的某个行为逻辑,但原方法被封装得严严实实。这时候,传统的继承、接口实现可能都派不上用场,你需要一种更“外科手术”式的方法——直接修改游戏运行时内存中的代码。这就是Harmony库大显身手的地方。
Harmony是一个强大的.NET库,它允许你在不接触原始程序集源代码的情况下,对已编译的C#方法进行动态的“打补丁”(Patch)。你可以前置(Prefix)、后置(Postfix)或完全替换(Transpiler)目标方法的执行逻辑。对于Rimworld Mod开发者来说,这几乎是实现复杂功能修改、修复原版Bug或与其他Mod兼容的必备技能。它让你从“遵守游戏规则”的Modder,变成了能在一定程度上“定义游戏规则”的开发者。本指南将带你深入Harmony的核心,不止于简单的属性标签使用,而是理解其原理,并掌握实现动态、灵活Patch的高级技巧。
2. Harmony核心机制深度解析
要玩转Harmony,不能只停留在[HarmonyPatch]和[HarmonyPostfix]这几个属性上。你需要理解它底层在做什么。
2.1 IL指令与运行时修补原理
C#代码最终会被编译为中间语言(IL)指令。一个方法在内存中就是一系列IL指令的有序集合。Harmony的核心工作,就是在目标方法被JIT编译成本地代码之前,修改其IL指令流。
前缀(Prefix):在目标方法执行前运行。它可以访问并修改目标方法的参数,甚至可以通过返回false来完全阻止原始方法的执行。想象成在函数入口处设了一个检查站。后缀(Postfix):在目标方法执行后运行。无论原始方法正常返回还是抛出异常,它都会执行。它可以访问方法的参数、返回值(__result)以及可能抛出的异常(__exception)。这就像在函数出口处设了一个记录员或清理工。变织器(Transpiler):这是最强大也最复杂的Patch类型。它不直接运行逻辑,而是接收并返回一个IEnumerable<CodeInstruction>集合,即方法的IL指令列表。你可以在这个层级上对指令进行增、删、改。比如,你可以把一条call指令(调用某个方法)替换成调用你自己的方法,或者插入一段全新的条件判断逻辑。这相当于直接重写了方法的“源代码”(IL层面)。
2.2 Harmony实例与Patch过程的生命周期
很多教程只教了静态Patch(通过属性声明),但动态Patch才是灵活性的关键。这一切始于一个Harmony实例。
Harmony harmony = new Harmony("com.yourname.awesome.mod");这个ID必须是全局唯一的,通常用反向域名格式,这是Harmony管理不同Mod Patch的基础。当你调用harmony.PatchAll()时,它会扫描当前程序集所有带有[HarmonyPatch]属性的类,并自动应用Patch。这是静态方式。
动态Patch则更精细:
// 获取目标方法 MethodBase targetMethod = AccessTools.Method(typeof(SomeGameClass), "SomeMethod", new Type[] { typeof(int), typeof(string) }); // 获取你自己的补丁方法 MethodInfo prefix = SymbolExtensions.GetMethodInfo(() => MyPrefixMethod()); // 应用Patch harmony.Patch(targetMethod, new HarmonyMethod(prefix));动态Patch让你可以在游戏运行时,根据条件(如其他Mod是否加载、游戏难度等)决定是否应用某个Patch,或者应用不同版本的Patch,这是构建复杂、可配置Mod系统的基石。
3. 从静态到动态:高级Patch策略实战
掌握了原理,我们来看如何在实际的Rimworld Mod中运用动态Patch策略。
3.1 条件化Patch应用
假设你的Mod添加了一个“心理学”系统,你想修改小人心情计算逻辑,但前提是玩家没有安装另一个也修改此逻辑的知名Mod“Psychology”(假设)。硬编码Patch会导致冲突或功能异常。动态Patch可以优雅解决。
public class MyMod : Mod { public static Harmony harmony; public override void DoPatches() { harmony = new Harmony("com.myname.psychologyOverhaul"); MethodBase targetMethod = AccessTools.Method(typeof(Pawn), "get_MindState"); if (targetMethod == null) return; // 检查其他Mod是否已加载 ModMetaData otherMod = ModLister.GetModWithIdentifier("psychology.avilmask"); if (otherMod == null || !otherMod.Active) { // 只有目标Mod未加载时,才应用我们的Patch MethodInfo myPostfix = SymbolExtensions.GetMethodInfo(() => PawnMindState_Postfix(ref Pawn __instance, ref CachedMentalState __result)); harmony.Patch(targetMethod, postfix: new HarmonyMethod(myPostfix)); Log.Message("[MyMod] Psychology not detected, applied custom mind state patch."); } else { Log.Message("[MyMod] Psychology mod detected, skipped conflicting patch to ensure compatibility."); } } }注意:
ModLister.GetModWithIdentifier是Rimworld提供的API,用于检查Mod加载状态。动态Patch的关键在于将Patch逻辑从类属性转移到你的代码控制流中。
3.2 运行时Patch替换与移除
更高级的场景是,你的Mod可能有不同的“模式”或“版本”的Patch。例如,一个“硬核模式”需要更严厉的惩罚逻辑。你可以在游戏设置更改时,动态替换Patch。
public static HarmonyMethod currentPostfix; public static void ApplyEasyModePatch() { MethodBase targetMethod = AccessTools.Method(typeof(IncidentWorker), "TryExecuteWorker"); MethodInfo easyPostfix = SymbolExtensions.GetMethodInfo(() => IncidentWorker_EasyPostfix(ref bool __result)); harmony.Patch(targetMethod, postfix: new HarmonyMethod(easyPostfix)); currentPostfix = new HarmonyMethod(easyPostfix); } public static void SwitchToHardMode() { if (currentPostfix != null) { // 首先,需要移除旧的Patch。Harmony提供了Unpatch方法。 // 但更常见的做法是,我们设计Postfix时内部判断模式,或者直接重新Patch(Harmony的Patch是幂等的,但明确卸载更清晰)。 // 查找所有由我们实例应用的、针对此方法的、特定补丁方法的Patch。 var original = Harmony.GetOriginalMethod(currentPostfix); harmony.Unpatch(original, currentPostfix.method); } MethodBase targetMethod = AccessTools.Method(typeof(IncidentWorker), "TryExecuteWorker"); MethodInfo hardPostfix = SymbolExtensions.GetMethodInfo(() => IncidentWorker_HardPostfix(ref bool __result)); harmony.Patch(targetMethod, postfix: new HarmonyMethod(hardPostfix)); currentPostfix = new HarmonyMethod(hardPostfix); }实操心得:直接调用
harmony.Unpatch需要非常小心,确保你只移除了自己的Patch。一个更安全的设计模式是,在统一的补丁方法内部,通过一个静态变量(如ModSettings.difficultyMode)来决定执行哪段逻辑,从而避免频繁的Patch增删,性能更好,也更稳定。
3.3 使用Transpiler进行精细手术
当Prefix和Postfix无法满足需求时,比如你需要修改方法内部的某个局部变量,或者在循环体内插入逻辑,Transpiler是唯一选择。以修改Rimworld中食物中毒计算为例:
假设原方法FoodUtility.GetFoodPoisonChanceFactor内部有一个基于厨师烹饪技能的计算公式,你想为你的“美食家”特质添加一个乘数。
[HarmonyPatch(typeof(FoodUtility), nameof(FoodUtility.GetFoodPoisonChanceFactor))] static class Patch_FoodUtility_GetFoodPoisonChanceFactor { static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator) { var codes = new List<CodeInstruction>(instructions); bool found = false; // 寻找存储最终概率因子到局部变量或返回的指令位置 // 这需要借助dnSpy等反编译工具查看原方法IL for (int i = 0; i < codes.Count; i++) { // 假设我们找到了一条将最终结果(float类型)存储到局部变量0的指令:stloc.0 // 并且在这条指令之后,是返回这个局部变量的逻辑。 if (codes[i].opcode == OpCodes.Stloc_0) // 这只是示例,实际IL需分析 { // 在存储之后,返回之前,插入我们的自定义逻辑 // 1. 加载局部变量0(最终因子) codes.Insert(i + 1, new CodeInstruction(OpCodes.Ldloc_0)); // 2. 调用我们的调整方法 codes.Insert(i + 2, CodeInstruction.Call(typeof(Patch_FoodUtility_GetFoodPoisonChanceFactor), nameof(ApplyGourmetTraitFactor))); // 3. 将调整后的结果存回局部变量0 codes.Insert(i + 3, new CodeInstruction(OpCodes.Stloc_0)); found = true; Log.Message("Transpiler successfully injected gourmet trait factor."); break; } } if (!found) { Log.Error("Failed to find injection point in GetFoodPoisonChanceFactor transpiler!"); } return codes; } static float ApplyGourmetTraitFactor(float baseFactor) { // 如果当前活动的厨师Pawn有“美食家”特质,降低50%食物中毒几率 if (Find.CurrentMap != null && FoodUtility.lastMealCooker != null && FoodUtility.lastMealCooker.story?.traits?.HasTrait(MyDefOf.Gourmet) == true) { return baseFactor * 0.5f; } return baseFactor; } }重要提示:编写Transpiler是Harmony中最易出错的部分。你必须极其精确地理解目标方法的IL结构。强烈建议使用
Harmony.DEBUG = true;开启调试模式,并使用FileLog.Log输出修补前后的IL代码进行对比验证。一个错误的指令索引或操作码就可能导致游戏崩溃。
4. 调试、兼容性与性能优化
给运行中的代码打补丁,调试和确保稳定性是重中之重。
4.1 高效的调试与日志记录
- 开启Harmony调试:在Mod初始化时设置
Harmony.DEBUG = true;。这会让Harmony输出详细的日志到HarmonyFileLog.log,位于游戏根目录。你可以看到每个Patch应用的详细过程,以及Transpiler修改前后的IL代码对比。 - 条件编译与日志级别:在你的Mod代码中使用
#if DEBUG预处理指令来包裹详细的日志输出,在发布版本中关闭它们以避免日志 spam 影响性能。[HarmonyPostfix] public static void SomePostfix() { #if DEBUG Log.Message($"[MyMod DEBUG] Postfix called at {DateTime.Now:T}"); #endif // ... 实际逻辑 } - 使用Rimworld的
Log类:Log.Message,Log.Warning,Log.Error是好朋友。在Patch方法的关键分支和异常捕获块中合理使用。
4.2 处理Mod冲突与优先级
多个Mod Patch同一个方法是常态。Harmony使用优先级和[HarmonyBefore]、[HarmonyAfter]属性来管理执行顺序。
- 优先级(priority):在
[HarmonyPatch]或HarmonyMethod构造函数中设置。数字越小,优先级越高。同类型Patch(如多个Postfix)默认按优先级顺序执行。 - Before/After:更声明式地指定顺序。
[HarmonyBefore("other.mod.id")]确保你的Patch在指定ID的Mod的Patch之前运行。
最佳实践:对于修改核心游戏机制的Patch,尽量将优先级设为较低(数字较大),作为“最终调整者”。对于提供基础数据的Patch,优先级可以较高。同时,积极在Mod描述页面或社区(如GitHub)声明你Patch了哪些方法,方便其他Modder协调。
4.3 Patch性能考量
每一次方法调用,如果被多个Patch装饰,都会产生额外的调用开销。虽然对于大多数方法这微不足道,但对于每帧调用成千上万次的核心方法(如Tick,Update),不当的Patch会成为性能杀手。
优化建议:
- 减少不必要的Patch:仔细评估是否真的需要Patch。能否用事件(如果游戏提供)、覆写(Override)或监听器模式实现?
- 轻量级Patch逻辑:在Prefix/Postfix中避免复杂的计算、频繁的内存分配(如
new List<T>())和昂贵的查找(如Find.MapEverywhere)。将结果缓存起来。 - 使用Transpiler进行内联优化:有时,与其用一个Postfix来修正返回值,不如用Transpiler直接修改原方法中的一两条计算指令,避免额外的方法调用开销。
- 条件执行:在Patch方法开头进行快速的条件检查,如果条件不满足立即返回,跳过主要逻辑。
[HarmonyPrefix] public static bool SomePrefix(ref Pawn __instance) { // 快速失败:如果pawn为空或已死亡,不执行任何操作,并让原方法继续 if (__instance == null || __instance.Dead) { return true; // 继续执行原方法 } // ... 否则执行复杂的逻辑 }
5. 实战:构建一个动态配置的伤害调整Mod
让我们综合以上知识,创建一个允许玩家通过Mod设置动态调整所有武器伤害的Mod。
核心目标:PatchProjectile.GetDamageAmount方法,根据配置的全局乘数调整伤害值。
步骤:
- 创建Mod和设置类:使用Rimworld的
ModSettings基类创建一个可保存的配置类,包含一个伤害乘数字段。 - 条件化动态Patch:在Mod初始化时,读取配置。如果乘数不等于1.0(默认),则应用Patch;否则不应用,实现零开销。
- 实现Transpiler:在
GetDamageAmount方法返回最终伤害的IL指令前,插入一段加载配置乘数并进行乘法运算的指令。 - 提供热重载(可选):通过游戏内的设置窗口修改乘数后,可以调用一个方法,重新应用Transpiler(或通过一个静态变量让Patch逻辑即时生效)。
关键代码片段(Transpiler部分):
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { var field = AccessTools.Field(typeof(MyModSettings), nameof(MyModSettings.GlobalDamageMultiplier)); foreach (var instr in instructions) { yield return instr; // 假设在原方法中,计算出的伤害值被加载到评估栈顶,然后准备返回(ret) // 我们需要在ret之前插入乘操作。 // 这需要精确分析原IL。这里是一个概念性示例: if (instr.opcode == OpCodes.Ldloc_2 && SomeConditionToFindDamageValue()) // 找到加载最终伤害到栈的指令 { // 加载配置的乘数 yield return new CodeInstruction(OpCodes.Ldsfld, field); // 执行乘法 (float * float) yield return new CodeInstruction(OpCodes.Mul); } } }配置联动:
public class MyMod : Mod { public static MyModSettings settings; public static Harmony harmony; private static bool isPatched = false; public override void DoSettingsWindowContents(Rect inRect) { base.DoSettingsWindowContents(inRect); // 绘制一个滑块,用于调整GlobalDamageMultiplier float oldMultiplier = settings.GlobalDamageMultiplier; settings.GlobalDamageMultiplier = Widgets.HorizontalSlider(..., oldMultiplier, 0.5f, 2.0f); if (Math.Abs(oldMultiplier - settings.GlobalDamageMultiplier) > 0.01f) { // 设置改变,更新Patch状态 UpdateDamagePatch(); settings.Write(); // 保存设置 } } private static void UpdateDamagePatch() { MethodBase targetMethod = AccessTools.Method(typeof(Projectile), "GetDamageAmount"); if (targetMethod == null) return; if (Math.Abs(settings.GlobalDamageMultiplier - 1.0f) < 0.01f) { // 乘数约为1,移除Patch以减少开销 if (isPatched) { harmony.Unpatch(targetMethod, HarmonyPatchType.All, harmony.Id); isPatched = false; Log.Message("Damage multiplier is 1.0, patch removed."); } } else { // 需要应用或重新应用Patch if (!isPatched) { harmony.Patch(targetMethod, transpiler: new HarmonyMethod(typeof(DamagePatch), nameof(DamagePatch.Transpiler))); isPatched = true; Log.Message($"Damage multiplier set to {settings.GlobalDamageMultiplier}, patch applied."); } // 如果已经Patch,由于Transpiler内部读取静态设置,修改会自动生效,无需重新Patch } } }这个例子展示了如何将动态Patch、条件化应用、性能考虑(乘数为1时卸载)和用户配置紧密结合,构建出一个专业、高效的Mod系统。记住,强大的能力意味着重大的责任。滥用Harmony可能导致游戏不稳定和难以排查的Mod冲突。始终追求最简洁、最兼容的Patch方案,并做好详尽的测试和日志记录。