Kelsidavis-WoWee/src/addons/lua_engine.cpp
Paul a916270a13 chore(lua): refactor addon Lua engine API + progress docs
- Refactor Lua addon integration:
  - Update CMakeLists.txt for addon build paths
  - Enhance addons API headers and Lua engine interface
  - Add new Lua API addon modules (`lua_api_helpers`, `lua_api_registrations`, `lua_services`, `lua_action_api`, `lua_inventory_api`, `lua_quest_api`, `lua_social_api`, `lua_spell_api`, `lua_system_api`, `lua_unit_api`)
  - Update implementation in addon_manager.cpp, lua_engine.cpp, application.cpp, game_handler.cpp
2026-04-03 07:31:06 +03:00

1742 lines
70 KiB
C++

#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 <imgui.h>
#include <fstream>
#include <filesystem>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
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<int>(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<int>(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<double>(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<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);
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<int>(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<int>(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<int>(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<double>(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_<NAME>1 through SLASH_<NAME>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<long long>(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<TocFile>& addons) {
if (!L_) return;
lua_pushnumber(L_, static_cast<double>(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<int>(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<char>(f)), std::istreambuf_iterator<char>());
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<std::string>& 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