Skip to content
⬅ 返回

🧩 Harmony 补丁整理

本篇是 Harmony 在《缺氧(ONI)》Mod 开发中的实战速查表
👉 目标:快速了解以及什么时候该用哪一种 Patch


📖 阅读指引

  • 基础操作:了解 Harmony 初始化及 Prefix / Postfix 的基本用法。
  • 常用参数:了解 __instance / __state / AccessTools 读写游戏数据。
  • 特殊场景:了解 Transpiler / Finalizer 处理底层逻辑与异常的方式。

🧠 一、Harmony 是什么?

Harmony 是一个 运行时方法注入库,用于在不修改源代码的情况下:

  • 插入逻辑
  • 拦截原方法
  • 修改返回值
  • 重写部分执行流程

🧩 二、Patch 类型总览

Patch 类型执行时机常见用途
Prefix原方法执行前拦截 / 改参数 / 阻止执行
Postfix原方法执行后补充逻辑 / 改返回值
TranspilerIL 层改硬编码 / 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用于修改游戏“读取”到的数据
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 就是专门用来在 PrefixPostfix 之间带货的“临时储物柜”。

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 它的作用

如果你发现 PrefixPostfix 都没法改掉某个逻辑,通常是因为那个逻辑是硬编码在方法中间的。

  • 改常量: 比如代码里写死了一个 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<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.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 修改逻辑过多