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

Added 'RegisterHookFromBP' and 'UnregisterHookFromBP' for BP Mods #421

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

Okaetsu
Copy link
Contributor

@Okaetsu Okaetsu commented Mar 1, 2024

Description

I added ways to register and unregister hooks from within Blueprints since there's a lot of mods combining both Lua and Blueprints to achieve this at the moment. I've also put checks in place to prevent the same Actor from registering multiple hooks per function and type checking. Context from the hook is also passed to blueprints.

Param count must match the hooked function and this includes Context.

I've done some testing and it seems to be working as intended, but there might be improvements that could be made to the code. The following pictures are all that is needed for registering functions from within blueprints.

You will need the custom events RegisterHookFromBP, UnregisterHookFromBP and a custom function of your choice that will be called by the hook. Param types will be automatically resolved by the lua script handling the hooks so all you have to do is ensure that the Params for your custom function match that hook.

GeneralSetup1

Contents of MyFunction that gets called by the hook.

GeneralSetup2

Results in the following output when the hook gets called:

[15:49:30] [Lua] [Testing] Durability Updated:  119 -> 118
[15:49:31] [Lua] [Testing] Durability Updated:  118 -> 117
[15:49:31] [Lua] [Testing] Durability Updated:  117 -> 116
[15:49:32] [Lua] [Testing] Durability Updated:  116 -> 115

Palworld was used for the example.

Type of change

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

I've done some very basic testing in both UE 4.27 and UE 5.1 games by trying to hook to different functions with different types. Lua limitations will still apply to some types as of UE4SS 3.0.1.

Checklist

  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation.
  • I have added the necessary description of this PR to the changelog, and I have followed the same format as other entries.

@localcc
Copy link
Contributor

localcc commented Mar 1, 2024

I wonder if this can be made more ergonomic and less error prone, e.g. instead of passing the function name to call you pass an event

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 1, 2024

I wonder if this can be made more ergonomic and less error prone, e.g. instead of passing the function name to call you pass an event

I agree that this would be better, and it should be possible at least for the Function to Call param.
It might be trickier with the Hook Name param.
If you generate an SDK or at least a dummy function, and you pass the function, will that link up properly in the game ?
Even if it does link up properly, I think we should still keep the string version specifically for the Hook Name param for people that don't end up having an SDK or using dummy functions because why not, we'll just not recommend it and note that the name must be exact.

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Mar 1, 2024

I agree that this would be better, and it should be possible at least for the Function to Call param. It might be trickier with the Hook Name param. If you generate an SDK or at least a dummy function, and you pass the function, will that link up properly in the game ? Even if it does link up properly, I think we should still keep the string version specifically for the Hook Name param for people that don't end up having an SDK or using dummy functions because why not, we'll just not recommend it and note that the name must be exact.

Yeah, I personally think the HookName is fine, but I honestly don't like having 'FunctionToCall' as it is either and being able to pass a function as the param didn't cross my mind, so if it's possible I'd prefer it over passing an FString.

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 1, 2024

There is no type-checking to make sure the param types of Function to Call match the param types of the hooked function.
What happens if the mod supplies a Function to Call with incorrect param types ? Crash ? Undefined behavior ?

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 1, 2024

We should probably implement an overload to Property:IsA that takes an actual UE property type because the current one only takes the PropertyTypes custom Lua type which means testing one property type against the type of another property will involve extra code.
We already have Property:GetClass which returns a FieldClass, we just need to implement the IsA overload, shouldn't be hard.

EDIT:
The implementation would probably look something like this:

// UE4SS/src/LuaType/LuaXProperty.cpp -> XProperty::setup_member_functions
if (lua.is_table()) // PropertyTypes table
{
    // ...
}
else if (lua.is_userdata()) // FFieldClass, returned from Property:GetClass()
{
    auto ffield_class = lua.get_userdata<LuaType::XFieldClass>();
    lua.set_bool(lua_object.get_remote_cpp_object()->IsA(ffield_class.get_local_cpp_object()));
    return 1;
}

I don't have time to make any kind of PR for this right now but this would be the ideal way to compare the types.
Without this, you'll have to make a helper function that compares every single type to the PropertyTypes table.

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 1, 2024

@Okaetsu Maybe we can put the new IsA overload in your PR ? To keep things simple, and it's a related and dependent feature anyway.

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Mar 2, 2024

For implementing type-checking of params, see these links: https://docs.ue4ss.com/lua-api/classes/ustruct.html#foreachpropertyfunction-callback https://docs.ue4ss.com/lua-api/classes/property.html#isapropertytypes-propertytype https://docs.ue4ss.com/lua-api/table-definitions/propertytypes.html?#propertytypes

Do you mind elaborating on this part further? I assume you want me to type check the properties of both UFunctions which I'm not sure how to do exactly. From a quick look it doesn't seem to be possible to iterate the properties for UObjects/UFunctions with the functions from those Docs unless I missed something.

Also yeah I can include IsA in this PR.

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 2, 2024

For implementing type-checking of params, see these links: https://docs.ue4ss.com/lua-api/classes/ustruct.html#foreachpropertyfunction-callback https://docs.ue4ss.com/lua-api/classes/property.html#isapropertytypes-propertytype https://docs.ue4ss.com/lua-api/table-definitions/propertytypes.html?#propertytypes

Do you mind elaborating on this part further? I assume you want me to type check the properties of both UFunctions which I'm not sure how to do exactly. From a quick look it doesn't seem to be possible to iterate the properties for UObjects/UFunctions with the functions from those Docs unless I missed something.

Also yeah I can include IsA in this PR.

UFunction inherits from UStruct so you should be able to use ForEachProperty from UStruct.

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Mar 2, 2024

UFunction inherits from UStruct so you should be able to use ForEachProperty from UStruct.

    local HookFunctionObj = StaticFindObject(HookName_AsString)
    if HookFunctionObj == nil or not HookFunctionObj:IsValid() or HookFunctionObj:type() ~= "UFunction" then
        error("Tried to hook invalid function '" .. HookName_AsString .. "'")
    end
    
    print(HookFunctionObj:GetFullName()) -- Function /Script/Pal.PalPlayerInventoryData:OnUpdateAnyEquipmentDurability

    HookFunctionObj:ForEachProperty(function(prop)
        print(prop:GetFullName())
    end)

I assume the above should be correct? It results in the following:

[05:35:41] Error: [Lua::call_function] lua_pcall returned Tried calling a member function but the UObject instance is nullptr

stack traceback:
	[C]: in method 'ForEachProperty'
	...naries\Win64\Mods\BPML_GenericFunctions\scripts\main.lua:89: in function <...naries\Win64\Mods\BPML_GenericFunctions\scripts\main.lua:74>

I checked and HookFunctionObj is both valid and a UFunction so I'm not sure if I'm calling ForEachProperty in the wrong place?

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 2, 2024

I assume the above should be correct? It results in the following:

[05:35:41] Error: [Lua::call_function] lua_pcall returned Tried calling a member function but the UObject instance is nullptr

stack traceback:
	[C]: in method 'ForEachProperty'
	...naries\Win64\Mods\BPML_GenericFunctions\scripts\main.lua:89: in function <...naries\Win64\Mods\BPML_GenericFunctions\scripts\main.lua:74>

I checked and HookFunctionObj is both valid and a UFunction so I'm not sure if I'm calling ForEachProperty in the wrong place?

Your code is correct.
Apparently we're accidentally not calling UStructs setup_member_functions for UFunction.
To fix this, we need to add this to the LuaUFunction class:

    class UFunction : public UObjectBase<Unreal::UFunction, UFunctionName>
    {
+    public:
+        using Super = UStruct;
+
      private:
        // This is the 'this' pointer for this UFunction
        Unreal::UObject* m_base{};

And this to UFunction::setup_member_functions:

    template <LuaMadeSimple::Type::IsFinal is_final>
    auto UFunction::setup_member_functions(const LuaMadeSimple::Lua::Table& table) -> void
    {
+       Super::setup_member_functions<LuaMadeSimple::Type::IsFinal::No>(table);
+
        table.add_pair("GetFunctionFlags", [](const LuaMadeSimple::Lua& lua) -> int {

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 2, 2024

If the IsA overload isn't implemented, you'd have to do string comparisons with property:GetClass():GetFName():ToString().
With IsA implemented, the idea is that you can do this instead:

-- 'property1' being the property from the original function
-- 'property2' being the property from the user provided callback
if (property2:IsA(property1:GetClass())) then
    -- Success
else
    -- Type didn't match
end

This is one way to implement the type-checking:

local original_types = {}
local callback_types = {}
-- Pretend there's code here iterating types for both functions and
-- adding each property to the corresponding table above.
if #original_types != #callback_types then
    -- Failure, number of params (including return value) doesn't match.
    return
end
for i = 1, #original_types, 1 do
    if (callback_types[i]:IsA(original_types[i]:GetClass())) then
        -- Success
    else
        -- Failure
    end
end

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Mar 2, 2024

I've run into a small problem which I'm not sure how to solve myself:

    for i = 1, #OriginalTypes, 1 do
        if (CallbackTypes[i]:IsA(OriginalTypes[i]:GetClass())) then
            
        else
            error("Param #" .. i .. " did not match the expected type '" .. OriginalTypes[i]:GetClass():GetFName():ToString() .. 
            "' got '" .. CallbackTypes[i]:GetClass():GetFName():ToString() .. "'")
        end
    end

[14:46:25] Error: [Lua::call_function] lua_pcall returned LUA_ERRRUN => ...naries\Win64\Mods\BPML_GenericFunctions\scripts\main.lua:131: Param #1 did not match the expected type 'FloatProperty' got 'DoubleProperty'

To my knowledge it's not possible to have normal floats anymore atleast through blueprints in UE5 which means the params in my callback function are always going to be DoubleProperty while the original function has FloatProperty params.

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 2, 2024

I've run into a small problem which I'm not sure how to solve myself:

    for i = 1, #OriginalTypes, 1 do
        if (CallbackTypes[i]:IsA(OriginalTypes[i]:GetClass())) then
            
        else
            error("Param #" .. i .. " did not match the expected type '" .. OriginalTypes[i]:GetClass():GetFName():ToString() .. 
            "' got '" .. CallbackTypes[i]:GetClass():GetFName():ToString() .. "'")
        end
    end

[14:46:25] Error: [Lua::call_function] lua_pcall returned LUA_ERRRUN => ...naries\Win64\Mods\BPML_GenericFunctions\scripts\main.lua:131: Param #1 did not match the expected type 'FloatProperty' got 'DoubleProperty'

To my knowledge it's not possible to have normal floats anymore atleast through blueprints in UE5 which means the params in my callback function are always going to be DoubleProperty while the original function has FloatProperty params.

This is interesting.
I'm also not sure how to solve this.
How do you normally call a C++ function that takes single-precision floats from BP ? Does UE automatically decide whether it should be double or float depending on context ?

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 2, 2024

According to this, it looks to me like a BP float can be either double or single precision automatically depending on context.
So in theory, we should be able to treat both FloatProperty and DoubleProperty in the callback params as valid for both FloatProperty and DoubleProperty in the original params.
We should probably test this to make sure.

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 2, 2024

According to this, it looks to me like a BP float can be either double or single precision automatically depending on context. So in theory, we should be able to treat both FloatProperty and DoubleProperty in the callback params as valid for both FloatProperty and DoubleProperty in the original params. We should probably test this to make sure.

This means that we will either need a string comparison against Float/DoubleProperty for every param type, or we need to implement NumericProperty and its member function IsFloatingPoint and do:

if ((CallbackTypes[i]:IsA(OriginalTypes[i]:GetClass())) or (CallbackTypes[i]:IsFloatingPoint() and OriginalTypes[i]:IsFloatingPoint())) then
    -- Success
end

NumericProperty and IsFloatingPoint are already implemented in C++, we'd just need to implement the Lua part.

@UE4SS
Copy link
Collaborator

UE4SS commented Mar 2, 2024

I added NumericProperty & IsFloatingPoint to Lua.
The following code should work:

-- Excuse the extremely long line but I believe Lua has short circuiting so
-- this should be more performant than separating this into two variables.
if ((CallbackTypes[i]:IsA(OriginalTypes[i]:GetClass())) or (type(OriginalTypes[i].IsFloatingPoint) == "function" and CallbackTypes[i]:IsFloatingPoint() and OriginalTypes[i]:IsFloatingPoint())) then
    -- Success
end

@localcc
Copy link
Contributor

localcc commented Apr 5, 2024

What's the status of this PR? Ready for review?

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 5, 2024

Changelog & docs need updating to reflect the changes in this PR.

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 10, 2024

Just to be clear, my previous comment wasn't a response to the question about the status of this PR.
It's not ready for final review, and as far as the status is concerned, I believe we're waiting for @Okaetsu.
We were in the middle of discussing implementation details when this PR went quiet.

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 11, 2024

I think we can improve performance by removing all the string comparisons to "UFunction"`:

-- At file/global-scope:
local FunctionClass = StaticFindObject("/Script/CoreUObject.Function")
if not FunctionClass:IsValid() then
    -- No need to try again because the function class is guaranteed to exist when Lua mods are initialized.
    -- If it doesn't exist, then something's gone horribly wrong or a massive engine change has occurred that removed/renamed this object.
    error("/Script/CoreUObject.Function not found!")
end

-- Anywhere there's a comparison with "UFunction":
if not OriginalFunction:IsA(FunctionClass) then
    error("Not a function!")
end

This does dive back into C++ land to execute IsA and I haven't done any profiling to actually confirm if this is actually faster.

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Apr 11, 2024

Sorry for the wait, I've been busy with some stuff. It's ready for review now.

I had to add EPropertyFlag because it was also including ReferenceParams for the function params and I'd want this part double checked in case it could cause issues:

// LuaMadeSimple.hpp
else if constexpr (std::is_same_v<ValueType, unsigned long long>)
{
    lua_pushinteger(get_lua_instance().get_lua_state(), static_cast<long long>(value));
}

I had to include uint64_t aswell since that's what EPropertyFlags is using and I wasn't sure exactly how lua wants it passed down, but it did seem to work from my testing.

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 11, 2024

// LuaMadeSimple.hpp
else if constexpr (std::is_same_v<ValueType, unsigned long long>)
{
    lua_pushinteger(get_lua_instance().get_lua_state(), static_cast<long long>(value));
}

I had to include uint64_t aswell since that's what EPropertyFlags is using and I wasn't sure exactly how lua wants it passed down, but it did seem to work from my testing.

This is a problem because Lua doesn't support unsigned 64-bit integers.
The biggest integer that Lua supports is 64-bit signed.

I think we either need to implement EPropertyFlags as a non-flag type in Lua and convert to the real enum-flag when it's passed to C++, or do some fancy thing where the type is actually two 32-bit integers under the hood and we convert that to an unsigned 64-bit integer when we actually use it in C++.

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 11, 2024

Regarding the property flags problem, I wonder if it's possible to make an opaque Lua type that's just a uint64 on the C++ side.
We'd have to implement the proper binary operator functions for Lua for ease of use so that you can use | in Lua code.
Example implemented as header-only for simplicity (without binary operators), but obviously move stuff appropriately into the cpp file.
Then the EPropertyFlags table would take this new type for the values instead of regular Lua numbers.

struct UInt64Name
{
    constexpr static const char* ToString()
    {
        return "UInt64";
    }
};
class UInt64 : LocalObjectBase<uint64, UInt64Name>
{
private:
    explicit UInt64(uint64 number) : LocalObjectBase<uint64, UInt64Name>(number)
    {
    }

public:
    UInt64() = delete;
    auto static construct(const LuaMadeSimple::Lua& lua, uint64 number)
    {
        UInt64 lua_object{number};
        lua.transfer_stack_object(std::move(lua_object));
    }
};

Seems there are plenty of way to get around not having proper uint64 support.
Not sure which is best.

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 12, 2024

Current status: This needs to be resolved, see this for another possible solution.
After that, this is probably ready for final review.

Changed HasAll/AnyPropertyFlags to use this new type.
Also changed the EPropertyFlags table to use this type.

This required adding __bor and __band metamethods.
@UE4SS
Copy link
Collaborator

UE4SS commented Apr 12, 2024

I've gone ahead and added and switched to UInt64 for EPropertyFlags and the functions that use EPropertyFlags.
I believe this PR should now be ready for final testing & review, unless @Okaetsu has any other ideas ?
EDIT: I made sure to add support for | and & in Lua for the new UInt64 type to make it easier to combine flags.

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 12, 2024

A changelog entry is missing for this feature, and the docs haven't been updated either.

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Apr 12, 2024

I haven't done docs yet since I wasn't sure if we're going with the original way of registering a callback function with FunctionToCall using FString or if we're going with the Delegate route since that was discussed in the beginning.

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 12, 2024

I haven't done docs yet since I wasn't sure if we're going with the original way of registering a callback function with FunctionToCall using FString or if we're going with the Delegate route since that was discussed in the beginning.

I completely forgot about that.
After having a look around in the UE editor and searching a bit, I've been unable to find a way to pass a function as a parameter with BP alone so I think we might be stuck with using strings.

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Apr 12, 2024

Something I forgot is that this implementation defaults to using pre-hooks and there's no way to hook post-hooks at the moment. I'm wondering if I should add a third optional param (can be left empty) for specifying a post-hook callback function.

EDIT: I was also thinking of renaming the params since the implementation isn't final yet. Something like:
HookName, PreHookCallback, PostHookCallback

Any suggestions for other names are welcome.

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 12, 2024

Something I forgot is that this implementation defaults to using pre-hooks and there's no way to hook post-hooks at the moment. I'm wondering if I should add a third optional param (can be left empty) for specifying a post-hook callback function.

I'm not sure.
The RegisterHook function in Lua has a glaring flaw in that a post-hook isn't always a post-hook and a pre-hook isn't always a pre-hook, and since this system uses the same function it will have the same flaw.
See the two function params here: https://docs.ue4ss.com/lua-api/global-functions/registerhook.html

I guess adding an optional extra param to the BP function makes sense but it must be documented identically to the Lua function, and in fact, it must be documented identically even if an additional param isn't added because the defualt "pre-hook" still has to play by the same rules as the "pre-hook" of the Lua function.

This was making LuaType::ObjectProperty use the wrong metatable, and preventing a call to 'GetPropertyClass' from working.
Also added an extra check to make sure it's actually valid.

Note that there were some formatting errors in this file and this commit also fixes those.
@UE4SS
Copy link
Collaborator

UE4SS commented Apr 13, 2024

@Okaetsu I don't know if you've tested this after you added type-checking but for me it was broken due to a pre-existing bug in UE4SS.
I've fixed that bug and included the fix in this PR.

I've now tested this PR as of my last commit and it seems to be working.

I'm not sure that I'm happy with requiring the precise type for the context param in the BP callback.
I think it's better if we allow anything derived from the Object type, including Object itself.
That would allow users to not have to fake the real type if they aren't interested in it while still giving the precise type to anyone that wants it.
We know that the context will always be an object of some type so we should be able to continue safely as long as the context type is ObjectProperty.
The main downside with doing it this way is that we'd be leaving it completely up to the user to choose the correct type if they actually intend to use the context param, there wouldn't even be a warning or anything, just a game crash.

I've already made the code change so if you agree with this, I'll push the change.

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Apr 13, 2024

@Okaetsu I don't know if you've tested this after you added type-checking but for me it was broken due to a pre-existing bug in UE4SS. I've fixed that bug and included the fix in this PR.

I didn't notice anything from my personal testing, but good to know it's fixed now.

I'm not sure that I'm happy with requiring the precise type for the context param in the BP callback. I think it's better if we allow anything derived from the Object type, including Object itself.

I guess we can do that, but I'll just have to document it so people are aware in case any crashing happens. Feel free to push the change.

Also regarding the docs do you have any ideas for some generic Unreal functions that'd get called for every game? I tried using ClientRestart as an example, but depending on the game it'll get called before the ModActor is spawned. Another one I tried using was a KismetMathLibrary function and calling it manually, but you can't hook those at the moment and I'll make a separate issue for this since it's off-topic.

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 13, 2024

Also regarding the docs do you have any ideas for some generic Unreal functions that'd get called for every game? I tried using ClientRestart as an example, but depending on the game it'll get called before the ModActor is spawned. Another one I tried using was a KismetMathLibrary function and calling it manually, but you can't hook those at the moment and I'll make a separate issue for this since it's off-topic.

For what purpose ?
As a consistently working example for people to use just to make sure they did everything right ?

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Apr 13, 2024

For what purpose ? As a consistently working example for people to use just to make sure they did everything right ?

Yeah, I figured since there's a lot going on with the feature plus it's a blueprint so it might be better to have a full example that can showcase it actually working.

@UE4SS
Copy link
Collaborator

UE4SS commented Apr 13, 2024

For what purpose ? As a consistently working example for people to use just to make sure they did everything right ?

Yeah, I figured since there's a lot going on with the feature plus it's a blueprint so it might be better to have a full example that can showcase it actually working.

I don't know of any functions of the top of my head but here's some C++ code that you can use to save all functions that go through ProcessEvent and ProcessInternal/ProcessLocalScriptFunction.
You can implement this in a C++ mod or straight into UE4SS (probably in UE4SSProgram for the member vars, and the rest in on_program_start or something).

Here's some "working" code, it'll work as long as you put the code in the correct place.
It will log all functions that go through PE/PI/PLSF into FUNCTIONS_EXECUTED_NATIVE.txt for PE, even though PE isn't guaranteed to be native, and FUNCTIONS_EXECUTED_SCRIPT.txt for PI/PLSF.
Obviously you can use just one file if you want or even get rid of the custom file saving completely and use the regular logging facilities to log to UE4SS.log instead, but I figured if you're gonna compare multiple games, this might be easier.

class MyAwesomeMod : public RC::CppUserModBase
{
private:
    Output::Targets<Output::NewFileDevice> NativeFunctionsExecuted{};
    Output::Targets<Output::NewFileDevice> ScriptFunctionsExecuted{};

public:
    // Pretending the constructor exists for brevity.

    auto on_unreal_init() -> void override
    {
        auto& NativeFunctionsExecutedDevice = NativeFunctionsExecuted.get_device<Output::NewFileDevice>();
        NativeFunctionsExecutedDevice.set_file_name_and_path(StringType{UE4SSProgram::get_program().get_working_directory()} + STR("\\FUNCTIONS_EXECUTED_NATIVE.txt"));
        NativeFunctionsExecutedDevice.set_formatter([](File::StringViewType string) {
            return File::StringType{string};
        });
        Hook::RegisterProcessEventPreCallback([&](UObject* Context, UFunction* Function, ...) {
            if (!Function) { return; }
            NativeFunctionsExecuted.send(STR("{}.{}\n"), Context->GetName(), Function->GetName());
        });

        auto& ScriptFunctionsExecutedDevice = ScriptFunctionsExecuted.get_device<Output::NewFileDevice>();
        ScriptFunctionsExecutedDevice.set_file_name_and_path(StringType{UE4SSProgram::get_program().get_working_directory()} + STR("\\FUNCTIONS_EXECUTED_SCRIPT.txt"));
        ScriptFunctionsExecutedDevice.set_formatter([](File::StringViewType string) {
            return File::StringType{string};
        });
        auto script_callback = [&](UObject* Context, FFrame& Stack, ...) {
            ScriptFunctionsExecuted.send(STR("{}.{}\n"), Context->GetName(), Stack.Node()->GetName());
        };
        if (UObject::ProcessLocalScriptFunctionInternal.is_ready() && Version::IsAtLeast(4, 22))
        {
            Hook::RegisterProcessLocalScriptFunctionPreCallback(script_callback);
        }
        else if (UObject::ProcessInternalInternal.is_ready() && Version::IsBelow(4, 22))
        {
            Hook::RegisterProcessInternalPreCallback(script_callback);
        }
    }
};

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Apr 13, 2024

I don't know of any functions of the top of my head but here's some C++ code that you can use to save all functions that go through ProcessEvent and ProcessInternal/ProcessLocalScriptFunction. You can implement this in a C++ mod or straight into UE4SS (probably in UE4SSProgram for the member vars, and the rest in on_program_start or something).

Thanks! I'll do some testing.

@Okaetsu
Copy link
Contributor Author

Okaetsu commented Apr 14, 2024

Looking at this again, I realized there's another limitation to this implementation which is not being able to modify any of the params or return values compared to Lua, but I think just being able to hook functions is already very helpful. I'll note it in the docs.

@narknon
Copy link
Collaborator

narknon commented May 30, 2024

What's the status here?

@UE4SS
Copy link
Collaborator

UE4SS commented Jun 2, 2024

What's the status here?

I think this is the latest information we have + documentation still missing, but no guarantee from me that I'm not forgetting something.
Hopefully @Okaetsu can let us know exactly what the state of this PR is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants