appendix:harmonyがある時

SybarisプラグインだけどBepInEx環境で動いていて、HarmonyLib(HarmonyX)が使える場合での Harmony.Patch を呼ぶ方法。
Harmony.Patch が呼べるのであれば、任意のタイミングでパッチあてが行えるので強力な外道行為が行える。

あるかないかわからないHarmonyLib(HarmonyX)を参照するわけにはいかないので1)、リフレクションを駆使してインスタンスを作成する。

// HarmonyLib.Harmonyを探せ!
foreach (var loadedAssembly in AppDomain.CurrentDomain.GetAssemblies())
{
    var typeHarmony = loadedAssembly.GetType("HarmonyLib.Harmony");
    var typeHarmonyMethod = assembly.GetType("HarmonyLib.HarmonyMethod");
    // あったか!?
    if (typeHarmony != null && typeHarmonyMethod != null)
    {
        // あったぞ!!
        var harmony = Activator.CreateInstance(typeHarmony, "GUIDとか");
    }
}

これは簡単でGetMethodしてしまえばいいが、オーバーロードがあるので引数を指定してとる。

if (harmony != null)
{
    // 作れたぞ!!
    var harmonyPatch = typeHarmony.GetMethod("Patch", new Type[]
    {
        typeof(System.Reflection.MethodBase),   // original
        typeHarmonyMethod,                      // prefix
        typeHarmonyMethod,                      // postfix
        typeHarmonyMethod,                      // transpiler
        typeHarmonyMethod,                      // finalizer
        typeHarmonyMethod,                      // ilmanipulator
    });
}

1つめはMethodBaseなので単純にGetMethod取得すれば良い、残りはHarmonyMethod型だがMethodInfoとDelegateからの変換が用意されているのでGetMethod(Delegateがあるのでラムダ式でも可)で解決出来る。

public static implicit operator HarmonyMethod(MethodInfo method) => new(method);
public static implicit operator HarmonyMethod(Delegate @delegate) => new(@delegate);

それぞれ渡すべきプロトタイプは

prefixはパッチを当てるものと同じもの。
postfixはパッチを当てるものの戻り値を受け取る戻りに無しのもの。
finalizerはExceptionを貰ってExceptionを返すもの。
transpilerとilmanipulatorは以下の様なもの。

static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
static void ILManipulator(ILContext il, MethodBase original, ILLabel retLabel)
static void SomeOtherILManipulator(ILContext ctx, MethodBase orig)

渡すときはHarmonyMethod(MethodInfo method)の変換、つまりコンストラクターを呼んで変換させる。

var transpilermethod = typeof(トランスパイラーのあるクラス).GetMethod(トランスパイラーのメソッド名);
var transpiler = Activator.CreateInstance(typeHarmonyMethod, transpiler);

問題が起こりそうなのはTranspilerとILManipulatorのパターンでCodeInstructionがHarmonyの中で、ILLabelがMonoMod.Cliの中で宣言されているので参照出来ない状況だと面倒になる。
※ILContextはMono.CecilなのでSybaris2/BepInExでも参照してしまえば良い。

特に面倒が起こりそうな Transpiler の実装方法を示す。

static object Transpiler(IEnumerable instructions)
{
    var enumerator = instructions?.GetEnumerator();
    if (enumerator != null && enumerator.MoveNext())
    {
        var type = enumerator.Current.GetType();
        var fldOpecode = type.GetField("opcode");
        var fldOperand = type.GetField("operand");
        var newInstructions = Activator.CreateInstance(typeof(List<>).MakeGenericType(type)) as IList;
        foreach (var instruction in instructions)
        {
            var opecode = fldOpecode.GetValue(instruction);
            var operand = fldOperand.GetValue(instruction);
            if (opecode.Equals(変えたいところのOpCode) && operand != null && operand.ToString() == 変えたいところのOperand)
                newInstructions.Add(Activator.CreateInstance(type, 変えたいOpCode, 変えたいOperand));
            else
                newInstructions.Add(instruction);
        }
    }
    return newInstructions;
}

引数をIEnumerableで受けて戻り値をobjectとかで宣言してしまいます。
引数で貰った IEnumerable<CodeInstruction> を書き換えて返していいのかがわからない2)ので、リストを作り直しそこに詰め込んで返しています。

var newInstructions = Activator.CreateInstance(typeof(List<>).MakeGenericType(type)) as IList;
return newInstructions;

こんなのでいいの?って思うかもしれませんが多分これで良いのです。
あとは変えたいところだけ、全部変えたいなら全部詰め直して返せば良いです。
OpCodeはSystem.Reflection.Emit.OpCodeっぽいのでこれを参照すればHarmonyLibの参照はなしでいけます。
ILManipulatorの方もILLabel retLabelをobjectで受けてリフレクションを使えばいけると思います、必要が無い限りILLabelのない方を使えばいいと思います。


1)
いっそ参照してりぞるばで解決しても良いかもだけど
2)
Copilotは書き換えて構わないみたいなことを言いますが…

コメント

コメントを入力:
 
  • appendix/harmonyがある時.txt
  • 最終更新: 2024/12/29 00:31
  • by fumble