Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MonoModder support multi mods patch same method #208

Open
wants to merge 5 commits into
base: reorganize
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix TypeLoadException if ctor has been repatched
  • Loading branch information
MikiraSora committed Dec 30, 2024
commit 9497df810e7c47b213371edf3e4a22b402d87ebd
111 changes: 64 additions & 47 deletions src/MonoMod.Patcher/MonoModder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ public MonoModder()
Strict = Environment.GetEnvironmentVariable("MONOMOD_STRICT") == "1";
MissingDependencyThrow = Environment.GetEnvironmentVariable("MONOMOD_DEPENDENCY_MISSING_THROW") != "0";
RemovePatchReferences = Environment.GetEnvironmentVariable("MONOMOD_DEPENDENCY_REMOVE_PATCH") != "0";
CombineSameMethodMultiModPatches = Environment.GetEnvironmentVariable("MONOMOD_COMBINE_MORE_MODS_PATCH_SAME_METHOD") == "1";

var envDebugSymbolFormat = Environment.GetEnvironmentVariable("MONOMOD_DEBUG_FORMAT");
if (envDebugSymbolFormat != null)
Expand Down Expand Up @@ -1741,59 +1742,75 @@ public virtual MethodDefinition PatchMethod(TypeDefinition targetType, MethodDef

if (existingMethod != null)
{
//check if existingMethod also patched by other mods
//check if multi mods patch same method
MethodDefinition modOrigMethod = method.DeclaringType.FindMethod(method.GetID(type: typeName, name: method.GetOriginalName()));
if (CombineSameMethodMultiModPatches && modOrigMethod != null && alreadyPatched)
if (modOrigMethod != null && alreadyPatched)
{
string PickPatchedMethodNewName(int idx = 0)
if (CombineSameMethodMultiModPatches)
{
var name = $"patched_{(idx>0?($"{idx}_"):string.Empty)}{existingMethod.Name}";
if (targetType.Methods.Any(x=>x.Name == name))
return PickPatchedMethodNewName(idx + 1);
return name;
}
var patchedOrigMethod = new MethodDefinition(PickPatchedMethodNewName(), existingMethod.Attributes, Module.TypeSystem.Void);
patchedOrigMethod.MetadataToken = GetMetadataToken(TokenType.Method);
patchedOrigMethod.CallingConvention = existingMethod.CallingConvention;
patchedOrigMethod.ExplicitThis = existingMethod.ExplicitThis;
patchedOrigMethod.MethodReturnType = existingMethod.MethodReturnType;
patchedOrigMethod.Attributes = existingMethod.Attributes;
patchedOrigMethod.ImplAttributes = existingMethod.ImplAttributes;
patchedOrigMethod.SemanticsAttributes = existingMethod.SemanticsAttributes;
patchedOrigMethod.DeclaringType = targetType;
patchedOrigMethod.ReturnType = existingMethod.ReturnType;
//methodbody has been patched and debug information was broken and unavaliable.
//todo: maybe we could rebuild dbg info by something tools?
patchedOrigMethod.Body = existingMethod.Body.Clone(patchedOrigMethod, resolveDebugInformation:false);
patchedOrigMethod.PInvokeInfo = existingMethod.PInvokeInfo;
patchedOrigMethod.IsPInvokeImpl = existingMethod.IsPInvokeImpl;

foreach (GenericParameter genParam in existingMethod.GenericParameters)
patchedOrigMethod.GenericParameters.Add(genParam.Clone());

foreach (ParameterDefinition param in existingMethod.Parameters)
patchedOrigMethod.Parameters.Add(param.Clone());

foreach (CustomAttribute attrib in existingMethod.CustomAttributes)
patchedOrigMethod.CustomAttributes.Add(attrib.Clone());

foreach (MethodReference @override in existingMethod.Overrides)
patchedOrigMethod.Overrides.Add(@override);

patchedOrigMethod.CustomAttributes.Add(new CustomAttribute(GetMonoModAddedCtor()));

//replace call orig_() and redirect to patched__()
foreach (var inst in method.Body.Instructions)
{
if (inst.OpCode == OpCodes.Call && inst.Operand == modOrigMethod)
/*
* we have to move preview patched method to a new method named patched_()
* and enumerate next patched method instructions, find instructions which call orig_(), redirect their operand to patched_()
*/
string PickPatchedMethodNewName(int idx = 0)
{
//redirect to patched__()
inst.Operand = patchedOrigMethod;
var idxPart = idx > 0 ? ("_"+idx) : string.Empty;
var newName = $"patched_{existingMethod.Name}{idxPart}";
if (targetType.Methods.Any(x=>x.Name == newName))
return PickPatchedMethodNewName(idx + 1);
return newName;
}
var patchedOrigMethod = new MethodDefinition(PickPatchedMethodNewName(), existingMethod.Attributes, Module.TypeSystem.Void);
patchedOrigMethod.MetadataToken = GetMetadataToken(TokenType.Method);
patchedOrigMethod.CallingConvention = existingMethod.CallingConvention;
patchedOrigMethod.ExplicitThis = existingMethod.ExplicitThis;
patchedOrigMethod.MethodReturnType = existingMethod.MethodReturnType;
patchedOrigMethod.Attributes = existingMethod.Attributes;
patchedOrigMethod.ImplAttributes = existingMethod.ImplAttributes;
patchedOrigMethod.SemanticsAttributes = existingMethod.SemanticsAttributes;
patchedOrigMethod.DeclaringType = targetType;
patchedOrigMethod.ReturnType = existingMethod.ReturnType;
//methodbody has been patched and debug information was broken and unavaliable.
//todo: maybe we could rebuild dbg info by something tools?
patchedOrigMethod.Body = existingMethod.Body.Clone(patchedOrigMethod, resolveDebugInformation:false);
patchedOrigMethod.PInvokeInfo = existingMethod.PInvokeInfo;
patchedOrigMethod.IsPInvokeImpl = existingMethod.IsPInvokeImpl;

//or throw TypeLoadException if existingMethod is a ctor
patchedOrigMethod.IsSpecialName = false;
patchedOrigMethod.IsRuntimeSpecialName = false;

foreach (GenericParameter genParam in existingMethod.GenericParameters)
patchedOrigMethod.GenericParameters.Add(genParam.Clone());

foreach (ParameterDefinition param in existingMethod.Parameters)
patchedOrigMethod.Parameters.Add(param.Clone());

foreach (CustomAttribute attrib in existingMethod.CustomAttributes)
patchedOrigMethod.CustomAttributes.Add(attrib.Clone());

foreach (MethodReference @override in existingMethod.Overrides)
patchedOrigMethod.Overrides.Add(@override);

patchedOrigMethod.CustomAttributes.Add(new CustomAttribute(GetMonoModAddedCtor()));

//replace inst which call orig_()
foreach (var inst in method.Body.Instructions)
{
if (inst.OpCode == OpCodes.Call && inst.Operand == modOrigMethod)
{
//redirect to patched_()
inst.Operand = patchedOrigMethod;
}
}

targetType.Methods.Add(patchedOrigMethod);
LogVerbose($"[PatchMethod] Detected mod {method.Module.Name} repatched method {existingMethod.GetID(simple:true)}(), old patched implement moved to {patchedOrigMethod.GetID(simple:true)}()");
}
else
{
LogVerbose($"[PatchMethod] Detected mod {method.Module.Name} repatched method {existingMethod.GetID(simple:true)}(), old patched implement from preview mod will be replaced.");
}

targetType.Methods.Add(patchedOrigMethod);
LogVerbose($"[PatchMethod] more mods patched method {existingMethod.GetID(simple:true)}(), old patched implement move to {patchedOrigMethod.GetID(simple:true)}()");
}

existingMethod.Body = method.Body.Clone(existingMethod);
Expand Down