feat: add WoW event system for Lua addons

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.
This commit is contained in:
Kelsi 2026-03-20 11:23:38 -07:00
parent 7da1f6f5ca
commit 510f03fa32
6 changed files with 148 additions and 14 deletions

View file

@ -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)

View file

@ -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<std::string>& args = {});
void shutdown();
const std::vector<TocFile>& getAddons() const { return addons_; }

View file

@ -1,6 +1,7 @@
#pragma once
#include <string>
#include <vector>
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<std::string>& 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

View file

@ -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<std::string>& args) {
luaEngine_.fireEvent(event, args);
}
void AddonManager::shutdown() {
addons_.clear();
luaEngine_.shutdown();

View file

@ -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<int>(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<int>(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<std::string>& 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<int>(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<int>(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;

View file

@ -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");
}
}