From 510f03fa323b4d45c4dcb4d41a1998e807f38a67 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 20 Mar 2026 11:23:38 -0700 Subject: [PATCH] feat: add WoW event system for Lua addons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the WoW-compatible event system that lets addons react to gameplay events in real-time: - RegisterEvent(eventName, handler) — register a Lua function for an event - UnregisterEvent(eventName, handler) — remove a handler - fireEvent() dispatches events to all registered handlers with args Currently fired events: - PLAYER_ENTERING_WORLD — after addons load and world entry completes - PLAYER_LEAVING_WORLD — before logout/disconnect Events are stored in a __WoweeEvents Lua table, dispatched via LuaEngine::fireEvent() which is called from AddonManager::fireEvent(). Error handling logs Lua errors without crashing. Updated HelloWorld addon to use RegisterEvent for world entry/exit. --- .../AddOns/HelloWorld/HelloWorld.lua | 34 +++--- include/addons/addon_manager.hpp | 1 + include/addons/lua_engine.hpp | 7 ++ src/addons/addon_manager.cpp | 4 + src/addons/lua_engine.cpp | 112 ++++++++++++++++++ src/core/application.cpp | 4 + 6 files changed, 148 insertions(+), 14 deletions(-) diff --git a/Data/interface/AddOns/HelloWorld/HelloWorld.lua b/Data/interface/AddOns/HelloWorld/HelloWorld.lua index 689cfec3..fe253bfc 100644 --- a/Data/interface/AddOns/HelloWorld/HelloWorld.lua +++ b/Data/interface/AddOns/HelloWorld/HelloWorld.lua @@ -1,18 +1,24 @@ -- HelloWorld addon — test the WoWee addon system print("|cff00ff00[HelloWorld]|r Addon loaded! Lua 5.1 is working.") -print("|cff00ff00[HelloWorld]|r GetTime() = " .. string.format("%.2f", GetTime()) .. " seconds") --- Query player info (will show real data when called after world entry) -local name = UnitName("player") -local level = UnitLevel("player") -local health = UnitHealth("player") -local maxHealth = UnitHealthMax("player") -local gold = math.floor(GetMoney() / 10000) +-- Register for game events +RegisterEvent("PLAYER_ENTERING_WORLD", function(event) + local name = UnitName("player") + local level = UnitLevel("player") + local health = UnitHealth("player") + local maxHealth = UnitHealthMax("player") + local _, _, classId = UnitClass("player") + local gold = math.floor(GetMoney() / 10000) -print("|cff00ff00[HelloWorld]|r Player: " .. name .. " (Level " .. level .. ")") -if maxHealth > 0 then - print("|cff00ff00[HelloWorld]|r Health: " .. health .. "/" .. maxHealth) -end -if gold > 0 then - print("|cff00ff00[HelloWorld]|r Gold: " .. gold .. "g") -end + print("|cff00ff00[HelloWorld]|r Welcome, " .. name .. "! (Level " .. level .. ")") + if maxHealth > 0 then + print("|cff00ff00[HelloWorld]|r Health: " .. health .. "/" .. maxHealth) + end + if gold > 0 then + print("|cff00ff00[HelloWorld]|r Gold: " .. gold .. "g") + end +end) + +RegisterEvent("PLAYER_LEAVING_WORLD", function(event) + print("|cff00ff00[HelloWorld]|r Goodbye!") +end) diff --git a/include/addons/addon_manager.hpp b/include/addons/addon_manager.hpp index 64831a93..c0f019c8 100644 --- a/include/addons/addon_manager.hpp +++ b/include/addons/addon_manager.hpp @@ -17,6 +17,7 @@ public: void scanAddons(const std::string& addonsPath); void loadAllAddons(); bool runScript(const std::string& code); + void fireEvent(const std::string& event, const std::vector& args = {}); void shutdown(); const std::vector& getAddons() const { return addons_; } diff --git a/include/addons/lua_engine.hpp b/include/addons/lua_engine.hpp index b886acc4..bbd814af 100644 --- a/include/addons/lua_engine.hpp +++ b/include/addons/lua_engine.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include struct lua_State; @@ -24,6 +25,11 @@ public: void setGameHandler(game::GameHandler* handler); + // Fire a WoW event to all registered Lua handlers. + // Extra string args are pushed as event arguments. + void fireEvent(const std::string& eventName, + const std::vector& args = {}); + lua_State* getState() { return L_; } bool isInitialized() const { return L_ != nullptr; } @@ -32,6 +38,7 @@ private: game::GameHandler* gameHandler_ = nullptr; void registerCoreAPI(); + void registerEventAPI(); }; } // namespace wowee::addons diff --git a/src/addons/addon_manager.cpp b/src/addons/addon_manager.cpp index 4a62b71b..289ae15a 100644 --- a/src/addons/addon_manager.cpp +++ b/src/addons/addon_manager.cpp @@ -85,6 +85,10 @@ bool AddonManager::runScript(const std::string& code) { return luaEngine_.executeString(code); } +void AddonManager::fireEvent(const std::string& event, const std::vector& args) { + luaEngine_.fireEvent(event, args); +} + void AddonManager::shutdown() { addons_.clear(); luaEngine_.shutdown(); diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index b1c1874e..cfabe0aa 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -243,6 +243,7 @@ bool LuaEngine::initialize() { } registerCoreAPI(); + registerEventAPI(); LOG_INFO("LuaEngine: initialized (Lua 5.1)"); return true; @@ -298,6 +299,117 @@ void LuaEngine::registerCoreAPI() { } } +// ---- Event System ---- +// Lua-side: WoweeEvents table holds { ["EVENT_NAME"] = { handler1, handler2, ... } } +// RegisterEvent("EVENT", handler) adds a handler function +// UnregisterEvent("EVENT", handler) removes it + +static int lua_RegisterEvent(lua_State* L) { + const char* eventName = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + + // Get or create the WoweeEvents table + lua_getglobal(L, "__WoweeEvents"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setglobal(L, "__WoweeEvents"); + } + + // Get or create the handler list for this event + lua_getfield(L, -1, eventName); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setfield(L, -3, eventName); + } + + // Append the handler function to the list + int len = static_cast(lua_objlen(L, -1)); + lua_pushvalue(L, 2); // push the handler function + lua_rawseti(L, -2, len + 1); + + lua_pop(L, 2); // pop handler list + WoweeEvents + return 0; +} + +static int lua_UnregisterEvent(lua_State* L) { + const char* eventName = luaL_checkstring(L, 1); + luaL_checktype(L, 2, LUA_TFUNCTION); + + lua_getglobal(L, "__WoweeEvents"); + if (lua_isnil(L, -1)) { lua_pop(L, 1); return 0; } + + lua_getfield(L, -1, eventName); + if (lua_isnil(L, -1)) { lua_pop(L, 2); return 0; } + + // Remove matching handler from the list + int len = static_cast(lua_objlen(L, -1)); + for (int i = 1; i <= len; i++) { + lua_rawgeti(L, -1, i); + if (lua_rawequal(L, -1, 2)) { + lua_pop(L, 1); + // Shift remaining elements down + for (int j = i; j < len; j++) { + lua_rawgeti(L, -1, j + 1); + lua_rawseti(L, -2, j); + } + lua_pushnil(L); + lua_rawseti(L, -2, len); + break; + } + lua_pop(L, 1); + } + lua_pop(L, 2); + return 0; +} + +void LuaEngine::registerEventAPI() { + lua_pushcfunction(L_, lua_RegisterEvent); + lua_setglobal(L_, "RegisterEvent"); + + lua_pushcfunction(L_, lua_UnregisterEvent); + lua_setglobal(L_, "UnregisterEvent"); + + // Create the events table + lua_newtable(L_); + lua_setglobal(L_, "__WoweeEvents"); +} + +void LuaEngine::fireEvent(const std::string& eventName, + const std::vector& args) { + if (!L_) return; + + lua_getglobal(L_, "__WoweeEvents"); + if (lua_isnil(L_, -1)) { lua_pop(L_, 1); return; } + + lua_getfield(L_, -1, eventName.c_str()); + if (lua_isnil(L_, -1)) { lua_pop(L_, 2); return; } + + int handlerCount = static_cast(lua_objlen(L_, -1)); + for (int i = 1; i <= handlerCount; i++) { + lua_rawgeti(L_, -1, i); + if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1); continue; } + + // Push arguments: event name first, then extra args + lua_pushstring(L_, eventName.c_str()); + for (const auto& arg : args) { + lua_pushstring(L_, arg.c_str()); + } + + int nargs = 1 + static_cast(args.size()); + if (lua_pcall(L_, nargs, 0, 0) != 0) { + const char* err = lua_tostring(L_, -1); + LOG_ERROR("LuaEngine: event '", eventName, "' handler error: ", + err ? err : "(unknown)"); + lua_pop(L_, 1); + } + } + lua_pop(L_, 2); // pop handler list + WoweeEvents +} + bool LuaEngine::executeFile(const std::string& path) { if (!L_) return false; diff --git a/src/core/application.cpp b/src/core/application.cpp index 64947c01..96a0d570 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -660,6 +660,9 @@ void Application::setState(AppState newState) { } // Ensure no stale in-world player model leaks into the next login attempt. // If we reuse a previously spawned instance without forcing a respawn, appearance (notably hair) can desync. + if (addonManager_ && addonsLoaded_) { + addonManager_->fireEvent("PLAYER_LEAVING_WORLD"); + } npcsSpawned = false; playerCharacterSpawned = false; addonsLoaded_ = false; @@ -5049,6 +5052,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float if (addonManager_ && !addonsLoaded_) { addonManager_->loadAllAddons(); addonsLoaded_ = true; + addonManager_->fireEvent("PLAYER_ENTERING_WORLD"); } }