#include "addons/lua_engine.hpp" #include "addons/lua_api_helpers.hpp" #include "addons/lua_api_registrations.hpp" #include "addons/toc_parser.hpp" #include "core/window.hpp" #include #include #include extern "C" { #include #include #include } namespace wowee::addons { static int lua_wow_print(lua_State* L) { int nargs = lua_gettop(L); std::string result; for (int i = 1; i <= nargs; i++) { if (i > 1) result += '\t'; // Lua 5.1: use lua_tostring (luaL_tolstring is 5.3+) if (lua_isstring(L, i) || lua_isnumber(L, i)) { const char* s = lua_tostring(L, i); if (s) result += s; } else if (lua_isboolean(L, i)) { result += lua_toboolean(L, i) ? "true" : "false"; } else if (lua_isnil(L, i)) { result += "nil"; } else { result += lua_typename(L, lua_type(L, i)); } } auto* gh = getGameHandler(L); if (gh) { game::MessageChatData msg; msg.type = game::ChatType::SYSTEM; msg.language = game::ChatLanguage::UNIVERSAL; msg.message = result; gh->addLocalChatMessage(msg); } LOG_INFO("[Lua] ", result); return 0; } // WoW-compatible message() — same as print for now static int lua_wow_message(lua_State* L) { return lua_wow_print(L); } // Helper: resolve WoW unit IDs to GUID // Read UNIT_FIELD_TARGET_LO/HI from an entity's update fields to get what it's targeting // --- Frame system functions --- static int lua_Frame_RegisterEvent(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); // self const char* eventName = luaL_checkstring(L, 2); // Get frame's registered events table (create if needed) lua_getfield(L, 1, "__events"); if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_newtable(L); lua_pushvalue(L, -1); lua_setfield(L, 1, "__events"); } lua_pushboolean(L, 1); lua_setfield(L, -2, eventName); lua_pop(L, 1); // Also register in global __WoweeFrameEvents for dispatch lua_getglobal(L, "__WoweeFrameEvents"); if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_newtable(L); lua_pushvalue(L, -1); lua_setglobal(L, "__WoweeFrameEvents"); } 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 frame reference int len = static_cast(lua_objlen(L, -1)); lua_pushvalue(L, 1); // push frame lua_rawseti(L, -2, len + 1); lua_pop(L, 2); // pop list + __WoweeFrameEvents return 0; } // Frame method: frame:UnregisterEvent("EVENT") static int lua_Frame_UnregisterEvent(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); const char* eventName = luaL_checkstring(L, 2); // Remove from frame's own events lua_getfield(L, 1, "__events"); if (lua_istable(L, -1)) { lua_pushnil(L); lua_setfield(L, -2, eventName); } lua_pop(L, 1); return 0; } // Frame method: frame:SetScript("handler", func) static int lua_Frame_SetScript(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); const char* scriptType = luaL_checkstring(L, 2); // arg 3 can be function or nil lua_getfield(L, 1, "__scripts"); if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_newtable(L); lua_pushvalue(L, -1); lua_setfield(L, 1, "__scripts"); } lua_pushvalue(L, 3); lua_setfield(L, -2, scriptType); lua_pop(L, 1); // Track frames with OnUpdate in __WoweeOnUpdateFrames if (strcmp(scriptType, "OnUpdate") == 0) { lua_getglobal(L, "__WoweeOnUpdateFrames"); if (!lua_istable(L, -1)) { lua_pop(L, 1); return 0; } if (lua_isfunction(L, 3)) { // Add frame to the list int len = static_cast(lua_objlen(L, -1)); lua_pushvalue(L, 1); lua_rawseti(L, -2, len + 1); } lua_pop(L, 1); } return 0; } // Frame method: frame:GetScript("handler") static int lua_Frame_GetScript(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); const char* scriptType = luaL_checkstring(L, 2); lua_getfield(L, 1, "__scripts"); if (lua_istable(L, -1)) { lua_getfield(L, -1, scriptType); } else { lua_pushnil(L); } return 1; } // Frame method: frame:GetName() static int lua_Frame_GetName(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, 1, "__name"); return 1; } // Frame method: frame:Show() / frame:Hide() / frame:IsShown() / frame:IsVisible() static int lua_Frame_Show(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_pushboolean(L, 1); lua_setfield(L, 1, "__visible"); return 0; } static int lua_Frame_Hide(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_pushboolean(L, 0); lua_setfield(L, 1, "__visible"); return 0; } static int lua_Frame_IsShown(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, 1, "__visible"); lua_pushboolean(L, lua_toboolean(L, -1)); return 1; } // Frame method: frame:CreateTexture(name, layer) → texture stub static int lua_Frame_CreateTexture(lua_State* L) { lua_newtable(L); // Add noop methods for common texture operations luaL_dostring(L, "return function(t) " "function t:SetTexture() end " "function t:SetTexCoord() end " "function t:SetVertexColor() end " "function t:SetAllPoints() end " "function t:SetPoint() end " "function t:SetSize() end " "function t:SetWidth() end " "function t:SetHeight() end " "function t:Show() end " "function t:Hide() end " "function t:SetAlpha() end " "function t:GetTexture() return '' end " "function t:SetDesaturated() end " "function t:SetBlendMode() end " "function t:SetDrawLayer() end " "end"); lua_pushvalue(L, -2); // push the table lua_call(L, 1, 0); // call the function with the table return 1; } // Frame method: frame:CreateFontString(name, layer, template) → fontstring stub static int lua_Frame_CreateFontString(lua_State* L) { lua_newtable(L); luaL_dostring(L, "return function(fs) " "fs._text = '' " "function fs:SetText(t) self._text = t or '' end " "function fs:GetText() return self._text end " "function fs:SetFont() end " "function fs:SetFontObject() end " "function fs:SetTextColor() end " "function fs:SetJustifyH() end " "function fs:SetJustifyV() end " "function fs:SetPoint() end " "function fs:SetAllPoints() end " "function fs:Show() end " "function fs:Hide() end " "function fs:SetAlpha() end " "function fs:GetStringWidth() return 0 end " "function fs:GetStringHeight() return 0 end " "function fs:SetWordWrap() end " "function fs:SetNonSpaceWrap() end " "function fs:SetMaxLines() end " "function fs:SetShadowOffset() end " "function fs:SetShadowColor() end " "function fs:SetWidth() end " "function fs:SetHeight() end " "end"); lua_pushvalue(L, -2); lua_call(L, 1, 0); return 1; } // GetFramerate() → fps static int lua_GetFramerate(lua_State* L) { lua_pushnumber(L, static_cast(ImGui::GetIO().Framerate)); return 1; } // GetCursorPosition() → x, y (screen coordinates, origin top-left) static int lua_GetCursorPosition(lua_State* L) { const auto& io = ImGui::GetIO(); lua_pushnumber(L, io.MousePos.x); lua_pushnumber(L, io.MousePos.y); return 2; } // GetScreenWidth() → width static int lua_GetScreenWidth(lua_State* L) { auto* svc = getLuaServices(L); auto* window = svc ? svc->window : nullptr; lua_pushnumber(L, window ? window->getWidth() : 1920); return 1; } // GetScreenHeight() → height static int lua_GetScreenHeight(lua_State* L) { auto* svc = getLuaServices(L); auto* window = svc ? svc->window : nullptr; lua_pushnumber(L, window ? window->getHeight() : 1080); return 1; } // Modifier key state queries using ImGui IO static int lua_Frame_SetPoint(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); const char* point = luaL_optstring(L, 2, "CENTER"); // Store point info in frame table lua_pushstring(L, point); lua_setfield(L, 1, "__point"); // Optional x/y offsets (args 4,5 if relativeTo is given, or 3,4 if not) double xOfs = 0, yOfs = 0; if (lua_isnumber(L, 4)) { xOfs = lua_tonumber(L, 4); yOfs = lua_tonumber(L, 5); } else if (lua_isnumber(L, 3)) { xOfs = lua_tonumber(L, 3); yOfs = lua_tonumber(L, 4); } lua_pushnumber(L, xOfs); lua_setfield(L, 1, "__xOfs"); lua_pushnumber(L, yOfs); lua_setfield(L, 1, "__yOfs"); return 0; } static int lua_Frame_SetSize(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); double w = luaL_optnumber(L, 2, 0); double h = luaL_optnumber(L, 3, 0); lua_pushnumber(L, w); lua_setfield(L, 1, "__width"); lua_pushnumber(L, h); lua_setfield(L, 1, "__height"); return 0; } static int lua_Frame_SetWidth(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_pushnumber(L, luaL_checknumber(L, 2)); lua_setfield(L, 1, "__width"); return 0; } static int lua_Frame_SetHeight(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_pushnumber(L, luaL_checknumber(L, 2)); lua_setfield(L, 1, "__height"); return 0; } static int lua_Frame_GetWidth(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, 1, "__width"); if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, 0); } return 1; } static int lua_Frame_GetHeight(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, 1, "__height"); if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, 0); } return 1; } static int lua_Frame_GetCenter(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, 1, "__xOfs"); double x = lua_isnumber(L, -1) ? lua_tonumber(L, -1) : 0; lua_pop(L, 1); lua_getfield(L, 1, "__yOfs"); double y = lua_isnumber(L, -1) ? lua_tonumber(L, -1) : 0; lua_pop(L, 1); lua_pushnumber(L, x); lua_pushnumber(L, y); return 2; } static int lua_Frame_SetAlpha(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_pushnumber(L, luaL_checknumber(L, 2)); lua_setfield(L, 1, "__alpha"); return 0; } static int lua_Frame_GetAlpha(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, 1, "__alpha"); if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, 1.0); } return 1; } static int lua_Frame_SetParent(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); if (lua_istable(L, 2) || lua_isnil(L, 2)) { lua_pushvalue(L, 2); lua_setfield(L, 1, "__parent"); } return 0; } static int lua_Frame_GetParent(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, 1, "__parent"); return 1; } // CreateFrame(frameType, name, parent, template) static int lua_CreateFrame(lua_State* L) { const char* frameType = luaL_optstring(L, 1, "Frame"); const char* name = luaL_optstring(L, 2, nullptr); (void)frameType; // All frame types use the same table structure for now // Create the frame table lua_newtable(L); // Set frame name if (name && *name) { lua_pushstring(L, name); lua_setfield(L, -2, "__name"); // Also set as a global so other addons can find it by name lua_pushvalue(L, -1); lua_setglobal(L, name); } // Set initial visibility lua_pushboolean(L, 1); lua_setfield(L, -2, "__visible"); // Apply frame metatable with methods lua_getglobal(L, "__WoweeFrameMT"); lua_setmetatable(L, -2); return 1; } // --- WoW Utility Functions --- // strsplit(delimiter, str) — WoW's string split LuaEngine::LuaEngine() = default; LuaEngine::~LuaEngine() { shutdown(); } bool LuaEngine::initialize() { if (L_) return true; L_ = luaL_newstate(); if (!L_) { LOG_ERROR("LuaEngine: failed to create Lua state"); return false; } // Open safe standard libraries (no io, os, debug, package) luaopen_base(L_); luaopen_table(L_); luaopen_string(L_); luaopen_math(L_); // Remove unsafe globals from base library const char* unsafeGlobals[] = { "dofile", "loadfile", "load", "collectgarbage", "newproxy", nullptr }; for (const char** g = unsafeGlobals; *g; ++g) { lua_pushnil(L_); lua_setglobal(L_, *g); } registerCoreAPI(); registerEventAPI(); LOG_INFO("LuaEngine: initialized (Lua 5.1)"); return true; } void LuaEngine::shutdown() { if (L_) { lua_close(L_); L_ = nullptr; LOG_INFO("LuaEngine: shut down"); } } void LuaEngine::setGameHandler(game::GameHandler* handler) { gameHandler_ = handler; if (L_) { lua_pushlightuserdata(L_, handler); lua_setfield(L_, LUA_REGISTRYINDEX, "wowee_game_handler"); } } void LuaEngine::setLuaServices(const LuaServices& services) { luaServices_ = services; if (L_) { lua_pushlightuserdata(L_, &luaServices_); lua_setfield(L_, LUA_REGISTRYINDEX, "wowee_lua_services"); } } void LuaEngine::registerCoreAPI() { // Override print() to go to chat lua_pushcfunction(L_, lua_wow_print); lua_setglobal(L_, "print"); // WoW API stubs lua_pushcfunction(L_, lua_wow_message); lua_setglobal(L_, "message"); // --- Per-domain Lua API registration --- registerUnitLuaAPI(L_); registerSpellLuaAPI(L_); registerInventoryLuaAPI(L_); registerQuestLuaAPI(L_); registerSocialLuaAPI(L_); registerSystemLuaAPI(L_); registerActionLuaAPI(L_); // WoW aliases lua_getglobal(L_, "string"); lua_getfield(L_, -1, "format"); lua_setglobal(L_, "format"); lua_pop(L_, 1); // pop string table // tinsert/tremove aliases lua_getglobal(L_, "table"); lua_getfield(L_, -1, "insert"); lua_setglobal(L_, "tinsert"); lua_getfield(L_, -1, "remove"); lua_setglobal(L_, "tremove"); lua_pop(L_, 1); // pop table // SlashCmdList table — addons register slash commands here lua_newtable(L_); lua_setglobal(L_, "SlashCmdList"); // Frame metatable with methods lua_newtable(L_); // metatable lua_pushvalue(L_, -1); lua_setfield(L_, -2, "__index"); // metatable.__index = metatable static const struct luaL_Reg frameMethods[] = { {"RegisterEvent", lua_Frame_RegisterEvent}, {"UnregisterEvent", lua_Frame_UnregisterEvent}, {"SetScript", lua_Frame_SetScript}, {"GetScript", lua_Frame_GetScript}, {"GetName", lua_Frame_GetName}, {"Show", lua_Frame_Show}, {"Hide", lua_Frame_Hide}, {"IsShown", lua_Frame_IsShown}, {"IsVisible", lua_Frame_IsShown}, // alias {"SetPoint", lua_Frame_SetPoint}, {"SetSize", lua_Frame_SetSize}, {"SetWidth", lua_Frame_SetWidth}, {"SetHeight", lua_Frame_SetHeight}, {"GetWidth", lua_Frame_GetWidth}, {"GetHeight", lua_Frame_GetHeight}, {"GetCenter", lua_Frame_GetCenter}, {"SetAlpha", lua_Frame_SetAlpha}, {"GetAlpha", lua_Frame_GetAlpha}, {"SetParent", lua_Frame_SetParent}, {"GetParent", lua_Frame_GetParent}, {"CreateTexture", lua_Frame_CreateTexture}, {"CreateFontString", lua_Frame_CreateFontString}, {nullptr, nullptr} }; for (const luaL_Reg* r = frameMethods; r->name; r++) { lua_pushcfunction(L_, r->func); lua_setfield(L_, -2, r->name); } lua_setglobal(L_, "__WoweeFrameMT"); // Add commonly called no-op frame methods to prevent addon errors luaL_dostring(L_, "local mt = __WoweeFrameMT\n" "function mt:SetFrameLevel(level) self.__frameLevel = level end\n" "function mt:GetFrameLevel() return self.__frameLevel or 1 end\n" "function mt:SetFrameStrata(strata) self.__strata = strata end\n" "function mt:GetFrameStrata() return self.__strata or 'MEDIUM' end\n" "function mt:EnableMouse(enable) end\n" "function mt:EnableMouseWheel(enable) end\n" "function mt:SetMovable(movable) end\n" "function mt:SetResizable(resizable) end\n" "function mt:RegisterForDrag(...) end\n" "function mt:SetClampedToScreen(clamped) end\n" "function mt:SetBackdrop(backdrop) end\n" "function mt:SetBackdropColor(...) end\n" "function mt:SetBackdropBorderColor(...) end\n" "function mt:ClearAllPoints() end\n" "function mt:SetID(id) self.__id = id end\n" "function mt:GetID() return self.__id or 0 end\n" "function mt:SetScale(scale) self.__scale = scale end\n" "function mt:GetScale() return self.__scale or 1.0 end\n" "function mt:GetEffectiveScale() return self.__scale or 1.0 end\n" "function mt:SetToplevel(top) end\n" "function mt:Raise() end\n" "function mt:Lower() end\n" "function mt:GetLeft() return 0 end\n" "function mt:GetRight() return 0 end\n" "function mt:GetTop() return 0 end\n" "function mt:GetBottom() return 0 end\n" "function mt:GetNumPoints() return 0 end\n" "function mt:GetPoint(n) return 'CENTER', nil, 'CENTER', 0, 0 end\n" "function mt:SetHitRectInsets(...) end\n" "function mt:RegisterForClicks(...) end\n" "function mt:SetAttribute(name, value) self['attr_'..name] = value end\n" "function mt:GetAttribute(name) return self['attr_'..name] end\n" "function mt:HookScript(scriptType, fn)\n" " local orig = self.__scripts and self.__scripts[scriptType]\n" " if orig then\n" " self:SetScript(scriptType, function(...) orig(...); fn(...) end)\n" " else\n" " self:SetScript(scriptType, fn)\n" " end\n" "end\n" "function mt:SetMinResize(...) end\n" "function mt:SetMaxResize(...) end\n" "function mt:StartMoving() end\n" "function mt:StopMovingOrSizing() end\n" "function mt:IsMouseOver() return false end\n" "function mt:GetObjectType() return 'Frame' end\n" ); // CreateFrame function lua_pushcfunction(L_, lua_CreateFrame); lua_setglobal(L_, "CreateFrame"); // Cursor/screen/FPS functions lua_pushcfunction(L_, lua_GetCursorPosition); lua_setglobal(L_, "GetCursorPosition"); lua_pushcfunction(L_, lua_GetScreenWidth); lua_setglobal(L_, "GetScreenWidth"); lua_pushcfunction(L_, lua_GetScreenHeight); lua_setglobal(L_, "GetScreenHeight"); lua_pushcfunction(L_, lua_GetFramerate); lua_setglobal(L_, "GetFramerate"); // Frame event dispatch table lua_newtable(L_); lua_setglobal(L_, "__WoweeFrameEvents"); // OnUpdate frame tracking table lua_newtable(L_); lua_setglobal(L_, "__WoweeOnUpdateFrames"); // C_Timer implementation via Lua (uses OnUpdate internally) luaL_dostring(L_, "C_Timer = {}\n" "local timers = {}\n" "local timerFrame = CreateFrame('Frame', '__WoweeTimerFrame')\n" "timerFrame:SetScript('OnUpdate', function(self, elapsed)\n" " local i = 1\n" " while i <= #timers do\n" " timers[i].remaining = timers[i].remaining - elapsed\n" " if timers[i].remaining <= 0 then\n" " local cb = timers[i].callback\n" " table.remove(timers, i)\n" " cb()\n" " else\n" " i = i + 1\n" " end\n" " end\n" " if #timers == 0 then self:Hide() end\n" "end)\n" "timerFrame:Hide()\n" "function C_Timer.After(seconds, callback)\n" " tinsert(timers, {remaining = seconds, callback = callback})\n" " timerFrame:Show()\n" "end\n" "function C_Timer.NewTicker(seconds, callback, iterations)\n" " local count = 0\n" " local maxIter = iterations or -1\n" " local ticker = {cancelled = false}\n" " local function tick()\n" " if ticker.cancelled then return end\n" " count = count + 1\n" " callback(ticker)\n" " if maxIter > 0 and count >= maxIter then return end\n" " C_Timer.After(seconds, tick)\n" " end\n" " C_Timer.After(seconds, tick)\n" " function ticker:Cancel() self.cancelled = true end\n" " return ticker\n" "end\n" ); // DEFAULT_CHAT_FRAME with AddMessage method (used by many addons) luaL_dostring(L_, "DEFAULT_CHAT_FRAME = {}\n" "function DEFAULT_CHAT_FRAME:AddMessage(text, r, g, b)\n" " if r and g and b then\n" " local hex = format('|cff%02x%02x%02x', " " math.floor(r*255), math.floor(g*255), math.floor(b*255))\n" " print(hex .. tostring(text) .. '|r')\n" " else\n" " print(tostring(text))\n" " end\n" "end\n" "ChatFrame1 = DEFAULT_CHAT_FRAME\n" ); // hooksecurefunc — hook a function to run additional code after it luaL_dostring(L_, "function hooksecurefunc(tblOrName, nameOrFunc, funcOrNil)\n" " local tbl, name, hook\n" " if type(tblOrName) == 'table' then\n" " tbl, name, hook = tblOrName, nameOrFunc, funcOrNil\n" " else\n" " tbl, name, hook = _G, tblOrName, nameOrFunc\n" " end\n" " local orig = tbl[name]\n" " if type(orig) ~= 'function' then return end\n" " tbl[name] = function(...)\n" " local r = {orig(...)}\n" " hook(...)\n" " return unpack(r)\n" " end\n" "end\n" ); // LibStub — universal library version management used by Ace3 and virtually all addon libs. // This is the standard WoW LibStub implementation that addons embed/expect globally. luaL_dostring(L_, "local LibStub = LibStub or {}\n" "LibStub.libs = LibStub.libs or {}\n" "LibStub.minors = LibStub.minors or {}\n" "function LibStub:NewLibrary(major, minor)\n" " assert(type(major) == 'string', 'LibStub:NewLibrary: bad argument #1 (string expected)')\n" " minor = assert(tonumber(minor or (type(minor) == 'string' and minor:match('(%d+)'))), 'LibStub:NewLibrary: bad argument #2 (number expected)')\n" " local oldMinor = self.minors[major]\n" " if oldMinor and oldMinor >= minor then return nil end\n" " local lib = self.libs[major] or {}\n" " self.libs[major] = lib\n" " self.minors[major] = minor\n" " return lib, oldMinor\n" "end\n" "function LibStub:GetLibrary(major, silent)\n" " if not self.libs[major] and not silent then\n" " error('Cannot find a library instance of \"' .. tostring(major) .. '\".')\n" " end\n" " return self.libs[major], self.minors[major]\n" "end\n" "function LibStub:IterateLibraries() return pairs(self.libs) end\n" "setmetatable(LibStub, { __call = LibStub.GetLibrary })\n" "_G['LibStub'] = LibStub\n" ); // CallbackHandler-1.0 — minimal implementation for Ace3-based addons luaL_dostring(L_, "if LibStub then\n" " local CBH = LibStub:NewLibrary('CallbackHandler-1.0', 7)\n" " if CBH then\n" " CBH.mixins = { 'RegisterCallback', 'UnregisterCallback', 'UnregisterAllCallbacks', 'Fire' }\n" " function CBH:New(target, regName, unregName, unregAllName, onUsed)\n" " local registry = setmetatable({}, { __index = CBH })\n" " registry.callbacks = {}\n" " target = target or {}\n" " target[regName or 'RegisterCallback'] = function(self, event, method, ...)\n" " if not registry.callbacks[event] then registry.callbacks[event] = {} end\n" " local handler = type(method) == 'function' and method or self[method]\n" " registry.callbacks[event][self] = handler\n" " end\n" " target[unregName or 'UnregisterCallback'] = function(self, event)\n" " if registry.callbacks[event] then registry.callbacks[event][self] = nil end\n" " end\n" " target[unregAllName or 'UnregisterAllCallbacks'] = function(self)\n" " for event, handlers in pairs(registry.callbacks) do handlers[self] = nil end\n" " end\n" " registry.Fire = function(self, event, ...)\n" " if not self.callbacks[event] then return end\n" " for obj, handler in pairs(self.callbacks[event]) do\n" " handler(obj, event, ...)\n" " end\n" " end\n" " return registry\n" " end\n" " end\n" "end\n" ); // Noop stubs for commonly called functions that don't need implementation luaL_dostring(L_, "function SetDesaturation() end\n" "function SetPortraitTexture() end\n" "function StopSound() end\n" "function UIParent_OnEvent() end\n" "UIParent = CreateFrame('Frame', 'UIParent')\n" "UIPanelWindows = {}\n" "WorldFrame = CreateFrame('Frame', 'WorldFrame')\n" // GameTooltip: global tooltip frame used by virtually all addons "GameTooltip = CreateFrame('Frame', 'GameTooltip')\n" "GameTooltip.__lines = {}\n" "function GameTooltip:SetOwner(owner, anchor) self.__owner = owner; self.__anchor = anchor end\n" "function GameTooltip:ClearLines() self.__lines = {} end\n" "function GameTooltip:AddLine(text, r, g, b, wrap) table.insert(self.__lines, {text=text or '',r=r,g=g,b=b}) end\n" "function GameTooltip:AddDoubleLine(l, r, lr, lg, lb, rr, rg, rb) table.insert(self.__lines, {text=(l or '')..' '..(r or '')}) end\n" "function GameTooltip:SetText(text, r, g, b) self.__lines = {{text=text or '',r=r,g=g,b=b}} end\n" "function GameTooltip:GetItem()\n" " if self.__itemId and self.__itemId > 0 then\n" " local name = GetItemInfo(self.__itemId)\n" " local _, itemLink = GetItemInfo(self.__itemId)\n" " return name, itemLink or ('|cffffffff|Hitem:'..self.__itemId..':0|h['..tostring(name)..']|h|r')\n" " end\n" " return nil\n" "end\n" "function GameTooltip:GetSpell()\n" " if self.__spellId and self.__spellId > 0 then\n" " local name = GetSpellInfo(self.__spellId)\n" " return name, nil, self.__spellId\n" " end\n" " return nil\n" "end\n" "function GameTooltip:GetUnit() return nil end\n" "function GameTooltip:NumLines() return #self.__lines end\n" "function GameTooltip:GetText() return self.__lines[1] and self.__lines[1].text or '' end\n" "function GameTooltip:SetUnitBuff(unit, index, filter)\n" " self:ClearLines()\n" " local name, rank, icon, count, debuffType, duration, expTime, caster, steal, consolidate, spellId = UnitBuff(unit, index, filter)\n" " if name then\n" " self:SetText(name, 1, 1, 1)\n" " if duration and duration > 0 then\n" " self:AddLine(string.format('%.0f sec remaining', expTime - GetTime()), 1, 1, 1)\n" " end\n" " self.__spellId = spellId\n" " end\n" "end\n" "function GameTooltip:SetUnitDebuff(unit, index, filter)\n" " self:ClearLines()\n" " local name, rank, icon, count, debuffType, duration, expTime, caster, steal, consolidate, spellId = UnitDebuff(unit, index, filter)\n" " if name then\n" " self:SetText(name, 1, 0, 0)\n" " if debuffType then self:AddLine(debuffType, 0.5, 0.5, 0.5) end\n" " self.__spellId = spellId\n" " end\n" "end\n" "function GameTooltip:SetHyperlink(link)\n" " self:ClearLines()\n" " if not link then return end\n" " local id = link:match('item:(%d+)')\n" " if id then\n" " _WoweePopulateItemTooltip(self, tonumber(id))\n" " return\n" " end\n" " id = link:match('spell:(%d+)')\n" " if id then\n" " self:SetSpellByID(tonumber(id))\n" " return\n" " end\n" "end\n" // Shared item tooltip builder using GetItemInfo return values "function _WoweePopulateItemTooltip(self, itemId)\n" " local name, itemLink, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture, sellPrice = GetItemInfo(itemId)\n" " if not name then return false end\n" " local qColors = {[0]={0.62,0.62,0.62},[1]={1,1,1},[2]={0.12,1,0},[3]={0,0.44,0.87},[4]={0.64,0.21,0.93},[5]={1,0.5,0},[6]={0.9,0.8,0.5},[7]={0,0.8,1}}\n" " local c = qColors[quality or 1] or {1,1,1}\n" " self:SetText(name, c[1], c[2], c[3])\n" " -- Item level for equipment\n" " if equipSlot and equipSlot ~= '' and iLevel and iLevel > 0 then\n" " self:AddLine('Item Level '..iLevel, 1, 0.82, 0)\n" " end\n" " -- Equip slot and subclass on same line\n" " if equipSlot and equipSlot ~= '' then\n" " local slotNames = {INVTYPE_HEAD='Head',INVTYPE_NECK='Neck',INVTYPE_SHOULDER='Shoulder',\n" " INVTYPE_CHEST='Chest',INVTYPE_WAIST='Waist',INVTYPE_LEGS='Legs',INVTYPE_FEET='Feet',\n" " INVTYPE_WRIST='Wrist',INVTYPE_HAND='Hands',INVTYPE_FINGER='Finger',\n" " INVTYPE_TRINKET='Trinket',INVTYPE_CLOAK='Back',INVTYPE_WEAPON='One-Hand',\n" " INVTYPE_SHIELD='Off Hand',INVTYPE_2HWEAPON='Two-Hand',INVTYPE_RANGED='Ranged',\n" " INVTYPE_WEAPONMAINHAND='Main Hand',INVTYPE_WEAPONOFFHAND='Off Hand',\n" " INVTYPE_HOLDABLE='Held In Off-Hand',INVTYPE_TABARD='Tabard',INVTYPE_ROBE='Chest'}\n" " local slotText = slotNames[equipSlot] or ''\n" " local subText = (subclass and subclass ~= '') and subclass or ''\n" " if slotText ~= '' or subText ~= '' then\n" " self:AddDoubleLine(slotText, subText, 1,1,1, 1,1,1)\n" " end\n" " elseif class and class ~= '' then\n" " self:AddLine(class, 1, 1, 1)\n" " end\n" " -- Fetch detailed stats from C side\n" " local data = _GetItemTooltipData(itemId)\n" " if data then\n" " -- Bind type\n" " if data.isHeroic then self:AddLine('Heroic', 0, 1, 0) end\n" " if data.isUnique then self:AddLine('Unique', 1, 1, 1)\n" " elseif data.isUniqueEquipped then self:AddLine('Unique-Equipped', 1, 1, 1) end\n" " if data.bindType == 1 then self:AddLine('Binds when picked up', 1, 1, 1)\n" " elseif data.bindType == 2 then self:AddLine('Binds when equipped', 1, 1, 1)\n" " elseif data.bindType == 3 then self:AddLine('Binds when used', 1, 1, 1) end\n" " -- Armor\n" " if data.armor and data.armor > 0 then\n" " self:AddLine(data.armor..' Armor', 1, 1, 1)\n" " end\n" " -- Weapon damage and speed\n" " if data.damageMin and data.damageMax and data.damageMin > 0 then\n" " local speed = (data.speed or 0) / 1000\n" " if speed > 0 then\n" " self:AddDoubleLine(string.format('%.0f - %.0f Damage', data.damageMin, data.damageMax), string.format('Speed %.2f', speed), 1,1,1, 1,1,1)\n" " local dps = (data.damageMin + data.damageMax) / 2 / speed\n" " self:AddLine(string.format('(%.1f damage per second)', dps), 1, 1, 1)\n" " end\n" " end\n" " -- Stats\n" " if data.stamina then self:AddLine('+'..data.stamina..' Stamina', 0, 1, 0) end\n" " if data.strength then self:AddLine('+'..data.strength..' Strength', 0, 1, 0) end\n" " if data.agility then self:AddLine('+'..data.agility..' Agility', 0, 1, 0) end\n" " if data.intellect then self:AddLine('+'..data.intellect..' Intellect', 0, 1, 0) end\n" " if data.spirit then self:AddLine('+'..data.spirit..' Spirit', 0, 1, 0) end\n" " -- Extra stats (hit, crit, haste, AP, SP, etc.)\n" " if data.extraStats then\n" " local statNames = {[3]='Agility',[4]='Strength',[5]='Intellect',[6]='Spirit',[7]='Stamina',\n" " [12]='Defense Rating',[13]='Dodge Rating',[14]='Parry Rating',[15]='Block Rating',\n" " [16]='Melee Hit Rating',[17]='Ranged Hit Rating',[18]='Spell Hit Rating',\n" " [19]='Melee Crit Rating',[20]='Ranged Crit Rating',[21]='Spell Crit Rating',\n" " [28]='Melee Haste Rating',[29]='Ranged Haste Rating',[30]='Spell Haste Rating',\n" " [31]='Hit Rating',[32]='Crit Rating',[36]='Haste Rating',\n" " [33]='Resilience Rating',[34]='Attack Power',[35]='Spell Power',\n" " [37]='Expertise Rating',[38]='Attack Power',[39]='Ranged Attack Power',\n" " [43]='Mana per 5 sec.',[44]='Armor Penetration Rating',\n" " [45]='Spell Power',[46]='Health per 5 sec.',[47]='Spell Penetration'}\n" " for _, stat in ipairs(data.extraStats) do\n" " local name = statNames[stat.type]\n" " if name and stat.value ~= 0 then\n" " local prefix = stat.value > 0 and '+' or ''\n" " self:AddLine(prefix..stat.value..' '..name, 0, 1, 0)\n" " end\n" " end\n" " end\n" " -- Resistances\n" " if data.fireRes and data.fireRes ~= 0 then self:AddLine('+'..data.fireRes..' Fire Resistance', 0, 1, 0) end\n" " if data.natureRes and data.natureRes ~= 0 then self:AddLine('+'..data.natureRes..' Nature Resistance', 0, 1, 0) end\n" " if data.frostRes and data.frostRes ~= 0 then self:AddLine('+'..data.frostRes..' Frost Resistance', 0, 1, 0) end\n" " if data.shadowRes and data.shadowRes ~= 0 then self:AddLine('+'..data.shadowRes..' Shadow Resistance', 0, 1, 0) end\n" " if data.arcaneRes and data.arcaneRes ~= 0 then self:AddLine('+'..data.arcaneRes..' Arcane Resistance', 0, 1, 0) end\n" " -- Item spell effects (Use: / Equip: / Chance on Hit:)\n" " if data.itemSpells then\n" " local triggerLabels = {[0]='Use: ',[1]='Equip: ',[2]='Chance on hit: ',[5]=''}\n" " for _, sp in ipairs(data.itemSpells) do\n" " local label = triggerLabels[sp.trigger] or ''\n" " local text = sp.description or sp.name or ''\n" " if text ~= '' then\n" " self:AddLine(label .. text, 0, 1, 0)\n" " end\n" " end\n" " end\n" " -- Gem sockets\n" " if data.sockets then\n" " local socketNames = {[1]='Meta',[2]='Red',[4]='Yellow',[8]='Blue'}\n" " for _, sock in ipairs(data.sockets) do\n" " local colorName = socketNames[sock.color] or 'Prismatic'\n" " self:AddLine('[' .. colorName .. ' Socket]', 0.5, 0.5, 0.5)\n" " end\n" " end\n" " -- Required level\n" " if data.requiredLevel and data.requiredLevel > 1 then\n" " self:AddLine('Requires Level '..data.requiredLevel, 1, 1, 1)\n" " end\n" " -- Flavor text\n" " if data.description then self:AddLine('\"'..data.description..'\"', 1, 0.82, 0) end\n" " if data.startsQuest then self:AddLine('This Item Begins a Quest', 1, 0.82, 0) end\n" " end\n" " -- Sell price from GetItemInfo\n" " if sellPrice and sellPrice > 0 then\n" " local gold = math.floor(sellPrice / 10000)\n" " local silver = math.floor((sellPrice % 10000) / 100)\n" " local copper = sellPrice % 100\n" " local parts = {}\n" " if gold > 0 then table.insert(parts, gold..'g') end\n" " if silver > 0 then table.insert(parts, silver..'s') end\n" " if copper > 0 then table.insert(parts, copper..'c') end\n" " if #parts > 0 then self:AddLine('Sell Price: '..table.concat(parts, ' '), 1, 1, 1) end\n" " end\n" " self.__itemId = itemId\n" " return true\n" "end\n" "function GameTooltip:SetInventoryItem(unit, slot)\n" " self:ClearLines()\n" " if unit ~= 'player' then return false, false, 0 end\n" " local link = GetInventoryItemLink(unit, slot)\n" " if not link then return false, false, 0 end\n" " local id = link:match('item:(%d+)')\n" " if not id then return false, false, 0 end\n" " local ok = _WoweePopulateItemTooltip(self, tonumber(id))\n" " return ok or false, false, 0\n" "end\n" "function GameTooltip:SetBagItem(bag, slot)\n" " self:ClearLines()\n" " local tex, count, locked, quality, readable, lootable, link = GetContainerItemInfo(bag, slot)\n" " if not link then return end\n" " local id = link:match('item:(%d+)')\n" " if not id then return end\n" " _WoweePopulateItemTooltip(self, tonumber(id))\n" " if count and count > 1 then self:AddLine('Count: '..count, 0.5, 0.5, 0.5) end\n" "end\n" "function GameTooltip:SetSpellByID(spellId)\n" " self:ClearLines()\n" " if not spellId or spellId == 0 then return end\n" " local name, rank, icon, castTime, minRange, maxRange = GetSpellInfo(spellId)\n" " if name then\n" " self:SetText(name, 1, 1, 1)\n" " if rank and rank ~= '' then self:AddLine(rank, 0.5, 0.5, 0.5) end\n" " -- Mana cost\n" " local cost, costType = GetSpellPowerCost(spellId)\n" " if cost and cost > 0 then\n" " local powerNames = {[0]='Mana',[1]='Rage',[2]='Focus',[3]='Energy',[6]='Runic Power'}\n" " self:AddLine(cost..' '..(powerNames[costType] or 'Mana'), 1, 1, 1)\n" " end\n" " -- Range\n" " if maxRange and maxRange > 0 then\n" " self:AddDoubleLine(string.format('%.0f yd range', maxRange), '', 1,1,1, 1,1,1)\n" " end\n" " -- Cast time\n" " if castTime and castTime > 0 then\n" " self:AddDoubleLine(string.format('%.1f sec cast', castTime / 1000), '', 1,1,1, 1,1,1)\n" " else\n" " self:AddDoubleLine('Instant', '', 1,1,1, 1,1,1)\n" " end\n" " -- Description\n" " local desc = GetSpellDescription(spellId)\n" " if desc and desc ~= '' then\n" " self:AddLine(desc, 1, 0.82, 0)\n" " end\n" " -- Cooldown\n" " local start, dur = GetSpellCooldown(spellId)\n" " if dur and dur > 0 then\n" " local rem = start + dur - GetTime()\n" " if rem > 0.1 then self:AddLine(string.format('%.0f sec cooldown', rem), 1, 0, 0) end\n" " end\n" " self.__spellId = spellId\n" " end\n" "end\n" "function GameTooltip:SetAction(slot)\n" " self:ClearLines()\n" " if not slot then return end\n" " local actionType, id = GetActionInfo(slot)\n" " if actionType == 'spell' and id and id > 0 then\n" " self:SetSpellByID(id)\n" " elseif actionType == 'item' and id and id > 0 then\n" " _WoweePopulateItemTooltip(self, id)\n" " end\n" "end\n" "function GameTooltip:FadeOut() end\n" "function GameTooltip:SetFrameStrata(...) end\n" "function GameTooltip:SetClampedToScreen(...) end\n" "function GameTooltip:IsOwned(f) return self.__owner == f end\n" // ShoppingTooltip: used by comparison tooltips "ShoppingTooltip1 = CreateFrame('Frame', 'ShoppingTooltip1')\n" "ShoppingTooltip2 = CreateFrame('Frame', 'ShoppingTooltip2')\n" // Error handling stubs (used by many addons) "local _errorHandler = function(err) return err end\n" "function geterrorhandler() return _errorHandler end\n" "function seterrorhandler(fn) if type(fn)=='function' then _errorHandler=fn end end\n" "function debugstack(start, count1, count2) return '' end\n" "function securecall(fn, ...) if type(fn)=='function' then return fn(...) end end\n" "function issecurevariable(...) return false end\n" "function issecure() return false end\n" // GetCVarBool wraps C-side GetCVar (registered in table) for boolean queries "function GetCVarBool(name) return GetCVar(name) == '1' end\n" // Misc compatibility stubs // GetScreenWidth, GetScreenHeight, GetNumLootItems are now C functions // GetFramerate is now a C function "function GetNetStats() return 0, 0, 0, 0 end\n" "function IsLoggedIn() return true end\n" "function StaticPopup_Show() end\n" "function StaticPopup_Hide() end\n" // UI Panel management — Show/Hide standard WoW panels "UIPanelWindows = {}\n" "function ShowUIPanel(frame, force)\n" " if frame and frame.Show then frame:Show() end\n" "end\n" "function HideUIPanel(frame)\n" " if frame and frame.Hide then frame:Hide() end\n" "end\n" "function ToggleFrame(frame)\n" " if frame then\n" " if frame:IsShown() then frame:Hide() else frame:Show() end\n" " end\n" "end\n" "function GetUIPanel(which) return nil end\n" "function CloseWindows(ignoreCenter) return false end\n" // TEXT localization stub — returns input string unchanged "function TEXT(text) return text end\n" // Faux scroll frame helpers (used by many list UIs) "function FauxScrollFrame_GetOffset(frame)\n" " return frame and frame.offset or 0\n" "end\n" "function FauxScrollFrame_Update(frame, numItems, numVisible, valueStep, button, smallWidth, bigWidth, highlightFrame, smallHighlightWidth, bigHighlightWidth)\n" " if not frame then return false end\n" " frame.offset = frame.offset or 0\n" " local showScrollBar = numItems > numVisible\n" " return showScrollBar\n" "end\n" "function FauxScrollFrame_SetOffset(frame, offset)\n" " if frame then frame.offset = offset or 0 end\n" "end\n" "function FauxScrollFrame_OnVerticalScroll(frame, value, itemHeight, updateFunction)\n" " if not frame then return end\n" " frame.offset = math.floor(value / (itemHeight or 1) + 0.5)\n" " if updateFunction then updateFunction() end\n" "end\n" // SecureCmdOptionParse — parses conditional macros like [target=focus] "function SecureCmdOptionParse(options)\n" " if not options then return nil end\n" " -- Simple: return the unconditional fallback (text after last semicolon or the whole string)\n" " local result = options:match(';%s*(.-)$') or options:match('^%[.*%]%s*(.-)$') or options\n" " return result\n" "end\n" // ChatFrame message group stubs "function ChatFrame_AddMessageGroup(frame, group) end\n" "function ChatFrame_RemoveMessageGroup(frame, group) end\n" "function ChatFrame_AddChannel(frame, channel) end\n" "function ChatFrame_RemoveChannel(frame, channel) end\n" // CreateTexture/CreateFontString are now C frame methods in the metatable "do\n" " local function cc(r,g,b)\n" " local t = {r=r, g=g, b=b}\n" " t.colorStr = string.format('%02x%02x%02x', math.floor(r*255), math.floor(g*255), math.floor(b*255))\n" " function t:GenerateHexColor() return '|cff' .. self.colorStr end\n" " function t:GenerateHexColorMarkup() return '|cff' .. self.colorStr end\n" " return t\n" " end\n" " RAID_CLASS_COLORS = {\n" " WARRIOR=cc(0.78,0.61,0.43), PALADIN=cc(0.96,0.55,0.73),\n" " HUNTER=cc(0.67,0.83,0.45), ROGUE=cc(1.0,0.96,0.41),\n" " PRIEST=cc(1.0,1.0,1.0), DEATHKNIGHT=cc(0.77,0.12,0.23),\n" " SHAMAN=cc(0.0,0.44,0.87), MAGE=cc(0.41,0.80,0.94),\n" " WARLOCK=cc(0.58,0.51,0.79), DRUID=cc(1.0,0.49,0.04),\n" " }\n" "end\n" // GetClassColor(className) — returns r, g, b, colorString "function GetClassColor(className)\n" " local c = RAID_CLASS_COLORS[className]\n" " if c then return c.r, c.g, c.b, c.colorStr end\n" " return 1, 1, 1, 'ffffffff'\n" "end\n" // QuestDifficultyColors table for quest level coloring "QuestDifficultyColors = {\n" " impossible = {r=1.0,g=0.1,b=0.1,font='QuestDifficulty_Impossible'},\n" " verydifficult = {r=1.0,g=0.5,b=0.25,font='QuestDifficulty_VeryDifficult'},\n" " difficult = {r=1.0,g=1.0,b=0.0,font='QuestDifficulty_Difficult'},\n" " standard = {r=0.25,g=0.75,b=0.25,font='QuestDifficulty_Standard'},\n" " trivial = {r=0.5,g=0.5,b=0.5,font='QuestDifficulty_Trivial'},\n" " header = {r=1.0,g=0.82,b=0.0,font='QuestDifficulty_Header'},\n" "}\n" // Money formatting utility "function GetCoinTextureString(copper)\n" " if not copper or copper == 0 then return '0c' end\n" " copper = math.floor(copper)\n" " local g = math.floor(copper / 10000)\n" " local s = math.floor(math.fmod(copper, 10000) / 100)\n" " local c = math.fmod(copper, 100)\n" " local r = ''\n" " if g > 0 then r = r .. g .. 'g ' end\n" " if s > 0 then r = r .. s .. 's ' end\n" " if c > 0 or r == '' then r = r .. c .. 'c' end\n" " return r\n" "end\n" "GetCoinText = GetCoinTextureString\n" ); // UIDropDownMenu framework — minimal compat for addons using dropdown menus luaL_dostring(L_, "UIDROPDOWNMENU_MENU_LEVEL = 1\n" "UIDROPDOWNMENU_MENU_VALUE = nil\n" "UIDROPDOWNMENU_OPEN_MENU = nil\n" "local _ddMenuList = {}\n" "function UIDropDownMenu_Initialize(frame, initFunc, displayMode, level, menuList)\n" " if frame then frame.__initFunc = initFunc end\n" "end\n" "function UIDropDownMenu_CreateInfo() return {} end\n" "function UIDropDownMenu_AddButton(info, level) table.insert(_ddMenuList, info) end\n" "function UIDropDownMenu_SetWidth(frame, width) end\n" "function UIDropDownMenu_SetButtonWidth(frame, width) end\n" "function UIDropDownMenu_SetText(frame, text)\n" " if frame then frame.__text = text end\n" "end\n" "function UIDropDownMenu_GetText(frame)\n" " return frame and frame.__text or ''\n" "end\n" "function UIDropDownMenu_SetSelectedID(frame, id) end\n" "function UIDropDownMenu_SetSelectedValue(frame, value) end\n" "function UIDropDownMenu_GetSelectedID(frame) return 1 end\n" "function UIDropDownMenu_GetSelectedValue(frame) return nil end\n" "function UIDropDownMenu_JustifyText(frame, justify) end\n" "function UIDropDownMenu_EnableDropDown(frame) end\n" "function UIDropDownMenu_DisableDropDown(frame) end\n" "function CloseDropDownMenus() end\n" "function ToggleDropDownMenu(level, value, frame, anchor, xOfs, yOfs) end\n" ); // UISpecialFrames: frames in this list close on Escape key luaL_dostring(L_, "UISpecialFrames = {}\n" // Font object stubs — addons reference these for CreateFontString templates "GameFontNormal = {}\n" "GameFontNormalSmall = {}\n" "GameFontNormalLarge = {}\n" "GameFontHighlight = {}\n" "GameFontHighlightSmall = {}\n" "GameFontHighlightLarge = {}\n" "GameFontDisable = {}\n" "GameFontDisableSmall = {}\n" "GameFontWhite = {}\n" "GameFontRed = {}\n" "GameFontGreen = {}\n" "NumberFontNormal = {}\n" "ChatFontNormal = {}\n" "SystemFont = {}\n" // InterfaceOptionsFrame: addons register settings panels here "InterfaceOptionsFrame = CreateFrame('Frame', 'InterfaceOptionsFrame')\n" "InterfaceOptionsFramePanelContainer = CreateFrame('Frame', 'InterfaceOptionsFramePanelContainer')\n" "function InterfaceOptions_AddCategory(panel) end\n" "function InterfaceOptionsFrame_OpenToCategory(panel) end\n" // Commonly expected global tables "SLASH_RELOAD1 = '/reload'\n" "SLASH_RELOADUI1 = '/reloadui'\n" "GRAY_FONT_COLOR = {r=0.5,g=0.5,b=0.5}\n" "NORMAL_FONT_COLOR = {r=1.0,g=0.82,b=0.0}\n" "HIGHLIGHT_FONT_COLOR = {r=1.0,g=1.0,b=1.0}\n" "GREEN_FONT_COLOR = {r=0.1,g=1.0,b=0.1}\n" "RED_FONT_COLOR = {r=1.0,g=0.1,b=0.1}\n" // C_ChatInfo — addon message prefix API used by some addons "C_ChatInfo = C_ChatInfo or {}\n" "C_ChatInfo.RegisterAddonMessagePrefix = RegisterAddonMessagePrefix\n" "C_ChatInfo.IsAddonMessagePrefixRegistered = IsAddonMessagePrefixRegistered\n" "C_ChatInfo.SendAddonMessage = SendAddonMessage\n" ); // Action bar constants and functions used by action bar addons luaL_dostring(L_, "NUM_ACTIONBAR_BUTTONS = 12\n" "NUM_ACTIONBAR_PAGES = 6\n" "ACTION_BUTTON_SHOW_GRID_REASON_CVAR = 1\n" "ACTION_BUTTON_SHOW_GRID_REASON_EVENT = 2\n" // Action bar page tracking "local _actionBarPage = 1\n" "function GetActionBarPage() return _actionBarPage end\n" "function ChangeActionBarPage(page) _actionBarPage = page end\n" "function GetBonusBarOffset() return 0 end\n" // Action type query "function GetActionText(slot) return nil end\n" "function GetActionCount(slot) return 0 end\n" // Binding functions "function GetBindingKey(action) return nil end\n" "function GetBindingAction(key) return nil end\n" "function SetBinding(key, action) end\n" "function SaveBindings(which) end\n" "function GetCurrentBindingSet() return 1 end\n" // Macro functions "function GetNumMacros() return 0, 0 end\n" "function GetMacroInfo(id) return nil end\n" "function GetMacroBody(id) return nil end\n" "function GetMacroIndexByName(name) return 0 end\n" // Stance bar "function GetNumShapeshiftForms() return 0 end\n" "function GetShapeshiftFormInfo(index) return nil, nil, nil, nil end\n" // Pet action bar "NUM_PET_ACTION_SLOTS = 10\n" // Common WoW constants used by many addons "MAX_TALENT_TABS = 3\n" "MAX_NUM_TALENTS = 100\n" "BOOKTYPE_SPELL = 0\n" "BOOKTYPE_PET = 1\n" "MAX_PARTY_MEMBERS = 4\n" "MAX_RAID_MEMBERS = 40\n" "MAX_ARENA_TEAMS = 3\n" "INVSLOT_FIRST_EQUIPPED = 1\n" "INVSLOT_LAST_EQUIPPED = 19\n" "NUM_BAG_SLOTS = 4\n" "NUM_BANKBAGSLOTS = 7\n" "CONTAINER_BAG_OFFSET = 0\n" "MAX_SKILLLINE_TABS = 8\n" "TRADE_ENCHANT_SLOT = 7\n" "function GetPetActionInfo(slot) return nil end\n" "function GetPetActionsUsable() return false end\n" ); // WoW table/string utility functions used by many addons luaL_dostring(L_, // Table utilities "function tContains(tbl, item)\n" " for _, v in pairs(tbl) do if v == item then return true end end\n" " return false\n" "end\n" "function tInvert(tbl)\n" " local inv = {}\n" " for k, v in pairs(tbl) do inv[v] = k end\n" " return inv\n" "end\n" "function CopyTable(src)\n" " if type(src) ~= 'table' then return src end\n" " local copy = {}\n" " for k, v in pairs(src) do copy[k] = CopyTable(v) end\n" " return setmetatable(copy, getmetatable(src))\n" "end\n" "function tDeleteItem(tbl, item)\n" " for i = #tbl, 1, -1 do if tbl[i] == item then table.remove(tbl, i) end end\n" "end\n" // Mixin pattern — used by modern addons for OOP-style object creation "function Mixin(obj, ...)\n" " for i = 1, select('#', ...) do\n" " local mixin = select(i, ...)\n" " for k, v in pairs(mixin) do obj[k] = v end\n" " end\n" " return obj\n" "end\n" "function CreateFromMixins(...)\n" " return Mixin({}, ...)\n" "end\n" "function CreateAndInitFromMixin(mixin, ...)\n" " local obj = CreateFromMixins(mixin)\n" " if obj.Init then obj:Init(...) end\n" " return obj\n" "end\n" "function MergeTable(dest, src)\n" " for k, v in pairs(src) do dest[k] = v end\n" " return dest\n" "end\n" // String utilities (WoW globals that alias Lua string functions) "strupper = string.upper\n" "strlower = string.lower\n" "strfind = string.find\n" "strsub = string.sub\n" "strlen = string.len\n" "strrep = string.rep\n" "strbyte = string.byte\n" "strchar = string.char\n" "strgfind = string.gmatch\n" "function tostringall(...)\n" " local n = select('#', ...)\n" " if n == 0 then return end\n" " local r = {}\n" " for i = 1, n do r[i] = tostring(select(i, ...)) end\n" " return unpack(r, 1, n)\n" "end\n" "strrev = string.reverse\n" "gsub = string.gsub\n" "gmatch = string.gmatch\n" "strjoin = function(delim, ...)\n" " return table.concat({...}, delim)\n" "end\n" // Math utilities "function Clamp(val, lo, hi) return math.min(math.max(val, lo), hi) end\n" "function Round(val) return math.floor(val + 0.5) end\n" // Bit operations (WoW provides these; Lua 5.1 doesn't have native bit ops) "bit = bit or {}\n" "bit.band = bit.band or function(a, b) local r,m=0,1 for i=0,31 do if a%2==1 and b%2==1 then r=r+m end a=math.floor(a/2) b=math.floor(b/2) m=m*2 end return r end\n" "bit.bor = bit.bor or function(a, b) local r,m=0,1 for i=0,31 do if a%2==1 or b%2==1 then r=r+m end a=math.floor(a/2) b=math.floor(b/2) m=m*2 end return r end\n" "bit.bxor = bit.bxor or function(a, b) local r,m=0,1 for i=0,31 do if (a%2==1)~=(b%2==1) then r=r+m end a=math.floor(a/2) b=math.floor(b/2) m=m*2 end return r end\n" "bit.bnot = bit.bnot or function(a) return 4294967295 - a end\n" "bit.lshift = bit.lshift or function(a, n) return a * (2^n) end\n" "bit.rshift = bit.rshift or function(a, n) return math.floor(a / (2^n)) end\n" ); } // ---- 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); std::string errStr = err ? err : "(unknown)"; LOG_ERROR("LuaEngine: event '", eventName, "' handler error: ", errStr); if (luaErrorCallback_) luaErrorCallback_(errStr); lua_pop(L_, 1); } } lua_pop(L_, 2); // pop handler list + WoweeEvents // Also dispatch to frames that registered for this event via frame:RegisterEvent() lua_getglobal(L_, "__WoweeFrameEvents"); if (lua_istable(L_, -1)) { lua_getfield(L_, -1, eventName.c_str()); if (lua_istable(L_, -1)) { int frameCount = static_cast(lua_objlen(L_, -1)); for (int i = 1; i <= frameCount; i++) { lua_rawgeti(L_, -1, i); if (!lua_istable(L_, -1)) { lua_pop(L_, 1); continue; } // Get the frame's OnEvent script lua_getfield(L_, -1, "__scripts"); if (lua_istable(L_, -1)) { lua_getfield(L_, -1, "OnEvent"); if (lua_isfunction(L_, -1)) { lua_pushvalue(L_, -3); // self (frame) lua_pushstring(L_, eventName.c_str()); for (const auto& arg : args) lua_pushstring(L_, arg.c_str()); int nargs = 2 + static_cast(args.size()); if (lua_pcall(L_, nargs, 0, 0) != 0) { const char* ferr = lua_tostring(L_, -1); std::string ferrStr = ferr ? ferr : "(unknown)"; LOG_ERROR("LuaEngine: frame OnEvent error: ", ferrStr); if (luaErrorCallback_) luaErrorCallback_(ferrStr); lua_pop(L_, 1); } } else { lua_pop(L_, 1); // pop non-function } } lua_pop(L_, 2); // pop __scripts + frame } } lua_pop(L_, 1); // pop event frame list } lua_pop(L_, 1); // pop __WoweeFrameEvents } void LuaEngine::dispatchOnUpdate(float elapsed) { if (!L_) return; lua_getglobal(L_, "__WoweeOnUpdateFrames"); if (!lua_istable(L_, -1)) { lua_pop(L_, 1); return; } int count = static_cast(lua_objlen(L_, -1)); for (int i = 1; i <= count; i++) { lua_rawgeti(L_, -1, i); if (!lua_istable(L_, -1)) { lua_pop(L_, 1); continue; } // Check if frame is visible lua_getfield(L_, -1, "__visible"); bool visible = lua_toboolean(L_, -1); lua_pop(L_, 1); if (!visible) { lua_pop(L_, 1); continue; } // Get OnUpdate script lua_getfield(L_, -1, "__scripts"); if (lua_istable(L_, -1)) { lua_getfield(L_, -1, "OnUpdate"); if (lua_isfunction(L_, -1)) { lua_pushvalue(L_, -3); // self (frame) lua_pushnumber(L_, static_cast(elapsed)); if (lua_pcall(L_, 2, 0, 0) != 0) { const char* uerr = lua_tostring(L_, -1); std::string uerrStr = uerr ? uerr : "(unknown)"; LOG_ERROR("LuaEngine: OnUpdate error: ", uerrStr); if (luaErrorCallback_) luaErrorCallback_(uerrStr); lua_pop(L_, 1); } } else { lua_pop(L_, 1); } } lua_pop(L_, 2); // pop __scripts + frame } lua_pop(L_, 1); // pop __WoweeOnUpdateFrames } bool LuaEngine::dispatchSlashCommand(const std::string& command, const std::string& args) { if (!L_) return false; // Check each SlashCmdList entry: for key NAME, check SLASH_NAME1, SLASH_NAME2, etc. lua_getglobal(L_, "SlashCmdList"); if (!lua_istable(L_, -1)) { lua_pop(L_, 1); return false; } std::string cmdLower = command; toLowerInPlace(cmdLower); lua_pushnil(L_); while (lua_next(L_, -2) != 0) { // Stack: SlashCmdList, key, handler if (!lua_isfunction(L_, -1) || !lua_isstring(L_, -2)) { lua_pop(L_, 1); continue; } const char* name = lua_tostring(L_, -2); // Check SLASH_1 through SLASH_9 for (int i = 1; i <= 9; i++) { std::string globalName = "SLASH_" + std::string(name) + std::to_string(i); lua_getglobal(L_, globalName.c_str()); if (lua_isstring(L_, -1)) { std::string slashStr = lua_tostring(L_, -1); toLowerInPlace(slashStr); if (slashStr == cmdLower) { lua_pop(L_, 1); // pop global // Call the handler with args lua_pushvalue(L_, -1); // copy handler lua_pushstring(L_, args.c_str()); if (lua_pcall(L_, 1, 0, 0) != 0) { LOG_ERROR("LuaEngine: SlashCmdList['", name, "'] error: ", lua_tostring(L_, -1)); lua_pop(L_, 1); } lua_pop(L_, 3); // pop handler, key, SlashCmdList return true; } } lua_pop(L_, 1); // pop global } lua_pop(L_, 1); // pop handler, keep key for next iteration } lua_pop(L_, 1); // pop SlashCmdList return false; } // ---- SavedVariables serialization ---- static void serializeLuaValue(lua_State* L, int idx, std::string& out, int indent); static void serializeLuaTable(lua_State* L, int idx, std::string& out, int indent) { out += "{\n"; std::string pad(indent + 2, ' '); lua_pushnil(L); while (lua_next(L, idx) != 0) { out += pad; // Key if (lua_type(L, -2) == LUA_TSTRING) { const char* k = lua_tostring(L, -2); out += "[\""; for (const char* p = k; *p; ++p) { if (*p == '"' || *p == '\\') out += '\\'; out += *p; } out += "\"] = "; } else if (lua_type(L, -2) == LUA_TNUMBER) { out += "[" + std::to_string(static_cast(lua_tonumber(L, -2))) + "] = "; } else { lua_pop(L, 1); continue; } // Value serializeLuaValue(L, lua_gettop(L), out, indent + 2); out += ",\n"; lua_pop(L, 1); } out += std::string(indent, ' ') + "}"; } static void serializeLuaValue(lua_State* L, int idx, std::string& out, int indent) { switch (lua_type(L, idx)) { case LUA_TNIL: out += "nil"; break; case LUA_TBOOLEAN: out += lua_toboolean(L, idx) ? "true" : "false"; break; case LUA_TNUMBER: { double v = lua_tonumber(L, idx); char buf[64]; snprintf(buf, sizeof(buf), "%.17g", v); out += buf; break; } case LUA_TSTRING: { const char* s = lua_tostring(L, idx); out += "\""; for (const char* p = s; *p; ++p) { if (*p == '"' || *p == '\\') out += '\\'; else if (*p == '\n') { out += "\\n"; continue; } else if (*p == '\r') continue; out += *p; } out += "\""; break; } case LUA_TTABLE: serializeLuaTable(L, idx, out, indent); break; default: out += "nil"; // Functions, userdata, etc. can't be serialized break; } } void LuaEngine::setAddonList(const std::vector& addons) { if (!L_) return; lua_pushnumber(L_, static_cast(addons.size())); lua_setfield(L_, LUA_REGISTRYINDEX, "wowee_addon_count"); lua_newtable(L_); for (size_t i = 0; i < addons.size(); i++) { lua_newtable(L_); lua_pushstring(L_, addons[i].addonName.c_str()); lua_setfield(L_, -2, "name"); lua_pushstring(L_, addons[i].getTitle().c_str()); lua_setfield(L_, -2, "title"); auto notesIt = addons[i].directives.find("Notes"); lua_pushstring(L_, notesIt != addons[i].directives.end() ? notesIt->second.c_str() : ""); lua_setfield(L_, -2, "notes"); // Store all TOC directives for GetAddOnMetadata lua_newtable(L_); for (const auto& [key, val] : addons[i].directives) { lua_pushstring(L_, val.c_str()); lua_setfield(L_, -2, key.c_str()); } lua_setfield(L_, -2, "metadata"); lua_rawseti(L_, -2, static_cast(i + 1)); } lua_setfield(L_, LUA_REGISTRYINDEX, "wowee_addon_info"); } bool LuaEngine::loadSavedVariables(const std::string& path) { if (!L_) return false; std::ifstream f(path); if (!f.is_open()) return false; // No saved data yet — not an error std::string content((std::istreambuf_iterator(f)), std::istreambuf_iterator()); if (content.empty()) return true; int err = luaL_dostring(L_, content.c_str()); if (err != 0) { LOG_WARNING("LuaEngine: error loading saved variables from '", path, "': ", lua_tostring(L_, -1)); lua_pop(L_, 1); return false; } return true; } bool LuaEngine::saveSavedVariables(const std::string& path, const std::vector& varNames) { if (!L_ || varNames.empty()) return false; std::string output; for (const auto& name : varNames) { lua_getglobal(L_, name.c_str()); if (!lua_isnil(L_, -1)) { output += name + " = "; serializeLuaValue(L_, lua_gettop(L_), output, 0); output += "\n"; } lua_pop(L_, 1); } if (output.empty()) return true; // Ensure directory exists size_t lastSlash = path.find_last_of("/\\"); if (lastSlash != std::string::npos) { std::error_code ec; std::filesystem::create_directories(path.substr(0, lastSlash), ec); } std::ofstream f(path); if (!f.is_open()) { LOG_WARNING("LuaEngine: cannot write saved variables to '", path, "'"); return false; } f << output; LOG_INFO("LuaEngine: saved variables to '", path, "' (", output.size(), " bytes)"); return true; } bool LuaEngine::executeFile(const std::string& path) { if (!L_) return false; int err = luaL_dofile(L_, path.c_str()); if (err != 0) { const char* errMsg = lua_tostring(L_, -1); std::string msg = errMsg ? errMsg : "(unknown error)"; LOG_ERROR("LuaEngine: error loading '", path, "': ", msg); if (luaErrorCallback_) luaErrorCallback_(msg); if (gameHandler_) { game::MessageChatData errChat; errChat.type = game::ChatType::SYSTEM; errChat.language = game::ChatLanguage::UNIVERSAL; errChat.message = "|cffff4040[Lua Error] " + msg + "|r"; gameHandler_->addLocalChatMessage(errChat); } lua_pop(L_, 1); return false; } return true; } bool LuaEngine::executeString(const std::string& code) { if (!L_) return false; int err = luaL_dostring(L_, code.c_str()); if (err != 0) { const char* errMsg = lua_tostring(L_, -1); std::string msg = errMsg ? errMsg : "(unknown error)"; LOG_ERROR("LuaEngine: script error: ", msg); if (luaErrorCallback_) luaErrorCallback_(msg); if (gameHandler_) { game::MessageChatData errChat; errChat.type = game::ChatType::SYSTEM; errChat.language = game::ChatLanguage::UNIVERSAL; errChat.message = "|cffff4040[Lua Error] " + msg + "|r"; gameHandler_->addLocalChatMessage(errChat); } lua_pop(L_, 1); return false; } return true; } } // namespace wowee::addons