Skip to content

Commit c7df707

Browse files
committed
feat(Lua): Option to auto-reload individual mods when a change is detected in the Scripts directory
1 parent 4acf640 commit c7df707

File tree

18 files changed

+158
-44
lines changed

18 files changed

+158
-44
lines changed

UE4SS/include/Mod/Mod.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ namespace RC
4848

4949
public:
5050
auto get_name() const -> StringViewType;
51+
auto get_path() const -> const std::filesystem::path&;
5152

5253
virtual auto start_mod() -> void = 0;
5354
virtual auto uninstall() -> void = 0;

UE4SS/include/SettingsManager.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ namespace RC
2222
{
2323
bool EnableHotReloadSystem{};
2424
Input::Key HotReloadKey{Input::Key::R};
25+
bool EnableAutoReloadingLuaMods{};
2526
bool UseCache{true};
2627
bool InvalidateCacheIfDLLDiffers{true};
2728
bool EnableDebugKeyBindings{false};

UE4SS/include/UE4SSProgram.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,13 @@ namespace RC
261261
RC_UE4SS_API auto is_keydown_event_registered(Input::Key) -> bool;
262262
RC_UE4SS_API auto is_keydown_event_registered(Input::Key, const Input::Handler::ModifierKeyArray&) -> bool;
263263
RC_UE4SS_API auto get_all_input_events(std::function<void(Input::KeySet&)> callback) -> void;
264+
enum class AllMods
265+
{
266+
Yes,
267+
No,
268+
};
269+
// If 'AllMods' is 'Yes', then 'mod' can be nullptr.
270+
auto unregister_keydown_events_for_lua_mod(LuaMod* mod, AllMods = AllMods::No) -> void;
264271

265272
private:
266273
static auto install_cpp_mods() -> void;

UE4SS/src/Mod/LuaMod.cpp

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4119,25 +4119,7 @@ No overload found for function 'FPackageName:IsValidLongPackageName'.
41194119
erase_from_container(this, m_local_player_exec_post_callbacks);
41204120
erase_from_container(this, m_script_hook_callbacks);
41214121

4122-
UE4SSProgram::get_program().get_all_input_events([&](auto& key_set) {
4123-
std::erase_if(key_set.key_data, [&](auto& item) -> bool {
4124-
auto& [_, key_data] = item;
4125-
std::erase_if(key_data, [&](Input::KeyData& key_data) -> bool {
4126-
// custom_data == 1: Bind came from Lua, and custom_data2 is a pointer to LuaMod.
4127-
// custom_data == 2: Bind came from C++, and custom_data2 is a pointer to KeyDownEventData. Must free it.
4128-
if (key_data.custom_data == 1)
4129-
{
4130-
return key_data.custom_data2 == this;
4131-
}
4132-
else
4133-
{
4134-
return false;
4135-
}
4136-
});
4137-
4138-
return key_data.empty();
4139-
});
4140-
});
4122+
UE4SSProgram::get_program().unregister_keydown_events_for_lua_mod(this);
41414123

41424124
if (m_hook_lua.size() > 0)
41434125
{

UE4SS/src/Mod/Mod.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ namespace RC
6060
return m_mod_name;
6161
}
6262

63+
auto Mod::get_path() const -> const std::filesystem::path&
64+
{
65+
return m_mod_path;
66+
}
67+
6368
auto Mod::set_installable(bool is_installable) -> void
6469
{
6570
m_installable = is_installable;

UE4SS/src/SettingsManager.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ namespace RC
6666
throw std::runtime_error{fmt::format("Invalid value for 'General.HotReloadKey': {}\n", to_string(hot_reload_key))};
6767
}
6868
}
69+
REGISTER_BOOL_SETTING(General.EnableAutoReloadingLuaMods, section_general, EnableAutoReloadingLuaMods)
6970
REGISTER_BOOL_SETTING(General.UseCache, section_general, UseCache)
7071
REGISTER_BOOL_SETTING(General.InvalidateCacheIfDLLDiffers, section_general, InvalidateCacheIfDLLDiffers)
7172
REGISTER_BOOL_SETTING(General.EnableDebugKeyBindings, section_general, EnableDebugKeyBindings)

UE4SS/src/UE4SSProgram.cpp

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959

6060
#include <polyhook2/PE/IatHook.hpp>
6161

62+
#include <FilesystemWatcher.hpp>
63+
6264
namespace RC
6365
{
6466
// Commented out because this system (turn off hotkeys when in-game console is open) it doesn't work properly.
@@ -1068,6 +1070,68 @@ namespace RC
10681070

10691071
on_program_start();
10701072

1073+
FilesystemWatcher filesystem_watcher{};
1074+
if (settings_manager.General.EnableAutoReloadingLuaMods)
1075+
{
1076+
filesystem_watcher.m_min_duration_between_notifications = std::chrono::milliseconds{100};
1077+
for (const auto& mod : m_mods)
1078+
{
1079+
if (dynamic_cast<CppMod*>(mod.get()))
1080+
{
1081+
filesystem_watcher.add_dir(std::filesystem::path{get_mods_directory()} / mod->get_name() / "dlls");
1082+
}
1083+
else if (dynamic_cast<LuaMod*>(mod.get()))
1084+
{
1085+
filesystem_watcher.add_dir(std::filesystem::path{get_mods_directory()} / mod->get_name() / "Scripts");
1086+
}
1087+
}
1088+
filesystem_watcher.start_async_polling([&](const std::filesystem::path& file, bool match_all) {
1089+
ScopedThreadSynchronizer thread_synchronizer{filesystem_watcher.get_thread_state()};
1090+
const auto mod_name = file.parent_path().filename();
1091+
auto dir_name = file.filename().string();
1092+
const auto is_cpp_mod = String::iequal(dir_name, "dlls");
1093+
if (is_cpp_mod)
1094+
{
1095+
auto staged_file = file / mod_name;
1096+
staged_file.replace_extension(".dll");
1097+
if (!std::filesystem::exists(staged_file))
1098+
{
1099+
return;
1100+
}
1101+
// TODO: Unload the dll (uninstall).
1102+
// Delete 'main.dll'.
1103+
// Rename 'staged_file' to 'main.dll'.
1104+
// Load 'main.dll' (install & start).
1105+
1106+
// TODO: To reload C++ mods, we need to add a way to unregister hooks, and then C++ mods need to unregister on they get notified that they're getting unloaded.
1107+
// For Lua mods, there's no notification, but we track all the hooks internally, so we can unregister automatically, we just need to
1108+
// call Lua::Uninstall, and We must also remove the keybinds like we do in UE4SSProgram::reinstall_mods.
1109+
// Unsure about loading and starting mods again.
1110+
}
1111+
else
1112+
{
1113+
auto mod = find_lua_mod_by_name(ensure_str(mod_name), IsInstalled::Yes, IsStarted::Yes);
1114+
if (!mod)
1115+
{
1116+
return;
1117+
}
1118+
m_pause_events_processing = true;
1119+
mod->uninstall();
1120+
auto& mod_ref = *std::ranges::find_if(m_mods, [&](const std::unique_ptr<Mod>& mod_ptr) {
1121+
return mod_ptr.get() == mod;
1122+
});
1123+
if (!mod_ref)
1124+
{
1125+
return;
1126+
}
1127+
mod_ref = std::make_unique<LuaMod>(*this, StringType{mod_ref->get_name()}, mod_ref->get_path());
1128+
m_pause_events_processing = false;
1129+
Output::send(STR("Auto-reloading Lua mod '{}'\n"), mod_ref->get_name());
1130+
mod_ref->start_mod();
1131+
}
1132+
});
1133+
}
1134+
10711135
Output::send(STR("Event loop start\n"));
10721136
for (m_processing_events = true; m_processing_events;)
10731137
{
@@ -1133,6 +1197,12 @@ namespace RC
11331197
}
11341198
}
11351199

1200+
if (settings_manager.General.EnableAutoReloadingLuaMods)
1201+
{
1202+
// Process any mod file changes, and wait until done.
1203+
process_sync_request(filesystem_watcher.get_thread_state());
1204+
}
1205+
11361206
std::this_thread::sleep_for(std::chrono::milliseconds(5));
11371207
ProfilerFrameMark();
11381208
}
@@ -1308,6 +1378,23 @@ namespace RC
13081378
}
13091379
}
13101380

1381+
auto UE4SSProgram::unregister_keydown_events_for_lua_mod(LuaMod* mod, AllMods all_mods) -> void
1382+
{
1383+
#ifdef HAS_INPUT
1384+
m_input_handler.get_events_safe([&](auto& key_set) {
1385+
std::erase_if(key_set.key_data, [&](auto& item) -> bool {
1386+
auto& [_, key_data] = item;
1387+
std::erase_if(key_data, [&](Input::KeyData& key_data) -> bool {
1388+
// custom_data == 1: Bind came from Lua, and custom_data2 is a pointer to LuaMod.
1389+
// custom_data == 2: Bind came from C++, and custom_data2 is a pointer to KeyDownEventData. Must free it.
1390+
return key_data.custom_data == 1 && (all_mods == AllMods::Yes || static_cast<LuaMod*>(key_data.custom_data2) == mod);
1391+
});
1392+
return key_data.empty();
1393+
});
1394+
});
1395+
#endif
1396+
}
1397+
13111398
template <typename ModType>
13121399
auto start_mods() -> std::string
13131400
{
@@ -1506,31 +1593,6 @@ namespace RC
15061593

15071594
uninstall_mods();
15081595

1509-
// Remove key binds that were set from Lua scripts
1510-
#ifdef HAS_INPUT
1511-
m_input_handler.get_events_safe([&](auto& key_set) {
1512-
std::erase_if(key_set.key_data, [&](auto& item) -> bool {
1513-
auto& [_, key_data] = item;
1514-
bool were_all_events_registered_from_lua = true;
1515-
std::erase_if(key_data, [&](Input::KeyData& key_data) -> bool {
1516-
// custom_data == 1: Bind came from Lua, and custom_data2 is a pointer to LuaMod.
1517-
// custom_data == 2: Bind came from C++, and custom_data2 is a pointer to KeyDownEventData. Must free it.
1518-
if (key_data.custom_data == 1)
1519-
{
1520-
return true;
1521-
}
1522-
else
1523-
{
1524-
were_all_events_registered_from_lua = false;
1525-
return false;
1526-
}
1527-
});
1528-
1529-
return were_all_events_registered_from_lua;
1530-
});
1531-
});
1532-
#endif
1533-
15341596
// Remove all custom properties
15351597
// Uncomment when custom properties are working
15361598
LuaType::LuaCustomProperty::StaticStorage::property_list.clear();

assets/CustomGameConfigs/Atomic Heart/UE4SS-settings.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ EnableHotReloadSystem = 1
1414
; Default: R
1515
HotReloadKey = R
1616

17+
; Whether to automatically reload mods when a change is detected.
18+
; The reload triggers when any file is edited or a new file is added to the 'Scripts' directory.
19+
; Default: 0
20+
EnableAutoReloadingLuaMods = 0
21+
1722
; Whether caches will be invalidated if ue4ss.dll has changed
1823
; Default: 1
1924
InvalidateCacheIfDLLDiffers = 1

assets/CustomGameConfigs/Borderlands 3/UE4SS-settings.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ EnableHotReloadSystem = 0
1414
; Default: R
1515
HotReloadKey = R
1616

17+
; Whether to automatically reload mods when a change is detected.
18+
; The reload triggers when any file is edited or a new file is added to the 'Scripts' directory.
19+
; Default: 0
20+
EnableAutoReloadingLuaMods = 0
21+
1722
; Whether caches will be invalidated if ue4ss.dll has changed
1823
; Default: 1
1924
InvalidateCacheIfDLLDiffers = 1

assets/CustomGameConfigs/Final Fantasy 7 Rebirth/UE4SS-settings.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ EnableHotReloadSystem = 1
1414
; Default: R
1515
HotReloadKey = R
1616

17+
; Whether to automatically reload mods when a change is detected.
18+
; The reload triggers when any file is edited or a new file is added to the 'Scripts' directory.
19+
; Default: 0
20+
EnableAutoReloadingLuaMods = 0
21+
1722
; Whether the cache system for AOBs will be used.
1823
; Default: 1
1924
UseCache = 1

0 commit comments

Comments
 (0)