feat(lua): improve prototype

This commit is contained in:
maybegreat48 committed 2025-06-26 00:16:10 -04:00
1 parent 52411d2ebe
commit b3d741ca51
10 files changed
+520 -29

No files matched your search

+5 -1
View File
@@ -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<std::chrono::high_resolution_clock::duration> time)
{
if (auto script = static_cast<Script*>(GetFiberData()))
// TODO: this wouldn't work!!! fix ASAP!
if (auto script = LuaManager::GetRunningCoroutine())
LuaScript::GetScript(script).Yield(script, time ? std::chrono::duration_cast<std::chrono::milliseconds>(*time).count() : 0);
else if (auto script = static_cast<Script*>(GetFiberData()))
script->Yield(time);
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include "LuaScript.hpp"
namespace YimMenu::Lua
{
int LatentFunction(lua_State* lua)
{
}
}
+83 -11
View File
@@ -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<LuaScript>(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);
}
}
+39
View File
@@ -12,18 +12,36 @@ namespace YimMenu
{
};
struct UnloadedScript
{
std::string m_Name;
std::string m_Path;
};
using ForAllLoadedScriptsCallback = void(*)(std::shared_ptr<LuaScript>& script);
using ForAllUnloadedScriptsCallback = void(*)(UnloadedScript& script);
private:
std::vector<std::shared_ptr<LuaScript>> m_LoadedScripts;
std::vector<UnloadedScript> m_UnloadedScripts;
std::chrono::system_clock::time_point m_LastRefreshedUnloadedScripts;
std::vector<LuaLibrary*> m_Libraries;
std::queue<std::string> 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);
}
};
}
+137 -10
View File
@@ -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::high_resolution_clock::duration>(std::chrono::milliseconds(static_cast<std::uint64_t>(time)));
}
callback.m_LastYieldFromCode = from_code;
return false;
});
m_RunningScriptCallbacks = false;
std::ranges::move(m_QueuedScriptCallbacks, std::back_inserter(m_ScriptCallbacks));
m_QueuedScriptCallbacks.clear();
}
}
+80 -7
View File
@@ -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<std::chrono::high_resolution_clock::time_point> 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<ScriptCallback> m_ScriptCallbacks;
std::vector<ScriptCallback> 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();
};
}
+71
View File
@@ -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;
}
+2
View File
@@ -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());
}
}
@@ -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<Category> BuildLuaScriptsMenu()
{
auto menu = std::make_shared<Category>("Lua Scripts");
static std::shared_ptr<LuaScript> selectedScript;
menu->AddItem(std::make_unique<ImGuiItem>([] {
const float height = 15 * ImGui::GetTextLineHeightWithSpacing();
if (ImGui::BeginListBox("##vehicles", {300.f, height}))
{
static std::optional<std::string> loadingScript;
LuaManager::ForAllLoadedScripts([](std::shared_ptr<LuaScript>& 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;
}
}
@@ -0,0 +1,8 @@
#pragma once
#include "core/frontend/manager/Category.hpp"
#include "game/frontend/items/Items.hpp"
namespace YimMenu::Submenus
{
std::shared_ptr<Category> BuildLuaScriptsMenu();
}