Patch Attributes API
The MDB Framework provides a declarative patching system using attributes. This is the primary and recommended way to hook game methods. Patches are automatically discovered and applied when mods load, giving you type-safe parameter handling without any manual hook management.
Namespace: GameSDK.ModHost.Patching
Overview
The patching system consists of:
- [Patch] - Declares the target type to patch
- [PatchMethod] - Specifies the method name and parameter count
- [Prefix] - Runs before the original method
- [Postfix] - Runs after the original method
- [Finalizer] - Runs even if the original throws an exception
Patch methods receive parameters by name. You can use special names like __instance, __result, __state, and __exception, or use the actual IL2CPP parameter names from the generated SDK to receive method arguments with full type safety.
Attributes
[Patch]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class PatchAttribute : Attribute
Marks a class as containing patches for a specific type. Can be used in three ways:
Constructor Overloads
1. Patch by Type (Generated Wrapper)
[Patch(Type targetType)]
Uses a generated wrapper type reference. The framework extracts the original namespace and type name from the wrapper.
Example:
[Patch(typeof(Player))]
[PatchMethod("TakeDamage", 1)]
public static class PlayerPatch
{
[Prefix]
public static bool Prefix(IntPtr __instance, int __0)
{
return true;
}
}
2. Patch by Namespace and Name
[Patch(string namespace, string typeName)]
Directly specifies the IL2CPP namespace and type name. Use this for types without generated wrappers, for obfuscated types, or for types in the global namespace.
Example — Named namespace:
[Patch("UnityEngine", "Debug")]
[PatchMethod("Log", 1)]
public static class DebugLogPatch
{
[Prefix]
public static bool Prefix(string __0)
{
Logger.Info($"Unity logged: {__0}");
return true;
}
}
Example — Global namespace (empty string):
// For types in the global namespace, use an empty string ""
[Patch("", "ClassName")]
[PatchMethod("MethodName", 1)]
public static class CustomPatch
{
[Prefix]
public static bool Prefix(IntPtr __instance)
{
Logger.Info("Method called");
return true;
}
}
3. Specify Method Name Only
[Patch(string methodName)]
Use as a second [Patch] attribute to specify the method name without using [PatchMethod].
Example:
[Patch("UnityEngine", "Screen")]
[Patch("get_width")] // Second [Patch] specifies method
public static class ScreenWidthPatch
{
[Postfix]
public static void Postfix(ref int __result)
{
__result = 1920;
}
}
Properties
| Property | Type | Description | Default |
|---|---|---|---|
TargetType |
Type |
The wrapper type to patch (read-only) | null |
Namespace |
string |
IL2CPP namespace of target type (read-only) | "" |
TypeName |
string |
IL2CPP type name (read-only) | "" |
MethodName |
string |
Target method name (settable) | null |
Assembly |
string |
Assembly containing the target type | "Assembly-CSharp" |
[PatchMethod]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class PatchMethodAttribute : Attribute
Specifies the target method name and the parameter count. The parameter count is important for overload resolution — IL2CPP games frequently have multiple methods with the same name but different parameter counts.
Constructors
[PatchMethod(string methodName)]
[PatchMethod(string methodName, int parameterCount)]
| Parameter | Type | Description |
|---|---|---|
methodName |
string |
Name of the method to patch |
parameterCount |
int |
Number of IL2CPP parameters (use -1 for any) |
Always specify the parameter count when you know it — this prevents accidentally hooking the wrong overload.
Example:
[Patch("UnityEngine", "Debug")]
[PatchMethod("Log", 1)] // Specifically targets Debug.Log(object)
public static class DebugLogPatch { }
[Prefix]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class PrefixAttribute : Attribute
Marks a method as a prefix patch. Prefix methods run BEFORE the original method.
Behavior
- Return
falseto skip executing the original method - Return
true(orvoid) to continue to the original method - Can modify parameters passed to the original via
refparameters - Can set the return value via
ref __resultwhen skipping the original
Example — Log and continue:
[Patch("GameNamespace", "Player")]
[PatchMethod("TakeDamage", 1)]
public static class LogDamagePatch
{
[Prefix]
public static bool Prefix(IntPtr __instance, ref int __0)
{
Logger.Info($"Received {__0} damage");
return true; // Continue to original = damage taken
}
}
Example — Skip original:
[Patch("GameNamespace", "Player")]
[PatchMethod("TakeDamage", 1)]
public static class InvincibilityPatch
{
[Prefix]
public static bool Prefix(IntPtr __instance, ref int __0)
{
Logger.Info($"Blocked {__0} damage");
return false; // Skip original = no damage taken
}
}
[Postfix]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class PostfixAttribute : Attribute
Marks a method as a postfix patch. Postfix methods run AFTER the original method completes successfully.
Behavior
- Always runs if the original method completes without throwing
- Can inspect or modify the return value via
ref __result - Can access the original parameters and instance
- Cannot skip the original (it already ran)
Example — Modify return value:
[Patch("UnityEngine", "Screen")]
[PatchMethod("get_width", 0)]
public static class ForceResolutionPatch
{
[Postfix]
public static void Postfix(ref int __result)
{
__result = 1920; // Override screen width
}
}
Example — Log method results:
[Patch("GameNamespace", "SaveManager")]
[PatchMethod("SaveGame", 1)]
public static class SaveGameLogger
{
[Postfix]
public static void Postfix(string __0, bool __result)
{
if (__result)
Logger.Info($"Game saved to slot: {__0}");
else
Logger.Warning($"Failed to save game to slot: {__0}");
}
}
[Finalizer]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class FinalizerAttribute : Attribute
Marks a method as a finalizer patch. Finalizer methods run EVEN IF the original method throws an exception.
Behavior
- Always runs after the original method, regardless of success or failure
- Receives the exception via
__exceptionparameter (null if no exception) - Return
nullto swallow the exception - Return an
Exceptioninstance to throw a different exception - Return the original
__exceptionto re-throw it
Example — Catch and log exceptions:
[Patch("GameNamespace", "FileManager")]
[PatchMethod("LoadFile", 1)]
public static class LoadFileSafety
{
[Finalizer]
public static Exception Finalizer(string __0, Exception __exception)
{
if (__exception != null)
{
Logger.Error($"Failed to load file {__0}: {__exception.Message}");
return null; // Swallow exception - game continues
}
return null;
}
}
Parameter Naming
Patch methods support two approaches for receiving method parameters:
Approach 1: Named Parameters (Recommended)
Use the actual IL2CPP parameter names from the generated SDK or dump files. The framework maps them by position automatically. This is the most readable approach for obfuscated games where the parameter names come directly from the game’s metadata.
[Patch("", "HBEAKBIHANL")]
[PatchMethod("KOBMINBDOBD", 12)]
public static class BulletSpawnPatch
{
[Prefix]
public static bool Prefix(
IntPtr __instance,
ref ObjectProperties ODEMIJKAJMH, // Positional arg 0
ref ProjectileProperties GIEJOHKLGJO, // Positional arg 1
ref int HHCCBONIIOM, // Positional arg 2
ref uint KLHOFENGJNM, // Positional arg 3
ref float FFFFKPDHEFP, // Positional arg 4
ref int GHEBEMMJLDJ, // Positional arg 5
ref string CFJBHEKKLNF, // Positional arg 6
ref string JCADLABDPIO, // Positional arg 7
ref float AFCNMCJIKFD, // Positional arg 8
ref float KDAJOMOFMJB, // Positional arg 9
ref bool KCHJBMCNIIA, // Positional arg 10
ref bool PBGHBKMHACI) // Positional arg 11
{
Logger.Info($"Damage={HHCCBONIIOM}, Count={KLHOFENGJNM}");
return true;
}
}
Non-special parameter names (anything that doesn’t start with __) are mapped positionally — the first non-special parameter maps to arg 0, the second to arg 1, etc.
Approach 2: Indexed Parameters
Use __0, __1, __2, etc. to reference parameters by zero-based index:
[Patch("GameNamespace", "Calculator")]
[PatchMethod("Add", 2)]
public static class CalculatorPatch
{
[Prefix]
public static bool Prefix(int __0, int __1, ref int __result)
{
Logger.Info($"Adding {__0} + {__1}");
__result = __0 + __1 + 10; // Add bonus
return false; // Skip original
}
}
Special Parameters
These parameter names have special meaning and are not mapped positionally:
| Name | Type | Description |
|---|---|---|
__instance |
IntPtr |
The this pointer (IL2CPP object). IntPtr.Zero for static methods. |
__result |
ref T |
The method return value. Must use ref. |
__state |
ref object |
Shared state between Prefix and Postfix. |
__exception |
Exception |
The thrown exception (Finalizer only). |
__0, __1, … |
T |
Parameter by zero-based index. |
Supported Parameter Types
The patching system automatically marshals between IL2CPP native pointers and managed types:
| C# Type | IL2CPP Representation | Notes |
|---|---|---|
int |
Passed directly in pointer | |
uint |
Passed directly in pointer | |
long |
Passed directly in pointer | |
ulong |
Passed directly in pointer | |
short |
Passed directly in pointer | |
ushort |
Passed directly in pointer | |
byte |
Passed directly in pointer | |
sbyte |
Passed directly in pointer | |
bool |
Non-zero = true | |
float |
Bit-cast from int32 | Handled automatically |
double |
Bit-cast from int64 | Handled automatically |
string |
IL2CPP String pointer | Auto-converted to/from managed string |
IntPtr |
Raw pointer passthrough | Use for object references |
| IL2CPP wrapper types | Pointer wrapped via Activator.CreateInstance |
e.g., ObjectProperties, PlayerClass |
Using ref Parameters
All non-special parameters can be declared with ref to allow modification. When you modify a ref parameter, the change is written back to the native args array, affecting the original method call:
[Prefix]
public static bool Prefix(ref int HHCCBONIIOM, ref float FFFFKPDHEFP)
{
HHCCBONIIOM = 0; // Zero out damage
FFFFKPDHEFP *= 0.5f; // Halve the angle
return true; // Continue with modified args
}
Complete Examples
Example 1: Intercepting Game Methods with Named Parameters
A real-world example hooking an obfuscated method in a Unity IL2CPP game. The method has 12 parameters including structs, primitives, strings, and booleans.
using System;
using GameSDK.ModHost;
using GameSDK.ModHost.Patching;
[Mod("Author.BulletMonitor", "Bullet Monitor", "1.0.0")]
public class BulletMonitorMod : ModBase
{
public static ModLogger Log;
public override void OnLoad()
{
Log = Logger;
Logger.Info("Bullet monitor loaded");
}
}
### Example 2: Skip Original with Return Value
```csharp
[Patch("GameNamespace", "Player")]
[PatchMethod("TakeDamage", 2)]
public static class GodModePatch
{
private static bool _godMode = true;
[Prefix]
public static bool Prefix(IntPtr __instance, float __0, string __1, ref bool __result)
{
if (_godMode)
{
Logger.Info($"God mode: blocked {__0} damage from {__1}");
__result = false;
return false; // Skip original
}
return true;
}
}
Example 3: Modify Return Values
[Patch("GameNamespace", "PlayerStats")]
[PatchMethod("GetMaxHealth", 0)]
public static class HealthBoostPatch
{
[Postfix]
public static void Postfix(IntPtr __instance, ref float __result)
{
__result *= 1.5f; // 50% health boost
}
}
Example 4: Track Method Duration with __state
[Patch("GameNamespace", "AssetLoader")]
[PatchMethod("LoadAsset", 1)]
public static class LoadAssetTimerPatch
{
[Prefix]
public static void Prefix(string __0, ref object __state)
{
Logger.Info($"Loading asset: {__0}");
__state = System.Diagnostics.Stopwatch.StartNew();
}
[Postfix]
public static void Postfix(string __0, ref object __state, IntPtr __result)
{
if (__state is System.Diagnostics.Stopwatch sw)
{
sw.Stop();
bool success = __result != IntPtr.Zero;
Logger.Info($"Loaded {__0} in {sw.ElapsedMilliseconds}ms (success: {success})");
}
}
}
Example 5: Exception Handling with Finalizer
[Patch("GameNamespace", "NetworkManager")]
[PatchMethod("SendPacket", 1)]
public static class NetworkSafetyPatch
{
[Finalizer]
public static Exception Finalizer(IntPtr __0, Exception __exception)
{
if (__exception != null)
{
Logger.Error($"Failed to send packet: {__exception.Message}");
if (__exception is System.Net.Sockets.SocketException)
{
Logger.Warning("Network error — swallowing exception");
return null; // Swallow
}
return __exception; // Re-throw other exceptions
}
return null;
}
}
Example 6: Multiple Patches on Same Type
[Patch("UnityEngine", "Debug")]
[PatchMethod("Log", 1)]
public static class DebugLogPatch
{
[Prefix]
public static bool Prefix(string __0)
{
Logger.Info($"[Log] {__0}");
return true;
}
}
[Patch("UnityEngine", "Debug")]
[PatchMethod("LogWarning", 1)]
public static class DebugLogWarningPatch
{
[Prefix]
public static bool Prefix(string __0)
{
Logger.Warning($"[Warning] {__0}");
return true;
}
}
[Patch("UnityEngine", "Debug")]
[PatchMethod("LogError", 1)]
public static class DebugLogErrorPatch
{
[Prefix]
public static bool Prefix(string __0)
{
Logger.Error($"[Error] {__0}");
return true;
}
}
Example 7: Combining Prefix, Postfix, and Finalizer
Execution order: Prefix → Original → Postfix → Finalizer (always)
[Patch("GameNamespace", "CriticalOperation")]
[PatchMethod("Execute", 1)]
public static class CompletePatch
{
[Prefix]
public static void Prefix(string __0, ref object __state)
{
Logger.Info($"Starting operation: {__0}");
__state = System.Diagnostics.Stopwatch.StartNew();
}
[Postfix]
public static void Postfix(ref object __state, bool __result)
{
if (__state is System.Diagnostics.Stopwatch sw)
{
sw.Stop();
Logger.Info($"Operation completed: {__result} ({sw.ElapsedMilliseconds}ms)");
}
}
[Finalizer]
public static Exception Finalizer(ref object __state, Exception __exception)
{
if (__exception != null && __state is System.Diagnostics.Stopwatch sw)
{
sw.Stop();
Logger.Error($"Operation failed after {sw.ElapsedMilliseconds}ms: {__exception.Message}");
}
return __exception;
}
}
Example 8: Hooking Properties
Unity properties compile to get_PropertyName and set_PropertyName methods:
[Patch("UnityEngine", "Screen")]
[PatchMethod("get_width", 0)]
public static class ScreenWidthPatch
{
[Postfix]
public static void Postfix(ref int __result)
{
__result = 1920; // Force screen width
}
}
[Patch("GameNamespace", "Settings")]
[PatchMethod("set_Volume", 1)]
public static class VolumeSetterPatch
{
[Prefix]
public static void Prefix(ref float __0)
{
__0 = Math.Max(0f, Math.Min(__0, 1f)); // Clamp to safe range
}
}
Best Practices
Do
- Use static classes and static methods — patches must be fully static
- Always specify the parameter count in
[PatchMethod]for reliable overload resolution - Use named parameters from the generated SDK for readability
- Use
refon__result— without it, modifications are lost - Set
__resultbefore returningfalse— the caller expects a return value - Wrap expensive logic in try/catch — exceptions in patches can crash the game
- Throttle logging in frequently-called methods to avoid log spam
- Use
[Patch("", "TypeName")]for global namespace types (empty string, not omitted)
Don’t
- Don’t use instance classes or methods — the framework can’t instantiate patch classes
- Don’t skip the original without setting
__result— caller gets undefined values - Don’t assume parameter types — check the generated SDK or dump for exact types
- Don’t patch the same method twice in the same mod — undefined behavior
- Don’t log in hot-path methods without throttling — you’ll flood the log
Common Mistakes
Mistake 1: Non-Static Patch Class
// WRONG — Instance class
[Patch("Namespace", "Type")]
public class MyPatch
{
[Prefix]
public static bool Prefix() { return true; }
}
// CORRECT — Static class
[Patch("Namespace", "Type")]
public static class MyPatch
{
[Prefix]
public static bool Prefix() { return true; }
}
Mistake 2: Skipping Original Without Setting __result
// WRONG — Caller gets garbage return value
[Prefix]
public static bool Prefix()
{
return false;
}
// CORRECT — Provide the return value
[Prefix]
public static bool Prefix(ref int __result)
{
__result = 42;
return false;
}
Mistake 3: Forgetting ref on __result
// WRONG — Modifies local copy only
[Postfix]
public static void Postfix(int __result)
{
__result *= 2; // Does nothing
}
// CORRECT
[Postfix]
public static void Postfix(ref int __result)
{
__result *= 2; // Actually modifies the return value
}
Mistake 4: Logging in Hot Paths
// WRONG — Logs every frame
[Patch("UnityEngine", "Time")]
[PatchMethod("get_deltaTime", 0)]
public static class BadPatch
{
[Postfix]
public static void Postfix(float __result)
{
Logger.Info($"deltaTime: {__result}"); // Spams logs!
}
}
// CORRECT — Throttled logging
[Patch("UnityEngine", "Time")]
[PatchMethod("get_deltaTime", 0)]
public static class GoodPatch
{
private static int _callCount = 0;
[Postfix]
public static void Postfix(float __result)
{
if (++_callCount % 60 == 0) // Log once per second at 60fps
Logger.Info($"deltaTime: {__result}");
}
}
Troubleshooting
Patch Not Applied
Problem: Your patch class is defined but the method isn’t being hooked.
Solutions:
- Ensure the patch class is static
- Ensure patch methods are static
- Check that namespace and type name exactly match the IL2CPP names
- Verify method name and parameter count are correct
- Check the logs — patch discovery errors are logged to
MDB/Logs/MDB.log - For global namespace types, use
[Patch("", "TypeName")](empty string, not null)
Parameters Show as Zero/Default
Problem: Patch applies but parameters are always 0/null/false.
Solutions:
- Make sure your parameter names don’t accidentally match a special name (
__instance,__result, etc.) - Verify the parameter count in
[PatchMethod]matches exactly - Check that parameter types match the IL2CPP types (see type mapping table)
- For unsigned types, use
uint/ulong/ushort/byte— notint
Strings Show as Empty or <object>
Problem: String parameters appear empty or as pointer addresses.
Solutions:
- Declare string parameters as
ref string— IL2CPP strings are pointers - Verify the method actually passes non-empty strings (some parameters are genuinely empty)
- Check the logs for string conversion errors
Prefix Not Skipping Original
Problem: Returning false from Prefix but original still runs.
Solutions:
- Ensure the return type is
bool, notvoid - Make sure the method signature is correct
- Check logs for patch application errors
Game Crashes on Hook
Problem: Game crashes when the hooked method is called.
Solutions:
- Check that parameter types match exactly
- Wrap your patch logic in try/catch
- Verify the parameter count matches the actual IL2CPP method
- Try a minimal Prefix that just returns
trueto isolate the issue
See Also
- HookManager — Manual hook API (advanced fallback)
- ModBase — Mod lifecycle and base class
- IL2CPP Bridge — Direct IL2CPP runtime access
- Examples — Working mod examples
- Getting Started — Creating your first mod
| ← Back to API Index | HookManager → |