Quick introduction to Hooks
This guide is intended for people new to PAYDAY 2 Lua modding and explains different methods of hooking functions and when to use them. I'll go over the most commonly used methods of function hooking and explain how they work and why we need them.
Why bother adding hooks?
You may ask yourself: Why all this complicated stuff? Can't I just take the original function, make my edits and use the edited function? Well yes, you can do a complete function override but you really shouldn't - unless it is absolutely unavoidable. Doing this will crash your game in the worst case and is more likely to break whenever the game is updated. You will also run into the risk of breaking mods that rely on the same function, as your mod would completely undo changes of other mods if it loaded afterwards. The major takeaway is therefore: Doing proper function hooks increases compatibility with other mods and game updates and is less likely to break over time.
With that out of the way, let's see what kind of hooking techniques exist.
Types of hooks
PostHook
Depending on what your mod is trying to achieve, you will have different needs when hooking functions. The most common scenario is probably adding additional data or code on top of an existing function after it executes.
For this we generally use BLT'SuperBLT's PostHook, which registers a custom function that is executed whenever the game executes the target function. Using a PostHook is simple and can be used if you don't care about the content of the original function and just want to add some custom code toor it.change existing fields.
Let's look at an example: Let's say weWe want to change the HP of the regular street cops. We can do this after the cop's stats have been initialized, which is donehappens in the function CharacterTweakData:_init_cop
.
SinceLooking at this function in the game code we don'tcan reallysee care whatthat the originalcop's codehealth doesis andset here. Since we just want to change the HP we can add code in the form of a PostHook.
We would do that like this:
Hooks:PostHook(CharacterTweakData, "_init_cop", "our_unique_hook_id", function (self)
self.cop.HEALTH_INIT = 10
end)
ThisWhen wouldthe causegame runs the original CharacterTweakData:_init_cop
function, all the cop todata will be initialized likeas normal,usual, but directlyimmediately afterwardsafterwards, BLT will execute our customhook function would be executed,function, changing the copscop's health to ourthe ownvalue suppliedwe value.supplied.
Note that yourThe function you specify for the PostHook will be supplied with the same arguments that the original function will be called, so if you need them, you can specify them.
InNote additionthat CharacterTweakData:_init_cop(presets)
is functionally identical to CharacterTweakData._init_cop(self, presets)
(specifically note .
instead of :
between CharacterTweakData
and _init_cop
). This is important for your hook function if you plan to make use of the function arguments and the reason why you usually see self
as the first argument in a hook function even if the original function parameters,doesn't ahave functionit.
of the form function object:do_stuff(a, b) (: instead of . between object and function name) will provide the calling object itself as the original parameter (usually called self) so ifIf you wantedare unsure about wether to capture the parameters a and b of that function in a PostHook you would add an additionalinclude self
in frontthe list of arguments for your parameterhook, list.just remember that if the function is defined with a :
in the game code, the first argument to it in a hook should be self
.
PreHook
Very similar to a PostHook, the only difference is that it will be executed before the original function is called. Less commonly used but useful if you need to change some values or add additional code right before a function call. ForNote obviousthat reasons,setting you don't have access to any of the member variablesfields that are created or set in the function in the original code will have no effect since your code runs before the original function inand the original call will just override any values that it sets.
Another niche use case for a PreHook. is changing function arguments that are of table type, as tables are passed by reference and you can therefore change the content of the table which will then be passed to the original function. As other data types are passed by value this only works for tables.
Function wrapping
The above mentioned hooking methods should cover a lot of usecases already, but there are some cases where they are not usable. Let's say you want to intercept the return value of a function and replace it with something else, or add additional checks depending onchange the returnarguments value.the original function is called with. This simply can not be done with a PostHook or PreHook since you don't have access to what the original function returned.returned and all function arguments except tables are passed by value.
In this case, you need to do a function wrap, often referred to as old_init.
This involves saving the original function into a variable and then overriding the function with a new one. In the new function you would then call the original function manually and do whatever else you need to do.
Let's say you made a new custom enemy and for making it work properly you need to add it to the character map (a list of all enemies the game goes over and generates contour mappings for). The character map is returned by the function CharacterTweakData:character_map
and the function itself creates and returns a local table with all characters in it, so we don't have access to this table via PostHook or PreHook.
Adding your custom enemy to the list would mean you have to override the entire function, copy the list and add your enemy, right? While that works, I already went over why you shouldn't do this when it's avoidable, and in this case it is: We can simply let the original function run, and before it actually returns the value, add our own stuff to it.
This is as simple as:
local character_map_original = CharacterTweakData.character_map
function CharacterTweakData:character_map(...)
local char_map = character_map_original(self, ...)
table.insert(char_map.basic.list, "custom_enemy_name")
return char_map
end
Let's go over this code line by line:
- First we save the original function into a local variable, we're backing it up to still have access to it after we override it. We have to always use
.
as the connector between the class and the function name. - Next, we are redefining the function,
essentiallyoverriding it with a completely new one.- Using
...
in the function argumentssimplyrepresentsmeansany"takenumberallofargumentsadditionalthatunspecifiedare given when the function is called".arguments. We can use this to make sure wealwayspasssupplyevery argument that our function is called with to the original function callwith all the arguments it is called with,without actuallylistingcaringthem.what they are. If you need some of the actual arguments, you can simply list all arguments up to the ones you need and then follow them by...
. It is suggested to always use...
at the end of the argument list, even if youusealready listed all of the original argumentstoinmaintaincasecompatibility withany othermodsmodandor gameupdatesupdate adds additional arguments to thatmayfunctionadd additional arguments.call.
- Using
- Then, we are calling the original function via the local variable we have created on line 1 and saving whatever it returns into
char_map
.- When calling the original function inside the new function, we have to supply
self
as the first argument if the function references an object (denoted by a:
connection betweenclassCharacterTweakData
andfunctioncharacter_map
),name)see notes on PostHook.This basically restores the object reference that is "lost" when saving the function to a local variable.
- When calling the original function inside the new function, we have to supply
- We are doing our changes to the return value, in this case the return value is a table and we are inserting our custom enemy into it.
- Finally we have to make sure to return whatever the original function is expected to return, as returning a wrong type or nothing at all
willcan lead to crasheswhenthatthearegamehardexpectstoapinspecific thing (in this case, a table).down.
Whenever the game calls the new CharacterTweakData:character_map
now, it will first call its original function, then our custom enemy is added to the result of the original call. If another mod adds something to the character map in (hopefully) the same way, both changes will merge instead of override each other.
IfSo you chose to override after all else fails
If none of the methods above can be applied to what you want to do, you will have to override the entire function. However, SuperBLT provides a way to do this in a way that at least keeps any hooks made by other mods intact.
ThisAn looksexample likecould be the following:
Hooks:OverrideFunction(GroupAIStateBesiege, "assign_enemy_to_group_ai", function (self)
-- Your code here
end)
Note that if another mod used SuperBLTs PostHook or PreHook on the same function, they will still be called and you can maintain some compatibility with other mods. Redefining the function without making use of Hooks:OverrideFunction
will remove all hooks that were made by mods that ran before yours which could lead to unexpected behavior.