--- sidebar: false --- ⬅ 返回 # 🧩 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()。你不需要再手动调用它,除非你有特殊的动态补丁需求。 ```csharp 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 即可。 ```csharp [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))]`上打补丁。 ```csharp // 情况 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 实战代码:追加额外逻辑 - 作用: 在原逻辑跑完后,顺便执行一些你自己的操作。 ```csharp [HarmonyPatch(typeof(ElectrolyzerConfig), "CreateBuildingDef")] public class ElectrolyzerCreateBuildingDef_Patch { public static void Postfix(ref BuildingDef __result) { // 将电解器的功耗改为 1 瓦 __result.EnergyConsumptionWhenActive = 1f; Debug.Log("电解器的功耗已被修改为 1 瓦"); } } ``` - 场景: 初始化后的属性修正、添加自定义的组件、触发额外的通知。 - 优势: 无论你的代码写得好不好,原版代码都已经安全跑完了,不会导致核心功能丢失。 --- ### 5.2 修改返回值:修改计算结果 - 作用: 劫持原方法的返回值,让游戏拿到你“调包”后的结果。 ```csharp [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 { get; } | 用于修改游戏“读取”到的数据 | | **Setter** | 代码里长这样:set { ... } | 用于在游戏尝试修改某个数值时进行拦截 | | **Constructor** | 跟类名长得一模一样的方法 | 在对象刚创建、还没放进世界时,提前修改它的默认字段或初始化逻辑 | | **StaticConstructor** |前面带 static 的构造函数 | 专门用来修改游戏加载时就定死的全局静态常量 | | **Enumerator** | 里面有一堆 yield return 的方法 | 专门对付 ONI 里的异步动作、动画序列或那些不是瞬间完成的任务 |

5.3 处理重载

在 ONI 源码中,经常会出现多个方法名相同但参数不同的方法。如果不指定参数类型,Harmony 会报错。 **案例:拦截 `ModUtil.AddBuildingToPlanScreen`** 这个方法在游戏源码中有多个重载版本,我们对比来看: ```csharp 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 实战用法:前后状态对比 - 作用: 记录一个初始值,等原逻辑跑完后再拿出来算差值。 ```csharp [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 提供了一个极简写法:在参数名前加 三个下划线。 ```csharp [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`(静态)私有变量时,建议先提前定义一个“后门”。 ```csharp 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`。 ```csharp [HarmonyPatch(typeof(WorldDamage), "OnDigComplete")] public static class WorldDamage_OnDigComplete_Patch { // Transpiler 接收的是一组 IL 指令集 private static IEnumerable Transpiler(IEnumerable 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.0` `call` `stfld` 等),就像在看天书。 --- ## 🧯 九、Finalizer(异常兜底) ```csharp [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 修改逻辑过多 ---