Skip to main content

Function Patching

This guide goes over the concept of function patching also sometimes referred to as monkey patching. If you're already familiar with this concept, consider checking out the Examples that show the utility provided by BdApi.Patcher.

Background

What is a function patch?

A function patch an advanced technique for plugins that allow you to modify existing functions including Discord's own functions. There are typically three different "kinds" of patches as well. There are ones where you run your own code before the original function usually with the goal to modify the arguments before they are passed to the original function. There are patches meant to run instead of the original function, taking full control over arguments, functionality, and return value. And there are ones meant to run after the original function with the goal of modifying the return value before it is passed elsewhere.

Why would I use one?

It's a great way to modify or extend Discord's functionality with your own while keeping integration mostly seemless. It can also act as a way to modify the way Discord works currently. Take the plugin HideDisabledEmojis for example, it uses function patching to modify the way Discord's internal functions work to stop trying to render emojis the user cannot use. Your possibilities for the plugins you can make increase exponentially, and the quality usually ends up being higher due to the tight integration with Discord.

How can I patch a function?

Unfortunately, you can't patch a function directly, you have to modify the reference to the function that other code uses. That means if your target function is just a locally or globally available function like this

function yourTarget() {}

then you can't really affect it. However, if your target is part of an object in some way, like being contained in an imported module, you can overwrite that reference with your own function causing everyone to call your function instead.

const someObject = {
yourTarget: function() {
console.log("red");
}
};

function targetUser() {
someObject.yourTarget();
}

targetUser(); // Logs "red"

function myNewFunction() {
console.log("green");
}

someObject.yourTarget = myNewFunction;

targetUser(); // Now logs "green"

If you take a look at the highlighted section, we are creating a new function myNewFunction that logs green and assigning it to someObject.yourTarget effectively overwriting the target function. That means when targetUser is called again, your function gets run successfully because it references the someObject object. This here is known as an instead patch because it completely replaces the target. All patches start this way but can expanded to become a before or after patch by storing a reference and calling the original function. This also opens the door to subpatches and multiple users, but that can get complicated very fast.

BetterDiscord

Luckily, BetterDiscord already has a system in place to manage multiple patches per function and allows you to target different patch types. This means if you want to do a before or after patch, you no longer have to manually replace the function and retain references and call the original. All of this is done for you with BdApi.Patcher. Let's take a look at how our example above could be done with this module.

const someObject = {
yourTarget: function() {
console.log("red");
}
};

function targetUser() {
someObject.yourTarget();
}

targetUser(); // Logs "red"

BdApi.Patcher.instead("MyPlugin", someObject, "yourTarget", () => console.log("green"));

targetUser(); // Now logs "green"

This code has the some effect as before, causing targetUser to instead log green. But lets take a closer look at the highlighted line. We have a call to BdApi.Patcher.instead which indicates we want to create an instead patch. We pass it "MyPlugin" which is an identifier used later to help removed all your patches with BdApi.Patcher.unpatchAll. Then we give it the target object someObject and the key of our target inside that object yourTarget and our new function to override the original. BetterDiscord takes care of the rest and even allows other plugins to patch on top of yours.

Examples

For all of these examples, our setup is the following:

function someGlobal() {
console.log("global function");
return 2;
}

const someModule = {
value: "foobar",
method(val = 0) {
const globalValue = someGlobal();
return globalValue + 1 + val;
},
otherMethod(someArg) {
console.log(`My value ${someArg}`);
}
};

In this setup, someGlobal is a function that cannot be patched because there is no reference to replace. However someModule.method and someModule.otherMethod can both be patched.

Before

If there's a function you want to modify the arguments for, a before patch is the right one for you. Take a look at this patch below.

BdApi.Patcher.before("MyPlugin", someModule, "otherMethod", (thisObject, args) => {
console.log(args);
});

someModule.otherMethod("something");
// > ["something"]
// > My value something

In this example we didn't modify the arguments, we just wanted to log them out to see what kind of values we might get. This is a good technique to help modify arguments selectively. Suppose we don't mind that something is logged, but we don't like when token is logged. How might that look?

BdApi.Patcher.before("MyPlugin", someModule, "otherMethod", (thisObject, args) => {
const firstArgument = args[0];
if (firstArgument === "token") {
args[0] = "redacted";
}
});

someModule.otherMethod("something"); // > My value something
someModule.otherMethod("token"); // > My value redacted

This highlighted section checks when someone passes token as the value to otherMethod and replaces it with redacted. Note the replacement that happens inside the if statement. This is another case of using a reference to overwrite, except this time it is in the arguments array. This is something to keep in mind as you do more before patches.

Instead

You may have already seen a basic instead patch in the section above but let's take a look at a slightly more complex version.

function myFunction(val) {
console.log(`Intercepted ${val}`);
}

BdApi.Patcher.instead("MyPlugin", someModule, "method", (thisObject, args, originalFunction) => {
const firstArgument = args[0];
if (firstArgument === 5) return originalFunction(...args);
if (firstArgument === 1) return myFunction(...args);
});

someModule.method(5); // > 8
someModule.method(1); // > Intercepted other
someModule.method(1); // > undefined

Take alook at the function we define in the instead patch. We have a new parameter originalFunction that BetterDiscord gives us to use as we see fit. In this example we use it for a specific value. If the value is 5 we let the original function run and return without modification. If the value is 1 we pass it to an external function and let that handle the arguments and the return. Otherwise, the function has no return value at all. This is a huge change to the function. It used to always return a value and now it only returns values for two cases. This is a good demonstration how much power function patching can have.

After

This type of patch is perhaps the most frequently used in plugins, but if you've stuck with us for the first two, this one will be easy to get the hang of.

BdApi.Patcher.after("MyPlugin", someModule, "method", (thisObject, args, returnValue) => {
return returnValue * 2;
});

someModule.method(5); // > 16
someModule.method(); // > 6

You'll notice that originalFunction from before has turned into returnValue. Here we simply multiply that by 2 every time and return the value to the caller. So that means for any number we pass, the original function applies and returns, then our patch picks up that value and multiplies by 2, then the function caller finally gets their value. The BetterDiscord Patcher will use whatever return value you use. However if you don't return anything, then the original return value is used. This can have profound effects. Consider this case below:

const myNewNumber = 5 / someModule.method(5);

Now let's switch up our return value for only 5.

BdApi.Patcher.after("MyPlugin", someModule, "method", (thisObject, args, returnValue) => {
if (args[0] === 5) return {};
});

In our patch this time, we return a value only in the case of 5, in all other cases the default return of the original function is used because we didn't return anything. If we wanted to stop that we could put a return null; on the next line. You may have also noticed that our return is no longer a value. So what happens to our case above?

BdApi.Patcher.after("MyPlugin", someModule, "method", (thisObject, args, returnValue) => {
if (args[0] === 5) return {};
});

const myNewNumber = 5 / someModule.method(5); // NaN

This lead myNewNumber to become NaN or not a number. Which is ironic considering the variable name. But it's a good example of how careful we need to be when it comes to modifying the returns of functions.