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

fix tps hooks #47

Merged
merged 5 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ build configurations.
| `UNREALSDK_UCONSOLE_CONSOLE_COMMAND_VF_INDEX` | Overrides the virtual function index used when hooking `UConsole::ConsoleCommand`. |
| `UNREALSDK_UCONSOLE_OUTPUT_TEXT_VF_INDEX` | Overrides the virtual function index used when calling `UConsole::OutputText`. |
| `UNREALSDK_LOCKING_PROCESS_EVENT` | If defined, locks simultaneous ProcessEvent calls from different threads. This is used both for hooks and for calling unreal functions - external code must take care wrt. deadlocks. |
| `UNREALSDK_LOG_ALL_CALLS_FILE` | After enabling `unrealsdk::hook_manager::log_all_calls`, the file to write calls to. |

You can also define any of these in an env file, which will automatically be loaded when the sdk
starts (excluding `UNREALSDK_ENV_FILE` of course). This file should contain lines of equals
Expand Down
15 changes: 14 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,20 @@

[4e17d06d](https://github.com/bl-sdk/unrealsdk/commit/4e17d06d),
[270ef4bf](https://github.com/bl-sdk/unrealsdk/commit/270ef4bf)


- Changed `unrealsdk::hook_manager::log_all_calls` to write to a dedicated file.

[270ef4bf](https://github.com/bl-sdk/unrealsdk/commit/270ef4bf)

- Fixed missing all `CallFunction` based hooks in TPS - notably including the say bypass.

[011fd8a2](https://github.com/bl-sdk/unrealsdk/commit/270ef4bf)

- Added the offline mode say crash fix for BL2+TPS as a base sdk hook.

[2d9a36c7](https://github.com/bl-sdk/unrealsdk/commit/270ef4bf)


## v1.3.0
- Added a `WeakPointer` wrapper class with better ergonomics, including an emulated implementation
when built under UE3.
Expand Down
2 changes: 2 additions & 0 deletions src/unrealsdk/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const constexpr env_var_key TREFERENCE_CONTROLLER_DESTRUCTOR_VF_INDEX =
const constexpr env_var_key FTEXT_GET_DISPLAY_STRING_VF_INDEX =
"UNREALSDK_FTEXT_GET_DISPLAY_STRING_VF_INDEX";
const constexpr env_var_key LOCKING_PROCESS_EVENT = "UNREALSDK_LOCKING_PROCESS_EVENT";
const constexpr env_var_key LOG_ALL_CALLS_FILE = "UNREALSDK_LOG_ALL_CALLS_FILE";

namespace defaults {

Expand All @@ -44,6 +45,7 @@ const constexpr auto TREFERENCE_CONTROLLER_DESTROY_OBJ_VF_INDEX = 0;
const constexpr auto TREFERENCE_CONTROLLER_DESTRUCTOR_VF_INDEX = 1;
const constexpr auto FTEXT_GET_DISPLAY_STRING_VF_INDEX = 2;
// LOCKING_PROCESS_EVENT - defaults to empty string (only used in defined checks)
const constexpr env_var_key LOG_ALL_CALLS_FILE = "unrealsdk.calls.tsv";

} // namespace defaults

Expand Down
101 changes: 99 additions & 2 deletions src/unrealsdk/game/bl2/console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
#include "unrealsdk/game/bl2/bl2.h"
#include "unrealsdk/hook_manager.h"
#include "unrealsdk/unreal/classes/properties/copyable_property.h"
#include "unrealsdk/unreal/classes/properties/uboolproperty.h"
#include "unrealsdk/unreal/classes/properties/uinterfaceproperty.h"
#include "unrealsdk/unreal/classes/properties/uobjectproperty.h"
#include "unrealsdk/unreal/classes/properties/ustrproperty.h"
#include "unrealsdk/unreal/classes/uclass.h"
#include "unrealsdk/unreal/classes/ufunction.h"
#include "unrealsdk/unreal/classes/uobject.h"
#include "unrealsdk/unreal/classes/uobject_funcs.h"
#include "unrealsdk/unreal/find_class.h"
#include "unrealsdk/unreal/structs/fname.h"
#include "unrealsdk/unreal/wrappers/bound_function.h"
#include "unrealsdk/unreal/wrappers/unreal_pointer.h"
#include "unrealsdk/unreal/wrappers/unreal_pointer_funcs.h"
#include "unrealsdk/unreal/wrappers/wrapped_struct.h"
#include "unrealsdk/unrealsdk.h"

#if defined(UE3) && defined(ARCH_X86) && !defined(UNREALSDK_IMPORTING)

Expand All @@ -25,12 +29,21 @@ namespace unrealsdk::game {

namespace {

// Two extra useful hooks, which we don't strictly need for the interface:
// - By default the game prepends 'say ' to every command as a primitive way to disable console.
// Bypass it so you can actually use it.
// - When they rewrote the networking for cross platform, they caused a crash if you tried chatting
// without being connected to shift. Fix it.
const std::wstring SAY_BYPASS_FUNC = L"Engine.Console:ShippingConsoleCommand";
const constexpr auto SAY_BYPASS_TYPE = hook_manager::Type::PRE;
const std::wstring SAY_BYPASS_ID = L"unrealsdk_bl2_say_bypass";

// We could combine this with the above, but by keeping them separate it'll let users disable one if
// they really want to
const std::wstring SAY_CRASH_FIX_FUNC = L"WillowGame.TextChatGFxMovie:AddChatMessage";
const constexpr auto SAY_CRASH_FIX_TYPE = hook_manager::Type::PRE;
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";
const constexpr auto CONSOLE_COMMAND_TYPE = hook_manager::Type::PRE;
const std::wstring CONSOLE_COMMAND_ID = L"unrealsdk_bl2_console_command";
Expand All @@ -40,6 +53,17 @@ const constexpr auto INJECT_CONSOLE_TYPE = hook_manager::Type::PRE;
const std::wstring INJECT_CONSOLE_ID = L"unrealsdk_bl2_inject_console";

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:
```
function ShippingConsoleCommand(string Command) {
ConsoleCommand("say" @ Command);
}
```

We simply call straight through to console command without adding anything.
*/

static const auto console_command_func =
hook.obj->Class->find_func_and_validate(L"ConsoleCommand"_fn);
static const auto command_property =
Expand All @@ -50,6 +74,76 @@ bool say_bypass_hook(hook_manager::Details& hook) {
return true;
}

bool say_crash_fix_hook(hook_manager::Details& hook) {
/*
Reference unrealscript implementation:
```
function AddChatMessage(PlayerReplicationInfo PRI, string msg) {
local string TimeStamp;
local OnlinePlayerInterfaceEx PlayerInt;

PlayerInt = class'GameEngine'.static.GetOnlineSubsystem().PlayerInterfaceEx;
if(PlayerInt.NetIdIsBlockedForLocalUser(PRI.UniqueId)) {
return;
}
TimeStamp = GetTimestampString(class'WillowSaveGameManager'.default.TimeFormat);
AddChatMessageInternal(PRI.PlayerName @ TimeStamp, msg);
}
```

The crash occurs in `NetIdIsBlockedForLocalUser`. We cannot block it directly because there are
multiple online subsystems, so we need to do it here instead.

If we're online, we allow normal processing. If offline, we re-implement this ourselves,
skipping that call.
*/

static const auto engine =
unrealsdk::find_object(L"WillowGameEngine", L"Transient.WillowGameEngine_0");
static const auto spark_interface_prop =
engine->Class->find_prop_and_validate<UInterfaceProperty>(L"SparkInterface"_fn);
static const auto is_spark_enabled_func =
spark_interface_prop->get_interface_class()->find_func_and_validate(L"IsSparkEnabled"_fn);

// Check if we're online, if so allow normal processing
if (BoundFunction{.func = is_spark_enabled_func,
.object = engine->get<UInterfaceProperty>(spark_interface_prop)}
.call<UBoolProperty>()) {
return false;
}

static const auto get_timestamp_string_func =
hook.obj->Class->find_func_and_validate(L"GetTimestampString"_fn);
static const auto default_save_game_manager =
find_class(L"WillowSaveGameManager"_fn)->ClassDefaultObject;
static const auto time_format_prop =
default_save_game_manager->Class->find_prop_and_validate<UStrProperty>(L"TimeFormat"_fn);

auto timestamp = BoundFunction{.func = get_timestamp_string_func, .object = hook.obj}
.call<UStrProperty, UStrProperty>(
default_save_game_manager->get<UStrProperty>(time_format_prop));

static const auto pri_prop =
hook.args->type->find_prop_and_validate<UObjectProperty>(L"PRI"_fn);
static const auto player_name_prop =
pri_prop->get_property_class()->find_prop_and_validate<UStrProperty>(L"PlayerName"_fn);

auto player_name =
hook.args->get<UObjectProperty>(pri_prop)->get<UStrProperty>(player_name_prop);
player_name.reserve(player_name.size() + 1 + timestamp.size());
player_name += L' ';
player_name += timestamp;

static const auto add_chat_message_internal_func =
hook.obj->Class->find_func_and_validate(L"AddChatMessageInternal"_fn);
static const auto msg_prop = hook.args->type->find_prop_and_validate<UStrProperty>(L"msg"_fn);

BoundFunction{.func = add_chat_message_internal_func, .object = hook.obj}
.call<void, UStrProperty, UStrProperty>(player_name,
hook.args->get<UStrProperty>(msg_prop));
return true;
}

bool console_command_hook(hook_manager::Details& hook) {
static const auto command_property =
hook.args->type->find_prop_and_validate<UStrProperty>(L"Command"_fn);
Expand Down Expand Up @@ -148,6 +242,9 @@ bool inject_console_hook(hook_manager::Details& hook) {

void BL2Hook::inject_console(void) {
hook_manager::add_hook(SAY_BYPASS_FUNC, SAY_BYPASS_TYPE, SAY_BYPASS_ID, &say_bypass_hook);
hook_manager::add_hook(SAY_CRASH_FIX_FUNC, SAY_CRASH_FIX_TYPE, SAY_CRASH_FIX_ID,
&say_crash_fix_hook);

hook_manager::add_hook(CONSOLE_COMMAND_FUNC, CONSOLE_COMMAND_TYPE, CONSOLE_COMMAND_ID,
&console_command_hook);
hook_manager::add_hook(INJECT_CONSOLE_FUNC, INJECT_CONSOLE_TYPE, INJECT_CONSOLE_ID,
Expand Down
22 changes: 17 additions & 5 deletions src/unrealsdk/game/bl2/hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ void __fastcall process_event_hook(UObject* obj,
func->get_path_name(), obj->get_path_name());
}

auto data = hook_manager::impl::preprocess_hook("ProcessEvent", func, obj);
auto data = hook_manager::impl::preprocess_hook(L"ProcessEvent", func, obj);
if (data != nullptr) {
// Copy args so that hooks can't modify them, for parity with call function
const WrappedStruct args_base{func, params};
Expand Down Expand Up @@ -166,14 +166,26 @@ typedef void(__fastcall* call_function_func)(UObject* obj,
UFunction* func);
call_function_func call_function_ptr;

const constinit Pattern<23> CALL_FUNCTION_SIG{
const constinit Pattern<55> CALL_FUNCTION_SIG{
"55" // push ebp
"8B EC" // mov ebp, esp
"6A FF" // push -01
"68 ????????" // push Borderlands2.exe+1110791
"68 ????????" // push BorderlandsPreSequel.exe+108D4B6
"64 A1 ????????" // mov eax, fs:[00000000]
"50" // push eax
"81 EC 58040000" // sub esp, 00000458
"81 EC ????????" // sub esp, 000000A4
"A1 ????????" // mov eax, [BorderlandsPreSequel.g_LEngineDefaultPoolId+D2FC]
"33 C5" // xor eax, ebp
"89 45 ??" // mov [ebp-10], eax
"53" // push ebx
"56" // push esi
"57" // push edi
"50" // push eax
"8D 45 ??" // lea eax, [ebp-0C]
"64 A3 ????????" // mov fs:[00000000], eax
"8B 7D ??" // mov edi, [ebp+08]
"8B 45 ??" // mov eax, [ebp+14]
"8B 5D ??" // mov ebx, [ebp+0C]
};

void __fastcall call_function_hook(UObject* obj,
Expand All @@ -182,7 +194,7 @@ void __fastcall call_function_hook(UObject* obj,
void* result,
UFunction* func) {
try {
auto data = hook_manager::impl::preprocess_hook("CallFunction", func, obj);
auto data = hook_manager::impl::preprocess_hook(L"CallFunction", func, obj);
if (data != nullptr) {
WrappedStruct args{func};
auto original_code = stack->extract_current_args(args);
Expand Down
4 changes: 2 additions & 2 deletions src/unrealsdk/game/bl3/hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const constinit Pattern<19> PROCESS_EVENT_SIG{

void process_event_hook(UObject* obj, UFunction* func, void* params) {
try {
auto data = hook_manager::impl::preprocess_hook("ProcessEvent", func, obj);
auto data = hook_manager::impl::preprocess_hook(L"ProcessEvent", func, obj);
if (data != nullptr) {
// Copy args so that hooks can't modify them, for parity with call function
const WrappedStruct args_base{func, params};
Expand Down Expand Up @@ -163,7 +163,7 @@ void call_function_hook(UObject* obj, FFrame* stack, void* result, UFunction* fu
implementation simpler.
*/

auto data = hook_manager::impl::preprocess_hook("CallFunction", func, obj);
auto data = hook_manager::impl::preprocess_hook(L"CallFunction", func, obj);
if (data != nullptr) {
WrappedStruct args{func};
auto original_code = stack->extract_current_args(args);
Expand Down
31 changes: 25 additions & 6 deletions src/unrealsdk/hook_manager.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "unrealsdk/pch.h"

#include "unrealsdk/env.h"
#include "unrealsdk/hook_manager.h"
#include "unrealsdk/unreal/classes/ufunction.h"
#include "unrealsdk/unreal/classes/uobject.h"
Expand Down Expand Up @@ -85,13 +86,30 @@ struct List {

namespace {

bool should_log_all_calls = false;
bool should_inject_next_call = false;

bool should_log_all_calls = false;
std::unique_ptr<std::wostream> log_all_calls_stream;
std::mutex log_all_calls_stream_mutex{};

std::unordered_map<FName, utils::StringViewMap<std::wstring, List>> hooks{};

void log_all_calls(bool should_log) {
// Only keep this file stream open while we need it
if (should_log) {
const std::lock_guard<std::mutex> lock(log_all_calls_stream_mutex);
log_all_calls_stream = std::make_unique<std::wofstream>(
utils::get_this_dll().parent_path()
/ env::get(env::LOG_ALL_CALLS_FILE, env::defaults::LOG_ALL_CALLS_FILE),
std::ofstream::trunc);
}

should_log_all_calls = should_log;

if (!should_log) {
const std::lock_guard<std::mutex> lock(log_all_calls_stream_mutex);
log_all_calls_stream = nullptr;
}
}

void inject_next_call(void) {
Expand Down Expand Up @@ -191,20 +209,21 @@ bool remove_hook(std::wstring_view func, Type type, std::wstring_view identifier

} // namespace

const List* preprocess_hook(std::string_view source, const UFunction* func, const UObject* obj) {
const List* preprocess_hook(std::wstring_view source, const UFunction* func, const UObject* obj) {
if (should_inject_next_call) {
should_inject_next_call = false;
return nullptr;
}

// Want to delay filling this, but if we're logging all calls
// Want to delay filling this, but if we're logging all calls we need it straight away
std::wstring func_name{};

if (should_log_all_calls) {
func_name = func->get_path_name();
LOG(MISC, "===== {} called =====", source);
LOG(MISC, L"Function: {}", func_name);
LOG(MISC, L"Object: {}", obj->get_path_name());
auto obj_name = obj->get_path_name();

const std::lock_guard<std::mutex> lock(log_all_calls_stream_mutex);
*log_all_calls_stream << source << L'\t' << func_name << L'\t' << obj_name << L'\n';
}

// Check if anything matches the function FName
Expand Down
3 changes: 2 additions & 1 deletion src/unrealsdk/hook_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ using Callback = DLLSafeCallback::InnerFunc;

/**
* @brief Toggles logging all unreal function calls. Best used in short bursts for debugging.
* @note This writes to it's own dedicated file, rather than going through the logging system.
*
* @param should_log True to turn on logging all calls, false to turn it off.
*/
Expand Down Expand Up @@ -141,7 +142,7 @@ to work out if to early exit again. If it does, it can spend a bit longer extrac
* @param obj The object which called the function.
* @return A pointer to the relevant hook list, or nullptr if no hooks match.
*/
const List* preprocess_hook(std::string_view source,
const List* preprocess_hook(std::wstring_view source,
const unreal::UFunction* func,
const unreal::UObject* obj);

Expand Down
5 changes: 3 additions & 2 deletions src/unrealsdk/memory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,17 @@ bool detour(uintptr_t addr, void* detour_func, void** original_func, std::string

status = MH_CreateHook(reinterpret_cast<LPVOID>(addr), detour_func, original_func);
if (status != MH_OK) {
LOG(ERROR, "Failed to create hook for {}", name);
LOG(ERROR, "Failed to create detour for {}", name);
return false;
}

status = MH_EnableHook(reinterpret_cast<LPVOID>(addr));
if (status != MH_OK) {
LOG(ERROR, "Failed to enable hook for {}", name);
LOG(ERROR, "Failed to enable detour for {}", name);
return false;
}

LOG(MISC, "Detoured {} at {:p}", name, reinterpret_cast<void*>(addr));
return true;
}
#endif
Expand Down
Loading