From b3d741ca512885d75b83d53f1b5429fc4e012b71 Mon Sep 17 00:00:00 2001 From: maybegreat48 <96936658+maybegreat48@users.noreply.github.com> Date: Thu, 26 Jun 2025 00:16:10 -0400 Subject: [PATCH] feat(lua): improve prototype --- src/core/backend/ScriptMgr.cpp | 6 +- src/core/scripting/LatentFunction.hpp | 10 ++ src/core/scripting/LuaManager.cpp | 94 +++++++++-- src/core/scripting/LuaManager.hpp | 39 +++++ src/core/scripting/LuaScript.cpp | 147 ++++++++++++++++-- src/core/scripting/LuaScript.hpp | 87 ++++++++++- src/core/scripting/libraries/Script.cpp | 71 +++++++++ src/game/frontend/submenus/Settings.cpp | 2 + .../frontend/submenus/Settings/LuaScripts.cpp | 85 ++++++++++ .../frontend/submenus/Settings/LuaScripts.hpp | 8 + 10 files changed, 520 insertions(+), 29 deletions(-) create mode 100644 src/core/scripting/LatentFunction.hpp create mode 100644 src/core/scripting/libraries/Script.cpp create mode 100644 src/game/frontend/submenus/Settings/LuaScripts.cpp create mode 100644 src/game/frontend/submenus/Settings/LuaScripts.hpp diff --git a/src/core/backend/ScriptMgr.cpp b/src/core/backend/ScriptMgr.cpp index 2f8bffe..97987b2 100644 --- a/src/core/backend/ScriptMgr.cpp +++ b/src/core/backend/ScriptMgr.cpp @@ -1,4 +1,5 @@ #include "ScriptMgr.hpp" +#include "core/scripting/LuaManager.hpp" namespace YimMenu { @@ -67,7 +68,10 @@ namespace YimMenu void ScriptMgr::YieldImpl(std::optional time) { - if (auto script = static_cast(GetFiberData())) + // TODO: this wouldn't work!!! fix ASAP! + if (auto script = LuaManager::GetRunningCoroutine()) + LuaScript::GetScript(script).Yield(script, time ? std::chrono::duration_cast(*time).count() : 0); + else if (auto script = static_cast(GetFiberData())) script->Yield(time); } diff --git a/src/core/scripting/LatentFunction.hpp b/src/core/scripting/LatentFunction.hpp new file mode 100644 index 0000000..d1d989c --- /dev/null +++ b/src/core/scripting/LatentFunction.hpp @@ -0,0 +1,10 @@ +#pragma once +#include "LuaScript.hpp" + +namespace YimMenu::Lua +{ + int LatentFunction(lua_State* lua) + { + + } +} \ No newline at end of file diff --git a/src/core/scripting/LuaManager.cpp b/src/core/scripting/LuaManager.cpp index 0257eb5..9b6a855 100644 --- a/src/core/scripting/LuaManager.cpp +++ b/src/core/scripting/LuaManager.cpp @@ -5,6 +5,11 @@ namespace YimMenu { + void LuaManager::AddUnloadedScript(std::string_view name, std::string_view path) + { + m_UnloadedScripts.push_back({std::string(name), std::string(path)}); + } + void LuaManager::RegisterLibraryImpl(LuaLibrary* library) { m_Libraries.push_back(library); @@ -42,28 +47,68 @@ namespace YimMenu std::lock_guard lock(m_LoadMutex); while (!m_ScriptsToLoad.empty()) { + std::erase_if(m_UnloadedScripts, [this](auto& script) { + return std::filesystem::equivalent(m_ScriptsToLoad.front(), script.m_Path); + }); m_LoadedScripts.push_back(std::make_shared(m_ScriptsToLoad.front())); m_ScriptsToLoad.pop(); } - // 2) remove malfunctioning scripts - std::erase_if(m_LoadedScripts, [](auto& script) { - auto is_malfunctioning = script->IsMalfunctioning(); - if (is_malfunctioning) + // 2) remove scripts if needed + std::erase_if(m_LoadedScripts, [this](auto& script) { + if (!script->SafeToUnload()) + return false; + + bool unload = false; + + if (script->IsMalfunctioning()) { - script->Invalidate(); Notifications::Show("Lua Scripting", std::format("Script {} has been unloaded due to a malfunction", script->GetName()), NotificationType::Warning); - return true; + unload = true; + } + else if (script->GetLoadState() == LuaScript::LoadState::WANT_UNLOAD) + { + unload = true; + } + else if (script->GetLoadState() == LuaScript::LoadState::WANT_RELOAD) + { + m_ScriptsToLoad.push(std::string(script->GetPath())); // will be reloaded next tick + unload = true; } - return false; + if (unload) + { + script->MarkUnloaded(); + AddUnloadedScript(script->GetName(), script->GetPath()); + } + + return unload; }); + + // 3) refresh unloaded scripts (every 10 seconds) + if (m_LastRefreshedUnloadedScripts + 10s < std::chrono::system_clock::now()) + { + m_UnloadedScripts.clear(); + for (const auto& entry : std::filesystem::directory_iterator(scripts_dir.Path())) + { + if (entry.path().extension() != ".lua") + continue; + + for (auto& script : m_LoadedScripts) + if (std::filesystem::equivalent(script->GetPath(), entry.path().string())) + continue; + + AddUnloadedScript(entry.path().filename().string(), entry.path().string()); + } + m_LastRefreshedUnloadedScripts = std::chrono::system_clock::now(); + } } - // 3) run tick coroutines - { - // TODO! - } + // 4) run tick coroutines + for (auto& script : m_LoadedScripts) + if (script->IsValid()) + script->Tick(); + ScriptMgr::Yield(); } } @@ -72,4 +117,31 @@ namespace YimMenu { return GetCurrentThreadId() == m_MainThreadId; } + + void LuaManager::SetRunningCoroutineImpl(lua_State* script) + { + if (!m_RunningCoroutine || !script) + { + m_RunningCoroutine = script; + } + else + { + LOGF(FATAL, "LuaManager::SetRunningCoroutineImpl: {} attempted to enter a coroutine when a coroutine from {} is already running", LuaScript::GetScript(script).GetName(), LuaScript::GetScript(m_RunningCoroutine).GetName()); + LuaScript::GetScript(script).SetMalfunctioning(); + } + } + + void LuaManager::ForAllLoadedScriptsImpl(ForAllLoadedScriptsCallback callback) + { + std::lock_guard lock(m_LoadMutex); + for (auto& script : m_LoadedScripts) + callback(script); + } + + void LuaManager::ForAllUnloadedScriptsImpl(ForAllUnloadedScriptsCallback callback) + { + std::lock_guard lock(m_LoadMutex); + for (auto& script : m_UnloadedScripts) + callback(script); + } } \ No newline at end of file diff --git a/src/core/scripting/LuaManager.hpp b/src/core/scripting/LuaManager.hpp index 89bfb3d..619eb15 100644 --- a/src/core/scripting/LuaManager.hpp +++ b/src/core/scripting/LuaManager.hpp @@ -12,18 +12,36 @@ namespace YimMenu { }; + struct UnloadedScript + { + std::string m_Name; + std::string m_Path; + }; + + using ForAllLoadedScriptsCallback = void(*)(std::shared_ptr& script); + using ForAllUnloadedScriptsCallback = void(*)(UnloadedScript& script); + private: std::vector> m_LoadedScripts; + std::vector m_UnloadedScripts; + std::chrono::system_clock::time_point m_LastRefreshedUnloadedScripts; std::vector m_Libraries; std::queue m_ScriptsToLoad; std::mutex m_LoadMutex; std::uint32_t m_MainThreadId; + lua_State* m_RunningCoroutine = nullptr; + + // m_LoadMutex MUST be locked when calling this function + void AddUnloadedScript(std::string_view name, std::string_view path); void RegisterLibraryImpl(LuaLibrary* library); void LoadLibrariesImpl(lua_State* state); void LoadScriptImpl(std::string path); void RunScriptImpl(); bool IsRunningInMainThreadImpl(); + void SetRunningCoroutineImpl(lua_State* script); + void ForAllLoadedScriptsImpl(ForAllLoadedScriptsCallback callback); + void ForAllUnloadedScriptsImpl(ForAllUnloadedScriptsCallback callback); static LuaManager& GetInstance() { @@ -56,5 +74,26 @@ namespace YimMenu { return GetInstance().IsRunningInMainThreadImpl(); } + + static void SetRunningCoroutine(lua_State* script) + { + GetInstance().SetRunningCoroutineImpl(script); + } + + static lua_State* GetRunningCoroutine() + { + return GetInstance().m_RunningCoroutine; + } + + // these can be safely called from the DX thread + static void ForAllLoadedScripts(ForAllLoadedScriptsCallback callback) + { + GetInstance().ForAllLoadedScriptsImpl(callback); + } + + static void ForAllUnloadedScripts(ForAllUnloadedScriptsCallback callback) + { + GetInstance().ForAllUnloadedScriptsImpl(callback); + } }; } \ No newline at end of file diff --git a/src/core/scripting/LuaScript.cpp b/src/core/scripting/LuaScript.cpp index 5674ec4..7ded134 100644 --- a/src/core/scripting/LuaScript.cpp +++ b/src/core/scripting/LuaScript.cpp @@ -24,38 +24,91 @@ namespace YimMenu return 1; } - bool LuaScript::CallFunction(int n_args, int n_results) + bool LuaScript::CallFunction(int n_args, int n_results, lua_State* override_state) { + auto state = override_state; + if (!state) + state = m_State; + if (!LuaManager::IsRunningInMainThread()) { LOGF(FATAL, "LuaScript::CallFunction: {} attempted to call a Lua function outside the main thread. This is not allowed", m_ModuleName); - lua_pop(m_State, 1); // pop the function from the stack since we aren't going to call it + lua_pop(state, 1); // pop the function from the stack since we aren't going to call it SetMalfunctioning(); return false; } - lua_pushcfunction(m_State, &ErrorHandler); - int handler_index = lua_gettop(m_State) - n_args - 1; - lua_insert(m_State, handler_index); // move it before all the args + lua_pushcfunction(state, &ErrorHandler); + int handler_index = lua_gettop(state) - n_args - 1; + lua_insert(state, handler_index); // move it before all the args - auto result = lua_pcall(m_State, n_args, n_results, handler_index); + auto result = lua_pcall(state, n_args, n_results, handler_index); if (result == LUA_OK) { // the lua runtime doesn't pop the handler if there's no error - lua_remove(m_State, handler_index); // TODO: does this actually work? What happens if your function returns stuff? + lua_remove(state, handler_index); // TODO: does this actually work? What happens if your function returns stuff? return true; } else { - auto trace = lua_tostring(m_State, -1); + auto trace = lua_tostring(state, -1); LOGF(FATAL, "{}: {}", m_ModuleName, trace); - lua_pop(m_State, 1); // pop the stack trace + lua_pop(state, 1); // pop the stack trace SetMalfunctioning(); return false; } } + int LuaScript::ResumeCoroutine(int n_args, int n_results, lua_State* coro_state) + { + if (!LuaManager::IsRunningInMainThread()) + { + LOGF(FATAL, "LuaScript::ResumeCoroutine: {} attempted to resume a Lua coroutine outside the main thread. This is not allowed", m_ModuleName); + SetMalfunctioning(); + return LUA_ERRRUN; + } + + auto result = lua_resume(coro_state, n_args); + if (result == LUA_OK || result == LUA_YIELD) + { + // move returns to the main stack + + auto num_rets = lua_gettop(coro_state); + if (num_rets != n_results && result == LUA_YIELD) + { + LOGF(FATAL, "LuaScript::ResumeCoroutine: {} yielded {} values when code expected {} values to be yielded", m_ModuleName, num_rets, n_results); + SetMalfunctioning(); + return LUA_ERRRUN; + } + + if (num_rets) + lua_xmove(coro_state, m_State, num_rets); + } + else + { + // there would be an error object on the top of stack + std::string error_msg = lua_tostring(coro_state, -1); + lua_pop(coro_state, 1); + + // traceback + luaL_traceback(coro_state, coro_state, error_msg.c_str(), 1); + LOGF(FATAL, "{}: {}", m_ModuleName, lua_tostring(coro_state, -1)); + lua_pop(coro_state, 1); + + SetMalfunctioning(); + } + + return result; + } + + void LuaScript::RemoveScriptCallback(ScriptCallback& callback) + { + luaL_unref(m_State, LUA_REGISTRYINDEX, callback.m_Coroutine); + if (callback.m_Fiber) + DeleteFiber(callback.m_Fiber); + } + LuaScript::LuaScript(std::string_view file_name) : m_FileName(file_name), m_ModuleName(std::filesystem::path(file_name).filename().string()) @@ -77,7 +130,7 @@ namespace YimMenu // we should have a function in the top of stack CallFunction(0, 0); - m_IsValid = true; + m_LoadState = LoadState::RUNNING; } LuaScript::~LuaScript() @@ -89,6 +142,15 @@ namespace YimMenu } } + bool LuaScript::SafeToUnload() + { + for (auto& callback : m_ScriptCallbacks) + if (callback.m_LastYieldFromCode) + return false; // don't unload if we're calling a latent function + + return true; + } + LuaScript& LuaScript::GetScript(lua_State* state) { lua_getfield(state, LUA_REGISTRYINDEX, "context"); @@ -96,4 +158,69 @@ namespace YimMenu lua_pop(state, 1); return *script; } + + void LuaScript::AddScriptCallback(int coro_handle) + { + ScriptCallback callback; + callback.m_Coroutine = coro_handle; + callback.m_LastYieldFromCode = false; + callback.m_TimeToResume = std::nullopt; + callback.m_Fiber = nullptr; + callback.m_ParentFiber = nullptr; + callback.m_LatentTarget = nullptr; + + // we don't want to push any additional callbacks to the main array when we're in the middle of running, and potentially deleting, them + if (m_RunningScriptCallbacks) + m_QueuedScriptCallbacks.push_back(callback); + else + m_ScriptCallbacks.push_back(callback); + } + + void LuaScript::Yield(lua_State* state, int millis, bool from_code) + { + lua_pushinteger(state, millis); + lua_pushboolean(state, from_code); + lua_yield(state, 2); + } + + void LuaScript::Tick() + { + m_RunningScriptCallbacks = true; + std::erase_if(m_ScriptCallbacks, [this](ScriptCallback& callback) { + if (callback.m_TimeToResume && *callback.m_TimeToResume > std::chrono::high_resolution_clock::now()) + return false; + + lua_rawgeti(m_State, LUA_REGISTRYINDEX, callback.m_Coroutine); + lua_State* coro_state = lua_tothread(m_State, -1); + lua_pop(m_State, 1); + + auto state = ResumeCoroutine(0, 2, coro_state); + + if (state != LUA_YIELD) + { + RemoveScriptCallback(callback); + return true; + } + + auto time = lua_tointeger(m_State, -2); + auto from_code = lua_toboolean(m_State, -1); + lua_pop(m_State, 2); + + if (time == 0) + { + callback.m_TimeToResume = std::nullopt; + } + else + { + callback.m_TimeToResume = std::chrono::high_resolution_clock::now() + std::chrono::duration_cast(std::chrono::milliseconds(static_cast(time))); + } + callback.m_LastYieldFromCode = from_code; + + return false; + }); + m_RunningScriptCallbacks = false; + + std::ranges::move(m_QueuedScriptCallbacks, std::back_inserter(m_ScriptCallbacks)); + m_QueuedScriptCallbacks.clear(); + } } \ No newline at end of file diff --git a/src/core/scripting/LuaScript.hpp b/src/core/scripting/LuaScript.hpp index 04e4e86..6264b83 100644 --- a/src/core/scripting/LuaScript.hpp +++ b/src/core/scripting/LuaScript.hpp @@ -5,14 +5,42 @@ namespace YimMenu { class LuaScript { - lua_State* m_State; + public: + enum class LoadState + { + EMPTY, + RUNNING, + PAUSED, + WANT_UNLOAD, + WANT_RELOAD, + UNLOADED + }; + + private: + struct ScriptCallback + { + int m_Coroutine; + std::optional m_TimeToResume; + bool m_LastYieldFromCode; + PVOID m_Fiber; + PVOID m_ParentFiber; + lua_CFunction m_LatentTarget; + int m_LastReturnValue; + }; + + LoadState m_LoadState = LoadState::EMPTY; + lua_State* m_State = nullptr; std::string m_FileName; std::string m_ModuleName; - bool m_IsMalfunctioning; // when this is set to true, the script will be unloaded next tick - bool m_IsValid; + bool m_IsMalfunctioning = false; // when this is set to true, the script will be unloaded next tick + std::vector m_ScriptCallbacks; + std::vector m_QueuedScriptCallbacks; + bool m_RunningScriptCallbacks = false; // Calls the function at the top of stack. If this returns false the stack would have nothing on it - bool CallFunction(int n_args, int n_results); + bool CallFunction(int n_args, int n_results, lua_State* override_state = nullptr); + int ResumeCoroutine(int n_args, int n_results, lua_State* coro_state); + void RemoveScriptCallback(ScriptCallback& callback); public: LuaScript(std::string_view file_name); @@ -25,12 +53,12 @@ namespace YimMenu bool IsValid() const { - return m_IsValid; + return m_LoadState == LoadState::RUNNING; } - void Invalidate() + LoadState GetLoadState() const { - m_IsValid = false; + return m_LoadState; } void SetMalfunctioning() @@ -38,12 +66,57 @@ namespace YimMenu m_IsMalfunctioning = true; } + void Unload() + { + if (m_LoadState == LoadState::RUNNING) + m_LoadState = LoadState::WANT_UNLOAD; + } + + void Reload() + { + if (m_LoadState == LoadState::RUNNING) + m_LoadState = LoadState::WANT_RELOAD; + } + + void Pause() + { + if (m_LoadState == LoadState::RUNNING) + m_LoadState = LoadState::PAUSED; + } + + void Resume() + { + if (m_LoadState == LoadState::PAUSED) + m_LoadState = LoadState::RUNNING; + } + + // Should only be called by LuaManager::RunScriptImpl + void MarkUnloaded() + { + m_LoadState = LoadState::UNLOADED; + } + + bool SafeToUnload(); + std::string_view GetName() { return m_ModuleName; } + std::string_view GetPath() + { + return m_FileName; + } + // we're guaranteed to have a LuaScript for each lua_State, so we can return it as a reference static LuaScript& GetScript(lua_State* state); + + + void AddScriptCallback(int coro_handle); + + // must be called from a coroutine + void Yield(lua_State* state, int millis = 0, bool from_code = true); + + void Tick(); }; } \ No newline at end of file diff --git a/src/core/scripting/libraries/Script.cpp b/src/core/scripting/libraries/Script.cpp new file mode 100644 index 0000000..f26bb39 --- /dev/null +++ b/src/core/scripting/libraries/Script.cpp @@ -0,0 +1,71 @@ +#include "core/scripting/LuaLibrary.hpp" +#include "core/scripting/LuaScript.hpp" + +namespace YimMenu::Lua +{ + static bool IsCoroutine(lua_State* state) + { + bool result = lua_pushthread(state); + lua_pop(state, 1); + return !result; // this is dumb af. Why do you need to push a value to the stack to figure out if you're in the main thread? + } + + class Script : LuaLibrary + { + using LuaLibrary::LuaLibrary; + + static int RunInCallback(lua_State* state) + { + auto& script = LuaScript::GetScript(state); + + luaL_checktype(state, 1, LUA_TFUNCTION); // will throw error if a1 isn't a function. not sure what happens if you don't pass any parameters + + lua_State* coro_state = lua_newthread(state); + lua_pushvalue(state, 1); // xmove can only move from top of stack, so we have to push the function again even if it's already in the stack + lua_xmove(state, coro_state, 1); + + auto coro_handle = luaL_ref(state, LUA_REGISTRYINDEX); + script.AddScriptCallback(coro_handle); + + return 0; + } + + static int Yield(lua_State* state) + { + auto& script = LuaScript::GetScript(state); + + if (!IsCoroutine(state)) + { + luaL_error(state, "Attempting to yield outside a script callback"); + } + + auto time = lua_gettop(state) >= 1 ? (int)luaL_checkinteger(state, 1) : 0; + script.Yield(state, time, false); + return -1; + } + + static int IsInsideCallback(lua_State* state) + { + lua_pushboolean(state, IsCoroutine(state)); + return 1; + } + + virtual void Register(lua_State* state) override + { + lua_newtable(state); + + lua_pushcfunction(state, RunInCallback); + lua_setfield(state, -2, "run_in_callback"); + + lua_pushcfunction(state, Yield); + lua_setfield(state, -2, "yield"); + + lua_pushcfunction(state, IsInsideCallback); + lua_setfield(state, -2, "is_inside_callback"); + + lua_setglobal(state, "script"); + } + }; + + Script _Script; +} \ No newline at end of file diff --git a/src/game/frontend/submenus/Settings.cpp b/src/game/frontend/submenus/Settings.cpp index f30299c..50b97dd 100644 --- a/src/game/frontend/submenus/Settings.cpp +++ b/src/game/frontend/submenus/Settings.cpp @@ -6,6 +6,7 @@ #include "game/backend/Self.hpp" #include "game/frontend/items/Items.hpp" #include "game/frontend/items/DrawHotkey.hpp" +#include "game/frontend/submenus/Settings/LuaScripts.hpp" namespace YimMenu::Submenus { @@ -108,5 +109,6 @@ namespace YimMenu::Submenus AddCategory(std::move(hotkeys)); AddCategory(std::move(gui)); AddCategory(std::move(game)); + AddCategory(BuildLuaScriptsMenu()); } } \ No newline at end of file diff --git a/src/game/frontend/submenus/Settings/LuaScripts.cpp b/src/game/frontend/submenus/Settings/LuaScripts.cpp new file mode 100644 index 0000000..5a9b270 --- /dev/null +++ b/src/game/frontend/submenus/Settings/LuaScripts.cpp @@ -0,0 +1,85 @@ +#include "LuaScripts.hpp" +#include "core/backend/ScriptMgr.hpp" +#include "core/backend/FiberPool.hpp" +#include "core/scripting/LuaManager.hpp" +#include "core/frontend/widgets/imgui_colors.h" + +namespace YimMenu::Submenus +{ + std::shared_ptr BuildLuaScriptsMenu() + { + auto menu = std::make_shared("Lua Scripts"); + + static std::shared_ptr selectedScript; + + menu->AddItem(std::make_unique([] { + const float height = 15 * ImGui::GetTextLineHeightWithSpacing(); + if (ImGui::BeginListBox("##vehicles", {300.f, height})) + { + static std::optional loadingScript; + + LuaManager::ForAllLoadedScripts([](std::shared_ptr& script) { + if (ImGui::Selectable(script->GetName().data(), script == selectedScript)) + { + selectedScript = script; + } + }); + + LuaManager::ForAllUnloadedScripts([](LuaManager::UnloadedScript& script) { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::Colors::Gray.Value); + if (ImGui::Selectable(script.m_Name.data(), false)) + { + loadingScript = script.m_Path; // can't load the script directly from this callback or it'll deadlock + } + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("Click to load this script"); + } + }); + + if (loadingScript) + { + LuaManager::LoadScript(*loadingScript); + loadingScript = std::nullopt; + } + + ImGui::EndListBox(); + } + + ImGui::SameLine(); + + ImGui::BeginGroup(); + if (selectedScript && selectedScript->GetLoadState() == LuaScript::LoadState::UNLOADED) + selectedScript.reset(); + + if (selectedScript) + { + ImGui::Text("%s", selectedScript->GetName().data()); + + bool paused = selectedScript->GetLoadState() == LuaScript::LoadState::PAUSED; + if (ImGui::Button(paused ? "Pause" : "Resume")) + { + if (paused) + selectedScript->Resume(); + else + selectedScript->Pause(); + } + ImGui::SameLine(); + if (ImGui::Button("Reload")) + { + selectedScript->Reload(); + } + ImGui::SameLine(); + if (ImGui::Button("Unload")) + { + selectedScript->Unload(); + } + } + ImGui::EndGroup(); + })); + + return menu; + } +} \ No newline at end of file diff --git a/src/game/frontend/submenus/Settings/LuaScripts.hpp b/src/game/frontend/submenus/Settings/LuaScripts.hpp new file mode 100644 index 0000000..9aaa77f --- /dev/null +++ b/src/game/frontend/submenus/Settings/LuaScripts.hpp @@ -0,0 +1,8 @@ +#pragma once +#include "core/frontend/manager/Category.hpp" +#include "game/frontend/items/Items.hpp" + +namespace YimMenu::Submenus +{ + std::shared_ptr BuildLuaScriptsMenu(); +} \ No newline at end of file