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。
核心目标
:Patch
Projectile.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方案,并做好详尽的测试和日志记录。

489

被折叠的 条评论
为什么被折叠?



