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
2630namespace {
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.
2837const std::wstring SAY_BYPASS_FUNC = L" Engine.Console:ShippingConsoleCommand" ;
2938const constexpr auto SAY_BYPASS_TYPE = hook_manager::Type::PRE;
3039const 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
3447const std::wstring CONSOLE_COMMAND_FUNC = L" Engine.Console:ConsoleCommand" ;
3548const constexpr auto CONSOLE_COMMAND_TYPE = hook_manager::Type::PRE;
3649const std::wstring CONSOLE_COMMAND_ID = L" unrealsdk_bl2_console_command" ;
@@ -40,6 +53,17 @@ const constexpr auto INJECT_CONSOLE_TYPE = hook_manager::Type::PRE;
4053const std::wstring INJECT_CONSOLE_ID = L" unrealsdk_bl2_inject_console" ;
4154
4255bool 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+
53147bool 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
149243void 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,
0 commit comments