Skip to content

Commit 9f99dd0

Browse files
committed
feat(patcher): support opt-in context injection via delegate parameters
1 parent 28fe59e commit 9f99dd0

18 files changed

+449
-62
lines changed

src/OTAPI.UnifiedServerProcess/Commons/Stack.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public AnalysisContext(Instruction current, int position) {
3131
/// <param name="caller">Executing method</param>
3232
/// <param name="afterThisExec">Analyze after this instruction</param>
3333
/// <returns>Return all possible uses of the top value on the stack/returns>
34-
public static Instruction[] AnalyzeStackTopValueUsage(MethodDefinition caller, Instruction afterThisExec) {
34+
public static Instruction[] TraceStackValueConsumers(MethodDefinition caller, Instruction afterThisExec) {
3535
var visited = new HashSet<(Instruction, int)>();
3636
var results = new HashSet<Instruction>();
3737
var workStack = new Stack<AnalysisContext>();
@@ -76,14 +76,14 @@ public static Instruction[] AnalyzeStackTopValueUsage(MethodDefinition caller, I
7676

7777
return results.ToArray();
7878
}
79-
public static Instruction[] AnalyzeStackTopValueFinalUsage(MethodDefinition caller, Instruction afterThisExec) {
79+
public static Instruction[] TraceStackValueFinalConsumers(MethodDefinition caller, Instruction afterThisExec) {
8080
List<Instruction> results = [];
8181
Stack<Instruction> works = [];
8282
works.Push(afterThisExec);
8383

8484
while (works.Count > 0) {
8585
var current = works.Pop();
86-
var usages = AnalyzeStackTopValueUsage(caller, current);
86+
var usages = TraceStackValueConsumers(caller, current);
8787
foreach (var usage in usages) {
8888
if (MonoModCommon.Stack.GetPushCount(caller.Body, usage) > 0) {
8989
works.Push(usage);

src/OTAPI.UnifiedServerProcess/Core/Analysis/StaticFieldModificationAnalysis/StaticFieldModificationAnalyzer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ static void AddField(Dictionary<string, FieldDefinition> dict, FieldDefinition f
307307
break;
308308
}
309309
case Code.Ldsflda: {
310-
if (MonoModCommon.Stack.AnalyzeStackTopValueUsage(caller, instruction).All(inst => inst.OpCode.Code is Code.Call or Code.Callvirt or Code.Ldfld or Code.Ldflda)) {
310+
if (MonoModCommon.Stack.TraceStackValueConsumers(caller, instruction).All(inst => inst.OpCode.Code is Code.Call or Code.Callvirt or Code.Ldfld or Code.Ldflda)) {
311311
break;
312312
}
313313
goto case Code.Stsfld;
@@ -321,7 +321,7 @@ static void AddField(Dictionary<string, FieldDefinition> dict, FieldDefinition f
321321
break;
322322
}
323323
case Code.Ldelema: {
324-
if (MonoModCommon.Stack.AnalyzeStackTopValueUsage(caller, instruction).All(inst => inst.OpCode.Code is Code.Call or Code.Callvirt or Code.Ldfld or Code.Ldflda)) {
324+
if (MonoModCommon.Stack.TraceStackValueConsumers(caller, instruction).All(inst => inst.OpCode.Code is Code.Call or Code.Callvirt or Code.Ldfld or Code.Ldflda)) {
325325
break;
326326
}
327327
goto case Code.Stelem_Any;

src/OTAPI.UnifiedServerProcess/Core/FunctionalFeatures/IContextInjectFeature.cs

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using Mono.Cecil;
22
using Mono.Cecil.Cil;
3+
using MonoMod.Utils;
4+
using NuGet.Packaging;
35
using OTAPI.UnifiedServerProcess.Commons;
46
using OTAPI.UnifiedServerProcess.Core.Patching;
57
using OTAPI.UnifiedServerProcess.Core.Patching.DataModels;
@@ -8,7 +10,6 @@
810
using System;
911
using System.Collections.Generic;
1012
using System.Data;
11-
using System.Diagnostics.CodeAnalysis;
1213
using System.Linq;
1314

1415
namespace OTAPI.UnifiedServerProcess.Core.FunctionalFeatures
@@ -21,7 +22,6 @@ public static bool AdjustMethodReferences(
2122
PatcherArguments arguments,
2223
ContextBoundMethodMap contextMethodMap,
2324
ref MethodReference methodRefToAdjust,
24-
[NotNullWhen(true)]
2525
out MethodDefinition? contextBoundMethod,
2626
out MethodReference originalMethodRef,
2727
out ContextTypeData? contextProvider) {
@@ -30,12 +30,68 @@ public static bool AdjustMethodReferences(
3030
contextProvider = null;
3131
var calleeId = methodRefToAdjust.GetIdentifier();
3232

33+
// Handle delegate-injected context parameters for Action.Invoke / Action.BeginInvoke
34+
if ((methodRefToAdjust.Name == nameof(Action.Invoke) || methodRefToAdjust.Name == nameof(Action.BeginInvoke))
35+
&& PatchingCommon.IsDelegateInjectedCtxParam(methodRefToAdjust.DeclaringType)) {
36+
37+
// Resolve the delegate method definition
38+
var delegateInvokeDef = methodRefToAdjust.DeclaringType.Resolve().GetMethod(methodRefToAdjust.Name);
39+
40+
// Skip adjustment if parameter count matches (already context-aware)
41+
if (delegateInvokeDef.Parameters.Count == methodRefToAdjust.Parameters.Count) {
42+
contextBoundMethod = null;
43+
return false;
44+
}
45+
46+
// Preserve original method reference for flow analysis
47+
originalMethodRef = PatchingCommon.GetVanillaMethodRef(
48+
arguments.RootContextDef,
49+
arguments.ContextTypes,
50+
methodRefToAdjust);
51+
52+
// Insert context parameter based on delegate definition
53+
if (delegateInvokeDef.Parameters[0].ParameterType is GenericParameter genericParameter) {
54+
// Map generic parameter from the delegate type
55+
var genericOwner = ((GenericInstanceType)methodRefToAdjust.DeclaringType).ElementType;
56+
57+
var duplicate = new MethodReference(methodRefToAdjust.Name, methodRefToAdjust.ReturnType, methodRefToAdjust.DeclaringType) {
58+
HasThis = methodRefToAdjust.HasThis,
59+
};
60+
duplicate.Parameters.Add(new ParameterDefinition(genericOwner.GenericParameters[genericParameter.Position]));
61+
duplicate.Parameters.AddRange(methodRefToAdjust.Parameters.Select(p => p.Clone()));
62+
63+
methodRefToAdjust = duplicate;
64+
contextBoundMethod = null;
65+
return true;
66+
}
67+
else if (delegateInvokeDef.Parameters[0].ParameterType.FullName is Constants.RootContextFullName) {
68+
// Insert root context type
69+
70+
var duplicate = new MethodReference(methodRefToAdjust.Name, methodRefToAdjust.ReturnType, methodRefToAdjust.DeclaringType) {
71+
HasThis = methodRefToAdjust.HasThis,
72+
};
73+
duplicate.Parameters.Add(new ParameterDefinition(arguments.RootContextDef));
74+
duplicate.Parameters.AddRange(methodRefToAdjust.Parameters.Select(p => p.Clone()));
75+
76+
methodRefToAdjust = duplicate;
77+
contextBoundMethod = null;
78+
return true;
79+
}
80+
else {
81+
// Unexpected parameter type
82+
throw new Exception();
83+
}
84+
}
85+
86+
3387
// Validate if the method reference points to an unmodified vanilla method through:
3488
// 1. Existence in original-context-bound mapping, OR
3589
// 2. Preservation of original state (unresolvable method indicates no patches)
3690
// Only valid candidates will be replaced with context-aware versions
3791
if (!contextMethodMap.originalToContextBound.TryGetValue(calleeId, out contextBoundMethod)) {
38-
// Check if method exists in tail assembly (indicates modified)
92+
93+
// If TryResolve() returns non-null, it means this MethodReference already points
94+
// to a modified context-bound method (caller IL already updated), so no further action is needed.
3995
if (methodRefToAdjust.TryResolve() is not null) {
4096
return false;
4197
}
@@ -85,7 +141,6 @@ public static void InjectContextParameterLoads(
85141
ref Instruction methodCallInstruction,
86142
out Instruction insertedFirstInstr,
87143
MethodDefinition modifyMethod,
88-
MethodDefinition contextBound,
89144
MethodReference calleeRef,
90145
MethodReference vanillaCalleeRef,
91146
ContextTypeData? contextTypeData,
@@ -288,15 +343,15 @@ public static void AdjustConstructorLoadRoot(this IContextInjectFeature point, T
288343
if (baseCtorCall is null && firstLoadRoot_shouldMoveCtorCallWhenNotNull is null) {
289344
bool isNotInit = false;
290345
if (check.OpCode == OpCodes.Ldarg_1) {
291-
foreach (var usage in MonoModCommon.Stack.AnalyzeStackTopValueUsage(ctor, check)) {
346+
foreach (var usage in MonoModCommon.Stack.TraceStackValueConsumers(ctor, check)) {
292347
if (usage is not { OpCode.Code: Code.Call, Operand: MethodReference { Name: ".ctor" } }) {
293348
isNotInit = true;
294349
break;
295350
}
296351
}
297352
}
298353
else if (check.OpCode == OpCodes.Ldarg_0) {
299-
foreach (var usage in MonoModCommon.Stack.AnalyzeStackTopValueUsage(ctor, check)) {
354+
foreach (var usage in MonoModCommon.Stack.TraceStackValueConsumers(ctor, check)) {
300355
if (usage.OpCode != OpCodes.Stfld && usage is not { OpCode.Code: Code.Call, Operand: MethodReference { Name: ".ctor" } }) {
301356
isNotInit = true;
302357
break;
@@ -379,7 +434,7 @@ static void TraceUsage(IContextInjectFeature feature, MethodDefinition method, H
379434

380435
while (works.Count > 0) {
381436
var current = works.Pop();
382-
var usages = MonoModCommon.Stack.AnalyzeStackTopValueUsage(method, current);
437+
var usages = MonoModCommon.Stack.TraceStackValueConsumers(method, current);
383438
ExtractSources(feature, method, checkInsts, checkLocals, usages);
384439
foreach (var usage in usages) {
385440
if (MonoModCommon.Stack.GetPushCount(method.Body, usage) > 0) {
@@ -404,7 +459,7 @@ static void TraceUsage(IContextInjectFeature feature, MethodDefinition method, H
404459
case Code.Ldloca_S:
405460
case Code.Ldloca:
406461
if (!checkInsts.Contains(inst)) {
407-
var usages = MonoModCommon.Stack.AnalyzeStackTopValueUsage(method, inst);
462+
var usages = MonoModCommon.Stack.TraceStackValueConsumers(method, inst);
408463
ExtractSources(feature, method, checkInsts, checkLocals, usages);
409464
checkInsts.Add(inst);
410465
}

src/OTAPI.UnifiedServerProcess/Core/FunctionalFeatures/IMethodCheckCacheFeature.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using Mono.Cecil;
22
using Mono.Cecil.Cil;
33
using OTAPI.UnifiedServerProcess.Core.Analysis.MethodCallAnalysis;
4+
using OTAPI.UnifiedServerProcess.Core.Patching;
45
using OTAPI.UnifiedServerProcess.Extensions;
6+
using System;
57
using System.Collections.Generic;
68
using System.Linq;
79

@@ -103,6 +105,13 @@ public static bool CheckUsedContextBoundField<TFeature>(
103105
}
104106
if (inst.OpCode == OpCodes.Call || inst.OpCode == OpCodes.Callvirt) {
105107
var methodRef = (MethodReference)inst.Operand;
108+
109+
if (methodRef.Name == nameof(Action.Invoke) || methodRef.Name == nameof(Action.BeginInvoke)) {
110+
if (PatchingCommon.IsDelegateInjectedCtxParam(methodRef.DeclaringType)) {
111+
return CacheReturn(true, useCache, methodId);
112+
}
113+
}
114+
106115
string? autoDeleFieldName = null;
107116
if (methodRef.Name.OrdinalStartsWith("add_")) {
108117
autoDeleFieldName = methodRef.Name[4..];
@@ -113,6 +122,7 @@ public static bool CheckUsedContextBoundField<TFeature>(
113122
if (autoDeleFieldName is null) {
114123
continue;
115124
}
125+
116126
var declaringType = methodRef.DeclaringType.TryResolve();
117127
if (declaringType is null) {
118128
continue;
@@ -126,6 +136,12 @@ public static bool CheckUsedContextBoundField<TFeature>(
126136
}
127137
}
128138
if (inst.OpCode == OpCodes.Ldftn || inst.OpCode == OpCodes.Ldvirtftn) {
139+
if (inst.Next is { OpCode.Code: Code.Newobj, Operand: MethodReference deleCtor }) {
140+
if (PatchingCommon.IsDelegateInjectedCtxParam(deleCtor.DeclaringType)) {
141+
continue;
142+
}
143+
}
144+
129145
var methodRef = (MethodReference)inst.Operand;
130146
if (methodRef.DeclaringType.Name == "<>c") {
131147
var mDef = methodRef.Resolve();

src/OTAPI.UnifiedServerProcess/Core/FunctionalFeatures/IStaticModificationCheckFeature.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static bool IsAboutStaticFieldModification(this IStaticModificationCheckF
3535
}
3636
}
3737
else if (inst.OpCode == OpCodes.Ldsflda) {
38-
foreach (var usage in AnalyzeStackTopValueUsage(method, inst)) {
38+
foreach (var usage in TraceStackValueConsumers(method, inst)) {
3939
modificationOperations.Add(usage);
4040
}
4141
var field = ((FieldReference)inst.Operand).TryResolve();
@@ -79,7 +79,7 @@ public static bool IsReferencedStaticFieldModified(this IStaticModificationCheck
7979
modified = [];
8080
modificationOperations = [];
8181

82-
foreach (var usage in AnalyzeStackTopValueUsage(method, pushedFieldReference)) {
82+
foreach (var usage in TraceStackValueConsumers(method, pushedFieldReference)) {
8383
if (usage.OpCode != OpCodes.Call && usage.OpCode != OpCodes.Callvirt) {
8484
continue;
8585
}

src/OTAPI.UnifiedServerProcess/Core/Patching/DataModels/ContextTypeData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ private static bool CheckIsReusableSingleton(TypeDefinition originalType, Immuta
225225
if (inst.OpCode != OpCodes.Newobj || inst.Operand is not MethodReference ctorRef || ctorRef.DeclaringType.FullName != originalType.FullName) {
226226
continue;
227227
}
228-
var stackValueUsages = MonoModCommon.Stack.AnalyzeStackTopValueUsage(usedByMethod, inst);
228+
var stackValueUsages = MonoModCommon.Stack.TraceStackValueConsumers(usedByMethod, inst);
229229
if (stackValueUsages.Length > 1) {
230230
return false;
231231
}

0 commit comments

Comments
 (0)