Skip to content

Instantly share code, notes, and snippets.

@gsuberland
Last active July 31, 2024 23:07
Show Gist options
  • Save gsuberland/6b20f735048a15a6bf4518331a420695 to your computer and use it in GitHub Desktop.
Save gsuberland/6b20f735048a15a6bf4518331a420695 to your computer and use it in GitHub Desktop.
C# code to generate Harmony prefix and postfix stubs against a target type
[HarmonyPatch]
class Target_Patch
{
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test1")]
[HarmonyPrefix]
public static void Test1_Prefix(UserQuery.Target __instance)
{
Log.Log("function Test1 is about to do a thing!");
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test1")]
[HarmonyPostfix]
public static void Test1_Postfix(UserQuery.Target __instance)
{
Log.Log("function Test1 did a thing!");
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test2")]
[HarmonyPrefix]
public static void Test2_Prefix(UserQuery.Target __instance)
{
Log.Log("function Test2 is about to do a thing!");
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test2")]
[HarmonyPostfix]
public static System.String Test2_Postfix(UserQuery.Target __instance, System.String __result)
{
Log.Log("function Test2 did a thing!"); return __result;
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test3")]
[HarmonyPrefix]
public static void Test3_Prefix(UserQuery.Target __instance, System.String foo)
{
Log.Log("function Test3 is about to do a thing!");
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test3")]
[HarmonyPostfix]
public static System.String Test3_Postfix(UserQuery.Target __instance, System.String __result, System.String foo)
{
Log.Log("function Test3 did a thing!"); return __result;
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test4")]
[HarmonyPrefix]
public static void Test4_Prefix(UserQuery.Target __instance, System.Collections.Generic.List<System.String> list)
{
Log.Log("function Test4 is about to do a thing!");
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test4")]
[HarmonyPostfix]
public static System.String Test4_Postfix(UserQuery.Target __instance, System.String __result, System.Collections.Generic.List<System.String> list)
{
Log.Log("function Test4 did a thing!"); return __result;
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test5")]
[HarmonyPrefix]
public static void Test5_Prefix(UserQuery.Target __instance, System.String[] array)
{
Log.Log("function Test5 is about to do a thing!");
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test5")]
[HarmonyPostfix]
public static System.Collections.Generic.List<System.String> Test5_Postfix(UserQuery.Target __instance, System.Collections.Generic.List<System.String> __result, System.String[] array)
{
Log.Log("function Test5 did a thing!"); return __result;
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test6")]
[HarmonyPrefix]
public static void Test6_Prefix(UserQuery.Target __instance)
{
Log.Log("function Test6 is about to do a thing!");
}
[HarmonyPatch(typeof(UserQuery.Target), @"UserQuery.Target.Test6")]
[HarmonyPostfix]
public static System.Collections.Generic.Dictionary<System.String, System.Int32> Test6_Postfix(UserQuery.Target __instance, System.Collections.Generic.Dictionary<System.String, System.Int32> __result)
{
Log.Log("function Test6 did a thing!"); return __result;
}
}
public class Target
{
private void Test1()
{
}
private string Test2()
{
return "";
}
private string Test3(string foo)
{
return foo;
}
private string Test4(List<string> list)
{
return list.Count.ToString();
}
public List<string> Test5(string[] array)
{
return array.ToList();
}
public Dictionary<string,int> Test6()
{
return new Dictionary<string,int>();
}
}
void Main()
{
Console.WriteLine(GenerateStubs<Target>());
}
string GenerateStubs<T>()
{
return GenerateStubs(typeof(T));
}
string GenerateStubs(Type type)
{
string prefixStubCode = "Log.Log(\"function $NAME$ is about to do a thing!\");";
string postfixStubCode = "Log.Log(\"function $NAME$ did a thing!\"); return __result;";
string postfixStubVoidCode = "Log.Log(\"function $NAME$ did a thing!\");";
var instanceMethods = type.GetMethods(
BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance |
BindingFlags.DeclaredOnly);
string targetTypeName = GetCSharpTypeName(type);
StringBuilder sb = new StringBuilder();
sb.AppendLine("[HarmonyPatch]");
sb.AppendLine($"class {type.Name}_Patch");
sb.AppendLine("{");
foreach (var method in instanceMethods)
{
// get the return type name as it would be written in C#
var returnTypeName = GetCSharpTypeName(method.ReturnType);
// normally we'd want to special-case special method names like get_Foo back to their property, but since this is for Harmony we actually don't want that
var methodName = method.Name;
// if the method has any args, tack them on to the pre/postfix method args
var methodArgs = string.Join(", ", method.GetParameters().Select(p => GetCSharpTypeName(p.ParameterType) + " " + p.Name));
if (methodArgs.Length > 0)
{
methodArgs = ", " + methodArgs;
}
// prefixes return void
sb.AppendLine($"\t[HarmonyPatch(typeof({targetTypeName}), @\"{targetTypeName}.{methodName}\")]");
sb.AppendLine($"\t[HarmonyPrefix]");
sb.AppendLine($"\tpublic static void {methodName}_Prefix({targetTypeName} __instance{methodArgs})");
sb.AppendLine("\t{");
sb.AppendLine("\t\t" + prefixStubCode.Replace("$NAME$", methodName));
sb.AppendLine("\t}");
sb.AppendLine();
// postfixes may or may not return void
sb.AppendLine($"\t[HarmonyPatch(typeof({targetTypeName}), @\"{targetTypeName}.{methodName}\")]");
sb.AppendLine($"\t[HarmonyPostfix]");
if (returnTypeName == "void")
{
sb.AppendLine($"\tpublic static {returnTypeName} {methodName}_Postfix({targetTypeName} __instance{methodArgs})");
sb.AppendLine("\t{");
sb.AppendLine("\t\t" + postfixStubVoidCode.Replace("$NAME$", methodName));
}
else
{
sb.AppendLine($"\tpublic static {returnTypeName} {methodName}_Postfix({targetTypeName} __instance, {returnTypeName} __result{methodArgs})");
sb.AppendLine("\t{");
sb.AppendLine("\t\t" + postfixStubCode.Replace("$NAME$", methodName));
}
sb.AppendLine("\t}");
sb.AppendLine();
}
sb.AppendLine("}");
sb.AppendLine();
return sb.ToString();
}
static string GetCSharpTypeName(Type t)
{
if (t == null)
{
throw new ArgumentNullException(nameof(t));
}
if (t == typeof(void))
{
return @"void";
}
if (t.IsArray)
{
return GetCSharpTypeName(t.GetElementType()) + "[]";
}
// if the full name is available, we want to use that rather than the simple class name
// we replace + with . to handle nested types
string fullTypeName = (t.FullName ?? t.Name).Replace('+', '.');
if (t.IsGenericType)
{
// .NET provides type names in a format like System.Collections.ObjectModel.ReadOnlyCollection`1[System.String]
// which is not how C# writes generics. we can grab the left side of the ` to get the original name, then the generic args.
string typeName = fullTypeName.Split('`').First();
string typeArgs = string.Join(", ", t.GetGenericArguments().Select(GetCSharpTypeName));
return $"{typeName}<{typeArgs}>";
}
return fullTypeName;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment