ONI Agent Bridge - initial mod implementation
This commit is contained in:
395
ONIModTutorial/ONIModTutorial-main/harmony-reference.md
Normal file
395
ONIModTutorial/ONIModTutorial-main/harmony-reference.md
Normal file
@ -0,0 +1,395 @@
|
||||
---
|
||||
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 修改逻辑过多
|
||||
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user