Skip to content

Instantly share code, notes, and snippets.

@gotmachine
Created June 10, 2022 11:48
Show Gist options
  • Save gotmachine/791b1da0067dc234b141cdca5f31796b to your computer and use it in GitHub Desktop.
Save gotmachine/791b1da0067dc234b141cdca5f31796b to your computer and use it in GitHub Desktop.
Harmony - Replace a method call in KSP and all loaded plugins
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using MonoMod.Utils;
using UnityEngine;
using Code = Mono.Cecil.Cil.Code;
namespace ShaderReplacer
{
[KSPAddon(KSPAddon.Startup.Instantly, true)]
class ShaderReplacer : MonoBehaviour
{
private static MethodInfo mInfo_ShaderFind_Original;
private static MethodInfo mInfo_ShaderFind_Replacement;
private void Start()
{
string cecilMethodName = "UnityEngine.Shader UnityEngine.Shader::Find(System.String)";
mInfo_ShaderFind_Original = AccessTools.Method(typeof(Shader), nameof(Shader.Find));
mInfo_ShaderFind_Replacement = AccessTools.Method(typeof(ShaderReplacer), nameof(ShaderReplacer.FindShader));
List<MethodBase> callSites = new List<MethodBase>();
// Don't use appdomain, we don't want to accidentally patch Unity itself and this avoid
// having to iterate on the BCL and Unity assemblies.
foreach (AssemblyLoader.LoadedAssembly kspAssembly in AssemblyLoader.loadedAssemblies)
{
if (string.IsNullOrEmpty(kspAssembly.assembly?.Location))
continue;
AssemblyDefinition assemblyDef;
try
{
assemblyDef = AssemblyDefinition.ReadAssembly(kspAssembly.assembly.Location);
if (assemblyDef == null)
throw new FileLoadException($"Couldn't read assembly \"{kspAssembly.assembly.Location}\"");
}
catch (Exception e)
{
Debug.LogWarning($"[ShaderReplacer] Replace failed for assembly {kspAssembly.name}\n{e}");
continue;
}
foreach (ModuleDefinition moduleDef in assemblyDef.Modules)
{
foreach (TypeDefinition typeDef in moduleDef.GetAllTypes())
{
foreach (MethodDefinition methodDef in typeDef.Methods)
{
if (!methodDef.HasBody)
continue;
foreach (Instruction instruction in methodDef.Body.Instructions)
{
if (instruction.OpCode.Code == Code.Call
&& instruction.Operand is MethodReference mRef
&& mRef.FullName == cecilMethodName)
{
MethodBase callSite;
try
{
callSite = methodDef.ResolveReflection();
if (callSite == null)
throw new MemberAccessException();
}
catch (Exception e)
{
Debug.LogWarning($"[ShaderReplacer] Failed to patch method {assemblyDef.Name}::{typeDef.Name}.{methodDef.Name}");
break;
}
callSites.Add(callSite);
break;
}
}
}
}
}
}
Harmony harmony = new Harmony("ShaderReplacer");
MethodInfo callSiteTranspiler = AccessTools.Method(typeof(ShaderReplacer), nameof(ShaderReplacer.CallSiteTranspiler));
foreach (MethodBase callSite in callSites)
{
if (callSite == mInfo_ShaderFind_Replacement)
continue;
Debug.Log($"[ShaderReplacer] Patching call site : {callSite.DeclaringType.Assembly.GetName().Name}::{callSite.DeclaringType}.{callSite.Name}");
harmony.Patch(callSite, null, null, new HarmonyMethod(callSiteTranspiler));
}
}
static IEnumerable<CodeInstruction> CallSiteTranspiler(IEnumerable<CodeInstruction> instructions)
{
foreach (CodeInstruction instruction in instructions)
{
if (instruction.opcode == OpCodes.Call && ReferenceEquals(instruction.operand, mInfo_ShaderFind_Original))
instruction.operand = mInfo_ShaderFind_Replacement;
yield return instruction;
}
}
public static Shader FindShader(string name)
{
Debug.LogError($"[ShaderReplacer] Shader requested : \"{name}\"");
return Shader.Find(name);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment