Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Doc updates for blueprint hooking and blueprint hook helper #356

2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"customizer",
"CVAR",
"Deantendo",
"decompiling",
"devcontainer",
"Digby",
"disqualifier",
Expand Down Expand Up @@ -89,6 +90,7 @@
"Photoshop",
"Quixel",
"redirectors",
"reparent",
"Robb",
"RTPC",
"sarisia",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
150 changes: 120 additions & 30 deletions modules/ROOT/pages/Development/Cpp/hooking.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ Hooking is a {cpp} exclusive feature of SML that allows you to attach a custom f
All C/{cpp} functioning hooking stuff can be found in `#include "Patching/NativeHookManager.h"`.
All Blueprint function hook stuff can be found in `#include "Patching/BlueprintHookManager.h"`.

== C/{cpp} Function Hooks
[id="CppFunctionHooks"]
== {cpp} Function Hooks

The hooking interface provides distinct 3 ways of hooking functions, each of which have two types of call order.
{cpp} hooks, also known as native hooks,
allow changing the behavior of game and engine functions implemented in {cpp} without modifying their source code.

SML's hooking interface provides distinct 3 ways of hooking functions, each of which have two types of call order.

If multiple hooks are attached to the same function, these hooks will then get called in the order they were registered.

Expand All @@ -20,13 +24,6 @@ If you hook a member function, the `this` pointer is handled like a parameter an
As long as you don't cancel the final function execution, or do it yourself by calling the scope object,
the final function will be implicitly called after your hook function returns.

[WARNING]
====
**Sometimes** when hooking functions that return custom types such as structs and FStrings, the game can unexpectedly crash!
The cause of this is not fully known and often depends on the function being hooked.
As such, try to avoid hooking functions that return said custom types wherever possible.
====

The call scope object allows you to:

- Cancel the final function execution (if the hook function returns void).
Expand Down Expand Up @@ -217,44 +214,137 @@ Macros will return a delegate that can be used with the
`UNSUBSCRIBE_METHOD` or `UNSUBSCRIBE_UOBJECT_METHOD` macro respectively
in order to unsubscribe from the function.

== Blueprint-Hooking

Blueprint function hooking works by changing the instructions of a Blueprint UFunction
so that your hook gets called before the original function.
[id="BpFunctionHooks"]
== Blueprint Function Hooks

Blueprint function hooking works by changing the instructions of a Blueprint UFunction so that your hook gets called at a specific point in the execution of that function.

Like native hooks, you can hook before and after the function execution. Unlike native hooks, you can also hook at any top-level statement in the function if you know its original instruction index (which itself requires decompiling the function - look at `DEBUG_BLUEPRINT_HOOKING` in SML's BlueprintHookManager.cpp for one way to get a JSON dump of the instructions).

[WARNING]
====
Some blueprints (like UI blueprints) do not exist in the dedicated server build. If your mod attempts to hook such a blueprint in a dedicated server, it will crash the server. You can use the global function `IsRunningDedicatedServer()` to skip hooking in this case.
====

[WARNING]
====
Once you have hooked a blueprint function, there is currently no way to unhook it without fully exiting Satisfactory. For this reason, it is recommended that you create/have a root UGameInstanceModule that installs all blueprint function hooks on game startup, usually when DispatchLifecycleEvent is first called.
====

The hook function signature is `void(FBlueprintHookHelper&)`.
This helper structure provides a couple of functions allowing you to read and write data
to local function (including parameters), output parameters and accessing the context pointer.

You can attach a hook with the `HookBlueprintFunction`-Macro which takes a pointer
to the UFunction you want to attach the hook to.
This FBlueprintHookHelper structure provides ways to:

Usage goes as following:
- Access the Context object (Blueprint instance on which the function is executing).
- Read/write variables of the Context, local variables of the hooked blueprint function (which include its Input variables), and Output variables of the function.
- Skip from the hooked point to the end of the function's execution (though all hooks at that location will be executed prior to this jump).

To attach a blueprint hook, you need a reference to the Blueprint _class_ containing the function you want to hook. There are {cpp}-only ways to do this using LoadClass, but they require hardcoding resource paths, which is not recommended. Instead, you should add these types as member variables to your UGameInstanceModule and then assign them using the picker in the Unreal Editor.

Here's an example of getting a reference to the `BPW_MapMenu` class for hooking (this widget is the left-hand-side menu in the map screen of Satisfactory that lists all the map markers):

First, determine the native parent class of the widget. A quick way to see this is to find the blueprint you wish to hook in the Content Browser of the Unreal Editor and hover over it to find the Native Parent Class line:

image:Development/Cpp/hooking/BPW_MapMenuHover.png[Hovering over BPW_MapMenu]

Next, define a `TSoftClassPtr` property on a {cpp}-backed Root Game Instance Module.
Use the Native Parent Class of the blueprint class you wish to hook as the generic type.
Make it an EditAnywhere UPROPERTY so it will be available in the Unreal Editor
Optionally, assing a `Category` name to help organize the property if you plan to hook multiple things.

[source,cpp]
----
UPROPERTY(EditAnywhere, Category = "UI Widget Types")
TSoftClassPtr<UFGUserWidget> BPW_MapMenuClass;
----

Next, close the editor and rebuild the project for Development Editor,
since you just changed the class and field structure of your mod.
After the build finishes, reopen the editor.

If your mod doesn't have a blueprint Root Instance Module yet,
create one by adding a new blueprint to your mod that uses your {cpp} Root Instance Module class as its base class.
If your mod already has an existing blueprint-implemented Root Instance Module, reparent it to your {cpp} class,
or use a submodule instead (remember, there can only be one root module of each type).

Regardless, open your Root Instance Module blueprint in Unreal Editor.
Find the appropriate row in the module blueprint's Details section under the Category you used, click the dropdown, and find/select the type:

image:Development/Cpp/hooking/BPW_MapMenuTypeSelected.png[BPW_MapMenu selected]

The class is now availabe to your module for hooking. If you don't already know the name of the blueprint function you wish to hook, these can found by opening the blueprint in the Unreal Editor, going to the Graph view, and then viewing the FUNCTIONS accordion under the My Blueprint tab:

image:Development/Cpp/hooking/BPW_MapMenuFunctions.png[BPW_MapMenu functions]

Now you can create the actual hook in {cpp}. Make sure you have the proper includes:

[source,cpp]
----
#include "Patching/BlueprintHookManager.h"
#include "Patching/BlueprintHookHelper.h"
----

void registerHooks() {
UClass* SomeClass = ...;
UFunction* SomeFunc = SomeClass->FindFunctionByName(TEXT("TestFunc"));
Get a reference to the UBlueprintHookManager like so:

HookBlueprintFunction(SomeFunc, [](FBlueprintHookHelper& helper) {
UObject* ctx = helper.GetContext(); // the object this function got called onto
FString* localStr = helper.GetLocalVarPtr<FString>("StrVariable"); // getting the pointer to a local variable
FString* output = helper.GetOutVariablePtr<FString>("OutValue"); // getting the pointer to a output variable
// do some nice stuff there
});
}
[source,cpp]
----
UBlueprintHookManager* hookManager = GEngine->GetEngineSubsystem<UBlueprintHookManager>();
----

[WARNING]
====
If you attempt to get the UBlueprintHookManager extremely early in startup, the game will crash.
It will be available by the time DispatchLifecycleEvent is called on your UGameInstanceModule.
Remember that DispatchLifecycleEvent is called three times with three different phase values as the game initializes
- be sure to only create the hooks in one of these phases (ELifecyclePhase::CONSTRUCTION should be fine).
====

Hooks can be created by calling `HookBlueprintFunction` on the hook manager:

[source,cpp]
----
hookManager->HookBlueprintFunction(
BPW_MapMenuClass->FindFunctionByName(TEXT("AddActorRepresentationToMenu")), // Will crash if you typo the function name
[](FBlueprintHookHelper& helper) {
// Hook code here
},
EPredefinedHookOffset::Start );
// EPredefinedHookOffset::Start hooks just before the function executes.
// For a hook just before the function returns, use EPredefinedHookOffset::Return
----

[WARNING]
====
You can also provide a count of instruction as third parameter to hook as instruction based offset from the top.
But we highly encourage you to not do so unless you know what you exactly do!
You can create hooks at nearly-arbitrary points in the function by passing the integer offset of the statement where you'd like to hook instead of an EPredefinedHookOffset. Only do this if you know exactly what you're doing and why!
====

FBlueprintHookHelper has all the functionality you should need if you wish to modify the state of the blueprint or function execution. To get and/or set the values of variables, use one of:

[source,cpp]
----
// For reading/writing variables on the blueprint that is being hooked (in the example above, this means member variables of BPW_MapMenu)
TSharedRef<FBlueprintHookVariableHelper_Context> contextHelper = helper.GetContextVariableHelper();

// For reading/writing Input variables of the function, as well as any local variables the function is using for execution (but you have to know their names by decompiling the function)
TSharedRef<FBlueprintHookVariableHelper_Local> localHelper = helper.GetLocalVariableHelper();

// For reading/writing Output variables of the function
TSharedRef<FBlueprintHookVariableHelper_Out> outHelper = helper.GetOutVariableHelper();
----

Check the header comments on each `Get*VariableHelper` method to learn which helper to use in what situation.
Here is a quick example:

[source,cpp]
----
TSharedRef<FBlueprintHookVariableHelper_Local> localHelper = helper.GetLocalVariableHelper();
ERepresentationType* representationType = localHelper->GetEnumVariablePtr<ERepresentationType>(TEXT("representationType"));
int* intValuePtr = localHelper->GetVariablePtr<FIntProperty>(TEXT("someIntValue"));
*intValuePtr = 42; // You can write values to variables simply by using the returned pointers
----


== Protected/Private Function Hooking

If the function you are attempting to hook is protected or private to that specific class, you must use the `friend` declaration.
Expand All @@ -278,7 +368,7 @@ You must first edit the `FGPlayerController.h` header and add the following bloc
----
namespace MyMod
{
class MyWatcher;
class MyWatcher;
}
----

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ To use it, right click on any Mod Configuration asset and run

You can also launch it from the right click menu of the editor widget or asset action utility.

// cspell:ignore Reparent Reparenter
// cspell:ignore Reparenter

== Mass Asset Reparenter

Expand Down
Loading