Skip to content

Commit 5eb3073

Browse files
authored
Merge pull request #1128 from UE4SS-RE/eigtdelay
Adds comprehensive delayed action system
2 parents 24a9779 + 026085b commit 5eb3073

File tree

47 files changed

+2661
-171
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2661
-171
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#pragma once
2+
3+
#include <thread>
4+
5+
#include <LuaType/LuaUObject.hpp>
6+
7+
namespace RC::LuaType
8+
{
9+
struct ThreadIdName
10+
{
11+
constexpr static const char* ToString()
12+
{
13+
return "ThreadId";
14+
}
15+
};
16+
class ThreadId : public LocalObjectBase<std::thread::id, ThreadIdName>
17+
{
18+
private:
19+
explicit ThreadId(std::thread::id object);
20+
21+
public:
22+
ThreadId() = delete;
23+
auto static construct(const LuaMadeSimple::Lua&, std::thread::id) -> const LuaMadeSimple::Lua::Table;
24+
auto static construct(const LuaMadeSimple::Lua&, BaseObject&) -> const LuaMadeSimple::Lua::Table;
25+
26+
private:
27+
auto static setup_metamethods(BaseObject&) -> void;
28+
29+
private:
30+
template <LuaMadeSimple::Type::IsFinal is_final>
31+
auto static setup_member_functions(LuaMadeSimple::Lua::Table&) -> void;
32+
};
33+
} // namespace RC::LuaType

UE4SS/include/Mod/LuaMod.hpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <File/File.hpp>
1313
#include <LuaMadeSimple/LuaMadeSimple.hpp>
1414
#include <Mod/Mod.hpp>
15+
#include <SettingsManager.hpp>
1516

1617
#include <String/StringType.hpp>
1718

@@ -54,6 +55,35 @@ namespace RC
5455
int32_t lua_action_thread_ref{};
5556
};
5657

58+
// Status of a delayed action (mirrors UE's ETimerStatus)
59+
enum class DelayedActionStatus : uint8_t
60+
{
61+
Pending, // Created but not yet started
62+
Active, // Running, waiting for delay to expire
63+
Paused, // Timer paused, will resume when unpaused
64+
Executing, // Currently executing callback
65+
PendingRemoval // Marked for removal, will be cleaned up
66+
};
67+
68+
struct DelayedGameThreadAction
69+
{
70+
const LuaMadeSimple::Lua* lua;
71+
int32_t lua_action_function_ref{};
72+
int32_t lua_action_thread_ref{};
73+
GameThreadExecutionMethod method{GameThreadExecutionMethod::EngineTick};
74+
DelayedActionStatus status{DelayedActionStatus::Active};
75+
std::chrono::steady_clock::time_point execute_at{}; // Absolute time when action should execute
76+
int64_t time_remaining_ms{0}; // Time remaining when paused (milliseconds)
77+
int64_t frames_remaining{0}; // Countdown for frame-based delays
78+
int64_t delay_ms{0}; // Original delay in milliseconds (for loop/reset)
79+
int64_t delay_frames{0}; // Original delay in frames (0 means use time-based delay)
80+
int64_t handle{0}; // Unique handle for this action
81+
bool is_retriggerable{false}; // If true, can be reset by calling with same handle
82+
bool is_looping{false}; // If true, re-schedule after each execution
83+
};
84+
85+
static inline int64_t m_next_delayed_action_handle{1};
86+
5787
struct AsyncAction
5888
{
5989
// TODO: Use LuaMadeSimple instead of lua_State*
@@ -111,6 +141,9 @@ namespace RC
111141
static inline std::unordered_map<File::StringType, LuaCallbackData> m_global_command_lua_callbacks;
112142
static inline std::unordered_map<File::StringType, LuaCallbackData> m_custom_command_lua_pre_callbacks;
113143
static inline std::vector<SimpleLuaAction> m_game_thread_actions{};
144+
static inline std::vector<SimpleLuaAction> m_engine_tick_actions{};
145+
static inline std::vector<DelayedGameThreadAction> m_delayed_game_thread_actions{};
146+
static inline GameThreadExecutionMethod m_default_game_thread_method{GameThreadExecutionMethod::EngineTick};
114147
// This is storage that persists through hot-reloads.
115148
static inline std::unordered_map<std::string, SharedLuaVariable> m_shared_lua_variables{};
116149
static inline std::vector<FunctionHookData> m_custom_event_callbacks{};
@@ -131,9 +164,11 @@ namespace RC
131164

132165
private:
133166
std::jthread m_async_thread;
167+
std::thread::id m_main_thread_id{};
134168
bool m_processing_events{};
135169
bool m_pause_events_processing{};
136170
bool m_is_process_event_hooked{};
171+
static inline bool m_is_engine_tick_hooked{};
137172
std::mutex m_actions_lock{};
138173

139174
public:
@@ -147,6 +182,9 @@ namespace RC
147182
}
148183

149184
private:
185+
static auto ensure_engine_tick_hooked() -> void;
186+
static auto ensure_process_event_hooked(LuaMod* mod) -> void;
187+
150188
static auto custom_module_searcher(lua_State* L) -> int;
151189
auto setup_custom_module_loader(const LuaMadeSimple::Lua* lua_state) -> void;
152190
auto load_and_execute_script(const std::filesystem::path& script_path) -> bool;
@@ -179,6 +217,16 @@ namespace RC
179217
m_actions_lock.unlock();
180218
}
181219

220+
[[nodiscard]] RC_UE4SS_API auto get_async_thread_id() const -> std::thread::id
221+
{
222+
return m_async_thread.get_id();
223+
}
224+
225+
[[nodiscard]] RC_UE4SS_API auto get_main_thread_id() const -> std::thread::id
226+
{
227+
return m_main_thread_id;
228+
}
229+
182230
public:
183231
// Called once when the program is starting, after mods are setup but before any mods have been started
184232
auto static on_program_start() -> void;

UE4SS/include/SettingsManager.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,17 @@
77
#include <File/File.hpp>
88
#include <GUI/GUI.hpp>
99
#include <Input/KeyDef.hpp>
10+
#include <Unreal/UnrealInitializer.hpp>
1011

1112
namespace RC
1213
{
14+
// Method for executing callbacks in game thread
15+
enum class GameThreadExecutionMethod
16+
{
17+
ProcessEvent, // Use ProcessEvent hook
18+
EngineTick // Use Engine Tick hook (once per frame)
19+
};
20+
1321
class RC_UE4SS_API SettingsManager
1422
{
1523
public:
@@ -31,6 +39,7 @@ namespace RC
3139
StringType InputSource{STR("Default")};
3240
bool DoEarlyScan{false};
3341
bool SearchByAddress{false};
42+
GameThreadExecutionMethod DefaultExecuteInGameThreadMethod{GameThreadExecutionMethod::EngineTick};
3443
} General;
3544

3645
struct SectionEngineVersionOverride
@@ -102,6 +111,7 @@ namespace RC
102111
bool HookLocalPlayerExec{true};
103112
bool HookAActorTick{true};
104113
bool HookEngineTick{true};
114+
Unreal::UnrealInitializer::FunctionResolveMethod EngineTickResolveMethod{Unreal::UnrealInitializer::FunctionResolveMethod::Scan};
105115
bool HookGameViewportClientTick{true};
106116
bool HookUObjectProcessEvent{true};
107117
bool HookProcessConsoleExec{true};

UE4SS/include/UE4SSProgram.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ namespace RC
272272
{
273273
return m_processing_events;
274274
}
275+
[[nodiscard]] RC_UE4SS_API auto get_event_loop_thread_id() const -> std::thread::id
276+
{
277+
return m_event_loop_thread_id;
278+
}
275279
RC_UE4SS_API auto is_event_loop_thread() -> bool
276280
{
277281
return std::this_thread::get_id() == m_event_loop_thread_id;

UE4SS/include/UE4SSRuntime.hpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#pragma once
2+
3+
#include <Common.hpp>
4+
5+
namespace RC
6+
{
7+
/**
8+
* UE4SS Runtime Information
9+
*
10+
* This struct provides static functions to query UE4SS runtime state.
11+
* These can be used by both C++ mods and Lua scripts to gracefully
12+
* handle missing functionality or adapt behavior based on user configuration.
13+
*
14+
* Categories:
15+
* - Hook availability: Whether AOB scans succeeded for various hooks
16+
* - User preferences: Configuration choices that affect behavior
17+
* - Feature availability: Whether certain engine features were found
18+
*/
19+
struct RC_UE4SS_API UE4SSRuntime
20+
{
21+
// ========== Hook Availability ==========
22+
// These check if the underlying functions were found during AOB scan.
23+
// The actual detours are created lazily when callbacks are registered.
24+
25+
/**
26+
* Check if the EngineTick function is available for hooking.
27+
* This checks if the AOB scan found UEngine::Tick.
28+
* Required for:
29+
* - Frame-based delays (ExecuteInGameThreadAfterFrames, LoopInGameThreadAfterFrames)
30+
* - ExecuteInGameThread with EGameThreadMethod.EngineTick
31+
* @return true if EngineTick function was found and can be hooked
32+
*/
33+
static auto IsEngineTickAvailable() -> bool;
34+
35+
/**
36+
* Check if the ProcessEvent function is available for hooking.
37+
* This checks if the AOB scan found UObject::ProcessEvent.
38+
* Required for:
39+
* - ExecuteInGameThread with EGameThreadMethod.ProcessEvent
40+
* - RegisterHook functionality
41+
* @return true if ProcessEvent function was found and can be hooked
42+
*/
43+
static auto IsProcessEventAvailable() -> bool;
44+
45+
// Add more runtime checks here as needed, e.g.,:
46+
// - User preferences (FavorVirtualWrappers vs FavorExplicitImplementation, etc.)
47+
// - Feature availability (FText constructor found, specific UE version features, etc.)
48+
};
49+
} // namespace RC

UE4SS/src/LuaType/LuaThreadId.cpp

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#include <limits>
2+
3+
#include <fmt/std.h>
4+
#include <DynamicOutput/DynamicOutput.hpp>
5+
#include <Helpers/String.hpp>
6+
#include <LuaType/LuaThreadId.hpp>
7+
8+
namespace RC::LuaType
9+
{
10+
ThreadId::ThreadId(std::thread::id object) : LocalObjectBase<std::thread::id, ThreadIdName>(std::move(object))
11+
{
12+
}
13+
14+
auto ThreadId::construct(const LuaMadeSimple::Lua& lua, std::thread::id unreal_object) -> const LuaMadeSimple::Lua::Table
15+
{
16+
LuaType::ThreadId lua_object{unreal_object};
17+
18+
auto metatable_name = "ThreadIdUserdata";
19+
20+
LuaMadeSimple::Lua::Table table = lua.get_metatable(metatable_name);
21+
if (lua.is_nil(-1))
22+
{
23+
lua.discard_value(-1);
24+
lua.prepare_new_table();
25+
setup_metamethods(lua_object);
26+
setup_member_functions<LuaMadeSimple::Type::IsFinal::Yes>(table);
27+
lua.new_metatable<LuaType::ThreadId>(metatable_name, lua_object.get_metamethods());
28+
}
29+
30+
// Create object & surrender ownership to Lua
31+
lua.transfer_stack_object(std::move(lua_object), metatable_name, lua_object.get_metamethods());
32+
33+
return table;
34+
}
35+
36+
auto ThreadId::construct(const LuaMadeSimple::Lua& lua, BaseObject& construct_to) -> const LuaMadeSimple::Lua::Table
37+
{
38+
LuaMadeSimple::Lua::Table table = lua.prepare_new_table();
39+
40+
setup_member_functions<LuaMadeSimple::Type::IsFinal::No>(table);
41+
42+
setup_metamethods(construct_to);
43+
44+
return table;
45+
}
46+
47+
auto ThreadId::setup_metamethods(BaseObject& base_object) -> void
48+
{
49+
base_object.get_metamethods().create(LuaMadeSimple::Lua::MetaMethod::Equal, []([[maybe_unused]] const LuaMadeSimple::Lua& lua) -> int {
50+
if (!lua.is_userdata(1) || !lua.is_userdata(2))
51+
{
52+
lua.throw_error("ThreadId __eq metamethod called but there was not two userdata to compare");
53+
}
54+
55+
auto a = lua.get_userdata<LuaType::ThreadId>();
56+
auto b = lua.get_userdata<LuaType::ThreadId>();
57+
58+
return a.get_local_cpp_object() == b.get_local_cpp_object();
59+
});
60+
}
61+
62+
template <LuaMadeSimple::Type::IsFinal is_final>
63+
auto ThreadId::setup_member_functions(LuaMadeSimple::Lua::Table& table) -> void
64+
{
65+
table.add_pair("ToString", [](const LuaMadeSimple::Lua& lua) -> int {
66+
auto& lua_object = lua.get_userdata<ThreadId>();
67+
68+
lua.set_string(fmt::format("{}", lua_object.get_local_cpp_object()));
69+
70+
return 1;
71+
});
72+
73+
if constexpr (is_final == LuaMadeSimple::Type::IsFinal::Yes)
74+
{
75+
table.add_pair("type", [](const LuaMadeSimple::Lua& lua) -> int {
76+
lua.set_string(ClassName::ToString());
77+
return 1;
78+
});
79+
80+
// If this is the final object then we also want to finalize creating the table
81+
// If not then it's the responsibility of the overriding object to call 'make_global()'
82+
// table.make_global("ThreadIdUserdata");
83+
}
84+
}
85+
} // namespace RC::LuaType

0 commit comments

Comments
 (0)