Custom Callbacks#
pyMHF is able to register custom callbacks which are useful for libraries in that they can be used to declare decorators which perform some certain action which is linked to the executable being modded.
The best way to see this is by an example taken from the NMS.py source code:
from pymhf.core import DetourTime
class main_loop:
@staticmethod
def before(func):
func._custom_trigger = "MAIN_LOOP"
func._hook_time = DetourTime.BEFORE
return func
@staticmethod
def after(func):
func._custom_trigger = "MAIN_LOOP"
func._hook_time = DetourTime.AFTER
return func
def on_fully_booted(func):
"""
Configure the decorated function to be run once the game is considered
"fully booted".
This occurs when the games' internal state first changes to "mode selector"
(ie. just before the game mode selection screen appears).
"""
func._custom_trigger = "MODESELECTOR"
return func
The critical piece of information to take away from the above is the assignment func._custom_trigger = <something>
. This line assigns the function as a custom trigger with the string value as the key.
We can define the hook time for this custom callback by specifying func._hook_time = DetourTime.BEFORE
or func._hook_time = DetourTime.AFTER
.
If this isn’t provided then the fallback time will be DetourTime.NONE
which essentially means “it doesn’t matter”.
These above custom callbacks can then be applied to some function in a mod like so:
import logging
from decorators import main_loop, on_fully_booted
from pymhf import Mod
class MyMod(Mod):
@main_loop.before
def before_mainloop(self):
logging.info("Before the main loop!")
@on_fully_booted
def booted(self):
logging.info("Game is booted!")
Note
The custom callback functions defined within the mod cannot take any arguments.
Now that we have defined the custom callbacks, and we have applied them to some functions in our mod, the last thing we need to do is write the actual code which will cause these callbacks to get triggered.
It is recommended that theses are implemented in “internal” mods which can be defined like so:
1from pymhf import Mod
2from pymhf.core import DetourTime
3from pymhf.gui.decorators import no_gui
4from pymhf.core.hooking import hook_manager
5import pymhf.core._internal as _internal
6
7# Internal imports for various constants etc defined by the library.
8import hooks
9import StateEnum
10
11@no_gui
12class _INTERNAL_Main(Mod):
13
14 @hooks.cTkFSMState.StateChange.after
15 def state_change(self, this, lNewStateID, lpUserData, lbForceRestart):
16 if lNewStateID == StateEnum.ApplicationGameModeSelectorState.value:
17 curr_gamestate = _internal.GameState.game_loaded
18 _internal.GameState.game_loaded = True
19 if _internal.GameState.game_loaded != curr_gamestate:
20 # Only call this the first time the game loads
21 hook_manager.call_custom_callbacks("MODESELECTOR", DetourTime.AFTER)
22 hook_manager.call_custom_callbacks("MODESELECTOR", DetourTime.NONE)
23 else:
24 hook_manager.call_custom_callbacks(lNewStateID.decode(), DetourTime.AFTER)
25 hook_manager.call_custom_callbacks(lNewStateID.decode(), DetourTime.NONE)
26
27 @hooks.cGcApplication.Update.before
28 def _main_loop_before(self, this):
29 """ The main application loop. Run any before functions here. """
30 hook_manager.call_custom_callbacks("MAIN_LOOP", DetourTime.BEFORE)
31
32 @hooks.cGcApplication.Update.after
33 def _main_loop_after(self, this):
34 """ The main application loop. Run any after functions here. """
35 hook_manager.call_custom_callbacks("MAIN_LOOP", DetourTime.AFTER)
The above shows off a few things.
The first thing to notice is that the no_gui
decorator which indicates that the mod won’t be displayed in the GUI. Since this is an internal mod we don’t need it to be reloadable or need to expose anything to users. However, it may make sense for a library to provide this to users, so it’s not something that necessarily needs to be applied.
The next thing to notice is that we are using hooks defined within the library (ie. hooks.cGcApplication.Update
). These make writing hooks significantly easier for users compared to having to use the manual_hook
decorator. In this case we have got hooks defined for the main update loop function, as well as a state change function.
Finally, we can see on a number of lines a call to call_custom_callbacks
.
This is the connection between the previous two code blocks.
We can read/understand this code as; when the _main_loop_before
detour is run, it will call the "MAIN_LOOP"
custom callback with the detour time being DetourTime.BEFORE
. This will call the MyMod.before_mainloop
method which we can see in main_loop_mod.py
as provided above.
Conclusion#
By utilising custom callbacks as shown above, library authors can create simple decorators to give mod authors a very easy way to hook into regularly used functions such are state changes or the main update loop of a game.
Warning
Providing a decorator for the main game loop is very useful, but it should be remembered that we are using python, and while the overhead is fairly low in calling detours in this loop, it should be stressed that if you put too much in the functions that run before or after the main loop function it could easily cause performance issues.