Harmonyについて
Harmonyは、実行中のメモリ上のプログラムにパッチを当てて、メソッドの動作を変更したり、戻り値を書き換えたりする事が出来るライブラリです。
主にゲームのMOD作成用途で使用される事が多いようですが、単体テスト等にも使えるようです。
インストール
まずはNugetでLib.Harmonyをインストールします。今回の記事では、2.3.3 を使用することにします。
パッチの当て方
HamonyPatch属性を付与して一括でパッチを当てる方法
Harmony.PatchAll() で、HamonyPatch属性を指定したクラス・メソッドにまとめてパッチを当てる事が出来ます。パッチを当てる対象が常に固定なら、この方法が簡単だと思います。
using HarmonyLib;
using System;
public class Dog
{
public string GetName() => "ハチ";
}
[HarmonyPatch(typeof(Dog))]
public class DogPatch
{
[HarmonyPatch(nameof(Dog.GetName)), HarmonyPostfix]
public static void GetNamePostfix(ref string __result) => __result = "タロウ"; // 戻り値を"タロウ"に書き換え
}
public class Program
{
public static void Main()
{
var harmony = new Harmony("MyPatch");
harmony.PatchAll();
var dog = new Dog();
Console.WriteLine($"犬の名前は{dog.GetName()}です");
Console.ReadLine();
}
}
リフレクションで取得したMethodInfoでパッチを当てる方法
Harmony.Patch() で、リフレクションで取得した個別のメソッドにパッチを当てます。パッチ適用のタイミングや対象を動的に制御したい場合はこちら。
using HarmonyLib;
using System;
public class Dog
{
public string GetName() => "ハチ";
}
public class DogPatch
{
public static void GetNamePostfix(ref string __result) => __result = "タロウ";
}
public class Program
{
public static void Main()
{
var getNameOriginal = typeof(Dog).GetMethod(nameof(Dog.GetName));
var getNamePostfix = typeof(DogPatch).GetMethod(nameof(DogPatch.GetNamePostfix));
var harmony = new Harmony("MyPatch");
harmony.Patch(original: getNameOriginal, postfix: getNamePostfix);
var dog = new Dog();
Console.WriteLine($"犬の名前は{dog.GetName()}です");
Console.ReadLine();
}
}
ファクトリーメソッドで取得したMethodInfoでパッチを当てる方法
引数がMethodBase originalMethod、戻り値がMethodInfoのファクトリーメソッドから、目的の動作をさせるメソッドを返してパッチを当てる方法です。DynamicMethodで作成したメソッド等を使う事も出来るらしいですが、やや敷居は高そうです。
using HarmonyLib;
using System;
using System.Reflection;
public class Dog
{
public string GetName() => "ハチ";
}
public class DogPatch
{
public static MethodInfo GetNamePostfixFactory(MethodBase originalMethod)
{
return typeof(DogPatch).GetMethod(nameof(GetNamePostfix));
}
public static void GetNamePostfix(ref string __result) => __result = "タロウ";
}
public class Program
{
public static void Main()
{
var getNameOriginal = typeof(Dog).GetMethod(nameof(Dog.GetName));
var getNamePostfixFactory = typeof(DogPatch).GetMethod(nameof(DogPatch.GetNamePostfixFactory));
var harmony = new Harmony("MyPatch");
harmony.Patch(original: getNameOriginal, postfix: new HarmonyMethod(getNamePostfixFactory));
var dog = new Dog();
Console.WriteLine($"犬の名前は{dog.GetName()}です");
Console.ReadLine();
}
}
基本的な使い方
戻り値を書き換える
__result は、元メソッドの実行結果が格納される特殊なパラメータで、書き換えるとその値がそのまま呼び出し元に返されます。
下記の例では、Postfixメソッド(元メソッドの後に実行される)で戻り値の書き換えを行っています。
using HarmonyLib;
using System;
public class Dog
{
public string GetName() => "ハチ";
}
[HarmonyPatch(typeof(Dog))]
public class DogPatch
{
// __result に格納された戻り値を "タロウ" に書き換え
[HarmonyPatch(nameof(Dog.GetName)), HarmonyPostfix]
public static void GetNamePostfix(ref string __result) => __result = "タロウ";
}
public class Program
{
public static void Main()
{
var harmony = new Harmony("MyPatch");
harmony.PatchAll();
var dog = new Dog();
Console.WriteLine($"犬の名前は{dog.GetName()}です");
Console.ReadLine();
}
}
犬の名前はタロウです
インスタンスにアクセスする
__instance パラメータは、インスタンスメソッドのインスタンスが格納される特殊なパラメータです。
下記の例では、Prefixメソッド(元メソッドの前に実行される)でプロパティを書き換えています。
using HarmonyLib;
using System;
using System.Reflection;
public class Dog
{
public string Name { get; set; } = "ハチ";
public void DoSomething() => Console.WriteLine($"犬の名前は{Name}です");
}
[HarmonyPatch(typeof(Dog))]
public class DogPatch
{
// DoSomething実行前に、Dog.Name を "タロウ" に書き換え
[HarmonyPatch(nameof(Dog.DoSomething)), HarmonyPrefix]
public static void DoSomethingPrefix(Dog __instance)
=> __instance.Name = "タロウ";
}
public class Program
{
public static void Main()
{
var harmony = new Harmony("MyPatch");
harmony.PatchAll();
var dog = new Dog();
dog.DoSomething();
Console.ReadLine();
}
}
犬の名前はタロウです
メソッドを独自の処理に置き換える
Prefixメソッドの戻り値をboolにし、falseを返す事で元メソッドの処理をスキップする事が出来ます。
下記の例では、x + y を x * y に置き換えています。
using HarmonyLib;
using System;
public class Calc
{
public int Add(int x, int y) => x + y;
}
[HarmonyPatch(typeof(Calc))]
public class CalcPatch
{
// 計算結果を x * y に変更し、元メソッドは実行しない
[HarmonyPatch(nameof(Calc.Add)), HarmonyPrefix]
public static bool AddPrefix(int x, int y, ref int __result)
{
__result = x * y;
return false;
}
}
public class Program
{
public static void Main()
{
var harmony = new Harmony("MyPatch");
harmony.PatchAll();
var calc = new Calc();
Console.WriteLine(calc.Add(5, 10));
Console.ReadLine();
}
}
例で使用した __result と __instance の他にも色々なパラメータが使えますが、公式のドキュメントを参照してください。