From b641706dd9112ac157f2a05656c7bdb688a5ff03 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Mon, 16 Dec 2024 16:28:43 +1300 Subject: [PATCH 1/5] fix has/remove command case handling being inconsistent with add --- src/unrealsdk/commands.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/unrealsdk/commands.cpp b/src/unrealsdk/commands.cpp index 315852c..7d787bd 100644 --- a/src/unrealsdk/commands.cpp +++ b/src/unrealsdk/commands.cpp @@ -29,7 +29,7 @@ UNREALSDK_CAPI(bool, add_command, const wchar_t* cmd, size_t size, DLLSafeCallba return false; } - commands.emplace(lower_cmd, callback); + commands.emplace(std::move(lower_cmd), callback); return true; } #endif @@ -44,8 +44,9 @@ UNREALSDK_CAPI(bool, has_command, const wchar_t* cmd, size_t size); #endif #ifndef UNREALSDK_IMPORTING UNREALSDK_CAPI(bool, has_command, const wchar_t* cmd, size_t size) { - const std::wstring_view view{cmd, size}; - return commands.contains(view); + std::wstring lower_cmd(size, '\0'); + std::transform(cmd, cmd + size, lower_cmd.begin(), &std::towlower); + return commands.contains(lower_cmd); } #endif @@ -58,8 +59,9 @@ UNREALSDK_CAPI(bool, remove_command, const wchar_t* cmd, size_t size); #endif #ifndef UNREALSDK_IMPORTING UNREALSDK_CAPI(bool, remove_command, const wchar_t* cmd, size_t size) { - const std::wstring_view view{cmd, size}; - auto iter = commands.find(view); + std::wstring lower_cmd(size, '\0'); + std::transform(cmd, cmd + size, lower_cmd.begin(), &std::towlower); + auto iter = commands.find(lower_cmd); if (iter == commands.end()) { return false; From bebaeab4e841f3fb1f87f9ceb4958f82b5d13712 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Mon, 16 Dec 2024 16:36:03 +1300 Subject: [PATCH 2/5] expand scope of locking function calls this is motivated by a lockup caused in bl2 calling `PC.ConsoleCommand("x")` in trying to print the `>>> x <<<` line, we woke the logger thread it then changed the function flags on `Engine.Console:OutputTextLine`, before getting stuck on waiting for the lock inside `process_event` at the same time, the main thread tried to print `Command not recognized: x`, also calling `OutputTextLine` something about the flags having been changed while calling it caused a lockup fix is to expand the scope of the lock, and only change the function flags while we hold it incidentally the native call to `OutputTextLine` went through `CallFunction`, figure it's a good idea to lock during it too --- src/unrealsdk/game/bl2/hooks.cpp | 48 +++++++++---------- src/unrealsdk/game/bl3/hooks.cpp | 43 ++++++++--------- src/unrealsdk/locks.cpp | 34 +++++++++++++ src/unrealsdk/locks.h | 34 +++++++++++++ .../unreal/wrappers/bound_function.cpp | 27 +++++++++-- supported_settings.toml | 5 +- 6 files changed, 137 insertions(+), 54 deletions(-) create mode 100644 src/unrealsdk/locks.cpp create mode 100644 src/unrealsdk/locks.h diff --git a/src/unrealsdk/game/bl2/hooks.cpp b/src/unrealsdk/game/bl2/hooks.cpp index b8ec007..75ab223 100644 --- a/src/unrealsdk/game/bl2/hooks.cpp +++ b/src/unrealsdk/game/bl2/hooks.cpp @@ -3,6 +3,7 @@ #include "unrealsdk/config.h" #include "unrealsdk/game/bl2/bl2.h" #include "unrealsdk/hook_manager.h" +#include "unrealsdk/locks.h" #include "unrealsdk/memory.h" #include "unrealsdk/unreal/classes/ufunction.h" #include "unrealsdk/unreal/classes/uobject.h" @@ -112,14 +113,12 @@ void __fastcall process_event_hook(UObject* obj, process_event_ptr(obj, edx, func, params, null); } -std::recursive_mutex process_event_mutex{}; - void __fastcall locking_process_event_hook(UObject* obj, void* edx, UFunction* func, void* params, void* null) { - const std::lock_guard lock{process_event_mutex}; + const locks::FunctionCall lock{}; process_event_hook(obj, edx, func, params, null); } @@ -127,33 +126,20 @@ static_assert(std::is_same_v, "process_event signature is incorrect"); static_assert(std::is_same_v, "process_event signature is incorrect"); - -/** - * @brief Checks if we should use a locking process event implementation. - * - * @return True if we should use locks. - */ -bool locking(void) { - // Basically just a function so we can be sure this static is initialized late - LTO hopefully - // takes care of it - static auto locking = config::get_bool("unrealsdk.locking_process_event").value_or(false); - return locking; -} - } // namespace void BL2Hook::hook_process_event(void) { - detour(PROCESS_EVENT_SIG, locking() ? locking_process_event_hook : process_event_hook, + detour(PROCESS_EVENT_SIG, + // If we don't need locks, it's slightly more efficient to detour directly to the + // non-locking version + locks::FunctionCall::enabled() ? locking_process_event_hook : process_event_hook, &process_event_ptr, "ProcessEvent"); } void BL2Hook::process_event(UObject* object, UFunction* func, void* params) const { - if (locking()) { - const std::lock_guard lock{process_event_mutex}; - process_event_hook(object, nullptr, func, params, nullptr); - } else { - process_event_hook(object, nullptr, func, params, nullptr); - } + // When we call it manually, always call the locking version, it will pass right through if + // locks are disabled + locking_process_event_hook(object, nullptr, func, params, nullptr); } namespace { @@ -244,13 +230,27 @@ void __fastcall call_function_hook(UObject* obj, call_function_ptr(obj, edx, stack, result, func); } + +void __fastcall locking_call_function_hook(UObject* obj, + void* edx, + FFrame* stack, + void* result, + UFunction* func) { + const locks::FunctionCall lock{}; + call_function_hook(obj, edx, stack, result, func); +} + static_assert(std::is_same_v, "call_function signature is incorrect"); +static_assert(std::is_same_v, + "call_function signature is incorrect"); } // namespace void BL2Hook::hook_call_function(void) { - detour(CALL_FUNCTION_SIG, call_function_hook, &call_function_ptr, "CallFunction"); + detour(CALL_FUNCTION_SIG, + locks::FunctionCall::enabled() ? locking_call_function_hook : call_function_hook, + &call_function_ptr, "CallFunction"); } } // namespace unrealsdk::game diff --git a/src/unrealsdk/game/bl3/hooks.cpp b/src/unrealsdk/game/bl3/hooks.cpp index 09a3ac0..7797540 100644 --- a/src/unrealsdk/game/bl3/hooks.cpp +++ b/src/unrealsdk/game/bl3/hooks.cpp @@ -3,6 +3,7 @@ #include "unrealsdk/config.h" #include "unrealsdk/game/bl3/bl3.h" #include "unrealsdk/hook_manager.h" +#include "unrealsdk/locks.h" #include "unrealsdk/memory.h" #include "unrealsdk/unreal/classes/ufunction.h" #include "unrealsdk/unreal/classes/uobject.h" @@ -81,10 +82,8 @@ void process_event_hook(UObject* obj, UFunction* func, void* params) { process_event_ptr(obj, func, params); } -std::recursive_mutex process_event_mutex{}; - void locking_process_event_hook(UObject* obj, UFunction* func, void* params) { - const std::lock_guard lock{process_event_mutex}; + const locks::FunctionCall lock{}; process_event_hook(obj, func, params); } @@ -93,32 +92,20 @@ static_assert(std::is_same_v, static_assert(std::is_same_v, "process_event signature is incorrect"); -/** - * @brief Checks if we should use a locking process event implementation. - * - * @return True if we should use locks. - */ -bool locking(void) { - // Basically just a function so we can be sure this static is initialized late - LTO hopefully - // takes care of it - static auto locking = config::get_bool("unrealsdk.locking_process_event").value_or(false); - return locking; -} - } // namespace void BL3Hook::hook_process_event(void) { - detour(PROCESS_EVENT_SIG, locking() ? locking_process_event_hook : process_event_hook, + detour(PROCESS_EVENT_SIG, + // If we don't need locks, it's slightly more efficient to detour directly to the + // non-locking version + locks::FunctionCall::enabled() ? locking_process_event_hook : process_event_hook, &process_event_ptr, "ProcessEvent"); } void BL3Hook::process_event(UObject* object, UFunction* func, void* params) const { - if (locking()) { - const std::lock_guard lock{process_event_mutex}; - process_event_hook(object, func, params); - } else { - process_event_hook(object, func, params); - } + // When we call it manually, always call the locking version, it will pass right through if + // locks are disabled + locking_process_event_hook(object, func, params); } namespace { @@ -213,13 +200,23 @@ void call_function_hook(UObject* obj, FFrame* stack, void* result, UFunction* fu call_function_ptr(obj, stack, result, func); } + +void locking_call_function_hook(UObject* obj, FFrame* stack, void* result, UFunction* func) { + const locks::FunctionCall lock{}; + call_function_hook(obj, stack, result, func); +} + static_assert(std::is_same_v, "call_function signature is incorrect"); +static_assert(std::is_same_v, + "call_function signature is incorrect"); } // namespace void BL3Hook::hook_call_function(void) { - detour(CALL_FUNCTION_SIG, call_function_hook, &call_function_ptr, "CallFunction"); + detour(CALL_FUNCTION_SIG, + locks::FunctionCall::enabled() ? locking_call_function_hook : call_function_hook, + &call_function_ptr, "CallFunction"); } } // namespace unrealsdk::game diff --git a/src/unrealsdk/locks.cpp b/src/unrealsdk/locks.cpp new file mode 100644 index 0000000..b6da14c --- /dev/null +++ b/src/unrealsdk/locks.cpp @@ -0,0 +1,34 @@ +#include "unrealsdk/pch.h" +#include "unrealsdk/locks.h" +#include "unrealsdk/config.h" + +#ifndef UNREALSDK_IMPORTING + +namespace unrealsdk::locks { + +namespace { + +std::recursive_mutex function_call_mutex; + +} + +bool FunctionCall::enabled(void) { + static auto enabled = config::get_bool("unrealsdk.locking_function_calls").value_or(false); + return enabled; +} + +FunctionCall::FunctionCall() { + if (enabled()) { + function_call_mutex.lock(); + } +} + +FunctionCall::~FunctionCall() { + if (enabled()) { + function_call_mutex.unlock(); + } +} + +} // namespace unrealsdk::locks + +#endif diff --git a/src/unrealsdk/locks.h b/src/unrealsdk/locks.h new file mode 100644 index 0000000..2a0077b --- /dev/null +++ b/src/unrealsdk/locks.h @@ -0,0 +1,34 @@ +#ifndef UNREALSDK_LOCKS_H +#define UNREALSDK_LOCKS_H + +#ifndef UNREALSDK_IMPORTING + +namespace unrealsdk::locks { + +/** + * @brief RAII class to hold the function call lock. + * @note Noop if locking function calls are not enabled. + */ +struct FunctionCall { + public: + FunctionCall(); + ~FunctionCall(); + + FunctionCall(const FunctionCall&) = delete; + FunctionCall(FunctionCall&&) noexcept = delete; + FunctionCall& operator=(const FunctionCall&) = delete; + FunctionCall& operator=(FunctionCall&&) noexcept = delete; + + /** + * @brief Checks if the function call lock is enabled. + * + * @return True if enabled, false if disabled. + */ + static bool enabled(void); +}; + +} // namespace unrealsdk::locks + +#endif + +#endif /* UNREALSDK_LOCKS_H */ diff --git a/src/unrealsdk/unreal/wrappers/bound_function.cpp b/src/unrealsdk/unreal/wrappers/bound_function.cpp index 46535b2..75ae72d 100644 --- a/src/unrealsdk/unreal/wrappers/bound_function.cpp +++ b/src/unrealsdk/unreal/wrappers/bound_function.cpp @@ -1,5 +1,7 @@ #include "unrealsdk/pch.h" +#include "unrealsdk/exports.h" +#include "unrealsdk/locks.h" #include "unrealsdk/unreal/classes/uproperty.h" #include "unrealsdk/unreal/prop_traits.h" #include "unrealsdk/unreal/structs/fname.h" @@ -37,13 +39,28 @@ void validate_no_more_params(UProperty* prop) { } // namespace func_params::impl -void BoundFunction::call_with_params(void* params) const { - auto original_flags = this->func->FunctionFlags; - this->func->FunctionFlags |= UFunction::FUNC_NATIVE; +UNREALSDK_CAPI(void, bound_function_call_with_params, const BoundFunction* self, void* params); + +#ifndef UNREALSDK_IMPORTING + +// This has to be implemented in the base dll so that we can lock it properly +UNREALSDK_CAPI(void, bound_function_call_with_params, const BoundFunction* self, void* params) { + const locks::FunctionCall lock{}; - unrealsdk::internal::process_event(this->object, this->func, params); + auto original_flags = self->func->FunctionFlags; + self->func->FunctionFlags |= UFunction::FUNC_NATIVE; - func->FunctionFlags = original_flags; + // Calling process event itself does hold the lock, but we also need to guard messing with the + // function flags + unrealsdk::internal::process_event(self->object, self->func, params); + + self->func->FunctionFlags = original_flags; +} + +#endif + +void BoundFunction::call_with_params(void* params) const { + UNREALSDK_MANGLE(bound_function_call_with_params)(this, params); } } // namespace unrealsdk::unreal diff --git a/supported_settings.toml b/supported_settings.toml index e68f04d..707efd5 100644 --- a/supported_settings.toml +++ b/supported_settings.toml @@ -45,11 +45,12 @@ treference_controller_destructor_vf_index = -1 # Overrides the virtual function index used when calling `FText::GetDisplayString`. ftext_get_display_string_vf_index = -1 -# If true, locks simultaneous ProcessEvent calls from different threads. This is used both for hooks -# and for calling unreal functions. +# If true, locks simultaneous unreal function calls from different threads. This lock is held both +# during hooks and when you manually call unreal functions. # When true, if external code must take care with it's own locks. If external code attempts to # acquire a lock inside a hook, while at the same time trying to call an unreal function on the # thread which holds that lock, the system will deadlock. +locking_function_calls = false locking_process_event = false # After enabling `unrealsdk::hook_manager::log_all_calls`, the file to calls are logged to. From b652da13a3d9e3e1b9962506820cf794ef86c040 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Mon, 16 Dec 2024 16:38:53 +1300 Subject: [PATCH 3/5] print executed command message directly, fix log level affecting it and it potentially being out of order --- src/unrealsdk/game/bl2/console.cpp | 24 +++++++++++++- src/unrealsdk/game/bl3/console.cpp | 50 +++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/unrealsdk/game/bl2/console.cpp b/src/unrealsdk/game/bl2/console.cpp index b50b86f..eb349c2 100644 --- a/src/unrealsdk/game/bl2/console.cpp +++ b/src/unrealsdk/game/bl2/console.cpp @@ -52,6 +52,10 @@ const std::wstring INJECT_CONSOLE_FUNC = L"WillowGame.WillowGameViewportClient:P const constexpr auto INJECT_CONSOLE_TYPE = hook_manager::Type::PRE; const std::wstring INJECT_CONSOLE_ID = L"unrealsdk_bl2_inject_console"; +// Would prefer to call a native function where possible, however best I can tell, OutputText is +// actually implemented directly in unrealscript (along most of the console mechanics). +BoundFunction console_output_text{}; + bool say_bypass_hook(hook_manager::Details& hook) { /* This is a native function so we don't have exact source, but we expect it's essentially: @@ -201,7 +205,25 @@ bool console_command_hook(hook_manager::Details& hook) { hook.obj->get(save_config_func).call(); } - LOG(INFO, L">>> {} <<<", line); + /* + This is a little awkward. + Since we can't let execution though to the unreal function, we're responsible for printing the + executed command line. + + We do this via output text directly, rather than the LOG macro, so that it's not affected by the + console log level, and so that it happens immediately (the LOG macro is queued, and can get out + of order with respect to native engine messages). + + However, for custom console commands it's also nice to see what the command was in the log file, + since you'll see all their output too. + + We don't really expose a "write to log file only", since it's not usually something useful, so + as a compromise just use the LOG macro on the lowest possible log level, and assume the lowest + people practically set their console log level to is dev warning. + */ + auto msg = unrealsdk::fmt::format(L">>> {} <<<", line); + console_output_text.call(msg); + LOG(MIN, L"{}", msg); try { callback->operator()(line.c_str(), line.size(), cmd_len); diff --git a/src/unrealsdk/game/bl3/console.cpp b/src/unrealsdk/game/bl3/console.cpp index 10f16dd..c335ffc 100644 --- a/src/unrealsdk/game/bl3/console.cpp +++ b/src/unrealsdk/game/bl3/console.cpp @@ -38,6 +38,22 @@ const constexpr auto MAX_HISTORY_ENTRIES = 50; UObject* console = nullptr; +void static_uconsole_output_text(const std::wstring& str) { + static auto idx = config::get_int("unrealsdk.uconsole_output_text_vf_index") + .value_or(81); // NOLINT(readability-magic-numbers) + + if (console == nullptr) { + return; + } + + // UConsole::OutputText does not print anything on a completely empty line - we would prefer to + // call UConsole::OutputTextLine, but that got completely inlined + // If we have an empty string, replace with with a single newline. + static const std::wstring newline = L"\n"; + TemporaryFString fstr{str.empty() ? newline : str}; + console->call_virtual_function(idx, &fstr); +} + using console_command_func = void(UObject* console_obj, UnmanagedFString* raw_line); console_command_func* console_command_ptr; @@ -105,7 +121,25 @@ void console_command_hook(UObject* console_obj, UnmanagedFString* raw_line) { // just this, and we'll see if people actually complain } - LOG(INFO, L">>> {} <<<", line); + /* + This is a little awkward. + Since we can't let execution though to the unreal function, we're responsible for + printing the executed command line. + + We do this via output text directly, rather than the LOG macro, so that it's not + affected by the console log level, and so that it happens immediately (the LOG macro is + queued, and can get out of order with respect to native engine messages). + + However, for custom console commands it's also nice to see what the command was in the + log file, since you'll see all their output too. + + We don't really expose a "write to log file only", since it's not usually something + useful, so as a compromise just use the LOG macro on the lowest possible log level, and + assume the lowest people practically set their console log level to is dev warning. + */ + auto msg = unrealsdk::fmt::format(L">>> {} <<<", line); + static_uconsole_output_text(msg); + LOG(MIN, L"{}", msg); try { callback->operator()(line.c_str(), line.size(), cmd_len); @@ -193,19 +227,7 @@ void BL3Hook::inject_console(void) { } void BL3Hook::uconsole_output_text(const std::wstring& str) const { - static auto idx = config::get_int("unrealsdk.uconsole_output_text_vf_index") - .value_or(81); // NOLINT(readability-magic-numbers) - - if (console == nullptr) { - return; - } - - // UConsole::OutputText does not print anything on a completely empty line - we would prefer to - // call UConsole::OutputTextLine, but that got completely inlined - // If we have an empty string, replace with with a single newline. - static const std::wstring newline = L"\n"; - TemporaryFString fstr{str.empty() ? newline : str}; - console->call_virtual_function(idx, &fstr); + static_uconsole_output_text(str); } bool BL3Hook::is_console_ready(void) const { From 1200fca47bc09da634e46433504da521345bf2d5 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Mon, 16 Dec 2024 16:55:09 +1300 Subject: [PATCH 4/5] add additional bl2 console command hook --- src/unrealsdk/game/bl2/console.cpp | 32 +++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/unrealsdk/game/bl2/console.cpp b/src/unrealsdk/game/bl2/console.cpp index eb349c2..25a5e4c 100644 --- a/src/unrealsdk/game/bl2/console.cpp +++ b/src/unrealsdk/game/bl2/console.cpp @@ -45,6 +45,11 @@ const std::wstring SAY_CRASH_FIX_ID = L"unrealsdk_bl2_say_crash_fix"; // We could combine this with the say bypass, but by keeping them separate it'll let users disable // one if they really want to const std::wstring CONSOLE_COMMAND_FUNC = L"Engine.Console:ConsoleCommand"; +// This is the actual end point of all console commands, the above function normally calls through +// into this one - but we needed to hook it to be able to manage the console history. If something +/// directly calls `PC.ConsoleCommand("my_cmd")`, we need this hook to be able to catch it. +const std::wstring PC_CONSOLE_COMMAND_FUNC = L"Engine.PlayerController:ConsoleCommand"; + const constexpr auto CONSOLE_COMMAND_TYPE = hook_manager::Type::PRE; const std::wstring CONSOLE_COMMAND_ID = L"unrealsdk_bl2_console_command"; @@ -73,6 +78,7 @@ bool say_bypass_hook(hook_manager::Details& hook) { static const auto command_property = hook.args->type->find_prop_and_validate(L"Command"_fn); + // Since these are different functions, we can't just forward the args struct, have to copy it hook.obj->get(console_command_func) .call(hook.args->get(command_property)); return true; @@ -234,9 +240,26 @@ bool console_command_hook(hook_manager::Details& hook) { return true; } -// Would prefer to call a native function where possible, however best I can tell, OutputText is -// actually implemented directly in unrealscript (along most of the console mechanics). -BoundFunction console_output_text{}; +bool pc_console_command_hook(hook_manager::Details& hook) { + static const auto command_property = + hook.args->type->find_prop_and_validate(L"Command"_fn); + + auto line = hook.args->get(command_property); + + auto [callback, cmd_len] = commands::impl::find_matching_command(line); + if (callback == nullptr) { + return false; + } + + // This hook does not go to console, so there's no extra processing to be done, we can just run + // the callback immediately + try { + callback->operator()(line.c_str(), line.size(), cmd_len); + } catch (const std::exception& ex) { + LOG(ERROR, "An exception occurred while running a console command: {}", ex.what()); + } + return true; +} bool inject_console_hook(hook_manager::Details& hook) { hook_manager::remove_hook(INJECT_CONSOLE_FUNC, INJECT_CONSOLE_TYPE, INJECT_CONSOLE_ID); @@ -269,6 +292,9 @@ void BL2Hook::inject_console(void) { hook_manager::add_hook(CONSOLE_COMMAND_FUNC, CONSOLE_COMMAND_TYPE, CONSOLE_COMMAND_ID, &console_command_hook); + hook_manager::add_hook(PC_CONSOLE_COMMAND_FUNC, CONSOLE_COMMAND_TYPE, CONSOLE_COMMAND_ID, + &pc_console_command_hook); + hook_manager::add_hook(INJECT_CONSOLE_FUNC, INJECT_CONSOLE_TYPE, INJECT_CONSOLE_ID, &inject_console_hook); } From 92211dcb1e6148e1fa4185fa0826eac88d64f37c Mon Sep 17 00:00:00 2001 From: apple1417 Date: Mon, 16 Dec 2024 17:19:14 +1300 Subject: [PATCH 5/5] update changelog --- changelog.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/changelog.md b/changelog.md index 6667548..8dd3de8 100644 --- a/changelog.md +++ b/changelog.md @@ -16,6 +16,29 @@ [427c8734](https://github.com/bl-sdk/unrealsdk/commit/427c8734) +- Fixed that `unrealsdk::commands::has_command` and `unrealsdk::commands::remove_command` were case + sensitive, while `unrealsdk::commands::add_command` and the callbacks were not. Commands should be + now be case insensitive everywhere. + + [b641706d](https://github.com/bl-sdk/unrealsdk/commit/b641706d) + +- Fixed that the executed command message of custom sdk commands would not appear in console if you + increased the minimum log level, and that they may have appeared out of order with respects to + native engine messages. + + [b652da13](https://github.com/bl-sdk/unrealsdk/commit/b652da13) + +- Added an additional console command hook in BL2, to cover commands not run directly via console. + + [1200fca4](https://github.com/bl-sdk/unrealsdk/commit/1200fca4) + +- Renamed the `unrealsdk.locking_process_event` (previously `UNREALSDK_LOCKING_PROCESS_EVENT`) + setting to `unrealsdk.locking_function_calls`, and expanded it's scope to cover all function + calls. This fixes a few more possibilities for lockups. + + [bebaeab4](https://github.com/bl-sdk/unrealsdk/commit/bebaeab4) + + ## v1.4.0 - Fixed that UE3 `WeakPointer`s would always return null, due to an incorrect offset in the `UObject` header layout.