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

Update with changes from Dev #357

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion antora-playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ content:
sources:
## Used for prod
- url: https://github.com/satisfactorymodding/Documentation.git
branches: master, v3.7.0, v3.6.1, v3.5.1, v3.4.1, v3.3.2, v3.1.1, v2.2.1, v2.1.1, v2.0.0, v1.0.2
branches: master, v3.8.0, v3.7.0, v3.6.1, v3.5.1, v3.4.1, v3.3.2, v3.1.1, v2.2.1, v2.1.1, v2.0.0, v1.0.2
edit_url: '{web_url}/blob/{refname}/{path}'

## Used for dev
Expand Down
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.
5 changes: 3 additions & 2 deletions modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
** xref:ManualInstallDirections.adoc[Manual Installation]

* xref:Development/index.adoc[Development]
** xref:Development/UpdatingFromSml37.adoc[Updating from SML 3.7.0]
** xref:Development/UpdatingFromSml38.adoc[Updating from SML 3.8.0]
** xref:Development/UpdatingToNewVersions.adoc[_Updating your Mod_]
** xref:Development/BeginnersGuide/index.adoc[Getting Started]
*** Installing Dependencies
Expand Down Expand Up @@ -101,8 +101,9 @@
*** xref:Development/ModLoader/GameMapRegistry.adoc[Game Map Registry]
*** xref:Development/ModLoader/ContentTagRegistry.adoc[Content Tag Registry]
*** xref:Development/ModLoader/ExtendedAttributeProvider.adoc[Extended Attribute Provider]
*** Legacy
*** xref:SMLConfiguration.adoc[SML Configuration]
*** Legacy
**** xref:Development/UpdatingFromSml37.adoc[Updating from SML 3.7.0]
**** xref:Development/UpdatingFromSml36.adoc[Updating from SML 3.6.1]
**** xref:Development/UpdatingFromSml35.adoc[Updating from SML 3.5.1]
**** xref:Development/UpdatingFromSml34.adoc[Updating from SML 3.4.1]
Expand Down
16 changes: 9 additions & 7 deletions modules/ROOT/pages/Development/BeginnersGuide/ReleaseMod.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -235,23 +235,25 @@ citing the offending uplugin file in the error message.
!===

|RemoteVersionRange
| A Semver range of versions accepted from the remote clients.
This requires other players to have a certain version of the
mod installed to be able to join hosts.
See the Plugins SemVersion item above for the format for this field.
| A Semver range of versions accepted from the remote side in multiplayer.
This requires remotes to have a certain version of the mod installed to be able to join.
See the Plugins `SemVersion` item above for the format for this field.
{blank} +
{blank} +
This field is optional, and defaults to your SemVersion if unspecified. If you aren't using this behavior, you should exclude the field.
This field is optional, and defaults to your `SemVersion` if unspecified,
meaning both sides must have the exact same mod version installed.
If you aren't using this behavior, you should exclude the field.

|RequiredOnRemote
| Controls if the mod is required to be on both sides in multiplayer.
When a client connects, the host checks its own mod list against what the client is connecting with.
If the host's mod has `RequiredOnRemote` set to true,
`RemoteVersionRange` is used to check the client's reported version to ensure it's compatible.
The reverse (client checking host) is not currently implemented but may be in the future.
The reverse (client checking host) is implemented as of SML 3.9.0.
{blank} +
{blank} +
This field is optional, and defaults to `true`. If you aren't using this behavior, you should exclude the field.
This field is optional, and defaults to `true`.
If you aren't using this behavior, you should exclude the field.

|===

Expand Down
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
34 changes: 25 additions & 9 deletions modules/ROOT/pages/Development/ModLoader/index.adoc
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
= SML

The Satisfactory Mod Loader provides framework for all the stuff you as modder might encounter
but unreal doesn't provide help with.
but Unreal doesn't provide help with.

== Mod "Types"

There are two main ways to load a mod into a game, each of those
variants have there advantages and disadvantages.
All Satisfactory mods are full Unreal Engine plugins.
There are three main ways to create a mod with this approach,
each with advantages and disadvantages.

* {blank}
.dll (C++) Mods::
`.dll` s are written in C++ and provide deep access to the
Satisfactory runtime and the logic written in there is extremely fast.
Blueprint Mods::
Blueprint mods consist of Unreal Blueprint assets which can contain data (like meshes, sounds, textures)
and code written in Unreal Blueprint scripting.
SML provides a number of utilities and entry points to enable blueprint mods to function.
Blueprint mods can do a lot, but they can generally only access fields and methods have been made blueprint accessible from the {cpp} side.
+
* {blank}
Pak Mods::
Paks are archives with classical assets like meshes, sounds and more.
SML also provides the SPL so you could make a mod without any C++ coding.
{cpp} Mods::
Writing a mod in Unreal {cpp} provides deep access to the Satisfactory runtime
and is usually more performant than blueprint code because it does not need to go through the overhead of the blueprint VM.
However, it's a pain to reference blueprint assets (a frequent requirement for working with base game content)
in a non-brittle manner, and working with widgets is agonizingly verbose.
Performing delayed actions is also much more involved than in Blueprint scripting.
+
* {blank}
Hybrid Blueprint and {cpp} Mods::
Writing a mod with {cpp} does not mean replacing all of your Blueprint assets with {cpp} code.
The best mods are implemented with a careful combination of both {cpp} and Blueprint code
since each side covers for the other's disadvantages.
For example, you can implement references to blueprint assets and widgets on the
However, changes to your mod's class {cpp} structure will require closing the editor and rebuilding Development Editor
for the blueprint side to recognize the changes.
+
2 changes: 1 addition & 1 deletion modules/ROOT/pages/Development/OpenSourceExamples.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ _Notable Techniques Used:_
* Save/loading of soft class and soft object fields
* Hybrid Blueprint/{cpp} subsystems
** Subsystems' final implementations are in Blueprint,
but they are backed by custom {cpp} parent classes to allow
but they are backed by custom {cpp} parent classes to allow {cpp} mods to reference the data fields.
* Zero-dependency cross mod interaction
** Mod is coded to inspect other mod's assets for fields with a certain name,
and then change behavior based on those fields values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,3 +502,44 @@ bool AMyModHologram::TryUpgrade(const FHitResult& hitResult)
return Super::TryUpgrade(hitResult);
}
```

== Showing Additional Visualizations

Blueprint Machine Holograms can spawn custom visuals to assist the user with placement of some buildable objects.
Using a custom visualization replaces the default visualization.
This approach is commonly used when a custom buildable requires extra settings and configuration to look correct.
Consider the following example:

```cpp
void FMyModModule::StartupModule() {
AFGBlueprintHologram::FCreateBuildableVisualizationDelegate visualizingDelegate;
visualizingDelegate.BindLambda([](AFGBlueprintHologram* blueprintHologram, AFGBuildable* buildable, USceneComponent* buildableRootComponent) {
// Get your buildable
AMyBuildable* myBuildable = Cast<AMyBuildable>(buildable);

// - your cool code here for all the relevant components-
// likely involves referencing stuff myBuildable
// note that the buildable reference is only scoped to this lambda so don't expect it to persist

// example: if we had one custom spline mesh component to show it might look like this:
USplineMeshComponent* splineMesh = Cast<USplineMeshComponent>(
// we use the built in setup component so we don't have to worry about things like: attaching to the actor, transformations, mobility, customizer data, hologramFX, collision channels, and more you would need to set manually if you didn't
blueprintHologram->SetupComponent(
buildableRootComponent,
buildable->GetComponentByClass<USplineMeshComponent>(),
buildable->GetFName(),
FName()
)
);

// example: now do the configuration the vanilla configuration won't, since we are using a custom one
splineMesh->SetStartPosition(curve->StartPosition, false);
splineMesh->SetEndPosition(curve->EndPosition, false);
splineMesh->SetStartTangent(curve->StartTangent, false);
splineMesh->SetEndTangent(curve->EndTangent, false);
splineMesh->UpdateMesh_Concurrent();
});

AFGBlueprintHologram::RegisterCustomBuildableVisualization(AABCurvedDecorBuildable::StaticClass(), visualizingDelegate);
}
```
Loading
Loading