Skip to content

Commit b47370a

Browse files
authored
Merge pull request #53 from apple1417/master
console command fixes
2 parents 4df8620 + 92211dc commit b47370a

File tree

10 files changed

+255
-77
lines changed

10 files changed

+255
-77
lines changed

changelog.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,29 @@
1616

1717
[427c8734](https://github.com/bl-sdk/unrealsdk/commit/427c8734)
1818

19+
- Fixed that `unrealsdk::commands::has_command` and `unrealsdk::commands::remove_command` were case
20+
sensitive, while `unrealsdk::commands::add_command` and the callbacks were not. Commands should be
21+
now be case insensitive everywhere.
22+
23+
[b641706d](https://github.com/bl-sdk/unrealsdk/commit/b641706d)
24+
25+
- Fixed that the executed command message of custom sdk commands would not appear in console if you
26+
increased the minimum log level, and that they may have appeared out of order with respects to
27+
native engine messages.
28+
29+
[b652da13](https://github.com/bl-sdk/unrealsdk/commit/b652da13)
30+
31+
- Added an additional console command hook in BL2, to cover commands not run directly via console.
32+
33+
[1200fca4](https://github.com/bl-sdk/unrealsdk/commit/1200fca4)
34+
35+
- Renamed the `unrealsdk.locking_process_event` (previously `UNREALSDK_LOCKING_PROCESS_EVENT`)
36+
setting to `unrealsdk.locking_function_calls`, and expanded it's scope to cover all function
37+
calls. This fixes a few more possibilities for lockups.
38+
39+
[bebaeab4](https://github.com/bl-sdk/unrealsdk/commit/bebaeab4)
40+
41+
1942
## v1.4.0
2043
- Fixed that UE3 `WeakPointer`s would always return null, due to an incorrect offset in the
2144
`UObject` header layout.

src/unrealsdk/commands.cpp

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ UNREALSDK_CAPI(bool, add_command, const wchar_t* cmd, size_t size, DLLSafeCallba
2929
return false;
3030
}
3131

32-
commands.emplace(lower_cmd, callback);
32+
commands.emplace(std::move(lower_cmd), callback);
3333
return true;
3434
}
3535
#endif
@@ -44,8 +44,9 @@ UNREALSDK_CAPI(bool, has_command, const wchar_t* cmd, size_t size);
4444
#endif
4545
#ifndef UNREALSDK_IMPORTING
4646
UNREALSDK_CAPI(bool, has_command, const wchar_t* cmd, size_t size) {
47-
const std::wstring_view view{cmd, size};
48-
return commands.contains(view);
47+
std::wstring lower_cmd(size, '\0');
48+
std::transform(cmd, cmd + size, lower_cmd.begin(), &std::towlower);
49+
return commands.contains(lower_cmd);
4950
}
5051
#endif
5152

@@ -58,8 +59,9 @@ UNREALSDK_CAPI(bool, remove_command, const wchar_t* cmd, size_t size);
5859
#endif
5960
#ifndef UNREALSDK_IMPORTING
6061
UNREALSDK_CAPI(bool, remove_command, const wchar_t* cmd, size_t size) {
61-
const std::wstring_view view{cmd, size};
62-
auto iter = commands.find(view);
62+
std::wstring lower_cmd(size, '\0');
63+
std::transform(cmd, cmd + size, lower_cmd.begin(), &std::towlower);
64+
auto iter = commands.find(lower_cmd);
6365

6466
if (iter == commands.end()) {
6567
return false;

src/unrealsdk/game/bl2/console.cpp

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,22 @@ const std::wstring SAY_CRASH_FIX_ID = L"unrealsdk_bl2_say_crash_fix";
4545
// We could combine this with the say bypass, but by keeping them separate it'll let users disable
4646
// one if they really want to
4747
const std::wstring CONSOLE_COMMAND_FUNC = L"Engine.Console:ConsoleCommand";
48+
// This is the actual end point of all console commands, the above function normally calls through
49+
// into this one - but we needed to hook it to be able to manage the console history. If something
50+
/// directly calls `PC.ConsoleCommand("my_cmd")`, we need this hook to be able to catch it.
51+
const std::wstring PC_CONSOLE_COMMAND_FUNC = L"Engine.PlayerController:ConsoleCommand";
52+
4853
const constexpr auto CONSOLE_COMMAND_TYPE = hook_manager::Type::PRE;
4954
const std::wstring CONSOLE_COMMAND_ID = L"unrealsdk_bl2_console_command";
5055

5156
const std::wstring INJECT_CONSOLE_FUNC = L"WillowGame.WillowGameViewportClient:PostRender";
5257
const constexpr auto INJECT_CONSOLE_TYPE = hook_manager::Type::PRE;
5358
const std::wstring INJECT_CONSOLE_ID = L"unrealsdk_bl2_inject_console";
5459

60+
// Would prefer to call a native function where possible, however best I can tell, OutputText is
61+
// actually implemented directly in unrealscript (along most of the console mechanics).
62+
BoundFunction console_output_text{};
63+
5564
bool say_bypass_hook(hook_manager::Details& hook) {
5665
/*
5766
This is a native function so we don't have exact source, but we expect it's essentially:
@@ -69,6 +78,7 @@ bool say_bypass_hook(hook_manager::Details& hook) {
6978
static const auto command_property =
7079
hook.args->type->find_prop_and_validate<UStrProperty>(L"Command"_fn);
7180

81+
// Since these are different functions, we can't just forward the args struct, have to copy it
7282
hook.obj->get<UFunction, BoundFunction>(console_command_func)
7383
.call<void, UStrProperty>(hook.args->get<UStrProperty>(command_property));
7484
return true;
@@ -201,7 +211,25 @@ bool console_command_hook(hook_manager::Details& hook) {
201211
hook.obj->get<UFunction, BoundFunction>(save_config_func).call<void>();
202212
}
203213

204-
LOG(INFO, L">>> {} <<<", line);
214+
/*
215+
This is a little awkward.
216+
Since we can't let execution though to the unreal function, we're responsible for printing the
217+
executed command line.
218+
219+
We do this via output text directly, rather than the LOG macro, so that it's not affected by the
220+
console log level, and so that it happens immediately (the LOG macro is queued, and can get out
221+
of order with respect to native engine messages).
222+
223+
However, for custom console commands it's also nice to see what the command was in the log file,
224+
since you'll see all their output too.
225+
226+
We don't really expose a "write to log file only", since it's not usually something useful, so
227+
as a compromise just use the LOG macro on the lowest possible log level, and assume the lowest
228+
people practically set their console log level to is dev warning.
229+
*/
230+
auto msg = unrealsdk::fmt::format(L">>> {} <<<", line);
231+
console_output_text.call<void, UStrProperty>(msg);
232+
LOG(MIN, L"{}", msg);
205233

206234
try {
207235
callback->operator()(line.c_str(), line.size(), cmd_len);
@@ -212,9 +240,26 @@ bool console_command_hook(hook_manager::Details& hook) {
212240
return true;
213241
}
214242

215-
// Would prefer to call a native function where possible, however best I can tell, OutputText is
216-
// actually implemented directly in unrealscript (along most of the console mechanics).
217-
BoundFunction console_output_text{};
243+
bool pc_console_command_hook(hook_manager::Details& hook) {
244+
static const auto command_property =
245+
hook.args->type->find_prop_and_validate<UStrProperty>(L"Command"_fn);
246+
247+
auto line = hook.args->get<UStrProperty>(command_property);
248+
249+
auto [callback, cmd_len] = commands::impl::find_matching_command(line);
250+
if (callback == nullptr) {
251+
return false;
252+
}
253+
254+
// This hook does not go to console, so there's no extra processing to be done, we can just run
255+
// the callback immediately
256+
try {
257+
callback->operator()(line.c_str(), line.size(), cmd_len);
258+
} catch (const std::exception& ex) {
259+
LOG(ERROR, "An exception occurred while running a console command: {}", ex.what());
260+
}
261+
return true;
262+
}
218263

219264
bool inject_console_hook(hook_manager::Details& hook) {
220265
hook_manager::remove_hook(INJECT_CONSOLE_FUNC, INJECT_CONSOLE_TYPE, INJECT_CONSOLE_ID);
@@ -247,6 +292,9 @@ void BL2Hook::inject_console(void) {
247292

248293
hook_manager::add_hook(CONSOLE_COMMAND_FUNC, CONSOLE_COMMAND_TYPE, CONSOLE_COMMAND_ID,
249294
&console_command_hook);
295+
hook_manager::add_hook(PC_CONSOLE_COMMAND_FUNC, CONSOLE_COMMAND_TYPE, CONSOLE_COMMAND_ID,
296+
&pc_console_command_hook);
297+
250298
hook_manager::add_hook(INJECT_CONSOLE_FUNC, INJECT_CONSOLE_TYPE, INJECT_CONSOLE_ID,
251299
&inject_console_hook);
252300
}

src/unrealsdk/game/bl2/hooks.cpp

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "unrealsdk/config.h"
44
#include "unrealsdk/game/bl2/bl2.h"
55
#include "unrealsdk/hook_manager.h"
6+
#include "unrealsdk/locks.h"
67
#include "unrealsdk/memory.h"
78
#include "unrealsdk/unreal/classes/ufunction.h"
89
#include "unrealsdk/unreal/classes/uobject.h"
@@ -112,48 +113,33 @@ void __fastcall process_event_hook(UObject* obj,
112113
process_event_ptr(obj, edx, func, params, null);
113114
}
114115

115-
std::recursive_mutex process_event_mutex{};
116-
117116
void __fastcall locking_process_event_hook(UObject* obj,
118117
void* edx,
119118
UFunction* func,
120119
void* params,
121120
void* null) {
122-
const std::lock_guard<std::recursive_mutex> lock{process_event_mutex};
121+
const locks::FunctionCall lock{};
123122
process_event_hook(obj, edx, func, params, null);
124123
}
125124

126125
static_assert(std::is_same_v<decltype(&process_event_hook), process_event_func>,
127126
"process_event signature is incorrect");
128127
static_assert(std::is_same_v<decltype(&process_event_hook), decltype(&locking_process_event_hook)>,
129128
"process_event signature is incorrect");
130-
131-
/**
132-
* @brief Checks if we should use a locking process event implementation.
133-
*
134-
* @return True if we should use locks.
135-
*/
136-
bool locking(void) {
137-
// Basically just a function so we can be sure this static is initialized late - LTO hopefully
138-
// takes care of it
139-
static auto locking = config::get_bool("unrealsdk.locking_process_event").value_or(false);
140-
return locking;
141-
}
142-
143129
} // namespace
144130

145131
void BL2Hook::hook_process_event(void) {
146-
detour(PROCESS_EVENT_SIG, locking() ? locking_process_event_hook : process_event_hook,
132+
detour(PROCESS_EVENT_SIG,
133+
// If we don't need locks, it's slightly more efficient to detour directly to the
134+
// non-locking version
135+
locks::FunctionCall::enabled() ? locking_process_event_hook : process_event_hook,
147136
&process_event_ptr, "ProcessEvent");
148137
}
149138

150139
void BL2Hook::process_event(UObject* object, UFunction* func, void* params) const {
151-
if (locking()) {
152-
const std::lock_guard<std::recursive_mutex> lock{process_event_mutex};
153-
process_event_hook(object, nullptr, func, params, nullptr);
154-
} else {
155-
process_event_hook(object, nullptr, func, params, nullptr);
156-
}
140+
// When we call it manually, always call the locking version, it will pass right through if
141+
// locks are disabled
142+
locking_process_event_hook(object, nullptr, func, params, nullptr);
157143
}
158144

159145
namespace {
@@ -244,13 +230,27 @@ void __fastcall call_function_hook(UObject* obj,
244230

245231
call_function_ptr(obj, edx, stack, result, func);
246232
}
233+
234+
void __fastcall locking_call_function_hook(UObject* obj,
235+
void* edx,
236+
FFrame* stack,
237+
void* result,
238+
UFunction* func) {
239+
const locks::FunctionCall lock{};
240+
call_function_hook(obj, edx, stack, result, func);
241+
}
242+
247243
static_assert(std::is_same_v<decltype(&call_function_hook), call_function_func>,
248244
"call_function signature is incorrect");
245+
static_assert(std::is_same_v<decltype(&locking_call_function_hook), call_function_func>,
246+
"call_function signature is incorrect");
249247

250248
} // namespace
251249

252250
void BL2Hook::hook_call_function(void) {
253-
detour(CALL_FUNCTION_SIG, call_function_hook, &call_function_ptr, "CallFunction");
251+
detour(CALL_FUNCTION_SIG,
252+
locks::FunctionCall::enabled() ? locking_call_function_hook : call_function_hook,
253+
&call_function_ptr, "CallFunction");
254254
}
255255

256256
} // namespace unrealsdk::game

src/unrealsdk/game/bl3/console.cpp

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ const constexpr auto MAX_HISTORY_ENTRIES = 50;
3838

3939
UObject* console = nullptr;
4040

41+
void static_uconsole_output_text(const std::wstring& str) {
42+
static auto idx = config::get_int("unrealsdk.uconsole_output_text_vf_index")
43+
.value_or(81); // NOLINT(readability-magic-numbers)
44+
45+
if (console == nullptr) {
46+
return;
47+
}
48+
49+
// UConsole::OutputText does not print anything on a completely empty line - we would prefer to
50+
// call UConsole::OutputTextLine, but that got completely inlined
51+
// If we have an empty string, replace with with a single newline.
52+
static const std::wstring newline = L"\n";
53+
TemporaryFString fstr{str.empty() ? newline : str};
54+
console->call_virtual_function<void, TemporaryFString*>(idx, &fstr);
55+
}
56+
4157
using console_command_func = void(UObject* console_obj, UnmanagedFString* raw_line);
4258
console_command_func* console_command_ptr;
4359

@@ -105,7 +121,25 @@ void console_command_hook(UObject* console_obj, UnmanagedFString* raw_line) {
105121
// just this, and we'll see if people actually complain
106122
}
107123

108-
LOG(INFO, L">>> {} <<<", line);
124+
/*
125+
This is a little awkward.
126+
Since we can't let execution though to the unreal function, we're responsible for
127+
printing the executed command line.
128+
129+
We do this via output text directly, rather than the LOG macro, so that it's not
130+
affected by the console log level, and so that it happens immediately (the LOG macro is
131+
queued, and can get out of order with respect to native engine messages).
132+
133+
However, for custom console commands it's also nice to see what the command was in the
134+
log file, since you'll see all their output too.
135+
136+
We don't really expose a "write to log file only", since it's not usually something
137+
useful, so as a compromise just use the LOG macro on the lowest possible log level, and
138+
assume the lowest people practically set their console log level to is dev warning.
139+
*/
140+
auto msg = unrealsdk::fmt::format(L">>> {} <<<", line);
141+
static_uconsole_output_text(msg);
142+
LOG(MIN, L"{}", msg);
109143

110144
try {
111145
callback->operator()(line.c_str(), line.size(), cmd_len);
@@ -193,19 +227,7 @@ void BL3Hook::inject_console(void) {
193227
}
194228

195229
void BL3Hook::uconsole_output_text(const std::wstring& str) const {
196-
static auto idx = config::get_int("unrealsdk.uconsole_output_text_vf_index")
197-
.value_or(81); // NOLINT(readability-magic-numbers)
198-
199-
if (console == nullptr) {
200-
return;
201-
}
202-
203-
// UConsole::OutputText does not print anything on a completely empty line - we would prefer to
204-
// call UConsole::OutputTextLine, but that got completely inlined
205-
// If we have an empty string, replace with with a single newline.
206-
static const std::wstring newline = L"\n";
207-
TemporaryFString fstr{str.empty() ? newline : str};
208-
console->call_virtual_function<void, TemporaryFString*>(idx, &fstr);
230+
static_uconsole_output_text(str);
209231
}
210232

211233
bool BL3Hook::is_console_ready(void) const {

0 commit comments

Comments
 (0)