Skip to content

Commit 406b309

Browse files
authored
Merge pull request #47 from apple1417/master
fix tps hooks
2 parents 083147b + 76fb05d commit 406b309

File tree

9 files changed

+165
-19
lines changed

9 files changed

+165
-19
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ build configurations.
6565
| `UNREALSDK_UCONSOLE_CONSOLE_COMMAND_VF_INDEX` | Overrides the virtual function index used when hooking `UConsole::ConsoleCommand`. |
6666
| `UNREALSDK_UCONSOLE_OUTPUT_TEXT_VF_INDEX` | Overrides the virtual function index used when calling `UConsole::OutputText`. |
6767
| `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. |
68+
| `UNREALSDK_LOG_ALL_CALLS_FILE` | After enabling `unrealsdk::hook_manager::log_all_calls`, the file to write calls to. |
6869

6970
You can also define any of these in an env file, which will automatically be loaded when the sdk
7071
starts (excluding `UNREALSDK_ENV_FILE` of course). This file should contain lines of equals

changelog.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,20 @@
1010

1111
[4e17d06d](https://github.com/bl-sdk/unrealsdk/commit/4e17d06d),
1212
[270ef4bf](https://github.com/bl-sdk/unrealsdk/commit/270ef4bf)
13-
13+
14+
- Changed `unrealsdk::hook_manager::log_all_calls` to write to a dedicated file.
15+
16+
[270ef4bf](https://github.com/bl-sdk/unrealsdk/commit/270ef4bf)
17+
18+
- Fixed missing all `CallFunction` based hooks in TPS - notably including the say bypass.
19+
20+
[011fd8a2](https://github.com/bl-sdk/unrealsdk/commit/270ef4bf)
21+
22+
- Added the offline mode say crash fix for BL2+TPS as a base sdk hook.
23+
24+
[2d9a36c7](https://github.com/bl-sdk/unrealsdk/commit/270ef4bf)
25+
26+
1427
## v1.3.0
1528
- Added a `WeakPointer` wrapper class with better ergonomics, including an emulated implementation
1629
when built under UE3.

src/unrealsdk/env.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const constexpr env_var_key TREFERENCE_CONTROLLER_DESTRUCTOR_VF_INDEX =
2525
const constexpr env_var_key FTEXT_GET_DISPLAY_STRING_VF_INDEX =
2626
"UNREALSDK_FTEXT_GET_DISPLAY_STRING_VF_INDEX";
2727
const constexpr env_var_key LOCKING_PROCESS_EVENT = "UNREALSDK_LOCKING_PROCESS_EVENT";
28+
const constexpr env_var_key LOG_ALL_CALLS_FILE = "UNREALSDK_LOG_ALL_CALLS_FILE";
2829

2930
namespace defaults {
3031

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

4850
} // namespace defaults
4951

src/unrealsdk/game/bl2/console.cpp

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@
55
#include "unrealsdk/game/bl2/bl2.h"
66
#include "unrealsdk/hook_manager.h"
77
#include "unrealsdk/unreal/classes/properties/copyable_property.h"
8+
#include "unrealsdk/unreal/classes/properties/uboolproperty.h"
9+
#include "unrealsdk/unreal/classes/properties/uinterfaceproperty.h"
810
#include "unrealsdk/unreal/classes/properties/uobjectproperty.h"
911
#include "unrealsdk/unreal/classes/properties/ustrproperty.h"
1012
#include "unrealsdk/unreal/classes/uclass.h"
1113
#include "unrealsdk/unreal/classes/ufunction.h"
1214
#include "unrealsdk/unreal/classes/uobject.h"
1315
#include "unrealsdk/unreal/classes/uobject_funcs.h"
16+
#include "unrealsdk/unreal/find_class.h"
1417
#include "unrealsdk/unreal/structs/fname.h"
1518
#include "unrealsdk/unreal/wrappers/bound_function.h"
1619
#include "unrealsdk/unreal/wrappers/unreal_pointer.h"
1720
#include "unrealsdk/unreal/wrappers/unreal_pointer_funcs.h"
1821
#include "unrealsdk/unreal/wrappers/wrapped_struct.h"
22+
#include "unrealsdk/unrealsdk.h"
1923

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

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

2630
namespace {
2731

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

32-
// We could combine this with the above, but by keeping them separate it'll let users disable one if
33-
// they really want to
41+
const std::wstring SAY_CRASH_FIX_FUNC = L"WillowGame.TextChatGFxMovie:AddChatMessage";
42+
const constexpr auto SAY_CRASH_FIX_TYPE = hook_manager::Type::PRE;
43+
const std::wstring SAY_CRASH_FIX_ID = L"unrealsdk_bl2_say_crash_fix";
44+
45+
// We could combine this with the say bypass, but by keeping them separate it'll let users disable
46+
// one if they really want to
3447
const std::wstring CONSOLE_COMMAND_FUNC = L"Engine.Console:ConsoleCommand";
3548
const constexpr auto CONSOLE_COMMAND_TYPE = hook_manager::Type::PRE;
3649
const std::wstring CONSOLE_COMMAND_ID = L"unrealsdk_bl2_console_command";
@@ -40,6 +53,17 @@ const constexpr auto INJECT_CONSOLE_TYPE = hook_manager::Type::PRE;
4053
const std::wstring INJECT_CONSOLE_ID = L"unrealsdk_bl2_inject_console";
4154

4255
bool say_bypass_hook(hook_manager::Details& hook) {
56+
/*
57+
This is a native function so we don't have exact source, but we expect it's essentially:
58+
```
59+
function ShippingConsoleCommand(string Command) {
60+
ConsoleCommand("say" @ Command);
61+
}
62+
```
63+
64+
We simply call straight through to console command without adding anything.
65+
*/
66+
4367
static const auto console_command_func =
4468
hook.obj->Class->find_func_and_validate(L"ConsoleCommand"_fn);
4569
static const auto command_property =
@@ -50,6 +74,76 @@ bool say_bypass_hook(hook_manager::Details& hook) {
5074
return true;
5175
}
5276

77+
bool say_crash_fix_hook(hook_manager::Details& hook) {
78+
/*
79+
Reference unrealscript implementation:
80+
```
81+
function AddChatMessage(PlayerReplicationInfo PRI, string msg) {
82+
local string TimeStamp;
83+
local OnlinePlayerInterfaceEx PlayerInt;
84+
85+
PlayerInt = class'GameEngine'.static.GetOnlineSubsystem().PlayerInterfaceEx;
86+
if(PlayerInt.NetIdIsBlockedForLocalUser(PRI.UniqueId)) {
87+
return;
88+
}
89+
TimeStamp = GetTimestampString(class'WillowSaveGameManager'.default.TimeFormat);
90+
AddChatMessageInternal(PRI.PlayerName @ TimeStamp, msg);
91+
}
92+
```
93+
94+
The crash occurs in `NetIdIsBlockedForLocalUser`. We cannot block it directly because there are
95+
multiple online subsystems, so we need to do it here instead.
96+
97+
If we're online, we allow normal processing. If offline, we re-implement this ourselves,
98+
skipping that call.
99+
*/
100+
101+
static const auto engine =
102+
unrealsdk::find_object(L"WillowGameEngine", L"Transient.WillowGameEngine_0");
103+
static const auto spark_interface_prop =
104+
engine->Class->find_prop_and_validate<UInterfaceProperty>(L"SparkInterface"_fn);
105+
static const auto is_spark_enabled_func =
106+
spark_interface_prop->get_interface_class()->find_func_and_validate(L"IsSparkEnabled"_fn);
107+
108+
// Check if we're online, if so allow normal processing
109+
if (BoundFunction{.func = is_spark_enabled_func,
110+
.object = engine->get<UInterfaceProperty>(spark_interface_prop)}
111+
.call<UBoolProperty>()) {
112+
return false;
113+
}
114+
115+
static const auto get_timestamp_string_func =
116+
hook.obj->Class->find_func_and_validate(L"GetTimestampString"_fn);
117+
static const auto default_save_game_manager =
118+
find_class(L"WillowSaveGameManager"_fn)->ClassDefaultObject;
119+
static const auto time_format_prop =
120+
default_save_game_manager->Class->find_prop_and_validate<UStrProperty>(L"TimeFormat"_fn);
121+
122+
auto timestamp = BoundFunction{.func = get_timestamp_string_func, .object = hook.obj}
123+
.call<UStrProperty, UStrProperty>(
124+
default_save_game_manager->get<UStrProperty>(time_format_prop));
125+
126+
static const auto pri_prop =
127+
hook.args->type->find_prop_and_validate<UObjectProperty>(L"PRI"_fn);
128+
static const auto player_name_prop =
129+
pri_prop->get_property_class()->find_prop_and_validate<UStrProperty>(L"PlayerName"_fn);
130+
131+
auto player_name =
132+
hook.args->get<UObjectProperty>(pri_prop)->get<UStrProperty>(player_name_prop);
133+
player_name.reserve(player_name.size() + 1 + timestamp.size());
134+
player_name += L' ';
135+
player_name += timestamp;
136+
137+
static const auto add_chat_message_internal_func =
138+
hook.obj->Class->find_func_and_validate(L"AddChatMessageInternal"_fn);
139+
static const auto msg_prop = hook.args->type->find_prop_and_validate<UStrProperty>(L"msg"_fn);
140+
141+
BoundFunction{.func = add_chat_message_internal_func, .object = hook.obj}
142+
.call<void, UStrProperty, UStrProperty>(player_name,
143+
hook.args->get<UStrProperty>(msg_prop));
144+
return true;
145+
}
146+
53147
bool console_command_hook(hook_manager::Details& hook) {
54148
static const auto command_property =
55149
hook.args->type->find_prop_and_validate<UStrProperty>(L"Command"_fn);
@@ -148,6 +242,9 @@ bool inject_console_hook(hook_manager::Details& hook) {
148242

149243
void BL2Hook::inject_console(void) {
150244
hook_manager::add_hook(SAY_BYPASS_FUNC, SAY_BYPASS_TYPE, SAY_BYPASS_ID, &say_bypass_hook);
245+
hook_manager::add_hook(SAY_CRASH_FIX_FUNC, SAY_CRASH_FIX_TYPE, SAY_CRASH_FIX_ID,
246+
&say_crash_fix_hook);
247+
151248
hook_manager::add_hook(CONSOLE_COMMAND_FUNC, CONSOLE_COMMAND_TYPE, CONSOLE_COMMAND_ID,
152249
&console_command_hook);
153250
hook_manager::add_hook(INJECT_CONSOLE_FUNC, INJECT_CONSOLE_TYPE, INJECT_CONSOLE_ID,

src/unrealsdk/game/bl2/hooks.cpp

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ void __fastcall process_event_hook(UObject* obj,
6767
func->get_path_name(), obj->get_path_name());
6868
}
6969

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

169-
const constinit Pattern<23> CALL_FUNCTION_SIG{
169+
const constinit Pattern<55> CALL_FUNCTION_SIG{
170170
"55" // push ebp
171171
"8B EC" // mov ebp, esp
172172
"6A FF" // push -01
173-
"68 ????????" // push Borderlands2.exe+1110791
173+
"68 ????????" // push BorderlandsPreSequel.exe+108D4B6
174174
"64 A1 ????????" // mov eax, fs:[00000000]
175175
"50" // push eax
176-
"81 EC 58040000" // sub esp, 00000458
176+
"81 EC ????????" // sub esp, 000000A4
177+
"A1 ????????" // mov eax, [BorderlandsPreSequel.g_LEngineDefaultPoolId+D2FC]
178+
"33 C5" // xor eax, ebp
179+
"89 45 ??" // mov [ebp-10], eax
180+
"53" // push ebx
181+
"56" // push esi
182+
"57" // push edi
183+
"50" // push eax
184+
"8D 45 ??" // lea eax, [ebp-0C]
185+
"64 A3 ????????" // mov fs:[00000000], eax
186+
"8B 7D ??" // mov edi, [ebp+08]
187+
"8B 45 ??" // mov eax, [ebp+14]
188+
"8B 5D ??" // mov ebx, [ebp+0C]
177189
};
178190

179191
void __fastcall call_function_hook(UObject* obj,
@@ -182,7 +194,7 @@ void __fastcall call_function_hook(UObject* obj,
182194
void* result,
183195
UFunction* func) {
184196
try {
185-
auto data = hook_manager::impl::preprocess_hook("CallFunction", func, obj);
197+
auto data = hook_manager::impl::preprocess_hook(L"CallFunction", func, obj);
186198
if (data != nullptr) {
187199
WrappedStruct args{func};
188200
auto original_code = stack->extract_current_args(args);

src/unrealsdk/game/bl3/hooks.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const constinit Pattern<19> PROCESS_EVENT_SIG{
3636

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

166-
auto data = hook_manager::impl::preprocess_hook("CallFunction", func, obj);
166+
auto data = hook_manager::impl::preprocess_hook(L"CallFunction", func, obj);
167167
if (data != nullptr) {
168168
WrappedStruct args{func};
169169
auto original_code = stack->extract_current_args(args);

src/unrealsdk/hook_manager.cpp

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "unrealsdk/pch.h"
22

3+
#include "unrealsdk/env.h"
34
#include "unrealsdk/hook_manager.h"
45
#include "unrealsdk/unreal/classes/ufunction.h"
56
#include "unrealsdk/unreal/classes/uobject.h"
@@ -85,13 +86,30 @@ struct List {
8586

8687
namespace {
8788

88-
bool should_log_all_calls = false;
8989
bool should_inject_next_call = false;
9090

91+
bool should_log_all_calls = false;
92+
std::unique_ptr<std::wostream> log_all_calls_stream;
93+
std::mutex log_all_calls_stream_mutex{};
94+
9195
std::unordered_map<FName, utils::StringViewMap<std::wstring, List>> hooks{};
9296

9397
void log_all_calls(bool should_log) {
98+
// Only keep this file stream open while we need it
99+
if (should_log) {
100+
const std::lock_guard<std::mutex> lock(log_all_calls_stream_mutex);
101+
log_all_calls_stream = std::make_unique<std::wofstream>(
102+
utils::get_this_dll().parent_path()
103+
/ env::get(env::LOG_ALL_CALLS_FILE, env::defaults::LOG_ALL_CALLS_FILE),
104+
std::ofstream::trunc);
105+
}
106+
94107
should_log_all_calls = should_log;
108+
109+
if (!should_log) {
110+
const std::lock_guard<std::mutex> lock(log_all_calls_stream_mutex);
111+
log_all_calls_stream = nullptr;
112+
}
95113
}
96114

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

192210
} // namespace
193211

194-
const List* preprocess_hook(std::string_view source, const UFunction* func, const UObject* obj) {
212+
const List* preprocess_hook(std::wstring_view source, const UFunction* func, const UObject* obj) {
195213
if (should_inject_next_call) {
196214
should_inject_next_call = false;
197215
return nullptr;
198216
}
199217

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

203221
if (should_log_all_calls) {
204222
func_name = func->get_path_name();
205-
LOG(MISC, "===== {} called =====", source);
206-
LOG(MISC, L"Function: {}", func_name);
207-
LOG(MISC, L"Object: {}", obj->get_path_name());
223+
auto obj_name = obj->get_path_name();
224+
225+
const std::lock_guard<std::mutex> lock(log_all_calls_stream_mutex);
226+
*log_all_calls_stream << source << L'\t' << func_name << L'\t' << obj_name << L'\n';
208227
}
209228

210229
// Check if anything matches the function FName

src/unrealsdk/hook_manager.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ using Callback = DLLSafeCallback::InnerFunc;
6060

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

src/unrealsdk/memory.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,17 @@ bool detour(uintptr_t addr, void* detour_func, void** original_func, std::string
9393

9494
status = MH_CreateHook(reinterpret_cast<LPVOID>(addr), detour_func, original_func);
9595
if (status != MH_OK) {
96-
LOG(ERROR, "Failed to create hook for {}", name);
96+
LOG(ERROR, "Failed to create detour for {}", name);
9797
return false;
9898
}
9999

100100
status = MH_EnableHook(reinterpret_cast<LPVOID>(addr));
101101
if (status != MH_OK) {
102-
LOG(ERROR, "Failed to enable hook for {}", name);
102+
LOG(ERROR, "Failed to enable detour for {}", name);
103103
return false;
104104
}
105105

106+
LOG(MISC, "Detoured {} at {:p}", name, reinterpret_cast<void*>(addr));
106107
return true;
107108
}
108109
#endif

0 commit comments

Comments
 (0)