Files
oniagent/ONIModTutorial/ONIModTutorial-main/harmony-reference.md
2026-05-30 11:00:36 +08:00

396 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
sidebar: false
---
<a href="javascript:history.back()" class="back-button">
⬅ 返回
</a>
<style scoped>
.back-button {
position: fixed;
left: 20px;
top: 100px;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 15px;
background-color: var(--vp-c-brand);
color: white !important;
border-radius: 20px;
text-decoration: none !important;
font-weight: bold;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: transform 0.2s;
z-index: 100;
cursor: pointer;
}
.back-button:hover {
transform: scale(1.05);
opacity: 0.9;
}
</style>
# 🧩 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; // 原方法体内的代码一行都不会跑,功能被完全“冻结”
}
```
---
<h2 id="postfix">🟩 五、Postfix执行后补充</h2>
`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 里的异步动作、动画序列或那些不是瞬间完成的任务 |
<h3 id="postfix-Overloading">5.3 处理重载</h3>
在 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<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 修改逻辑过多
---