🧩 Harmony 补丁整理
本篇是 Harmony 在《缺氧(ONI)》Mod 开发中的实战速查表
👉 目标:快速了解以及什么时候该用哪一种 Patch
📖 阅读指引
- 基础操作:了解
Harmony初始化及Prefix / Postfix的基本用法。 - 常用参数:了解
__instance / __state / AccessTools读写游戏数据。 - 特殊场景:了解
Transpiler / Finalizer处理底层逻辑与异常的方式。
🧠 一、Harmony 是什么?
Harmony 是一个 运行时方法注入库,用于在不修改源代码的情况下:
- 插入逻辑
- 拦截原方法
- 修改返回值
- 重写部分执行流程
🧩 二、Patch 类型总览
| Patch 类型 | 执行时机 | 常见用途 |
|---|---|---|
| Prefix | 原方法执行前 | 拦截 / 改参数 / 阻止执行 |
| Postfix | 原方法执行后 | 补充逻辑 / 改返回值 |
| Transpiler | IL 层 | 改硬编码 / if / 常量 |
| Finalizer | 所有逻辑后 | 捕获异常 / 兜底 |
⚙️ 三、Harmony 初始化
在 ONI 中,继承 UserMod2 后,游戏会自动帮你执行 harmony.PatchAll()。你不需要再手动调用它,除非你有特殊的动态补丁需求。
using HarmonyLib;
using KMod;
namespace MyMod
{
public class MyFirstMod : UserMod2
{
public override void OnLoad(Harmony harmony)
{
// 1. 必须保留基类调用,它会处理自动 PatchAll
base.OnLoad(harmony);
// 2. 这里通常用来放“全局初始化”逻辑
// 比如:打印日志确认 Mod 已加载
Debug.Log("MyFirstMod 已成功加载!");
}
}
}🟦 四、Prefix(执行前拦截)
Prefix 用于在原代码跑起来之前先一步执行。
4.1 基础用法:单纯插入逻辑
如果你只想在某个动作发生时顺便做点事(比如记录日志),补丁返回 void 即可。
[HarmonyPatch(typeof(Operational), nameof(Operational.SetActive))]
public static class Log_Patch
{
public static void Prefix(bool value)
{
// 只是看一眼参数,不影响原逻辑执行
Debug.Log($"建筑状态尝试切换为: {value}");
}
}4.2 进阶用法:干预原逻辑
如果你想改变游戏的结果,需要注意 bool 返回值和 ref 关键字的作用。
| 方式 | 代码关键点 | 实际效果 |
|---|---|---|
| 强行篡改 | ref bool value | 改指令。 原代码照样跑,但你偷偷换掉了它的参数(比如让关机指令变成开机) |
| 彻底拦截 | return false | 断后路。 原代码被直接掐断,后面的逻辑(动画、音效、状态改变)全都不再执行 |
案例对比: 我们依然是在[HarmonyPatch(typeof(Operational), nameof(Operational.SetActive))]上打补丁。
// 情况 A:篡改参数(原逻辑继续跑,但用你的数)
public static void Prefix(ref bool value)
{
value = true; // 哪怕玩家点了关闭,建筑也会因为参数被改而保持开启
}
// 情况 B:彻底拦截(原逻辑直接罢工)
public static bool Prefix(bool value, bool force_ignore)
{
return false; // 原方法体内的代码一行都不会跑,功能被完全“冻结”
}🟩 五、Postfix(执行后补充)
Prefix 在原方法执行完毕后运行。它是最推荐的补丁方式,因为它不会中断游戏的正常逻辑,兼容性最好。
5.1 实战代码:追加额外逻辑
- 作用: 在原逻辑跑完后,顺便执行一些你自己的操作。
[HarmonyPatch(typeof(ElectrolyzerConfig), "CreateBuildingDef")]
public class ElectrolyzerCreateBuildingDef_Patch
{
public static void Postfix(ref BuildingDef __result)
{
// 将电解器的功耗改为 1 瓦
__result.EnergyConsumptionWhenActive = 1f;
Debug.Log("电解器的功耗已被修改为 1 瓦");
}
}- 场景: 初始化后的属性修正、添加自定义的组件、触发额外的通知。
- 优势: 无论你的代码写得好不好,原版代码都已经安全跑完了,不会导致核心功能丢失。
5.2 修改返回值:修改计算结果
- 作用: 劫持原方法的返回值,让游戏拿到你“调包”后的结果。
[HarmonyPatch(typeof(Overheatable), nameof(Overheatable.OverheatTemperature), MethodType.Getter)]
public static class Overheatable_OverheatTemperature_Patch
{
// ref __result 指向原方法算出来的那个返回值
public static void Postfix(ref float __result)
{
__result = 9999f; // 让建筑永远不会因为温度过高而损坏(强行改掉判定数值)
}
}| 枚举项 | 怎么认出它(看 ILSpy/dnSpy) | 干嘛的 |
|---|---|---|
| Normal | 方法名后面带个括号,比如 OnSpawn() | 在某个动作执行前后“插一脚” |
| Getter | 代码里长这样:float Speed | 用于修改游戏“读取”到的数据 |
| Setter | 代码里长这样:set | 用于在游戏尝试修改某个数值时进行拦截 |
| Constructor | 跟类名长得一模一样的方法 | 在对象刚创建、还没放进世界时,提前修改它的默认字段或初始化逻辑 |
| StaticConstructor | 前面带 static 的构造函数 | 专门用来修改游戏加载时就定死的全局静态常量 |
| Enumerator | 里面有一堆 yield return 的方法 | 专门对付 ONI 里的异步动作、动画序列或那些不是瞬间完成的任务 |
5.3 处理重载
在 ONI 源码中,经常会出现多个方法名相同但参数不同的方法。如果不指定参数类型,Harmony 会报错。案例:拦截 ModUtil.AddBuildingToPlanScreen 这个方法在游戏源码中有多个重载版本,我们对比来看:
public static class ModUtil
{
// 重载 A:只有 2 个参数
public static void AddBuildingToPlanScreen(HashedString category, string building_id) { ... }
// 重载 B:有 3 个参数
public static void AddBuildingToPlanScreen(HashedString category, string building_id, string subcategoryID) { ... }
// 重载 c:有 5 个参数
public static void AddBuildingToPlanScreen(HashedString category, string building_id, string subcategoryID, string relativeBuildingId, ModUtil.BuildingOrdering ordering = ModUtil.BuildingOrdering.After) { ... }
}
// 【对应的 HarmonyPatch 写法】
// 拦截“重载 B”,必须明确写出那 3 个参数的类型:
[HarmonyPatch(typeof(ModUtil), "AddBuildingToPlanScreen", new Type[] { typeof(HashedString), typeof(string), typeof(string) })]
// 试试举一反三 重载a会是怎么样的?
// ❌ 错误写法
[HarmonyPatch(typeof(ModUtil), "AddBuildingToPlanScreen")]🛠️ 总结:Prefix 与 Postfix 怎么选?
选 Prefix 的唯一理由:你需要在事情发生之前拦住它(比如阻止一个动作,或者在原逻辑使用参数前改掉参数)。
其他情况全选 Postfix:只要不需要拦截原逻辑,就往原逻辑后面接代码。这是保持 Mod 稳定、不坏档的金法则。
🔁 六、状态桥接(__state 传值)
有时候你需要在执行之后知道执行之前发生了什么(比如:存货前有多少,存货后剩多少)。__state 就是专门用来在 Prefix 和 Postfix 之间带货的“临时储物柜”。
6.1 实战用法:前后状态对比
- 作用: 记录一个初始值,等原逻辑跑完后再拿出来算差值。
[HarmonyPatch(typeof(Storage), "Store")]
public static class Storage_Monitor_Patch
{
// 1. 在执行前,把当前帧数塞进 __state 储物柜
public static void Prefix(out int __state)
{
__state = Time.frameCount;
}
// 2. 在执行后,从 __state 储物柜拿出刚才存的帧数
public static void Postfix(int __state)
{
int cost = Time.frameCount - __state;
if (cost > 0)
{
Debug.Log($"这次存货居然花了 {cost} 帧的时间!");
}
}
}💡 为什么用它?
- 性能追踪: 记录方法运行前后的时间差。
- 逻辑判定: 记录执行前的数值(比如生命值),执行后发现数值没变,就触发补救逻辑。
- 简洁性: 它只在这一次补丁调用中有效,跑完就销毁,不会像全局变量那样污染你的代码。
🔑 参数细节说明(避坑)
- Prefix 里: 必须用 out 或者 ref(比如 out int __state),否则存不进去。
- Postfix 里: 直接用(比如 int __state)即可拿到之前存的值。
🔓 七、访问 private 字段(AccessTools与 ___)
在 ONI 源码中,很多变量被声明为 private 你直接用 __instance 变量名 是访问不到的。这时有两种常用的“破门”方法。
7.1 方法一:三下划线暗号(最快、最推荐)
如果只是想简单读写一个实例变量,Harmony 提供了一个极简写法:在参数名前加 三个下划线。
[HarmonyPatch(typeof(Storage), "OnSpawn")]
public static class Storage_Easy_Patch
{
// ___capacityKg 对应 Storage 类里的私有变量 capacityKg
public static void Postfix(Storage __instance, ref float ___capacityKg)
{
___capacityKg = 9999f; // 直接像改普通变量一样改掉它
}
}7.2 方法二:使用 AccessTools(性能更好、更强大)
当你需要在多个地方频繁访问同一个变量,或者要处理 static(静态)私有变量时,建议先提前定义一个“后门”。
using System.Reflection;
public static class Storage_Hard_Patch
{
// 1. 先定义后门(只找一次,性能更优)
private static readonly FieldInfo CapacityField =
AccessTools.Field(typeof(Storage), "capacityKg");
[HarmonyPatch(typeof(Storage), "OnSpawn")]
public static class Patch
{
public static void Postfix(Storage __instance)
{
// 2. 暴力写入新值
CapacityField.SetValue(__instance, 9999f);
}
}
}💡 怎么选?
- 三下划线 (
___):日常主力。 只要能跑通,就优先用它,代码短且不容易写错。 - AccessTools:重型工具。
- 专门对付
static private(静态私有)变量。 - 适合在补丁函数体外面拿取数据。
- 如果你需要极致的性能优化,提前定义一个
static readonly FieldInfo会比三下划线快那么一点点。
- 专门对付
🧬 Transpiler(改底层指令)
这是 Harmony 中最硬核的补丁。
- 它不是在方法前后插话,而是直接冲进原代码内部,把某几行代码“抠掉”或者“掉包”。
8.1 它的作用
如果你发现 Prefix 和 Postfix 都没法改掉某个逻辑,通常是因为那个逻辑是硬编码在方法中间的。
- 改常量: 比如代码里写死了一个
if (speed > 10f),你想把10f改成100f。 - 删代码: 比如你想把原版中某一行烦人的检测逻辑彻底抹除。
- 加指令: 在方法的第 50 行和第 51 行中间塞进你自己的逻辑。
8.2 实战代码:修改硬编码数值
- 场景: 游戏原本规定挖矿只掉一半掉落物
0.5f,我们通过补丁把它改成全掉1.0f。
[HarmonyPatch(typeof(WorldDamage), "OnDigComplete")]
public static class WorldDamage_OnDigComplete_Patch
{
// Transpiler 接收的是一组 IL 指令集
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
foreach (var ins in instructions)
{
// 目标:找到指令是“加载浮点数 0.5”的那一行
if (ins.opcode == OpCodes.Ldc_R4 && (float)ins.operand == 0.5f)
{
// 改成 1.0f
ins.operand = 1.0f;
}
yield return ins;
}
}
}⚠️ 为什么说要慎用?
- 报错难找: 普通的补丁写错,通常只是功能失效;
Transpiler写错,可能会导致游戏直接闪退或整个类崩掉。 - 极易冲突: 如果有两个 Mod 同时对同一个方法的同一行代码动了“手术”,游戏大概率会当场报废。
- 门槛高: 你需要看懂 IL(中间语言) 指令(类似于
ldarg.0callstfld等),就像在看天书。
🧯 九、Finalizer(异常兜底)
[HarmonyPatch(typeof(SomeClass), "SomeMethod")]
public static class SomeMethod_Finalizer
{
public static void Finalizer(Exception __exception)
{
if (__exception != null)
{
Debug.LogError(__exception);
}
}
}⚠️ 十、ONI Mod 常见翻车点
- Patch 方法忘记
static - 参数签名与原方法不一致
- Prefix
return false但没处理返回值 - Transpiler 修改逻辑过多