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:

decorators.py#
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:

main_loop_mod.py#
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:

internal_mod.py#
 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.