2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[C#] Harmonyでメソッドの動作を変更する

Last updated at Posted at 2024-09-23

Harmonyについて

Harmonyは、実行中のメモリ上のプログラムにパッチを当てて、メソッドの動作を変更したり、戻り値を書き換えたりする事が出来るライブラリです。
主にゲームのMOD作成用途で使用される事が多いようですが、単体テスト等にも使えるようです。

インストール

まずはNugetでLib.Harmonyをインストールします。今回の記事では、2.3.3 を使用することにします。

NuGet

パッチの当て方

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 の他にも色々なパラメータが使えますが、公式のドキュメントを参照してください。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?