Merge pull request #39 from ldmonster/chore/refactor-lua-engine

[chore] lua: refactor addon Lua engine API + progress docs
This commit is contained in:
Kelsi Rae Davis 2026-04-03 03:20:20 -07:00 committed by GitHub
commit fda3245550
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 6261 additions and 6010 deletions

View file

@ -640,6 +640,13 @@ set(WOWEE_SOURCES
# Addons
src/addons/addon_manager.cpp
src/addons/lua_engine.cpp
src/addons/lua_unit_api.cpp
src/addons/lua_spell_api.cpp
src/addons/lua_inventory_api.cpp
src/addons/lua_quest_api.cpp
src/addons/lua_social_api.cpp
src/addons/lua_system_api.cpp
src/addons/lua_action_api.cpp
src/addons/toc_parser.cpp
# Main

View file

@ -13,7 +13,7 @@ public:
AddonManager();
~AddonManager();
bool initialize(game::GameHandler* gameHandler);
bool initialize(game::GameHandler* gameHandler, const LuaServices& services = {});
void scanAddons(const std::string& addonsPath);
void loadAllAddons();
bool runScript(const std::string& code);
@ -35,6 +35,7 @@ private:
LuaEngine luaEngine_;
std::vector<TocFile> addons_;
game::GameHandler* gameHandler_ = nullptr;
LuaServices luaServices_;
std::string addonsPath_;
bool loadAddon(const TocFile& addon);

View file

@ -0,0 +1,166 @@
// lua_api_helpers.hpp — Shared helpers, lookup tables, and utility functions
// used by all lua_*_api.cpp domain files.
// Extracted from lua_engine.cpp as part of §5.1 (Tame LuaEngine).
#pragma once
#include <string>
#include <chrono>
#include <cstring>
#include <algorithm>
#include "addons/lua_services.hpp"
#include "game/game_handler.hpp"
#include "game/entity.hpp"
#include "game/update_field_table.hpp"
#include "core/logger.hpp"
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
namespace wowee::addons {
// ---- String helper ----
inline void toLowerInPlace(std::string& s) {
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
// ---- Lua return helpers — used 200+ times as guard/fallback returns ----
inline int luaReturnNil(lua_State* L) { lua_pushnil(L); return 1; }
inline int luaReturnZero(lua_State* L) { lua_pushnumber(L, 0); return 1; }
inline int luaReturnFalse(lua_State* L){ lua_pushboolean(L, 0); return 1; }
// ---- Shared GetTime() epoch ----
// All time-returning functions must use this same origin
// so that addon calculations like (start + duration - GetTime()) are consistent.
inline const auto& luaTimeEpoch() {
static const auto epoch = std::chrono::steady_clock::now();
return epoch;
}
inline double luaGetTimeNow() {
return std::chrono::duration<double>(std::chrono::steady_clock::now() - luaTimeEpoch()).count();
}
// ---- Shared WoW class/race/power name tables (indexed by ID, element 0 = unknown) ----
inline constexpr const char* kLuaClasses[] = {
"","Warrior","Paladin","Hunter","Rogue","Priest",
"Death Knight","Shaman","Mage","Warlock","","Druid"
};
inline constexpr const char* kLuaRaces[] = {
"","Human","Orc","Dwarf","Night Elf","Undead",
"Tauren","Gnome","Troll","","Blood Elf","Draenei"
};
inline constexpr const char* kLuaPowerNames[] = {
"MANA","RAGE","FOCUS","ENERGY","HAPPINESS","","RUNIC_POWER"
};
// ---- Quality hex strings ----
// No alpha prefix — for item links
inline constexpr const char* kQualHexNoAlpha[] = {
"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"
};
// With ff alpha prefix — for Lua color returns
inline constexpr const char* kQualHexAlpha[] = {
"ff9d9d9d","ffffffff","ff1eff00","ff0070dd","ffa335ee","ffff8000","ffe6cc80","ff00ccff"
};
// ---- Retrieve GameHandler pointer stored in Lua registry ----
inline game::GameHandler* getGameHandler(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, "wowee_game_handler");
auto* gh = static_cast<game::GameHandler*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return gh;
}
// ---- Retrieve LuaServices pointer stored in Lua registry ----
inline LuaServices* getLuaServices(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, "wowee_lua_services");
auto* svc = static_cast<LuaServices*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return svc;
}
// ---- Unit resolution helpers ----
// Read UNIT_FIELD_TARGET_LO/HI from an entity's update fields to get what it's targeting
inline uint64_t getEntityTargetGuid(game::GameHandler* gh, uint64_t guid) {
if (guid == 0) return 0;
// If asking for the player's target, use direct accessor
if (guid == gh->getPlayerGuid()) return gh->getTargetGuid();
auto entity = gh->getEntityManager().getEntity(guid);
if (!entity) return 0;
const auto& fields = entity->getFields();
auto loIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO));
if (loIt == fields.end()) return 0;
uint64_t targetGuid = loIt->second;
auto hiIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI));
if (hiIt != fields.end())
targetGuid |= (static_cast<uint64_t>(hiIt->second) << 32);
return targetGuid;
}
// Resolve WoW unit IDs to GUID
inline uint64_t resolveUnitGuid(game::GameHandler* gh, const std::string& uid) {
if (uid == "player") return gh->getPlayerGuid();
if (uid == "target") return gh->getTargetGuid();
if (uid == "focus") return gh->getFocusGuid();
if (uid == "mouseover") return gh->getMouseoverGuid();
if (uid == "pet") return gh->getPetGuid();
// Compound unit IDs: targettarget, focustarget, pettarget, mouseovertarget
if (uid == "targettarget") return getEntityTargetGuid(gh, gh->getTargetGuid());
if (uid == "focustarget") return getEntityTargetGuid(gh, gh->getFocusGuid());
if (uid == "pettarget") return getEntityTargetGuid(gh, gh->getPetGuid());
if (uid == "mouseovertarget") return getEntityTargetGuid(gh, gh->getMouseoverGuid());
// party1-party4, raid1-raid40
if (uid.rfind("party", 0) == 0 && uid.size() > 5) {
int idx = 0;
try { idx = std::stoi(uid.substr(5)); } catch (...) { return 0; }
if (idx < 1 || idx > 4) return 0;
const auto& pd = gh->getPartyData();
// party members exclude self; index 1-based
int found = 0;
for (const auto& m : pd.members) {
if (m.guid == gh->getPlayerGuid()) continue;
if (++found == idx) return m.guid;
}
return 0;
}
if (uid.rfind("raid", 0) == 0 && uid.size() > 4 && uid[4] != 'p') {
int idx = 0;
try { idx = std::stoi(uid.substr(4)); } catch (...) { return 0; }
if (idx < 1 || idx > 40) return 0;
const auto& pd = gh->getPartyData();
if (idx <= static_cast<int>(pd.members.size()))
return pd.members[idx - 1].guid;
return 0;
}
return 0;
}
// Resolve unit IDs (player, target, focus, mouseover, pet, targettarget, etc.) to entity
inline game::Unit* resolveUnit(lua_State* L, const char* unitId) {
auto* gh = getGameHandler(L);
if (!gh || !unitId) return nullptr;
std::string uid(unitId);
toLowerInPlace(uid);
uint64_t guid = resolveUnitGuid(gh, uid);
if (guid == 0) return nullptr;
auto entity = gh->getEntityManager().getEntity(guid);
if (!entity) return nullptr;
return dynamic_cast<game::Unit*>(entity.get());
}
// Find GroupMember data for a GUID (for party members out of entity range)
inline const game::GroupMember* findPartyMember(game::GameHandler* gh, uint64_t guid) {
if (!gh || guid == 0) return nullptr;
for (const auto& m : gh->getPartyData().members) {
if (m.guid == guid && m.hasPartyStats) return &m;
}
return nullptr;
}
} // namespace wowee::addons

View file

@ -0,0 +1,18 @@
// lua_api_registrations.hpp — Forward declarations for per-domain Lua API
// registration functions. Called from LuaEngine::registerCoreAPI().
// Extracted from lua_engine.cpp as part of §5.1 (Tame LuaEngine).
#pragma once
struct lua_State;
namespace wowee::addons {
void registerUnitLuaAPI(lua_State* L);
void registerSpellLuaAPI(lua_State* L);
void registerInventoryLuaAPI(lua_State* L);
void registerQuestLuaAPI(lua_State* L);
void registerSocialLuaAPI(lua_State* L);
void registerSystemLuaAPI(lua_State* L);
void registerActionLuaAPI(lua_State* L);
} // namespace wowee::addons

View file

@ -1,5 +1,6 @@
#pragma once
#include "addons/lua_services.hpp"
#include <functional>
#include <string>
#include <vector>
@ -27,6 +28,7 @@ public:
bool executeString(const std::string& code);
void setGameHandler(game::GameHandler* handler);
void setLuaServices(const LuaServices& services);
// Fire a WoW event to all registered Lua handlers.
void fireEvent(const std::string& eventName,
@ -55,6 +57,7 @@ public:
private:
lua_State* L_ = nullptr;
game::GameHandler* gameHandler_ = nullptr;
LuaServices luaServices_;
LuaErrorCallback luaErrorCallback_;
void registerCoreAPI();

View file

@ -0,0 +1,17 @@
// lua_services.hpp — Dependency-injected services for Lua bindings.
// Replaces Application::getInstance() calls in domain API files (§5.2).
#pragma once
namespace wowee::core { class Window; }
namespace wowee::audio { class AudioCoordinator; }
namespace wowee::game { class ExpansionRegistry; }
namespace wowee::addons {
struct LuaServices {
core::Window* window = nullptr;
audio::AudioCoordinator* audioCoordinator = nullptr;
game::ExpansionRegistry* expansionRegistry = nullptr;
};
} // namespace wowee::addons

View file

@ -10,10 +10,12 @@ namespace wowee::addons {
AddonManager::AddonManager() = default;
AddonManager::~AddonManager() { shutdown(); }
bool AddonManager::initialize(game::GameHandler* gameHandler) {
bool AddonManager::initialize(game::GameHandler* gameHandler, const LuaServices& services) {
gameHandler_ = gameHandler;
luaServices_ = services;
if (!luaEngine_.initialize()) return false;
luaEngine_.setGameHandler(gameHandler);
luaEngine_.setLuaServices(luaServices_);
return true;
}
@ -155,6 +157,7 @@ bool AddonManager::reload() {
return false;
}
luaEngine_.setGameHandler(gameHandler_);
luaEngine_.setLuaServices(luaServices_);
if (!addonsPath_.empty()) {
scanAddons(addonsPath_);

View file

@ -0,0 +1,734 @@
// lua_action_api.cpp — Action bar, cursor/pickup, keyboard input, key bindings, and pet actions Lua API bindings.
// Extracted from lua_engine.cpp as part of §5.1 (Tame LuaEngine).
#include "addons/lua_api_helpers.hpp"
#include "imgui.h"
namespace wowee::addons {
enum class CursorType { NONE, SPELL, ITEM, ACTION };
static CursorType s_cursorType = CursorType::NONE;
static uint32_t s_cursorId = 0; // spellId, itemId, or action slot
static int s_cursorSlot = 0; // source slot for placement
static int s_cursorBag = -1; // source bag for container items
static int lua_HasAction(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnFalse(L); }
int slot = static_cast<int>(luaL_checknumber(L, 1)) - 1; // WoW uses 1-indexed slots
const auto& bar = gh->getActionBar();
if (slot < 0 || slot >= static_cast<int>(bar.size())) {
lua_pushboolean(L, 0);
return 1;
}
lua_pushboolean(L, !bar[slot].isEmpty());
return 1;
}
// GetActionTexture(slot) → texturePath or nil
static int lua_GetActionTexture(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
int slot = static_cast<int>(luaL_checknumber(L, 1)) - 1;
const auto& bar = gh->getActionBar();
if (slot < 0 || slot >= static_cast<int>(bar.size()) || bar[slot].isEmpty()) {
lua_pushnil(L);
return 1;
}
const auto& action = bar[slot];
if (action.type == game::ActionBarSlot::SPELL) {
std::string icon = gh->getSpellIconPath(action.id);
if (!icon.empty()) {
lua_pushstring(L, icon.c_str());
return 1;
}
} else if (action.type == game::ActionBarSlot::ITEM && action.id != 0) {
const auto* info = gh->getItemInfo(action.id);
if (info && info->displayInfoId != 0) {
std::string icon = gh->getItemIconPath(info->displayInfoId);
if (!icon.empty()) {
lua_pushstring(L, icon.c_str());
return 1;
}
}
}
lua_pushnil(L);
return 1;
}
// IsCurrentAction(slot) → boolean
static int lua_IsCurrentAction(lua_State* L) {
// Currently no "active action" tracking; return false
(void)L;
lua_pushboolean(L, 0);
return 1;
}
// IsUsableAction(slot) → usable, notEnoughMana
static int lua_IsUsableAction(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushboolean(L, 0); lua_pushboolean(L, 0); return 2; }
int slot = static_cast<int>(luaL_checknumber(L, 1)) - 1;
const auto& bar = gh->getActionBar();
if (slot < 0 || slot >= static_cast<int>(bar.size()) || bar[slot].isEmpty()) {
lua_pushboolean(L, 0);
lua_pushboolean(L, 0);
return 2;
}
const auto& action = bar[slot];
bool usable = action.isReady();
bool noMana = false;
if (action.type == game::ActionBarSlot::SPELL) {
usable = usable && gh->getKnownSpells().count(action.id);
// Check power cost
if (usable && action.id != 0) {
auto spellData = gh->getSpellData(action.id);
if (spellData.manaCost > 0) {
auto pe = gh->getEntityManager().getEntity(gh->getPlayerGuid());
if (pe) {
auto* unit = dynamic_cast<game::Unit*>(pe.get());
if (unit && unit->getPower() < spellData.manaCost) {
noMana = true;
usable = false;
}
}
}
}
}
lua_pushboolean(L, usable ? 1 : 0);
lua_pushboolean(L, noMana ? 1 : 0);
return 2;
}
// IsActionInRange(slot) → 1 if in range, 0 if out, nil if no range check applicable
static int lua_IsActionInRange(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
int slot = static_cast<int>(luaL_checknumber(L, 1)) - 1;
const auto& bar = gh->getActionBar();
if (slot < 0 || slot >= static_cast<int>(bar.size()) || bar[slot].isEmpty()) {
lua_pushnil(L);
return 1;
}
const auto& action = bar[slot];
uint32_t spellId = 0;
if (action.type == game::ActionBarSlot::SPELL) {
spellId = action.id;
} else {
// Items/macros: no range check for now
lua_pushnil(L);
return 1;
}
if (spellId == 0) { return luaReturnNil(L); }
auto data = gh->getSpellData(spellId);
if (data.maxRange <= 0.0f) {
// Melee or self-cast spells: no range indicator
lua_pushnil(L);
return 1;
}
// Need a target to check range against
uint64_t targetGuid = gh->getTargetGuid();
if (targetGuid == 0) { return luaReturnNil(L); }
auto targetEnt = gh->getEntityManager().getEntity(targetGuid);
auto playerEnt = gh->getEntityManager().getEntity(gh->getPlayerGuid());
if (!targetEnt || !playerEnt) { return luaReturnNil(L); }
float dx = playerEnt->getX() - targetEnt->getX();
float dy = playerEnt->getY() - targetEnt->getY();
float dz = playerEnt->getZ() - targetEnt->getZ();
float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
lua_pushnumber(L, dist <= data.maxRange ? 1 : 0);
return 1;
}
// GetActionInfo(slot) → actionType, id, subType
static int lua_GetActionInfo(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return 0; }
int slot = static_cast<int>(luaL_checknumber(L, 1)) - 1;
const auto& bar = gh->getActionBar();
if (slot < 0 || slot >= static_cast<int>(bar.size()) || bar[slot].isEmpty()) {
return 0;
}
const auto& action = bar[slot];
switch (action.type) {
case game::ActionBarSlot::SPELL:
lua_pushstring(L, "spell");
lua_pushnumber(L, action.id);
lua_pushstring(L, "spell");
return 3;
case game::ActionBarSlot::ITEM:
lua_pushstring(L, "item");
lua_pushnumber(L, action.id);
lua_pushstring(L, "item");
return 3;
case game::ActionBarSlot::MACRO:
lua_pushstring(L, "macro");
lua_pushnumber(L, action.id);
lua_pushstring(L, "macro");
return 3;
default:
return 0;
}
}
// GetActionCount(slot) → count (item stack count or 0)
static int lua_GetActionCount(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
int slot = static_cast<int>(luaL_checknumber(L, 1)) - 1;
const auto& bar = gh->getActionBar();
if (slot < 0 || slot >= static_cast<int>(bar.size()) || bar[slot].isEmpty()) {
lua_pushnumber(L, 0);
return 1;
}
const auto& action = bar[slot];
if (action.type == game::ActionBarSlot::ITEM && action.id != 0) {
// Count items across backpack + bags
uint32_t count = 0;
const auto& inv = gh->getInventory();
for (int i = 0; i < inv.getBackpackSize(); ++i) {
const auto& s = inv.getBackpackSlot(i);
if (!s.empty() && s.item.itemId == action.id)
count += (s.item.stackCount > 0 ? s.item.stackCount : 1);
}
for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS; ++b) {
int bagSize = inv.getBagSize(b);
for (int i = 0; i < bagSize; ++i) {
const auto& s = inv.getBagSlot(b, i);
if (!s.empty() && s.item.itemId == action.id)
count += (s.item.stackCount > 0 ? s.item.stackCount : 1);
}
}
lua_pushnumber(L, count);
} else {
lua_pushnumber(L, 0);
}
return 1;
}
// GetActionCooldown(slot) → start, duration, enable
static int lua_GetActionCooldown(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 1); return 3; }
int slot = static_cast<int>(luaL_checknumber(L, 1)) - 1;
const auto& bar = gh->getActionBar();
if (slot < 0 || slot >= static_cast<int>(bar.size()) || bar[slot].isEmpty()) {
lua_pushnumber(L, 0);
lua_pushnumber(L, 0);
lua_pushnumber(L, 1);
return 3;
}
const auto& action = bar[slot];
if (action.cooldownRemaining > 0.0f) {
// WoW returns GetTime()-based start time; approximate
double now = 0;
lua_getglobal(L, "GetTime");
if (lua_isfunction(L, -1)) {
lua_call(L, 0, 1);
now = lua_tonumber(L, -1);
lua_pop(L, 1);
} else {
lua_pop(L, 1);
}
double start = now - (action.cooldownTotal - action.cooldownRemaining);
lua_pushnumber(L, start);
lua_pushnumber(L, action.cooldownTotal);
lua_pushnumber(L, 1);
} else if (action.type == game::ActionBarSlot::SPELL && gh->isGCDActive()) {
// No individual cooldown but GCD is active — show GCD sweep
float gcdRem = gh->getGCDRemaining();
float gcdTotal = gh->getGCDTotal();
double now = 0;
lua_getglobal(L, "GetTime");
if (lua_isfunction(L, -1)) { lua_call(L, 0, 1); now = lua_tonumber(L, -1); lua_pop(L, 1); }
else lua_pop(L, 1);
double elapsed = gcdTotal - gcdRem;
lua_pushnumber(L, now - elapsed);
lua_pushnumber(L, gcdTotal);
lua_pushnumber(L, 1);
} else {
lua_pushnumber(L, 0);
lua_pushnumber(L, 0);
lua_pushnumber(L, 1);
}
return 3;
}
// UseAction(slot, checkCursor, onSelf) — activate action bar slot (1-indexed)
static int lua_UseAction(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
int slot = static_cast<int>(luaL_checknumber(L, 1)) - 1;
const auto& bar = gh->getActionBar();
if (slot < 0 || slot >= static_cast<int>(bar.size()) || bar[slot].isEmpty()) return 0;
const auto& action = bar[slot];
if (action.type == game::ActionBarSlot::SPELL && action.isReady()) {
uint64_t target = gh->hasTarget() ? gh->getTargetGuid() : 0;
gh->castSpell(action.id, target);
} else if (action.type == game::ActionBarSlot::ITEM && action.id != 0) {
gh->useItemById(action.id);
}
// Macro execution requires GameScreen context; not available from pure Lua API
return 0;
}
// --- Cursor / Drag-Drop System ---
// Tracks what the player is "holding" on the cursor (spell, item, action).
static int lua_ClearCursor(lua_State* L) {
(void)L;
s_cursorType = CursorType::NONE;
s_cursorId = 0;
s_cursorSlot = 0;
s_cursorBag = -1;
return 0;
}
static int lua_GetCursorInfo(lua_State* L) {
switch (s_cursorType) {
case CursorType::SPELL:
lua_pushstring(L, "spell");
lua_pushnumber(L, 0); // bookSlotIndex
lua_pushstring(L, "spell"); // bookType
lua_pushnumber(L, s_cursorId); // spellId
return 4;
case CursorType::ITEM:
lua_pushstring(L, "item");
lua_pushnumber(L, s_cursorId);
return 2;
case CursorType::ACTION:
lua_pushstring(L, "action");
lua_pushnumber(L, s_cursorSlot);
return 2;
default:
return 0;
}
}
static int lua_CursorHasItem(lua_State* L) {
lua_pushboolean(L, s_cursorType == CursorType::ITEM ? 1 : 0);
return 1;
}
static int lua_CursorHasSpell(lua_State* L) {
lua_pushboolean(L, s_cursorType == CursorType::SPELL ? 1 : 0);
return 1;
}
// PickupAction(slot) — picks up an action from the action bar
static int lua_PickupAction(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
int slot = static_cast<int>(luaL_checknumber(L, 1));
const auto& bar = gh->getActionBar();
if (slot < 1 || slot > static_cast<int>(bar.size())) return 0;
const auto& action = bar[slot - 1];
if (action.isEmpty()) {
// Empty slot — if cursor has something, place it
if (s_cursorType == CursorType::SPELL && s_cursorId != 0) {
gh->setActionBarSlot(slot - 1, game::ActionBarSlot::SPELL, s_cursorId);
s_cursorType = CursorType::NONE;
s_cursorId = 0;
}
} else {
// Pick up existing action
s_cursorType = (action.type == game::ActionBarSlot::SPELL) ? CursorType::SPELL :
(action.type == game::ActionBarSlot::ITEM) ? CursorType::ITEM :
CursorType::ACTION;
s_cursorId = action.id;
s_cursorSlot = slot;
}
return 0;
}
// PlaceAction(slot) — places cursor content into an action bar slot
static int lua_PlaceAction(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
int slot = static_cast<int>(luaL_checknumber(L, 1));
if (slot < 1 || slot > static_cast<int>(gh->getActionBar().size())) return 0;
if (s_cursorType == CursorType::SPELL && s_cursorId != 0) {
gh->setActionBarSlot(slot - 1, game::ActionBarSlot::SPELL, s_cursorId);
} else if (s_cursorType == CursorType::ITEM && s_cursorId != 0) {
gh->setActionBarSlot(slot - 1, game::ActionBarSlot::ITEM, s_cursorId);
}
s_cursorType = CursorType::NONE;
s_cursorId = 0;
return 0;
}
// PickupSpell(bookSlot, bookType) — picks up a spell from the spellbook
static int lua_PickupSpell(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
int slot = static_cast<int>(luaL_checknumber(L, 1));
const auto& tabs = gh->getSpellBookTabs();
int idx = slot;
for (const auto& tab : tabs) {
if (idx <= static_cast<int>(tab.spellIds.size())) {
s_cursorType = CursorType::SPELL;
s_cursorId = tab.spellIds[idx - 1];
return 0;
}
idx -= static_cast<int>(tab.spellIds.size());
}
return 0;
}
// PickupSpellBookItem(bookSlot, bookType) — alias for PickupSpell
static int lua_PickupSpellBookItem(lua_State* L) {
return lua_PickupSpell(L);
}
// PickupContainerItem(bag, slot) — picks up an item from a bag
static int lua_PickupContainerItem(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
int bag = static_cast<int>(luaL_checknumber(L, 1));
int slot = static_cast<int>(luaL_checknumber(L, 2));
const auto& inv = gh->getInventory();
const game::ItemSlot* itemSlot = nullptr;
if (bag == 0 && slot >= 1 && slot <= inv.getBackpackSize()) {
itemSlot = &inv.getBackpackSlot(slot - 1);
} else if (bag >= 1 && bag <= 4) {
int bagSize = inv.getBagSize(bag - 1);
if (slot >= 1 && slot <= bagSize) {
itemSlot = &inv.getBagSlot(bag - 1, slot - 1);
}
}
if (itemSlot && !itemSlot->empty()) {
s_cursorType = CursorType::ITEM;
s_cursorId = itemSlot->item.itemId;
s_cursorBag = bag;
s_cursorSlot = slot;
}
return 0;
}
// PickupInventoryItem(slot) — picks up an equipped item
static int lua_PickupInventoryItem(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
int slot = static_cast<int>(luaL_checknumber(L, 1));
if (slot < 1 || slot > 19) return 0;
const auto& inv = gh->getInventory();
const auto& eq = inv.getEquipSlot(static_cast<game::EquipSlot>(slot - 1));
if (!eq.empty()) {
s_cursorType = CursorType::ITEM;
s_cursorId = eq.item.itemId;
s_cursorSlot = slot;
s_cursorBag = -1;
}
return 0;
}
// DeleteCursorItem() — destroys the item on cursor
static int lua_DeleteCursorItem(lua_State* L) {
(void)L;
s_cursorType = CursorType::NONE;
s_cursorId = 0;
return 0;
}
// AutoEquipCursorItem() — equip item from cursor
static int lua_AutoEquipCursorItem(lua_State* L) {
auto* gh = getGameHandler(L);
if (gh && s_cursorType == CursorType::ITEM && s_cursorId != 0) {
gh->useItemById(s_cursorId);
}
s_cursorType = CursorType::NONE;
s_cursorId = 0;
return 0;
}
// --- Frame System ---
// Minimal WoW-compatible frame objects with RegisterEvent/SetScript/GetScript.
// Frames are Lua tables with a metatable that provides methods.
// Frame method: frame:RegisterEvent("EVENT")
static int lua_IsShiftKeyDown(lua_State* L) {
lua_pushboolean(L, ImGui::GetIO().KeyShift ? 1 : 0);
return 1;
}
static int lua_IsControlKeyDown(lua_State* L) {
lua_pushboolean(L, ImGui::GetIO().KeyCtrl ? 1 : 0);
return 1;
}
static int lua_IsAltKeyDown(lua_State* L) {
lua_pushboolean(L, ImGui::GetIO().KeyAlt ? 1 : 0);
return 1;
}
// IsModifiedClick(action) → boolean
// Checks if a modifier key combo matches a named click action.
// Common actions: "CHATLINK" (shift-click), "DRESSUP" (ctrl-click),
// "SPLITSTACK" (shift-click), "SELFCAST" (alt-click)
static int lua_IsModifiedClick(lua_State* L) {
const char* action = luaL_optstring(L, 1, "");
std::string act(action);
for (char& c : act) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
const auto& io = ImGui::GetIO();
bool result = false;
if (act == "CHATLINK" || act == "SPLITSTACK")
result = io.KeyShift;
else if (act == "DRESSUP" || act == "COMPAREITEMS")
result = io.KeyCtrl;
else if (act == "SELFCAST" || act == "FOCUSCAST")
result = io.KeyAlt;
else if (act == "STICKYCAMERA")
result = io.KeyCtrl;
else
result = io.KeyShift; // Default: shift for unknown actions
lua_pushboolean(L, result ? 1 : 0);
return 1;
}
// GetModifiedClick(action) → key name ("SHIFT", "CTRL", "ALT", "NONE")
static int lua_GetModifiedClick(lua_State* L) {
const char* action = luaL_optstring(L, 1, "");
std::string act(action);
for (char& c : act) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
if (act == "CHATLINK" || act == "SPLITSTACK")
lua_pushstring(L, "SHIFT");
else if (act == "DRESSUP" || act == "COMPAREITEMS")
lua_pushstring(L, "CTRL");
else if (act == "SELFCAST" || act == "FOCUSCAST")
lua_pushstring(L, "ALT");
else
lua_pushstring(L, "SHIFT");
return 1;
}
static int lua_SetModifiedClick(lua_State* L) { (void)L; return 0; }
// --- Keybinding API ---
// Maps WoW binding names like "ACTIONBUTTON1" to key display strings like "1"
// GetBindingKey(command) → key1, key2 (or nil)
static int lua_GetBindingKey(lua_State* L) {
const char* cmd = luaL_checkstring(L, 1);
std::string command(cmd);
// Return intuitive default bindings for action buttons
if (command.find("ACTIONBUTTON") == 0) {
std::string num = command.substr(12);
int n = 0;
try { n = std::stoi(num); } catch(...) {}
if (n >= 1 && n <= 9) {
lua_pushstring(L, num.c_str());
return 1;
} else if (n == 10) {
lua_pushstring(L, "0");
return 1;
} else if (n == 11) {
lua_pushstring(L, "-");
return 1;
} else if (n == 12) {
lua_pushstring(L, "=");
return 1;
}
}
lua_pushnil(L);
return 1;
}
// GetBindingAction(key) → command (or nil)
static int lua_GetBindingAction(lua_State* L) {
const char* key = luaL_checkstring(L, 1);
std::string k(key);
// Simple reverse mapping for number keys
if (k.size() == 1 && k[0] >= '1' && k[0] <= '9') {
lua_pushstring(L, ("ACTIONBUTTON" + k).c_str());
return 1;
} else if (k == "0") {
lua_pushstring(L, "ACTIONBUTTON10");
return 1;
}
lua_pushnil(L);
return 1;
}
static int lua_GetNumBindings(lua_State* L) { return luaReturnZero(L); }
static int lua_GetBinding(lua_State* L) { (void)L; lua_pushnil(L); return 1; }
static int lua_SetBinding(lua_State* L) { (void)L; return 0; }
static int lua_SaveBindings(lua_State* L) { (void)L; return 0; }
static int lua_SetOverrideBindingClick(lua_State* L) { (void)L; return 0; }
static int lua_ClearOverrideBindings(lua_State* L) { (void)L; return 0; }
// Frame methods: SetPoint, SetSize, SetWidth, SetHeight, GetWidth, GetHeight, GetCenter, SetAlpha, GetAlpha
void registerActionLuaAPI(lua_State* L) {
static const struct { const char* name; lua_CFunction func; } api[] = {
{"HasAction", lua_HasAction},
{"GetActionTexture", lua_GetActionTexture},
{"IsCurrentAction", lua_IsCurrentAction},
{"IsUsableAction", lua_IsUsableAction},
{"IsActionInRange", lua_IsActionInRange},
{"GetActionInfo", lua_GetActionInfo},
{"GetActionCount", lua_GetActionCount},
{"GetActionCooldown", lua_GetActionCooldown},
{"UseAction", lua_UseAction},
{"PickupAction", lua_PickupAction},
{"PlaceAction", lua_PlaceAction},
{"PickupSpell", lua_PickupSpell},
{"PickupSpellBookItem", lua_PickupSpellBookItem},
{"PickupContainerItem", lua_PickupContainerItem},
{"PickupInventoryItem", lua_PickupInventoryItem},
{"ClearCursor", lua_ClearCursor},
{"GetCursorInfo", lua_GetCursorInfo},
{"CursorHasItem", lua_CursorHasItem},
{"CursorHasSpell", lua_CursorHasSpell},
{"DeleteCursorItem", lua_DeleteCursorItem},
{"AutoEquipCursorItem", lua_AutoEquipCursorItem},
{"IsShiftKeyDown", lua_IsShiftKeyDown},
{"IsControlKeyDown", lua_IsControlKeyDown},
{"IsAltKeyDown", lua_IsAltKeyDown},
{"IsModifiedClick", lua_IsModifiedClick},
{"GetModifiedClick", lua_GetModifiedClick},
{"SetModifiedClick", lua_SetModifiedClick},
{"GetBindingKey", lua_GetBindingKey},
{"GetBindingAction", lua_GetBindingAction},
{"GetNumBindings", lua_GetNumBindings},
{"GetBinding", lua_GetBinding},
{"SetBinding", lua_SetBinding},
{"SaveBindings", lua_SaveBindings},
{"SetOverrideBindingClick", lua_SetOverrideBindingClick},
{"ClearOverrideBindings", lua_ClearOverrideBindings},
{"GetActionBarPage", [](lua_State* L) -> int {
// Return current action bar page (1-6)
lua_getglobal(L, "__WoweeActionBarPage");
if (lua_isnil(L, -1)) { lua_pop(L, 1); lua_pushnumber(L, 1); }
return 1;
}},
{"ChangeActionBarPage", [](lua_State* L) -> int {
int page = static_cast<int>(luaL_checknumber(L, 1));
if (page < 1) page = 1;
if (page > 6) page = 6;
lua_pushnumber(L, page);
lua_setglobal(L, "__WoweeActionBarPage");
// Fire ACTIONBAR_PAGE_CHANGED via the frame event system
lua_getglobal(L, "__WoweeEvents");
if (!lua_isnil(L, -1)) {
lua_getfield(L, -1, "ACTIONBAR_PAGE_CHANGED");
if (!lua_isnil(L, -1)) {
int n = static_cast<int>(lua_objlen(L, -1));
for (int i = 1; i <= n; i++) {
lua_rawgeti(L, -1, i);
if (lua_isfunction(L, -1)) {
lua_pushstring(L, "ACTIONBAR_PAGE_CHANGED");
if (lua_pcall(L, 1, 0, 0) != 0) {
LOG_ERROR("LuaEngine: ACTIONBAR_PAGE_CHANGED handler error: ",
lua_tostring(L, -1) ? lua_tostring(L, -1) : "(unknown)");
lua_pop(L, 1);
}
} else lua_pop(L, 1);
}
}
lua_pop(L, 1);
}
lua_pop(L, 1);
return 0;
}},
{"HasPetUI", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushboolean(L, gh && gh->hasPet() ? 1 : 0);
return 1;
}},
{"GetPetActionInfo", [](lua_State* L) -> int {
// GetPetActionInfo(index) → name, subtext, texture, isToken, isActive, autoCastAllowed, autoCastEnabled
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1 || index > game::GameHandler::PET_ACTION_BAR_SLOTS) {
return luaReturnNil(L);
}
uint32_t packed = gh->getPetActionSlot(index - 1);
uint32_t spellId = packed & 0x00FFFFFF;
uint8_t actionType = static_cast<uint8_t>((packed >> 24) & 0xFF);
if (spellId == 0) { return luaReturnNil(L); }
const std::string& name = gh->getSpellName(spellId);
std::string iconPath = gh->getSpellIconPath(spellId);
lua_pushstring(L, name.empty() ? "Unknown" : name.c_str()); // name
lua_pushstring(L, ""); // subtext
lua_pushstring(L, iconPath.empty() ? "Interface\\Icons\\INV_Misc_QuestionMark" : iconPath.c_str()); // texture
lua_pushboolean(L, 0); // isToken
lua_pushboolean(L, (actionType & 0xC0) != 0 ? 1 : 0); // isActive
lua_pushboolean(L, 1); // autoCastAllowed
lua_pushboolean(L, gh->isPetSpellAutocast(spellId) ? 1 : 0); // autoCastEnabled
return 7;
}},
{"GetPetActionCooldown", [](lua_State* L) -> int {
lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 1);
return 3;
}},
{"PetAttack", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh && gh->hasPet() && gh->hasTarget())
gh->sendPetAction(0x00000007 | (2u << 24), gh->getTargetGuid()); // CMD_ATTACK
return 0;
}},
{"PetFollow", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh && gh->hasPet())
gh->sendPetAction(0x00000007 | (1u << 24), 0); // CMD_FOLLOW
return 0;
}},
{"PetWait", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh && gh->hasPet())
gh->sendPetAction(0x00000007 | (0u << 24), 0); // CMD_STAY
return 0;
}},
{"PetPassiveMode", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh && gh->hasPet())
gh->sendPetAction(0x00000007 | (0u << 16), 0); // REACT_PASSIVE
return 0;
}},
{"CastPetAction", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || !gh->hasPet() || index < 1 || index > game::GameHandler::PET_ACTION_BAR_SLOTS) return 0;
uint32_t packed = gh->getPetActionSlot(index - 1);
uint32_t spellId = packed & 0x00FFFFFF;
if (spellId != 0) {
uint64_t target = gh->hasTarget() ? gh->getTargetGuid() : gh->getPetGuid();
gh->sendPetAction(packed, target);
}
return 0;
}},
{"TogglePetAutocast", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || !gh->hasPet() || index < 1 || index > game::GameHandler::PET_ACTION_BAR_SLOTS) return 0;
uint32_t packed = gh->getPetActionSlot(index - 1);
uint32_t spellId = packed & 0x00FFFFFF;
if (spellId != 0) gh->togglePetSpellAutocast(spellId);
return 0;
}},
{"PetDismiss", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh && gh->hasPet())
gh->sendPetAction(0x00000007 | (3u << 24), 0); // CMD_DISMISS
return 0;
}},
{"IsPetAttackActive", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushboolean(L, gh && gh->getPetCommand() == 2 ? 1 : 0); // 2=attack
return 1;
}},
{"PetDefensiveMode", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh && gh->hasPet())
gh->sendPetAction(0x00000007 | (1u << 16), 0); // REACT_DEFENSIVE
return 0;
}},
};
for (const auto& [name, func] : api) {
lua_pushcfunction(L, func);
lua_setglobal(L, name);
}
}
} // namespace wowee::addons

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,892 @@
// lua_inventory_api.cpp — Items, containers, merchant, loot, equipment, trading, auction, and mail Lua API bindings.
// Extracted from lua_engine.cpp as part of §5.1 (Tame LuaEngine).
#include "addons/lua_api_helpers.hpp"
namespace wowee::addons {
static int lua_GetMoney(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? static_cast<double>(gh->getMoneyCopper()) : 0.0);
return 1;
}
// --- Merchant/Vendor API ---
static int lua_GetMerchantNumItems(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
lua_pushnumber(L, gh->getVendorItems().items.size());
return 1;
}
// GetMerchantItemInfo(index) → name, texture, price, stackCount, numAvailable, isUsable
static int lua_GetMerchantItemInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
const auto& items = gh->getVendorItems().items;
if (index > static_cast<int>(items.size())) { return luaReturnNil(L); }
const auto& vi = items[index - 1];
const auto* info = gh->getItemInfo(vi.itemId);
std::string name = info ? info->name : ("Item #" + std::to_string(vi.itemId));
lua_pushstring(L, name.c_str()); // name
// texture
std::string iconPath;
if (info && info->displayInfoId != 0)
iconPath = gh->getItemIconPath(info->displayInfoId);
if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str());
else lua_pushnil(L);
lua_pushnumber(L, vi.buyPrice); // price (copper)
lua_pushnumber(L, vi.stackCount > 0 ? vi.stackCount : 1); // stackCount
lua_pushnumber(L, vi.maxCount == -1 ? -1 : vi.maxCount); // numAvailable (-1=unlimited)
lua_pushboolean(L, 1); // isUsable
return 6;
}
// GetMerchantItemLink(index) → item link
static int lua_GetMerchantItemLink(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
const auto& items = gh->getVendorItems().items;
if (index > static_cast<int>(items.size())) { return luaReturnNil(L); }
const auto& vi = items[index - 1];
const auto* info = gh->getItemInfo(vi.itemId);
if (!info) { return luaReturnNil(L); }
const char* ch = (info->quality < 8) ? kQualHexAlpha[info->quality] : "ffffffff";
char link[256];
snprintf(link, sizeof(link), "|c%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r", ch, vi.itemId, info->name.c_str());
lua_pushstring(L, link);
return 1;
}
static int lua_CanMerchantRepair(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushboolean(L, gh && gh->getVendorItems().canRepair ? 1 : 0);
return 1;
}
// UnitStat(unit, statIndex) → base, effective, posBuff, negBuff
static int lua_GetItemInfo(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
uint32_t itemId = 0;
if (lua_isnumber(L, 1)) {
itemId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
// Try to parse "item:12345" link format
const char* s = lua_tostring(L, 1);
std::string str(s ? s : "");
auto pos = str.find("item:");
if (pos != std::string::npos) {
try { itemId = static_cast<uint32_t>(std::stoul(str.substr(pos + 5))); } catch (...) {}
}
}
if (itemId == 0) { return luaReturnNil(L); }
const auto* info = gh->getItemInfo(itemId);
if (!info) { return luaReturnNil(L); }
lua_pushstring(L, info->name.c_str()); // 1: name
// Build item link with quality-colored text
const char* colorHex = (info->quality < 8) ? kQualHexAlpha[info->quality] : "ffffffff";
char link[256];
snprintf(link, sizeof(link), "|c%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
colorHex, itemId, info->name.c_str());
lua_pushstring(L, link); // 2: link
lua_pushnumber(L, info->quality); // 3: quality
lua_pushnumber(L, info->itemLevel); // 4: iLevel
lua_pushnumber(L, info->requiredLevel); // 5: requiredLevel
// 6: class (type string) — map itemClass to display name
{
static constexpr const char* kItemClasses[] = {
"Consumable", "Bag", "Weapon", "Gem", "Armor", "Reagent", "Projectile",
"Trade Goods", "Generic", "Recipe", "Money", "Quiver", "Quest", "Key",
"Permanent", "Miscellaneous", "Glyph"
};
if (info->itemClass < 17)
lua_pushstring(L, kItemClasses[info->itemClass]);
else
lua_pushstring(L, "Miscellaneous");
}
// 7: subclass — use subclassName from ItemDef if available, else generic
lua_pushstring(L, info->subclassName.empty() ? "" : info->subclassName.c_str());
lua_pushnumber(L, info->maxStack > 0 ? info->maxStack : 1); // 8: maxStack
// 9: equipSlot — WoW inventoryType to INVTYPE string
{
static constexpr const char* kInvTypes[] = {
"", "INVTYPE_HEAD", "INVTYPE_NECK", "INVTYPE_SHOULDER",
"INVTYPE_BODY", "INVTYPE_CHEST", "INVTYPE_WAIST", "INVTYPE_LEGS",
"INVTYPE_FEET", "INVTYPE_WRIST", "INVTYPE_HAND", "INVTYPE_FINGER",
"INVTYPE_TRINKET", "INVTYPE_WEAPON", "INVTYPE_SHIELD",
"INVTYPE_RANGED", "INVTYPE_CLOAK", "INVTYPE_2HWEAPON",
"INVTYPE_BAG", "INVTYPE_TABARD", "INVTYPE_ROBE",
"INVTYPE_WEAPONMAINHAND", "INVTYPE_WEAPONOFFHAND", "INVTYPE_HOLDABLE",
"INVTYPE_AMMO", "INVTYPE_THROWN", "INVTYPE_RANGEDRIGHT",
"INVTYPE_QUIVER", "INVTYPE_RELIC"
};
uint32_t invType = info->inventoryType;
lua_pushstring(L, invType < 29 ? kInvTypes[invType] : "");
}
// 10: texture (icon path from ItemDisplayInfo.dbc)
if (info->displayInfoId != 0) {
std::string iconPath = gh->getItemIconPath(info->displayInfoId);
if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str());
else lua_pushnil(L);
} else {
lua_pushnil(L);
}
lua_pushnumber(L, info->sellPrice); // 11: vendorPrice
return 11;
}
// GetItemQualityColor(quality) → r, g, b, hex
// Quality: 0=Poor(gray), 1=Common(white), 2=Uncommon(green), 3=Rare(blue),
// 4=Epic(purple), 5=Legendary(orange), 6=Artifact(gold), 7=Heirloom(gold)
static int lua_GetItemQualityColor(lua_State* L) {
int q = static_cast<int>(luaL_checknumber(L, 1));
struct QC { float r, g, b; const char* hex; };
static const QC colors[] = {
{0.62f, 0.62f, 0.62f, "ff9d9d9d"}, // 0 Poor
{1.00f, 1.00f, 1.00f, "ffffffff"}, // 1 Common
{0.12f, 1.00f, 0.00f, "ff1eff00"}, // 2 Uncommon
{0.00f, 0.44f, 0.87f, "ff0070dd"}, // 3 Rare
{0.64f, 0.21f, 0.93f, "ffa335ee"}, // 4 Epic
{1.00f, 0.50f, 0.00f, "ffff8000"}, // 5 Legendary
{0.90f, 0.80f, 0.50f, "ffe6cc80"}, // 6 Artifact
{0.00f, 0.80f, 1.00f, "ff00ccff"}, // 7 Heirloom
};
if (q < 0 || q > 7) q = 1;
lua_pushnumber(L, colors[q].r);
lua_pushnumber(L, colors[q].g);
lua_pushnumber(L, colors[q].b);
lua_pushstring(L, colors[q].hex);
return 4;
}
// GetItemCount(itemId [, includeBank]) → count
static int lua_GetItemCount(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
uint32_t itemId = static_cast<uint32_t>(luaL_checknumber(L, 1));
const auto& inv = gh->getInventory();
uint32_t count = 0;
// Backpack
for (int i = 0; i < inv.getBackpackSize(); ++i) {
const auto& s = inv.getBackpackSlot(i);
if (!s.empty() && s.item.itemId == itemId)
count += (s.item.stackCount > 0 ? s.item.stackCount : 1);
}
// Bags 1-4
for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS; ++b) {
int sz = inv.getBagSize(b);
for (int i = 0; i < sz; ++i) {
const auto& s = inv.getBagSlot(b, i);
if (!s.empty() && s.item.itemId == itemId)
count += (s.item.stackCount > 0 ? s.item.stackCount : 1);
}
}
lua_pushnumber(L, count);
return 1;
}
// UseContainerItem(bag, slot) — use/equip an item from a bag
static int lua_UseContainerItem(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
int bag = static_cast<int>(luaL_checknumber(L, 1));
int slot = static_cast<int>(luaL_checknumber(L, 2));
const auto& inv = gh->getInventory();
const game::ItemSlot* itemSlot = nullptr;
if (bag == 0 && slot >= 1 && slot <= inv.getBackpackSize())
itemSlot = &inv.getBackpackSlot(slot - 1);
else if (bag >= 1 && bag <= 4) {
int sz = inv.getBagSize(bag - 1);
if (slot >= 1 && slot <= sz)
itemSlot = &inv.getBagSlot(bag - 1, slot - 1);
}
if (itemSlot && !itemSlot->empty())
gh->useItemById(itemSlot->item.itemId);
return 0;
}
// _GetItemTooltipData(itemId) → table with armor, bind, stats, damage, description
// Returns a Lua table with detailed item info for tooltip building
static int lua_GetItemTooltipData(lua_State* L) {
auto* gh = getGameHandler(L);
uint32_t itemId = static_cast<uint32_t>(luaL_checknumber(L, 1));
if (!gh || itemId == 0) { return luaReturnNil(L); }
const auto* info = gh->getItemInfo(itemId);
if (!info) { return luaReturnNil(L); }
lua_newtable(L);
// Unique / Heroic flags
if (info->maxCount == 1) { lua_pushboolean(L, 1); lua_setfield(L, -2, "isUnique"); }
if (info->itemFlags & 0x8) { lua_pushboolean(L, 1); lua_setfield(L, -2, "isHeroic"); }
if (info->itemFlags & 0x1000000) { lua_pushboolean(L, 1); lua_setfield(L, -2, "isUniqueEquipped"); }
// Bind type
lua_pushnumber(L, info->bindType);
lua_setfield(L, -2, "bindType");
// Armor
lua_pushnumber(L, info->armor);
lua_setfield(L, -2, "armor");
// Damage
lua_pushnumber(L, info->damageMin);
lua_setfield(L, -2, "damageMin");
lua_pushnumber(L, info->damageMax);
lua_setfield(L, -2, "damageMax");
lua_pushnumber(L, info->delayMs);
lua_setfield(L, -2, "speed");
// Primary stats
if (info->stamina != 0) { lua_pushnumber(L, info->stamina); lua_setfield(L, -2, "stamina"); }
if (info->strength != 0) { lua_pushnumber(L, info->strength); lua_setfield(L, -2, "strength"); }
if (info->agility != 0) { lua_pushnumber(L, info->agility); lua_setfield(L, -2, "agility"); }
if (info->intellect != 0) { lua_pushnumber(L, info->intellect); lua_setfield(L, -2, "intellect"); }
if (info->spirit != 0) { lua_pushnumber(L, info->spirit); lua_setfield(L, -2, "spirit"); }
// Description
if (!info->description.empty()) {
lua_pushstring(L, info->description.c_str());
lua_setfield(L, -2, "description");
}
// Required level
lua_pushnumber(L, info->requiredLevel);
lua_setfield(L, -2, "requiredLevel");
// Extra stats (hit, crit, haste, AP, SP, etc.) as array of {type, value} pairs
if (!info->extraStats.empty()) {
lua_newtable(L);
for (size_t i = 0; i < info->extraStats.size(); ++i) {
lua_newtable(L);
lua_pushnumber(L, info->extraStats[i].statType);
lua_setfield(L, -2, "type");
lua_pushnumber(L, info->extraStats[i].statValue);
lua_setfield(L, -2, "value");
lua_rawseti(L, -2, static_cast<int>(i) + 1);
}
lua_setfield(L, -2, "extraStats");
}
// Resistances
if (info->fireRes != 0) { lua_pushnumber(L, info->fireRes); lua_setfield(L, -2, "fireRes"); }
if (info->natureRes != 0) { lua_pushnumber(L, info->natureRes); lua_setfield(L, -2, "natureRes"); }
if (info->frostRes != 0) { lua_pushnumber(L, info->frostRes); lua_setfield(L, -2, "frostRes"); }
if (info->shadowRes != 0) { lua_pushnumber(L, info->shadowRes); lua_setfield(L, -2, "shadowRes"); }
if (info->arcaneRes != 0) { lua_pushnumber(L, info->arcaneRes); lua_setfield(L, -2, "arcaneRes"); }
// Item spell effects (Use: / Equip: / Chance on Hit:)
{
lua_newtable(L);
int spellCount = 0;
for (int i = 0; i < 5; ++i) {
if (info->spells[i].spellId == 0) continue;
++spellCount;
lua_newtable(L);
lua_pushnumber(L, info->spells[i].spellId);
lua_setfield(L, -2, "spellId");
lua_pushnumber(L, info->spells[i].spellTrigger);
lua_setfield(L, -2, "trigger");
// Get spell name for display
const std::string& sName = gh->getSpellName(info->spells[i].spellId);
if (!sName.empty()) { lua_pushstring(L, sName.c_str()); lua_setfield(L, -2, "name"); }
// Get description
const std::string& sDesc = gh->getSpellDescription(info->spells[i].spellId);
if (!sDesc.empty()) { lua_pushstring(L, sDesc.c_str()); lua_setfield(L, -2, "description"); }
lua_rawseti(L, -2, spellCount);
}
if (spellCount > 0) lua_setfield(L, -2, "itemSpells");
else lua_pop(L, 1);
}
// Gem sockets (WotLK/TBC)
int numSockets = 0;
for (int i = 0; i < 3; ++i) {
if (info->socketColor[i] != 0) ++numSockets;
}
if (numSockets > 0) {
lua_newtable(L);
for (int i = 0; i < 3; ++i) {
if (info->socketColor[i] != 0) {
lua_newtable(L);
lua_pushnumber(L, info->socketColor[i]);
lua_setfield(L, -2, "color");
lua_rawseti(L, -2, i + 1);
}
}
lua_setfield(L, -2, "sockets");
}
// Item set
if (info->itemSetId != 0) {
lua_pushnumber(L, info->itemSetId);
lua_setfield(L, -2, "itemSetId");
}
// Quest-starting item
if (info->startQuestId != 0) {
lua_pushboolean(L, 1);
lua_setfield(L, -2, "startsQuest");
}
return 1;
}
// --- Locale/Build/Realm info ---
static int lua_GetContainerNumSlots(lua_State* L) {
auto* gh = getGameHandler(L);
int container = static_cast<int>(luaL_checknumber(L, 1));
if (!gh) { return luaReturnZero(L); }
const auto& inv = gh->getInventory();
if (container == 0) {
lua_pushnumber(L, inv.getBackpackSize());
} else if (container >= 1 && container <= 4) {
lua_pushnumber(L, inv.getBagSize(container - 1));
} else {
lua_pushnumber(L, 0);
}
return 1;
}
// GetContainerItemInfo(container, slot) → texture, count, locked, quality, readable, lootable, link
static int lua_GetContainerItemInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int container = static_cast<int>(luaL_checknumber(L, 1));
int slot = static_cast<int>(luaL_checknumber(L, 2));
if (!gh) { return luaReturnNil(L); }
const auto& inv = gh->getInventory();
const game::ItemSlot* itemSlot = nullptr;
if (container == 0 && slot >= 1 && slot <= inv.getBackpackSize()) {
itemSlot = &inv.getBackpackSlot(slot - 1); // WoW uses 1-based
} else if (container >= 1 && container <= 4) {
int bagIdx = container - 1;
int bagSize = inv.getBagSize(bagIdx);
if (slot >= 1 && slot <= bagSize)
itemSlot = &inv.getBagSlot(bagIdx, slot - 1);
}
if (!itemSlot || itemSlot->empty()) { return luaReturnNil(L); }
// Get item info for quality/icon
const auto* info = gh->getItemInfo(itemSlot->item.itemId);
lua_pushnil(L); // texture (icon path — would need ItemDisplayInfo icon resolver)
lua_pushnumber(L, itemSlot->item.stackCount); // count
lua_pushboolean(L, 0); // locked
lua_pushnumber(L, info ? info->quality : 0); // quality
lua_pushboolean(L, 0); // readable
lua_pushboolean(L, 0); // lootable
// Build item link with quality color
std::string name = info ? info->name : ("Item #" + std::to_string(itemSlot->item.itemId));
uint32_t q = info ? info->quality : 0;
uint32_t qi = q < 8 ? q : 1u;
char link[256];
snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
kQualHexNoAlpha[qi], itemSlot->item.itemId, name.c_str());
lua_pushstring(L, link); // link
return 7;
}
// GetContainerItemLink(container, slot) → item link string
static int lua_GetContainerItemLink(lua_State* L) {
auto* gh = getGameHandler(L);
int container = static_cast<int>(luaL_checknumber(L, 1));
int slot = static_cast<int>(luaL_checknumber(L, 2));
if (!gh) { return luaReturnNil(L); }
const auto& inv = gh->getInventory();
const game::ItemSlot* itemSlot = nullptr;
if (container == 0 && slot >= 1 && slot <= inv.getBackpackSize()) {
itemSlot = &inv.getBackpackSlot(slot - 1);
} else if (container >= 1 && container <= 4) {
int bagIdx = container - 1;
int bagSize = inv.getBagSize(bagIdx);
if (slot >= 1 && slot <= bagSize)
itemSlot = &inv.getBagSlot(bagIdx, slot - 1);
}
if (!itemSlot || itemSlot->empty()) { return luaReturnNil(L); }
const auto* info = gh->getItemInfo(itemSlot->item.itemId);
std::string name = info ? info->name : ("Item #" + std::to_string(itemSlot->item.itemId));
uint32_t q = info ? info->quality : 0;
char link[256];
uint32_t qi = q < 8 ? q : 1u;
snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
kQualHexNoAlpha[qi], itemSlot->item.itemId, name.c_str());
lua_pushstring(L, link);
return 1;
}
// GetContainerNumFreeSlots(container) → numFreeSlots, bagType
static int lua_GetContainerNumFreeSlots(lua_State* L) {
auto* gh = getGameHandler(L);
int container = static_cast<int>(luaL_checknumber(L, 1));
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
const auto& inv = gh->getInventory();
int freeSlots = 0;
int totalSlots = 0;
if (container == 0) {
totalSlots = inv.getBackpackSize();
for (int i = 0; i < totalSlots; ++i)
if (inv.getBackpackSlot(i).empty()) ++freeSlots;
} else if (container >= 1 && container <= 4) {
totalSlots = inv.getBagSize(container - 1);
for (int i = 0; i < totalSlots; ++i)
if (inv.getBagSlot(container - 1, i).empty()) ++freeSlots;
}
lua_pushnumber(L, freeSlots);
lua_pushnumber(L, 0); // bagType (0 = normal)
return 2;
}
// --- Equipment Slot API ---
// WoW inventory slot IDs: 1=Head,2=Neck,3=Shoulders,4=Shirt,5=Chest,
// 6=Waist,7=Legs,8=Feet,9=Wrists,10=Hands,11=Ring1,12=Ring2,
// 13=Trinket1,14=Trinket2,15=Back,16=MainHand,17=OffHand,18=Ranged,19=Tabard
// GetInventorySlotInfo("slotName") → slotId, textureName, checkRelic
// Maps WoW slot names (e.g. "HeadSlot", "HEADSLOT") to inventory slot IDs
static int lua_GetInventorySlotInfo(lua_State* L) {
const char* name = luaL_checkstring(L, 1);
std::string slot(name);
// Normalize: uppercase, strip trailing "SLOT" if present
for (char& c : slot) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
if (slot.size() > 4 && slot.substr(slot.size() - 4) == "SLOT")
slot = slot.substr(0, slot.size() - 4);
// WoW inventory slots are 1-indexed
struct SlotMap { const char* name; int id; const char* texture; };
static const SlotMap mapping[] = {
{"HEAD", 1, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Head"},
{"NECK", 2, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Neck"},
{"SHOULDER", 3, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Shoulder"},
{"SHIRT", 4, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Shirt"},
{"CHEST", 5, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Chest"},
{"WAIST", 6, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Waist"},
{"LEGS", 7, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Legs"},
{"FEET", 8, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Feet"},
{"WRIST", 9, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Wrists"},
{"HANDS", 10, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Hands"},
{"FINGER0", 11, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Finger"},
{"FINGER1", 12, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Finger"},
{"TRINKET0", 13, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Trinket"},
{"TRINKET1", 14, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Trinket"},
{"BACK", 15, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Chest"},
{"MAINHAND", 16, "Interface\\PaperDoll\\UI-PaperDoll-Slot-MainHand"},
{"SECONDARYHAND",17, "Interface\\PaperDoll\\UI-PaperDoll-Slot-SecondaryHand"},
{"RANGED", 18, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Ranged"},
{"TABARD", 19, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Tabard"},
};
for (const auto& m : mapping) {
if (slot == m.name) {
lua_pushnumber(L, m.id);
lua_pushstring(L, m.texture);
lua_pushboolean(L, m.id == 18 ? 1 : 0); // checkRelic: only ranged slot
return 3;
}
}
luaL_error(L, "Unknown inventory slot: %s", name);
return 0;
}
static int lua_GetInventoryItemLink(lua_State* L) {
auto* gh = getGameHandler(L);
const char* uid = luaL_optstring(L, 1, "player");
int slotId = static_cast<int>(luaL_checknumber(L, 2));
if (!gh || slotId < 1 || slotId > 19) { return luaReturnNil(L); }
std::string uidStr(uid);
toLowerInPlace(uidStr);
if (uidStr != "player") { return luaReturnNil(L); }
const auto& inv = gh->getInventory();
const auto& slot = inv.getEquipSlot(static_cast<game::EquipSlot>(slotId - 1));
if (slot.empty()) { return luaReturnNil(L); }
const auto* info = gh->getItemInfo(slot.item.itemId);
std::string name = info ? info->name : slot.item.name;
uint32_t q = info ? info->quality : static_cast<uint32_t>(slot.item.quality);
uint32_t qi = q < 8 ? q : 1u;
char link[256];
snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
kQualHexNoAlpha[qi], slot.item.itemId, name.c_str());
lua_pushstring(L, link);
return 1;
}
static int lua_GetInventoryItemID(lua_State* L) {
auto* gh = getGameHandler(L);
const char* uid = luaL_optstring(L, 1, "player");
int slotId = static_cast<int>(luaL_checknumber(L, 2));
if (!gh || slotId < 1 || slotId > 19) { return luaReturnNil(L); }
std::string uidStr(uid);
toLowerInPlace(uidStr);
if (uidStr != "player") { return luaReturnNil(L); }
const auto& inv = gh->getInventory();
const auto& slot = inv.getEquipSlot(static_cast<game::EquipSlot>(slotId - 1));
if (slot.empty()) { return luaReturnNil(L); }
lua_pushnumber(L, slot.item.itemId);
return 1;
}
static int lua_GetInventoryItemTexture(lua_State* L) {
auto* gh = getGameHandler(L);
const char* uid = luaL_optstring(L, 1, "player");
int slotId = static_cast<int>(luaL_checknumber(L, 2));
if (!gh || slotId < 1 || slotId > 19) { return luaReturnNil(L); }
std::string uidStr(uid);
toLowerInPlace(uidStr);
if (uidStr != "player") { return luaReturnNil(L); }
const auto& inv = gh->getInventory();
const auto& slot = inv.getEquipSlot(static_cast<game::EquipSlot>(slotId - 1));
if (slot.empty()) { return luaReturnNil(L); }
lua_pushnil(L);
return 1;
}
static int lua_GetNumLootItems(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh || !gh->isLootWindowOpen()) { return luaReturnZero(L); }
lua_pushnumber(L, gh->getCurrentLoot().items.size());
return 1;
}
// GetLootSlotInfo(slot) → texture, name, quantity, quality, locked
static int lua_GetLootSlotInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int slot = static_cast<int>(luaL_checknumber(L, 1)); // 1-indexed
if (!gh || !gh->isLootWindowOpen()) {
return luaReturnNil(L);
}
const auto& loot = gh->getCurrentLoot();
if (slot < 1 || slot > static_cast<int>(loot.items.size())) {
return luaReturnNil(L);
}
const auto& item = loot.items[slot - 1];
const auto* info = gh->getItemInfo(item.itemId);
// texture (icon path from ItemDisplayInfo.dbc)
std::string icon;
if (info && info->displayInfoId != 0) {
icon = gh->getItemIconPath(info->displayInfoId);
}
if (!icon.empty()) lua_pushstring(L, icon.c_str());
else lua_pushnil(L);
// name
if (info && !info->name.empty()) lua_pushstring(L, info->name.c_str());
else lua_pushstring(L, ("Item #" + std::to_string(item.itemId)).c_str());
lua_pushnumber(L, item.count); // quantity
lua_pushnumber(L, info ? info->quality : 1); // quality
lua_pushboolean(L, 0); // locked (not tracked)
return 5;
}
// GetLootSlotLink(slot) → itemLink
static int lua_GetLootSlotLink(lua_State* L) {
auto* gh = getGameHandler(L);
int slot = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || !gh->isLootWindowOpen()) { return luaReturnNil(L); }
const auto& loot = gh->getCurrentLoot();
if (slot < 1 || slot > static_cast<int>(loot.items.size())) {
return luaReturnNil(L);
}
const auto& item = loot.items[slot - 1];
const auto* info = gh->getItemInfo(item.itemId);
if (!info || info->name.empty()) { return luaReturnNil(L); }
uint32_t qi = info->quality < 8 ? info->quality : 1u;
char link[256];
snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
kQualHexNoAlpha[qi], item.itemId, info->name.c_str());
lua_pushstring(L, link);
return 1;
}
// LootSlot(slot) — take item from loot
static int lua_LootSlot(lua_State* L) {
auto* gh = getGameHandler(L);
int slot = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || !gh->isLootWindowOpen()) return 0;
const auto& loot = gh->getCurrentLoot();
if (slot < 1 || slot > static_cast<int>(loot.items.size())) return 0;
gh->lootItem(loot.items[slot - 1].slotIndex);
return 0;
}
// CloseLoot() — close loot window
static int lua_CloseLoot(lua_State* L) {
auto* gh = getGameHandler(L);
if (gh) gh->closeLoot();
return 0;
}
// GetLootMethod() → "freeforall"|"roundrobin"|"master"|"group"|"needbeforegreed", partyLoot, raidLoot
static int lua_GetLootMethod(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushstring(L, "freeforall"); lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 3; }
const auto& pd = gh->getPartyData();
const char* method = "freeforall";
switch (pd.lootMethod) {
case 0: method = "freeforall"; break;
case 1: method = "roundrobin"; break;
case 2: method = "master"; break;
case 3: method = "group"; break;
case 4: method = "needbeforegreed"; break;
}
lua_pushstring(L, method);
lua_pushnumber(L, 0); // partyLootMaster (index)
lua_pushnumber(L, 0); // raidLootMaster (index)
return 3;
}
// --- Additional WoW API ---
static int lua_GetItemLink(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
uint32_t itemId = static_cast<uint32_t>(luaL_checknumber(L, 1));
if (itemId == 0) { return luaReturnNil(L); }
const auto* info = gh->getItemInfo(itemId);
if (!info || info->name.empty()) { return luaReturnNil(L); }
uint32_t qi = info->quality < 8 ? info->quality : 1u;
char link[256];
snprintf(link, sizeof(link), "|cff%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r",
kQualHexNoAlpha[qi], itemId, info->name.c_str());
lua_pushstring(L, link);
return 1;
}
// GetSpellLink(spellIdOrName) → "|cFFxxxxxx|Hspell:ID|h[Name]|h|r"
void registerInventoryLuaAPI(lua_State* L) {
static const struct { const char* name; lua_CFunction func; } api[] = {
{"GetMoney", lua_GetMoney},
{"GetMerchantNumItems", lua_GetMerchantNumItems},
{"GetMerchantItemInfo", lua_GetMerchantItemInfo},
{"GetMerchantItemLink", lua_GetMerchantItemLink},
{"CanMerchantRepair", lua_CanMerchantRepair},
{"GetItemInfo", lua_GetItemInfo},
{"GetItemQualityColor", lua_GetItemQualityColor},
{"_GetItemTooltipData", lua_GetItemTooltipData},
{"GetItemCount", lua_GetItemCount},
{"UseContainerItem", lua_UseContainerItem},
{"GetContainerNumSlots", lua_GetContainerNumSlots},
{"GetContainerItemInfo", lua_GetContainerItemInfo},
{"GetContainerItemLink", lua_GetContainerItemLink},
{"GetContainerNumFreeSlots", lua_GetContainerNumFreeSlots},
{"GetInventorySlotInfo", lua_GetInventorySlotInfo},
{"GetInventoryItemLink", lua_GetInventoryItemLink},
{"GetInventoryItemID", lua_GetInventoryItemID},
{"GetInventoryItemTexture", lua_GetInventoryItemTexture},
{"GetItemLink", lua_GetItemLink},
{"GetNumLootItems", lua_GetNumLootItems},
{"GetLootSlotInfo", lua_GetLootSlotInfo},
{"GetLootSlotLink", lua_GetLootSlotLink},
{"LootSlot", lua_LootSlot},
{"CloseLoot", lua_CloseLoot},
{"GetLootMethod", lua_GetLootMethod},
{"BuyMerchantItem", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
int count = static_cast<int>(luaL_optnumber(L, 2, 1));
if (!gh || index < 1) return 0;
const auto& items = gh->getVendorItems().items;
if (index > static_cast<int>(items.size())) return 0;
const auto& vi = items[index - 1];
gh->buyItem(gh->getVendorGuid(), vi.itemId, vi.slot, count);
return 0;
}},
{"SellContainerItem", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int bag = static_cast<int>(luaL_checknumber(L, 1));
int slot = static_cast<int>(luaL_checknumber(L, 2));
if (!gh) return 0;
if (bag == 0) gh->sellItemBySlot(slot - 1);
else if (bag >= 1 && bag <= 4) gh->sellItemInBag(bag - 1, slot - 1);
return 0;
}},
{"RepairAllItems", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh && gh->getVendorItems().canRepair) {
bool useGuildBank = lua_toboolean(L, 1) != 0;
gh->repairAll(gh->getVendorGuid(), useGuildBank);
}
return 0;
}},
{"UnequipItemSlot", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int slot = static_cast<int>(luaL_checknumber(L, 1));
if (gh && slot >= 1 && slot <= 19)
gh->unequipToBackpack(static_cast<game::EquipSlot>(slot - 1));
return 0;
}},
{"AcceptTrade", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->acceptTrade();
return 0;
}},
{"CancelTrade", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh && gh->isTradeOpen()) gh->cancelTrade();
return 0;
}},
{"InitiateTrade", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* uid = luaL_checkstring(L, 1);
if (gh) {
uint64_t guid = resolveUnitGuid(gh, std::string(uid));
if (guid != 0) gh->initiateTrade(guid);
}
return 0;
}},
{"GetNumAuctionItems", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* listType = luaL_optstring(L, 1, "list");
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
std::string t(listType);
const game::AuctionListResult* r = nullptr;
if (t == "list" || t == "browse") r = &gh->getAuctionBrowseResults();
else if (t == "owner") r = &gh->getAuctionOwnerResults();
else if (t == "bidder") r = &gh->getAuctionBidderResults();
lua_pushnumber(L, r ? r->auctions.size() : 0);
lua_pushnumber(L, r ? r->totalCount : 0);
return 2;
}},
{"GetAuctionItemInfo", [](lua_State* L) -> int {
// GetAuctionItemInfo(type, index) → name, texture, count, quality, canUse, level, levelColHeader, minBid, minIncrement, buyoutPrice, bidAmount, highBidder, bidderFullName, owner, ownerFullName, saleStatus, itemId
auto* gh = getGameHandler(L);
const char* listType = luaL_checkstring(L, 1);
int index = static_cast<int>(luaL_checknumber(L, 2));
if (!gh || index < 1) { return luaReturnNil(L); }
std::string t(listType);
const game::AuctionListResult* r = nullptr;
if (t == "list") r = &gh->getAuctionBrowseResults();
else if (t == "owner") r = &gh->getAuctionOwnerResults();
else if (t == "bidder") r = &gh->getAuctionBidderResults();
if (!r || index > static_cast<int>(r->auctions.size())) { return luaReturnNil(L); }
const auto& a = r->auctions[index - 1];
const auto* info = gh->getItemInfo(a.itemEntry);
std::string name = info ? info->name : "Item #" + std::to_string(a.itemEntry);
std::string icon = (info && info->displayInfoId != 0) ? gh->getItemIconPath(info->displayInfoId) : "";
uint32_t quality = info ? info->quality : 1;
lua_pushstring(L, name.c_str()); // name
lua_pushstring(L, icon.empty() ? "Interface\\Icons\\INV_Misc_QuestionMark" : icon.c_str()); // texture
lua_pushnumber(L, a.stackCount); // count
lua_pushnumber(L, quality); // quality
lua_pushboolean(L, 1); // canUse
lua_pushnumber(L, info ? info->requiredLevel : 0); // level
lua_pushstring(L, ""); // levelColHeader
lua_pushnumber(L, a.startBid); // minBid
lua_pushnumber(L, a.minBidIncrement); // minIncrement
lua_pushnumber(L, a.buyoutPrice); // buyoutPrice
lua_pushnumber(L, a.currentBid); // bidAmount
lua_pushboolean(L, a.bidderGuid != 0 ? 1 : 0); // highBidder
lua_pushstring(L, ""); // bidderFullName
lua_pushstring(L, ""); // owner
lua_pushstring(L, ""); // ownerFullName
lua_pushnumber(L, 0); // saleStatus
lua_pushnumber(L, a.itemEntry); // itemId
return 17;
}},
{"GetAuctionItemTimeLeft", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* listType = luaL_checkstring(L, 1);
int index = static_cast<int>(luaL_checknumber(L, 2));
if (!gh || index < 1) { lua_pushnumber(L, 4); return 1; }
std::string t(listType);
const game::AuctionListResult* r = nullptr;
if (t == "list") r = &gh->getAuctionBrowseResults();
else if (t == "owner") r = &gh->getAuctionOwnerResults();
else if (t == "bidder") r = &gh->getAuctionBidderResults();
if (!r || index > static_cast<int>(r->auctions.size())) { lua_pushnumber(L, 4); return 1; }
// Return 1=short(<30m), 2=medium(<2h), 3=long(<12h), 4=very long(>12h)
uint32_t ms = r->auctions[index - 1].timeLeftMs;
int cat = (ms < 1800000) ? 1 : (ms < 7200000) ? 2 : (ms < 43200000) ? 3 : 4;
lua_pushnumber(L, cat);
return 1;
}},
{"GetAuctionItemLink", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* listType = luaL_checkstring(L, 1);
int index = static_cast<int>(luaL_checknumber(L, 2));
if (!gh || index < 1) { return luaReturnNil(L); }
std::string t(listType);
const game::AuctionListResult* r = nullptr;
if (t == "list") r = &gh->getAuctionBrowseResults();
else if (t == "owner") r = &gh->getAuctionOwnerResults();
else if (t == "bidder") r = &gh->getAuctionBidderResults();
if (!r || index > static_cast<int>(r->auctions.size())) { return luaReturnNil(L); }
uint32_t itemId = r->auctions[index - 1].itemEntry;
const auto* info = gh->getItemInfo(itemId);
if (!info) { return luaReturnNil(L); }
const char* ch = (info->quality < 8) ? kQualHexAlpha[info->quality] : "ffffffff";
char link[256];
snprintf(link, sizeof(link), "|c%s|Hitem:%u:0:0:0:0:0:0:0|h[%s]|h|r", ch, itemId, info->name.c_str());
lua_pushstring(L, link);
return 1;
}},
{"GetInboxNumItems", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getMailInbox().size() : 0);
return 1;
}},
{"GetInboxHeaderInfo", [](lua_State* L) -> int {
// GetInboxHeaderInfo(index) → packageIcon, stationeryIcon, sender, subject, money, COD, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
const auto& inbox = gh->getMailInbox();
if (index > static_cast<int>(inbox.size())) { return luaReturnNil(L); }
const auto& mail = inbox[index - 1];
lua_pushstring(L, "Interface\\Icons\\INV_Letter_15"); // packageIcon
lua_pushstring(L, "Interface\\Icons\\INV_Letter_15"); // stationeryIcon
lua_pushstring(L, mail.senderName.c_str()); // sender
lua_pushstring(L, mail.subject.c_str()); // subject
lua_pushnumber(L, mail.money); // money (copper)
lua_pushnumber(L, mail.cod); // COD
lua_pushnumber(L, mail.expirationTime / 86400.0f); // daysLeft
lua_pushboolean(L, mail.attachments.empty() ? 0 : 1); // hasItem
lua_pushboolean(L, mail.read ? 1 : 0); // wasRead
lua_pushboolean(L, 0); // wasReturned
lua_pushboolean(L, !mail.body.empty() ? 1 : 0); // textCreated
lua_pushboolean(L, mail.messageType == 0 ? 1 : 0); // canReply (player mail only)
lua_pushboolean(L, 0); // isGM
return 13;
}},
{"GetInboxText", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
const auto& inbox = gh->getMailInbox();
if (index > static_cast<int>(inbox.size())) { return luaReturnNil(L); }
lua_pushstring(L, inbox[index - 1].body.c_str());
return 1;
}},
{"HasNewMail", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnFalse(L); }
bool hasNew = false;
for (const auto& m : gh->getMailInbox()) {
if (!m.read) { hasNew = true; break; }
}
lua_pushboolean(L, hasNew ? 1 : 0);
return 1;
}},
};
for (const auto& [name, func] : api) {
lua_pushcfunction(L, func);
lua_setglobal(L, name);
}
}
} // namespace wowee::addons

View file

@ -0,0 +1,564 @@
// lua_quest_api.cpp — Quest log, skills, talents, glyphs, and achievements Lua API bindings.
// Extracted from lua_engine.cpp as part of §5.1 (Tame LuaEngine).
#include "addons/lua_api_helpers.hpp"
namespace wowee::addons {
static int lua_GetNumQuestLogEntries(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
const auto& ql = gh->getQuestLog();
lua_pushnumber(L, ql.size()); // numEntries
lua_pushnumber(L, 0); // numQuests (headers not tracked)
return 2;
}
// GetQuestLogTitle(index) → title, level, suggestedGroup, isHeader, isCollapsed, isComplete, frequency, questID
static int lua_GetQuestLogTitle(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
const auto& ql = gh->getQuestLog();
if (index > static_cast<int>(ql.size())) { return luaReturnNil(L); }
const auto& q = ql[index - 1]; // 1-based
lua_pushstring(L, q.title.c_str()); // title
lua_pushnumber(L, 0); // level (not tracked)
lua_pushnumber(L, 0); // suggestedGroup
lua_pushboolean(L, 0); // isHeader
lua_pushboolean(L, 0); // isCollapsed
lua_pushboolean(L, q.complete); // isComplete
lua_pushnumber(L, 0); // frequency
lua_pushnumber(L, q.questId); // questID
return 8;
}
// GetQuestLogQuestText(index) → description, objectives
static int lua_GetQuestLogQuestText(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
const auto& ql = gh->getQuestLog();
if (index > static_cast<int>(ql.size())) { return luaReturnNil(L); }
const auto& q = ql[index - 1];
lua_pushstring(L, ""); // description (not stored)
lua_pushstring(L, q.objectives.c_str()); // objectives
return 2;
}
// IsQuestComplete(questID) → boolean
static int lua_IsQuestComplete(lua_State* L) {
auto* gh = getGameHandler(L);
uint32_t questId = static_cast<uint32_t>(luaL_checknumber(L, 1));
if (!gh) { return luaReturnFalse(L); }
for (const auto& q : gh->getQuestLog()) {
if (q.questId == questId) {
lua_pushboolean(L, q.complete);
return 1;
}
}
lua_pushboolean(L, 0);
return 1;
}
// SelectQuestLogEntry(index) — select a quest in the quest log
static int lua_SelectQuestLogEntry(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (gh) gh->setSelectedQuestLogIndex(index);
return 0;
}
// GetQuestLogSelection() → index
static int lua_GetQuestLogSelection(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getSelectedQuestLogIndex() : 0);
return 1;
}
// GetNumQuestWatches() → count
static int lua_GetNumQuestWatches(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getTrackedQuestIds().size() : 0);
return 1;
}
// GetQuestIndexForWatch(watchIndex) → questLogIndex
// Maps the Nth watched quest to its quest log index (1-based)
static int lua_GetQuestIndexForWatch(lua_State* L) {
auto* gh = getGameHandler(L);
int watchIdx = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || watchIdx < 1) { return luaReturnNil(L); }
const auto& ql = gh->getQuestLog();
const auto& tracked = gh->getTrackedQuestIds();
int found = 0;
for (size_t i = 0; i < ql.size(); ++i) {
if (tracked.count(ql[i].questId)) {
found++;
if (found == watchIdx) {
lua_pushnumber(L, static_cast<int>(i) + 1); // 1-based
return 1;
}
}
}
lua_pushnil(L);
return 1;
}
// AddQuestWatch(questLogIndex) — add a quest to the watch list
static int lua_AddQuestWatch(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) return 0;
const auto& ql = gh->getQuestLog();
if (index <= static_cast<int>(ql.size())) {
gh->setQuestTracked(ql[index - 1].questId, true);
}
return 0;
}
// RemoveQuestWatch(questLogIndex) — remove a quest from the watch list
static int lua_RemoveQuestWatch(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) return 0;
const auto& ql = gh->getQuestLog();
if (index <= static_cast<int>(ql.size())) {
gh->setQuestTracked(ql[index - 1].questId, false);
}
return 0;
}
// IsQuestWatched(questLogIndex) → boolean
static int lua_IsQuestWatched(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnFalse(L); }
const auto& ql = gh->getQuestLog();
if (index <= static_cast<int>(ql.size())) {
lua_pushboolean(L, gh->isQuestTracked(ql[index - 1].questId) ? 1 : 0);
} else {
lua_pushboolean(L, 0);
}
return 1;
}
// GetQuestLink(questLogIndex) → "|cff...|Hquest:id:level|h[title]|h|r"
static int lua_GetQuestLink(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
const auto& ql = gh->getQuestLog();
if (index > static_cast<int>(ql.size())) { return luaReturnNil(L); }
const auto& q = ql[index - 1];
// Yellow quest link format matching WoW
std::string link = "|cff808000|Hquest:" + std::to_string(q.questId) +
":0|h[" + q.title + "]|h|r";
lua_pushstring(L, link.c_str());
return 1;
}
// GetNumQuestLeaderBoards(questLogIndex) → count of objectives
static int lua_GetNumQuestLeaderBoards(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnZero(L); }
const auto& ql = gh->getQuestLog();
if (index > static_cast<int>(ql.size())) { return luaReturnZero(L); }
const auto& q = ql[index - 1];
int count = 0;
for (const auto& ko : q.killObjectives) {
if (ko.npcOrGoId != 0 || ko.required > 0) ++count;
}
for (const auto& io : q.itemObjectives) {
if (io.itemId != 0 || io.required > 0) ++count;
}
lua_pushnumber(L, count);
return 1;
}
// GetQuestLogLeaderBoard(objIndex, questLogIndex) → text, type, finished
// objIndex is 1-based within the quest's objectives
static int lua_GetQuestLogLeaderBoard(lua_State* L) {
auto* gh = getGameHandler(L);
int objIdx = static_cast<int>(luaL_checknumber(L, 1));
int questIdx = static_cast<int>(luaL_optnumber(L, 2,
gh ? gh->getSelectedQuestLogIndex() : 0));
if (!gh || questIdx < 1 || objIdx < 1) { return luaReturnNil(L); }
const auto& ql = gh->getQuestLog();
if (questIdx > static_cast<int>(ql.size())) { return luaReturnNil(L); }
const auto& q = ql[questIdx - 1];
// Build ordered list: kill objectives first, then item objectives
int cur = 0;
for (int i = 0; i < 4; ++i) {
if (q.killObjectives[i].npcOrGoId == 0 && q.killObjectives[i].required == 0) continue;
++cur;
if (cur == objIdx) {
// Get current count from killCounts map (keyed by abs(npcOrGoId))
uint32_t key = static_cast<uint32_t>(std::abs(q.killObjectives[i].npcOrGoId));
uint32_t current = 0;
auto it = q.killCounts.find(key);
if (it != q.killCounts.end()) current = it->second.first;
uint32_t required = q.killObjectives[i].required;
bool finished = (current >= required);
// Build display text like "Kobold Vermin slain: 3/8"
std::string text = (q.killObjectives[i].npcOrGoId < 0 ? "Object" : "Creature")
+ std::string(" slain: ") + std::to_string(current) + "/" + std::to_string(required);
lua_pushstring(L, text.c_str());
lua_pushstring(L, q.killObjectives[i].npcOrGoId < 0 ? "object" : "monster");
lua_pushboolean(L, finished ? 1 : 0);
return 3;
}
}
for (int i = 0; i < 6; ++i) {
if (q.itemObjectives[i].itemId == 0 && q.itemObjectives[i].required == 0) continue;
++cur;
if (cur == objIdx) {
uint32_t current = 0;
auto it = q.itemCounts.find(q.itemObjectives[i].itemId);
if (it != q.itemCounts.end()) current = it->second;
uint32_t required = q.itemObjectives[i].required;
bool finished = (current >= required);
// Get item name if available
std::string itemName;
const auto* info = gh->getItemInfo(q.itemObjectives[i].itemId);
if (info && !info->name.empty()) itemName = info->name;
else itemName = "Item #" + std::to_string(q.itemObjectives[i].itemId);
std::string text = itemName + ": " + std::to_string(current) + "/" + std::to_string(required);
lua_pushstring(L, text.c_str());
lua_pushstring(L, "item");
lua_pushboolean(L, finished ? 1 : 0);
return 3;
}
}
lua_pushnil(L);
return 1;
}
// ExpandQuestHeader / CollapseQuestHeader — no-ops (flat quest list, no headers)
static int lua_ExpandQuestHeader(lua_State* L) { (void)L; return 0; }
static int lua_CollapseQuestHeader(lua_State* L) { (void)L; return 0; }
// GetQuestLogSpecialItemInfo(questLogIndex) — returns nil (no special items)
static int lua_GetQuestLogSpecialItemInfo(lua_State* L) { (void)L; lua_pushnil(L); return 1; }
static int lua_GetNumSkillLines(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
lua_pushnumber(L, gh->getPlayerSkills().size());
return 1;
}
// GetSkillLineInfo(index) → skillName, isHeader, isExpanded, skillRank, numTempPoints, skillModifier, skillMaxRank, isAbandonable, stepCost, rankCost, minLevel, skillCostType
static int lua_GetSkillLineInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) {
lua_pushnil(L);
return 1;
}
const auto& skills = gh->getPlayerSkills();
if (index > static_cast<int>(skills.size())) {
lua_pushnil(L);
return 1;
}
// Skills are in a map — iterate to the Nth entry
auto it = skills.begin();
std::advance(it, index - 1);
const auto& skill = it->second;
std::string name = gh->getSkillName(skill.skillId);
if (name.empty()) name = "Skill " + std::to_string(skill.skillId);
lua_pushstring(L, name.c_str()); // 1: skillName
lua_pushboolean(L, 0); // 2: isHeader (false — flat list)
lua_pushboolean(L, 1); // 3: isExpanded
lua_pushnumber(L, skill.effectiveValue()); // 4: skillRank
lua_pushnumber(L, skill.bonusTemp); // 5: numTempPoints
lua_pushnumber(L, skill.bonusPerm); // 6: skillModifier
lua_pushnumber(L, skill.maxValue); // 7: skillMaxRank
lua_pushboolean(L, 0); // 8: isAbandonable
lua_pushnumber(L, 0); // 9: stepCost
lua_pushnumber(L, 0); // 10: rankCost
lua_pushnumber(L, 0); // 11: minLevel
lua_pushnumber(L, 0); // 12: skillCostType
return 12;
}
// --- Friends/Ignore API ---
static int lua_GetNumTalentTabs(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
// Count tabs matching the player's class
uint8_t classId = gh->getPlayerClass();
uint32_t classMask = (classId > 0) ? (1u << (classId - 1)) : 0;
int count = 0;
for (const auto& [tabId, tab] : gh->getAllTalentTabs()) {
if (tab.classMask & classMask) count++;
}
lua_pushnumber(L, count);
return 1;
}
// GetTalentTabInfo(tabIndex) → name, iconTexture, pointsSpent, background
static int lua_GetTalentTabInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int tabIndex = static_cast<int>(luaL_checknumber(L, 1)); // 1-indexed
if (!gh || tabIndex < 1) {
return luaReturnNil(L);
}
uint8_t classId = gh->getPlayerClass();
uint32_t classMask = (classId > 0) ? (1u << (classId - 1)) : 0;
// Find the Nth tab for this class (sorted by orderIndex)
std::vector<const game::GameHandler::TalentTabEntry*> classTabs;
for (const auto& [tabId, tab] : gh->getAllTalentTabs()) {
if (tab.classMask & classMask) classTabs.push_back(&tab);
}
std::sort(classTabs.begin(), classTabs.end(),
[](const auto* a, const auto* b) { return a->orderIndex < b->orderIndex; });
if (tabIndex > static_cast<int>(classTabs.size())) {
return luaReturnNil(L);
}
const auto* tab = classTabs[tabIndex - 1];
// Count points spent in this tab
int pointsSpent = 0;
const auto& learned = gh->getLearnedTalents();
for (const auto& [talentId, rank] : learned) {
const auto* entry = gh->getTalentEntry(talentId);
if (entry && entry->tabId == tab->tabId) pointsSpent += rank;
}
lua_pushstring(L, tab->name.c_str()); // 1: name
lua_pushnil(L); // 2: iconTexture (not resolved)
lua_pushnumber(L, pointsSpent); // 3: pointsSpent
lua_pushstring(L, tab->backgroundFile.c_str()); // 4: background
return 4;
}
// GetNumTalents(tabIndex) → count
static int lua_GetNumTalents(lua_State* L) {
auto* gh = getGameHandler(L);
int tabIndex = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || tabIndex < 1) { return luaReturnZero(L); }
uint8_t classId = gh->getPlayerClass();
uint32_t classMask = (classId > 0) ? (1u << (classId - 1)) : 0;
std::vector<const game::GameHandler::TalentTabEntry*> classTabs;
for (const auto& [tabId, tab] : gh->getAllTalentTabs()) {
if (tab.classMask & classMask) classTabs.push_back(&tab);
}
std::sort(classTabs.begin(), classTabs.end(),
[](const auto* a, const auto* b) { return a->orderIndex < b->orderIndex; });
if (tabIndex > static_cast<int>(classTabs.size())) {
return luaReturnZero(L);
}
uint32_t targetTabId = classTabs[tabIndex - 1]->tabId;
int count = 0;
for (const auto& [talentId, entry] : gh->getAllTalents()) {
if (entry.tabId == targetTabId) count++;
}
lua_pushnumber(L, count);
return 1;
}
// GetTalentInfo(tabIndex, talentIndex) → name, iconTexture, tier, column, rank, maxRank, isExceptional, available
static int lua_GetTalentInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int tabIndex = static_cast<int>(luaL_checknumber(L, 1));
int talentIndex = static_cast<int>(luaL_checknumber(L, 2));
if (!gh || tabIndex < 1 || talentIndex < 1) {
for (int i = 0; i < 8; i++) lua_pushnil(L);
return 8;
}
uint8_t classId = gh->getPlayerClass();
uint32_t classMask = (classId > 0) ? (1u << (classId - 1)) : 0;
std::vector<const game::GameHandler::TalentTabEntry*> classTabs;
for (const auto& [tabId, tab] : gh->getAllTalentTabs()) {
if (tab.classMask & classMask) classTabs.push_back(&tab);
}
std::sort(classTabs.begin(), classTabs.end(),
[](const auto* a, const auto* b) { return a->orderIndex < b->orderIndex; });
if (tabIndex > static_cast<int>(classTabs.size())) {
for (int i = 0; i < 8; i++) lua_pushnil(L);
return 8;
}
uint32_t targetTabId = classTabs[tabIndex - 1]->tabId;
// Collect talents for this tab, sorted by row then column
std::vector<const game::GameHandler::TalentEntry*> tabTalents;
for (const auto& [talentId, entry] : gh->getAllTalents()) {
if (entry.tabId == targetTabId) tabTalents.push_back(&entry);
}
std::sort(tabTalents.begin(), tabTalents.end(),
[](const auto* a, const auto* b) {
return (a->row != b->row) ? a->row < b->row : a->column < b->column;
});
if (talentIndex > static_cast<int>(tabTalents.size())) {
for (int i = 0; i < 8; i++) lua_pushnil(L);
return 8;
}
const auto* talent = tabTalents[talentIndex - 1];
uint8_t rank = gh->getTalentRank(talent->talentId);
// Get spell name for rank 1 spell
std::string name = gh->getSpellName(talent->rankSpells[0]);
if (name.empty()) name = "Talent " + std::to_string(talent->talentId);
lua_pushstring(L, name.c_str()); // 1: name
lua_pushnil(L); // 2: iconTexture
lua_pushnumber(L, talent->row + 1); // 3: tier (1-indexed)
lua_pushnumber(L, talent->column + 1); // 4: column (1-indexed)
lua_pushnumber(L, rank); // 5: rank
lua_pushnumber(L, talent->maxRank); // 6: maxRank
lua_pushboolean(L, 0); // 7: isExceptional
lua_pushboolean(L, 1); // 8: available
return 8;
}
static int lua_GetActiveTalentGroup(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? (gh->getActiveTalentSpec() + 1) : 1);
return 1;
}
void registerQuestLuaAPI(lua_State* L) {
static const struct { const char* name; lua_CFunction func; } api[] = {
{"GetNumQuestLogEntries", lua_GetNumQuestLogEntries},
{"GetQuestLogTitle", lua_GetQuestLogTitle},
{"GetQuestLogQuestText", lua_GetQuestLogQuestText},
{"IsQuestComplete", lua_IsQuestComplete},
{"SelectQuestLogEntry", lua_SelectQuestLogEntry},
{"GetQuestLogSelection", lua_GetQuestLogSelection},
{"GetNumQuestWatches", lua_GetNumQuestWatches},
{"GetQuestIndexForWatch", lua_GetQuestIndexForWatch},
{"AddQuestWatch", lua_AddQuestWatch},
{"RemoveQuestWatch", lua_RemoveQuestWatch},
{"IsQuestWatched", lua_IsQuestWatched},
{"GetQuestLink", lua_GetQuestLink},
{"GetNumQuestLeaderBoards", lua_GetNumQuestLeaderBoards},
{"GetQuestLogLeaderBoard", lua_GetQuestLogLeaderBoard},
{"ExpandQuestHeader", lua_ExpandQuestHeader},
{"CollapseQuestHeader", lua_CollapseQuestHeader},
{"GetQuestLogSpecialItemInfo", lua_GetQuestLogSpecialItemInfo},
{"GetNumSkillLines", lua_GetNumSkillLines},
{"GetSkillLineInfo", lua_GetSkillLineInfo},
{"GetNumTalentTabs", lua_GetNumTalentTabs},
{"GetTalentTabInfo", lua_GetTalentTabInfo},
{"GetNumTalents", lua_GetNumTalents},
{"GetTalentInfo", lua_GetTalentInfo},
{"GetActiveTalentGroup", lua_GetActiveTalentGroup},
{"AcceptQuest", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->acceptQuest();
return 0;
}},
{"DeclineQuest", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->declineQuest();
return 0;
}},
{"CompleteQuest", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->completeQuest();
return 0;
}},
{"AbandonQuest", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
uint32_t questId = static_cast<uint32_t>(luaL_checknumber(L, 1));
if (gh) gh->abandonQuest(questId);
return 0;
}},
{"GetNumQuestRewards", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
int idx = gh->getSelectedQuestLogIndex();
if (idx < 1) { return luaReturnZero(L); }
const auto& ql = gh->getQuestLog();
if (idx > static_cast<int>(ql.size())) { return luaReturnZero(L); }
int count = 0;
for (const auto& r : ql[idx-1].rewardItems)
if (r.itemId != 0) ++count;
lua_pushnumber(L, count);
return 1;
}},
{"GetNumQuestChoices", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
int idx = gh->getSelectedQuestLogIndex();
if (idx < 1) { return luaReturnZero(L); }
const auto& ql = gh->getQuestLog();
if (idx > static_cast<int>(ql.size())) { return luaReturnZero(L); }
int count = 0;
for (const auto& r : ql[idx-1].rewardChoiceItems)
if (r.itemId != 0) ++count;
lua_pushnumber(L, count);
return 1;
}},
{"GetNumGlyphSockets", [](lua_State* L) -> int {
lua_pushnumber(L, game::GameHandler::MAX_GLYPH_SLOTS);
return 1;
}},
{"GetGlyphSocketInfo", [](lua_State* L) -> int {
// GetGlyphSocketInfo(index [, talentGroup]) → enabled, glyphType, glyphSpellID, icon
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
int spec = static_cast<int>(luaL_optnumber(L, 2, 0));
if (!gh || index < 1 || index > game::GameHandler::MAX_GLYPH_SLOTS) {
lua_pushboolean(L, 0); lua_pushnumber(L, 0); lua_pushnil(L); lua_pushnil(L);
return 4;
}
const auto& glyphs = (spec >= 1 && spec <= 2)
? gh->getGlyphs(static_cast<uint8_t>(spec - 1)) : gh->getGlyphs();
uint16_t glyphId = glyphs[index - 1];
// Glyph type: slots 1,2,3 = major (1), slots 4,5,6 = minor (2)
int glyphType = (index <= 3) ? 1 : 2;
lua_pushboolean(L, 1); // enabled
lua_pushnumber(L, glyphType); // glyphType (1=major, 2=minor)
if (glyphId != 0) {
lua_pushnumber(L, glyphId); // glyphSpellID
lua_pushstring(L, "Interface\\Icons\\INV_Glyph_MajorWarrior"); // placeholder icon
} else {
lua_pushnil(L);
lua_pushnil(L);
}
return 4;
}},
{"GetNumCompletedAchievements", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getEarnedAchievements().size() : 0);
return 1;
}},
{"GetAchievementInfo", [](lua_State* L) -> int {
// GetAchievementInfo(id) → id, name, points, completed, month, day, year, description, flags, icon, rewardText, isGuildAch
auto* gh = getGameHandler(L);
uint32_t id = static_cast<uint32_t>(luaL_checknumber(L, 1));
if (!gh) { return luaReturnNil(L); }
const std::string& name = gh->getAchievementName(id);
if (name.empty()) { return luaReturnNil(L); }
bool completed = gh->getEarnedAchievements().count(id) > 0;
uint32_t date = gh->getAchievementDate(id);
uint32_t points = gh->getAchievementPoints(id);
const std::string& desc = gh->getAchievementDescription(id);
// Parse date: packed as (month << 24 | day << 16 | year)
int month = completed ? static_cast<int>((date >> 24) & 0xFF) : 0;
int day = completed ? static_cast<int>((date >> 16) & 0xFF) : 0;
int year = completed ? static_cast<int>(date & 0xFFFF) : 0;
lua_pushnumber(L, id); // 1: id
lua_pushstring(L, name.c_str()); // 2: name
lua_pushnumber(L, points); // 3: points
lua_pushboolean(L, completed ? 1 : 0); // 4: completed
lua_pushnumber(L, month); // 5: month
lua_pushnumber(L, day); // 6: day
lua_pushnumber(L, year); // 7: year
lua_pushstring(L, desc.c_str()); // 8: description
lua_pushnumber(L, 0); // 9: flags
lua_pushstring(L, "Interface\\Icons\\Achievement_General"); // 10: icon
lua_pushstring(L, ""); // 11: rewardText
lua_pushboolean(L, 0); // 12: isGuildAchievement
return 12;
}},
};
for (const auto& [name, func] : api) {
lua_pushcfunction(L, func);
lua_setglobal(L, name);
}
}
} // namespace wowee::addons

View file

@ -0,0 +1,502 @@
// lua_social_api.cpp — Chat, guild, friends, ignore, gossip, party management, and emotes Lua API bindings.
// Extracted from lua_engine.cpp as part of §5.1 (Tame LuaEngine).
#include "addons/lua_api_helpers.hpp"
namespace wowee::addons {
static int lua_SendChatMessage(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
const char* msg = luaL_checkstring(L, 1);
const char* chatType = luaL_optstring(L, 2, "SAY");
// language arg (3) ignored — server determines language
const char* target = luaL_optstring(L, 4, "");
std::string typeStr(chatType);
for (char& c : typeStr) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
game::ChatType ct = game::ChatType::SAY;
if (typeStr == "SAY") ct = game::ChatType::SAY;
else if (typeStr == "YELL") ct = game::ChatType::YELL;
else if (typeStr == "PARTY") ct = game::ChatType::PARTY;
else if (typeStr == "GUILD") ct = game::ChatType::GUILD;
else if (typeStr == "OFFICER") ct = game::ChatType::OFFICER;
else if (typeStr == "RAID") ct = game::ChatType::RAID;
else if (typeStr == "WHISPER") ct = game::ChatType::WHISPER;
else if (typeStr == "BATTLEGROUND") ct = game::ChatType::BATTLEGROUND;
std::string targetStr(target && *target ? target : "");
gh->sendChatMessage(ct, msg, targetStr);
return 0;
}
// SendAddonMessage(prefix, text, chatType, target) — send addon message
static int lua_SendAddonMessage(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
const char* prefix = luaL_checkstring(L, 1);
const char* text = luaL_checkstring(L, 2);
const char* chatType = luaL_optstring(L, 3, "PARTY");
const char* target = luaL_optstring(L, 4, "");
// Build addon message: prefix + TAB + text, send via the appropriate channel
std::string typeStr(chatType);
for (char& c : typeStr) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
game::ChatType ct = game::ChatType::PARTY;
if (typeStr == "PARTY") ct = game::ChatType::PARTY;
else if (typeStr == "RAID") ct = game::ChatType::RAID;
else if (typeStr == "GUILD") ct = game::ChatType::GUILD;
else if (typeStr == "OFFICER") ct = game::ChatType::OFFICER;
else if (typeStr == "BATTLEGROUND") ct = game::ChatType::BATTLEGROUND;
else if (typeStr == "WHISPER") ct = game::ChatType::WHISPER;
// Encode as prefix\ttext (WoW addon message format)
std::string encoded = std::string(prefix) + "\t" + text;
std::string targetStr(target && *target ? target : "");
gh->sendChatMessage(ct, encoded, targetStr);
return 0;
}
// RegisterAddonMessagePrefix(prefix) — register prefix for receiving addon messages
static int lua_RegisterAddonMessagePrefix(lua_State* L) {
const char* prefix = luaL_checkstring(L, 1);
// Store in a global Lua table for filtering
lua_getglobal(L, "__WoweeAddonPrefixes");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
lua_pushvalue(L, -1);
lua_setglobal(L, "__WoweeAddonPrefixes");
}
lua_pushboolean(L, 1);
lua_setfield(L, -2, prefix);
lua_pop(L, 1);
lua_pushboolean(L, 1); // success
return 1;
}
// IsAddonMessagePrefixRegistered(prefix) → boolean
static int lua_IsAddonMessagePrefixRegistered(lua_State* L) {
const char* prefix = luaL_checkstring(L, 1);
lua_getglobal(L, "__WoweeAddonPrefixes");
if (lua_istable(L, -1)) {
lua_getfield(L, -1, prefix);
lua_pushboolean(L, lua_toboolean(L, -1));
return 1;
}
lua_pushboolean(L, 0);
return 1;
}
static int lua_GetNumFriends(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
int count = 0;
for (const auto& c : gh->getContacts())
if (c.isFriend()) count++;
lua_pushnumber(L, count);
return 1;
}
// GetFriendInfo(index) → name, level, class, area, connected, status, note
static int lua_GetFriendInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) {
return luaReturnNil(L);
}
int found = 0;
for (const auto& c : gh->getContacts()) {
if (!c.isFriend()) continue;
if (++found == index) {
lua_pushstring(L, c.name.c_str()); // 1: name
lua_pushnumber(L, c.level); // 2: level
lua_pushstring(L, c.classId < 12 ? kLuaClasses[c.classId] : "Unknown"); // 3: class
std::string area;
if (c.areaId != 0) area = gh->getWhoAreaName(c.areaId);
lua_pushstring(L, area.c_str()); // 4: area
lua_pushboolean(L, c.isOnline()); // 5: connected
lua_pushstring(L, c.status == 2 ? "<AFK>" : (c.status == 3 ? "<DND>" : "")); // 6: status
lua_pushstring(L, c.note.c_str()); // 7: note
return 7;
}
}
lua_pushnil(L);
return 1;
}
// --- Guild API ---
// IsInGuild() → boolean
static int lua_IsInGuild(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushboolean(L, gh && gh->isInGuild());
return 1;
}
// GetGuildInfo("player") → guildName, guildRankName, guildRankIndex
static int lua_GetGuildInfoFunc(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh || !gh->isInGuild()) { return luaReturnNil(L); }
lua_pushstring(L, gh->getGuildName().c_str());
// Get rank name for the player
const auto& roster = gh->getGuildRoster();
std::string rankName;
uint32_t rankIndex = 0;
for (const auto& m : roster.members) {
if (m.guid == gh->getPlayerGuid()) {
rankIndex = m.rankIndex;
const auto& rankNames = gh->getGuildRankNames();
if (rankIndex < rankNames.size()) rankName = rankNames[rankIndex];
break;
}
}
lua_pushstring(L, rankName.c_str());
lua_pushnumber(L, rankIndex);
return 3;
}
// GetNumGuildMembers() → totalMembers, onlineMembers
static int lua_GetNumGuildMembers(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
const auto& roster = gh->getGuildRoster();
int online = 0;
for (const auto& m : roster.members)
if (m.online) online++;
lua_pushnumber(L, roster.members.size());
lua_pushnumber(L, online);
return 2;
}
// GetGuildRosterInfo(index) → name, rank, rankIndex, level, class, zone, note, officerNote, online, status, classId
static int lua_GetGuildRosterInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
const auto& roster = gh->getGuildRoster();
if (index > static_cast<int>(roster.members.size())) { return luaReturnNil(L); }
const auto& m = roster.members[index - 1];
lua_pushstring(L, m.name.c_str()); // 1: name
const auto& rankNames = gh->getGuildRankNames();
lua_pushstring(L, m.rankIndex < rankNames.size()
? rankNames[m.rankIndex].c_str() : ""); // 2: rank name
lua_pushnumber(L, m.rankIndex); // 3: rankIndex
lua_pushnumber(L, m.level); // 4: level
lua_pushstring(L, m.classId < 12 ? kLuaClasses[m.classId] : "Unknown"); // 5: class
std::string zone;
if (m.zoneId != 0 && m.online) zone = gh->getWhoAreaName(m.zoneId);
lua_pushstring(L, zone.c_str()); // 6: zone
lua_pushstring(L, m.publicNote.c_str()); // 7: note
lua_pushstring(L, m.officerNote.c_str()); // 8: officerNote
lua_pushboolean(L, m.online); // 9: online
lua_pushnumber(L, 0); // 10: status (0=online, 1=AFK, 2=DND)
lua_pushnumber(L, m.classId); // 11: classId (numeric)
return 11;
}
// GetGuildRosterMOTD() → motd
static int lua_GetGuildRosterMOTD(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushstring(L, ""); return 1; }
lua_pushstring(L, gh->getGuildRoster().motd.c_str());
return 1;
}
// GetNumIgnores() → count
static int lua_GetNumIgnores(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
int count = 0;
for (const auto& c : gh->getContacts())
if (c.isIgnored()) count++;
lua_pushnumber(L, count);
return 1;
}
// GetIgnoreName(index) → name
static int lua_GetIgnoreName(lua_State* L) {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
int found = 0;
for (const auto& c : gh->getContacts()) {
if (!c.isIgnored()) continue;
if (++found == index) {
lua_pushstring(L, c.name.c_str());
return 1;
}
}
lua_pushnil(L);
return 1;
}
// --- Talent API ---
// GetNumTalentTabs() → count (usually 3)
void registerSocialLuaAPI(lua_State* L) {
static const struct { const char* name; lua_CFunction func; } api[] = {
{"SendChatMessage", lua_SendChatMessage},
{"SendAddonMessage", lua_SendAddonMessage},
{"RegisterAddonMessagePrefix", lua_RegisterAddonMessagePrefix},
{"IsAddonMessagePrefixRegistered", lua_IsAddonMessagePrefixRegistered},
{"IsInGuild", lua_IsInGuild},
{"GetGuildInfo", lua_GetGuildInfoFunc},
{"GetNumGuildMembers", lua_GetNumGuildMembers},
{"GuildRoster", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->requestGuildRoster();
return 0;
}},
{"SortGuildRoster", [](lua_State* L) -> int {
(void)L; // Sorting is client-side display only
return 0;
}},
{"GetGuildRosterInfo", lua_GetGuildRosterInfo},
{"GetGuildRosterMOTD", lua_GetGuildRosterMOTD},
{"GetNumFriends", lua_GetNumFriends},
{"GetFriendInfo", lua_GetFriendInfo},
{"GetNumIgnores", lua_GetNumIgnores},
{"GetIgnoreName", lua_GetIgnoreName},
{"GuildInvite", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->inviteToGuild(luaL_checkstring(L, 1));
return 0;
}},
{"GuildUninvite", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->kickGuildMember(luaL_checkstring(L, 1));
return 0;
}},
{"GuildPromote", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->promoteGuildMember(luaL_checkstring(L, 1));
return 0;
}},
{"GuildDemote", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->demoteGuildMember(luaL_checkstring(L, 1));
return 0;
}},
{"GuildLeave", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->leaveGuild();
return 0;
}},
{"GuildSetPublicNote", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->setGuildPublicNote(luaL_checkstring(L, 1), luaL_checkstring(L, 2));
return 0;
}},
{"DoEmote", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* token = luaL_checkstring(L, 1);
if (!gh) return 0;
std::string t(token);
for (char& c : t) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
// Map common emote tokens to DBC TextEmote IDs
static const std::unordered_map<std::string, uint32_t> emoteMap = {
{"WAVE", 67}, {"BOW", 2}, {"DANCE", 10}, {"CHEER", 5},
{"CHICKEN", 6}, {"CRY", 8}, {"EAT", 14}, {"DRINK", 13},
{"FLEX", 16}, {"KISS", 22}, {"LAUGH", 23}, {"POINT", 30},
{"ROAR", 34}, {"RUDE", 36}, {"SALUTE", 37}, {"SHY", 40},
{"SILLY", 41}, {"SIT", 42}, {"SLEEP", 43}, {"SPIT", 44},
{"THANK", 52}, {"CLAP", 7}, {"KNEEL", 21}, {"LAY", 24},
{"NO", 28}, {"YES", 70}, {"BEG", 1}, {"ANGRY", 64},
{"FAREWELL", 15}, {"HELLO", 18}, {"WELCOME", 68},
};
auto it = emoteMap.find(t);
uint64_t target = gh->hasTarget() ? gh->getTargetGuid() : 0;
if (it != emoteMap.end()) {
gh->sendTextEmote(it->second, target);
}
return 0;
}},
{"AddFriend", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* name = luaL_checkstring(L, 1);
const char* note = luaL_optstring(L, 2, "");
if (gh) gh->addFriend(name, note);
return 0;
}},
{"RemoveFriend", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* name = luaL_checkstring(L, 1);
if (gh) gh->removeFriend(name);
return 0;
}},
{"AddIgnore", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* name = luaL_checkstring(L, 1);
if (gh) gh->addIgnore(name);
return 0;
}},
{"DelIgnore", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* name = luaL_checkstring(L, 1);
if (gh) gh->removeIgnore(name);
return 0;
}},
{"ShowFriends", [](lua_State* L) -> int {
(void)L; // Friends panel is shown via ImGui, not Lua
return 0;
}},
{"GetNumWhoResults", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
lua_pushnumber(L, gh->getWhoResults().size());
lua_pushnumber(L, gh->getWhoOnlineCount());
return 2;
}},
{"GetWhoInfo", [](lua_State* L) -> int {
// GetWhoInfo(index) → name, guild, level, race, class, zone, classFileName
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
const auto& results = gh->getWhoResults();
if (index > static_cast<int>(results.size())) { return luaReturnNil(L); }
const auto& w = results[index - 1];
const char* raceName = (w.raceId < 12) ? kLuaRaces[w.raceId] : "Unknown";
const char* className = (w.classId < 12) ? kLuaClasses[w.classId] : "Unknown";
static constexpr const char* kClassFiles[] = {"","WARRIOR","PALADIN","HUNTER","ROGUE","PRIEST","DEATHKNIGHT","SHAMAN","MAGE","WARLOCK","","DRUID"};
const char* classFile = (w.classId < 12) ? kClassFiles[w.classId] : "WARRIOR";
lua_pushstring(L, w.name.c_str());
lua_pushstring(L, w.guildName.c_str());
lua_pushnumber(L, w.level);
lua_pushstring(L, raceName);
lua_pushstring(L, className);
lua_pushstring(L, ""); // zone name (would need area lookup)
lua_pushstring(L, classFile);
return 7;
}},
{"SendWho", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* query = luaL_optstring(L, 1, "");
if (gh) gh->queryWho(query);
return 0;
}},
{"SetWhoToUI", [](lua_State* L) -> int {
(void)L; return 0; // Stub
}},
{"GetNumGossipOptions", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getCurrentGossip().options.size() : 0);
return 1;
}},
{"GetGossipOptions", [](lua_State* L) -> int {
// Returns pairs of (text, type) for each option
auto* gh = getGameHandler(L);
if (!gh) return 0;
const auto& opts = gh->getCurrentGossip().options;
int n = 0;
static constexpr const char* kIcons[] = {"gossip","vendor","taxi","trainer","spiritguide","innkeeper","banker","petition","tabard","battlemaster","auctioneer"};
for (const auto& o : opts) {
lua_pushstring(L, o.text.c_str());
lua_pushstring(L, o.icon < 11 ? kIcons[o.icon] : "gossip");
n += 2;
}
return n;
}},
{"SelectGossipOption", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) return 0;
const auto& opts = gh->getCurrentGossip().options;
if (index <= static_cast<int>(opts.size()))
gh->selectGossipOption(opts[index - 1].id);
return 0;
}},
{"GetNumGossipAvailableQuests", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
int count = 0;
for (const auto& q : gh->getCurrentGossip().quests)
if (q.questIcon != 4) ++count; // 4 = active/in-progress
lua_pushnumber(L, count);
return 1;
}},
{"GetNumGossipActiveQuests", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
int count = 0;
for (const auto& q : gh->getCurrentGossip().quests)
if (q.questIcon == 4) ++count;
lua_pushnumber(L, count);
return 1;
}},
{"CloseGossip", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->closeGossip();
return 0;
}},
{"InviteUnit", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->inviteToGroup(luaL_checkstring(L, 1));
return 0;
}},
{"UninviteUnit", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->uninvitePlayer(luaL_checkstring(L, 1));
return 0;
}},
{"LeaveParty", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->leaveGroup();
return 0;
}},
{"FollowUnit", [](lua_State* L) -> int {
(void)L; // Follow requires movement system integration
return 0;
}},
{"RandomRoll", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int mn = static_cast<int>(luaL_optnumber(L, 1, 1));
int mx = static_cast<int>(luaL_optnumber(L, 2, 100));
if (gh) gh->randomRoll(mn, mx);
return 0;
}},
{"JoinChannelByName", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* name = luaL_checkstring(L, 1);
const char* pw = luaL_optstring(L, 2, "");
if (gh) gh->joinChannel(name, pw);
return 0;
}},
{"LeaveChannelByName", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const char* name = luaL_checkstring(L, 1);
if (gh) gh->leaveChannel(name);
return 0;
}},
{"GetChannelName", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
std::string name = gh->getChannelByIndex(index - 1);
if (!name.empty()) {
lua_pushstring(L, name.c_str());
lua_pushstring(L, ""); // header
lua_pushboolean(L, 0); // collapsed
lua_pushnumber(L, index); // channelNumber
lua_pushnumber(L, 0); // count
lua_pushboolean(L, 1); // active
lua_pushstring(L, "CHANNEL_CATEGORY_CUSTOM"); // category
return 7;
}
lua_pushnil(L);
return 1;
}},
};
for (const auto& [name, func] : api) {
lua_pushcfunction(L, func);
lua_setglobal(L, name);
}
}
} // namespace wowee::addons

View file

@ -0,0 +1,968 @@
// lua_spell_api.cpp — Spell info, casting, auras, and targeting Lua API bindings.
// Extracted from lua_engine.cpp as part of §5.1 (Tame LuaEngine).
#include "addons/lua_api_helpers.hpp"
namespace wowee::addons {
static int lua_IsSpellInRange(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
const char* spellNameOrId = luaL_checkstring(L, 1);
const char* uid = luaL_optstring(L, 2, "target");
// Resolve spell ID
uint32_t spellId = 0;
if (spellNameOrId[0] >= '0' && spellNameOrId[0] <= '9') {
spellId = static_cast<uint32_t>(strtoul(spellNameOrId, nullptr, 10));
} else {
std::string nameLow(spellNameOrId);
toLowerInPlace(nameLow);
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
toLowerInPlace(sn);
if (sn == nameLow) { spellId = sid; break; }
}
}
if (spellId == 0) { return luaReturnNil(L); }
// Get spell max range from DBC
auto data = gh->getSpellData(spellId);
if (data.maxRange <= 0.0f) { return luaReturnNil(L); }
// Resolve target position
std::string uidStr(uid);
toLowerInPlace(uidStr);
uint64_t guid = resolveUnitGuid(gh, uidStr);
if (guid == 0) { return luaReturnNil(L); }
auto targetEnt = gh->getEntityManager().getEntity(guid);
auto playerEnt = gh->getEntityManager().getEntity(gh->getPlayerGuid());
if (!targetEnt || !playerEnt) { return luaReturnNil(L); }
float dx = playerEnt->getX() - targetEnt->getX();
float dy = playerEnt->getY() - targetEnt->getY();
float dz = playerEnt->getZ() - targetEnt->getZ();
float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
lua_pushnumber(L, dist <= data.maxRange ? 1 : 0);
return 1;
}
// UnitIsVisible(unit) → boolean (entity exists in the client's entity manager)
static int lua_UnitAura(lua_State* L, bool wantBuff) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
const char* uid = luaL_optstring(L, 1, "player");
int index = static_cast<int>(luaL_optnumber(L, 2, 1));
if (index < 1) { return luaReturnNil(L); }
std::string uidStr(uid);
toLowerInPlace(uidStr);
const std::vector<game::AuraSlot>* auras = nullptr;
if (uidStr == "player") auras = &gh->getPlayerAuras();
else if (uidStr == "target") auras = &gh->getTargetAuras();
else {
// Try party/raid/focus via GUID lookup in unitAurasCache
uint64_t guid = resolveUnitGuid(gh, uidStr);
if (guid != 0) auras = gh->getUnitAuras(guid);
}
if (!auras) { return luaReturnNil(L); }
// Filter to buffs or debuffs and find the Nth one
int found = 0;
for (const auto& aura : *auras) {
if (aura.isEmpty() || aura.spellId == 0) continue;
bool isDebuff = (aura.flags & 0x80) != 0;
if (wantBuff ? isDebuff : !isDebuff) continue;
found++;
if (found == index) {
// Return: name, rank, icon, count, debuffType, duration, expirationTime, ...spellId
std::string name = gh->getSpellName(aura.spellId);
lua_pushstring(L, name.empty() ? "Unknown" : name.c_str()); // name
lua_pushstring(L, ""); // rank
std::string iconPath = gh->getSpellIconPath(aura.spellId);
if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str());
else lua_pushnil(L); // icon texture path
lua_pushnumber(L, aura.charges); // count
// debuffType: resolve from Spell.dbc dispel type
{
uint8_t dt = gh->getSpellDispelType(aura.spellId);
switch (dt) {
case 1: lua_pushstring(L, "Magic"); break;
case 2: lua_pushstring(L, "Curse"); break;
case 3: lua_pushstring(L, "Disease"); break;
case 4: lua_pushstring(L, "Poison"); break;
default: lua_pushnil(L); break;
}
}
lua_pushnumber(L, aura.maxDurationMs > 0 ? aura.maxDurationMs / 1000.0 : 0); // duration
// expirationTime: GetTime() + remaining seconds (so addons can compute countdown)
if (aura.durationMs > 0) {
uint64_t auraNowMs = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
int32_t remMs = aura.getRemainingMs(auraNowMs);
lua_pushnumber(L, luaGetTimeNow() + remMs / 1000.0);
} else {
lua_pushnumber(L, 0); // permanent aura
}
// caster: return unit ID string if caster is known
if (aura.casterGuid != 0) {
if (aura.casterGuid == gh->getPlayerGuid())
lua_pushstring(L, "player");
else if (aura.casterGuid == gh->getTargetGuid())
lua_pushstring(L, "target");
else if (aura.casterGuid == gh->getFocusGuid())
lua_pushstring(L, "focus");
else if (aura.casterGuid == gh->getPetGuid())
lua_pushstring(L, "pet");
else {
char cBuf[32];
snprintf(cBuf, sizeof(cBuf), "0x%016llX", (unsigned long long)aura.casterGuid);
lua_pushstring(L, cBuf);
}
} else {
lua_pushnil(L);
}
lua_pushboolean(L, 0); // isStealable
lua_pushboolean(L, 0); // shouldConsolidate
lua_pushnumber(L, aura.spellId); // spellId
return 11;
}
}
lua_pushnil(L);
return 1;
}
static int lua_UnitBuff(lua_State* L) { return lua_UnitAura(L, true); }
static int lua_UnitDebuff(lua_State* L) { return lua_UnitAura(L, false); }
// UnitAura(unit, index, filter) — generic aura query with filter string
// filter: "HELPFUL" = buffs, "HARMFUL" = debuffs, "PLAYER" = cast by player,
// "HELPFUL|PLAYER" = buffs cast by player, etc.
static int lua_UnitAuraGeneric(lua_State* L) {
const char* filter = luaL_optstring(L, 3, "HELPFUL");
std::string f(filter ? filter : "HELPFUL");
for (char& c : f) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
bool wantBuff = (f.find("HARMFUL") == std::string::npos);
return lua_UnitAura(L, wantBuff);
}
// ---------- UnitCastingInfo / UnitChannelInfo ----------
// Internal helper: pushes cast/channel info for a unit.
// Returns number of Lua return values (0 if not casting/channeling the requested type).
static int lua_UnitCastInfo(lua_State* L, bool wantChannel) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
const char* uid = luaL_optstring(L, 1, "player");
std::string uidStr(uid ? uid : "player");
// Use shared GetTime() epoch for consistent timestamps
double nowSec = luaGetTimeNow();
// Resolve cast state for the unit
bool isCasting = false;
bool isChannel = false;
uint32_t spellId = 0;
float timeTotal = 0.0f;
float timeRemaining = 0.0f;
bool interruptible = true;
if (uidStr == "player") {
isCasting = gh->isCasting();
isChannel = gh->isChanneling();
spellId = gh->getCurrentCastSpellId();
timeTotal = gh->getCastTimeTotal();
timeRemaining = gh->getCastTimeRemaining();
// Player interruptibility: always true for own casts (server controls actual interrupt)
interruptible = true;
} else {
uint64_t guid = resolveUnitGuid(gh, uidStr);
if (guid == 0) { return luaReturnNil(L); }
const auto* state = gh->getUnitCastState(guid);
if (!state) { return luaReturnNil(L); }
isCasting = state->casting;
isChannel = state->isChannel;
spellId = state->spellId;
timeTotal = state->timeTotal;
timeRemaining = state->timeRemaining;
interruptible = state->interruptible;
}
if (!isCasting) { return luaReturnNil(L); }
// UnitCastingInfo: only returns for non-channel casts
// UnitChannelInfo: only returns for channels
if (wantChannel != isChannel) { return luaReturnNil(L); }
// Spell name + icon
const std::string& name = gh->getSpellName(spellId);
std::string iconPath = gh->getSpellIconPath(spellId);
// Time values in milliseconds (WoW API convention)
double startTimeMs = (nowSec - (timeTotal - timeRemaining)) * 1000.0;
double endTimeMs = (nowSec + timeRemaining) * 1000.0;
// Return values match WoW API:
// UnitCastingInfo: name, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible
// UnitChannelInfo: name, text, texture, startTime, endTime, isTradeSkill, notInterruptible
lua_pushstring(L, name.empty() ? "Unknown" : name.c_str()); // name
lua_pushstring(L, ""); // text (sub-text, usually empty)
if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str());
else lua_pushstring(L, "Interface\\Icons\\INV_Misc_QuestionMark"); // texture
lua_pushnumber(L, startTimeMs); // startTime (ms)
lua_pushnumber(L, endTimeMs); // endTime (ms)
lua_pushboolean(L, gh->isProfessionSpell(spellId) ? 1 : 0); // isTradeSkill
if (!wantChannel) {
lua_pushnumber(L, spellId); // castID (UnitCastingInfo only)
}
lua_pushboolean(L, interruptible ? 0 : 1); // notInterruptible
return wantChannel ? 7 : 8;
}
static int lua_UnitCastingInfo(lua_State* L) { return lua_UnitCastInfo(L, false); }
static int lua_UnitChannelInfo(lua_State* L) { return lua_UnitCastInfo(L, true); }
static int lua_CastSpellByName(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
const char* name = luaL_checkstring(L, 1);
if (!name || !*name) return 0;
// Find highest rank of spell by name (same logic as /cast)
std::string nameLow(name);
toLowerInPlace(nameLow);
uint32_t bestId = 0;
int bestRank = -1;
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
toLowerInPlace(sn);
if (sn != nameLow) continue;
int rank = 0;
const std::string& rk = gh->getSpellRank(sid);
if (!rk.empty()) {
std::string rkl = rk;
toLowerInPlace(rkl);
if (rkl.rfind("rank ", 0) == 0) {
try { rank = std::stoi(rkl.substr(5)); } catch (...) {}
}
}
if (rank > bestRank) { bestRank = rank; bestId = sid; }
}
if (bestId != 0) {
uint64_t target = gh->hasTarget() ? gh->getTargetGuid() : 0;
gh->castSpell(bestId, target);
}
return 0;
}
static int lua_IsSpellKnown(lua_State* L) {
auto* gh = getGameHandler(L);
uint32_t spellId = static_cast<uint32_t>(luaL_checknumber(L, 1));
lua_pushboolean(L, gh && gh->getKnownSpells().count(spellId));
return 1;
}
// --- Spell Book Tab API ---
// GetNumSpellTabs() → count
static int lua_GetNumSpellTabs(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
lua_pushnumber(L, gh->getSpellBookTabs().size());
return 1;
}
// GetSpellTabInfo(tabIndex) → name, texture, offset, numSpells
// tabIndex is 1-based; offset is 1-based global spell book slot
static int lua_GetSpellTabInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int tabIdx = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || tabIdx < 1) {
return luaReturnNil(L);
}
const auto& tabs = gh->getSpellBookTabs();
if (tabIdx > static_cast<int>(tabs.size())) {
return luaReturnNil(L);
}
// Compute offset: sum of spells in all preceding tabs (1-based)
int offset = 0;
for (int i = 0; i < tabIdx - 1; ++i)
offset += static_cast<int>(tabs[i].spellIds.size());
const auto& tab = tabs[tabIdx - 1];
lua_pushstring(L, tab.name.c_str()); // name
lua_pushstring(L, tab.texture.c_str()); // texture
lua_pushnumber(L, offset); // offset (0-based for WoW compat)
lua_pushnumber(L, tab.spellIds.size()); // numSpells
return 4;
}
// GetSpellBookItemInfo(slot, bookType) → "SPELL", spellId
// slot is 1-based global spell book index
static int lua_GetSpellBookItemInfo(lua_State* L) {
auto* gh = getGameHandler(L);
int slot = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || slot < 1) {
lua_pushstring(L, "SPELL");
lua_pushnumber(L, 0);
return 2;
}
const auto& tabs = gh->getSpellBookTabs();
int idx = slot; // 1-based
for (const auto& tab : tabs) {
if (idx <= static_cast<int>(tab.spellIds.size())) {
lua_pushstring(L, "SPELL");
lua_pushnumber(L, tab.spellIds[idx - 1]);
return 2;
}
idx -= static_cast<int>(tab.spellIds.size());
}
lua_pushstring(L, "SPELL");
lua_pushnumber(L, 0);
return 2;
}
// GetSpellBookItemName(slot, bookType) → name, subName
static int lua_GetSpellBookItemName(lua_State* L) {
auto* gh = getGameHandler(L);
int slot = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || slot < 1) { return luaReturnNil(L); }
const auto& tabs = gh->getSpellBookTabs();
int idx = slot;
for (const auto& tab : tabs) {
if (idx <= static_cast<int>(tab.spellIds.size())) {
uint32_t spellId = tab.spellIds[idx - 1];
const std::string& name = gh->getSpellName(spellId);
lua_pushstring(L, name.empty() ? "Unknown" : name.c_str());
lua_pushstring(L, ""); // subName/rank
return 2;
}
idx -= static_cast<int>(tab.spellIds.size());
}
lua_pushnil(L);
return 1;
}
// GetSpellDescription(spellId) → description string
// Clean spell description template variables for display
static std::string cleanSpellDescription(const std::string& raw, const int32_t effectBase[3] = nullptr, float durationSec = 0.0f) {
if (raw.empty() || raw.find('$') == std::string::npos) return raw;
std::string result;
result.reserve(raw.size());
for (size_t i = 0; i < raw.size(); ++i) {
if (raw[i] == '$' && i + 1 < raw.size()) {
char next = raw[i + 1];
if (next == 's' || next == 'S') {
// $s1, $s2, $s3 — substitute with effect base points + 1
i += 1; // skip 's'
int idx = 0;
if (i + 1 < raw.size() && raw[i + 1] >= '1' && raw[i + 1] <= '3') {
idx = raw[i + 1] - '1';
++i;
}
if (effectBase && effectBase[idx] != 0) {
int32_t val = std::abs(effectBase[idx]) + 1;
result += std::to_string(val);
} else {
result += 'X';
}
while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i;
} else if (next == 'o' || next == 'O') {
// $o1 = periodic total (base * ticks). Ticks = duration / 3sec for most spells
i += 1;
int idx = 0;
if (i + 1 < raw.size() && raw[i + 1] >= '1' && raw[i + 1] <= '3') {
idx = raw[i + 1] - '1';
++i;
}
if (effectBase && effectBase[idx] != 0 && durationSec > 0.0f) {
int32_t perTick = std::abs(effectBase[idx]) + 1;
int ticks = static_cast<int>(durationSec / 3.0f);
if (ticks < 1) ticks = 1;
result += std::to_string(perTick * ticks);
} else {
result += 'X';
}
while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i;
} else if (next == 'e' || next == 'E' || next == 't' || next == 'T' ||
next == 'h' || next == 'H' || next == 'u' || next == 'U') {
// Other variables — insert "X" placeholder
result += 'X';
i += 1;
while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i;
} else if (next == 'd' || next == 'D') {
// $d = duration
if (durationSec > 0.0f) {
if (durationSec >= 60.0f)
result += std::to_string(static_cast<int>(durationSec / 60.0f)) + " min";
else
result += std::to_string(static_cast<int>(durationSec)) + " sec";
} else {
result += "X sec";
}
++i;
while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i;
} else if (next == 'a' || next == 'A') {
// $a1 = radius
result += "X";
++i;
while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i;
} else if (next == 'b' || next == 'B' || next == 'n' || next == 'N' ||
next == 'i' || next == 'I' || next == 'x' || next == 'X') {
// misc variables
result += "X";
++i;
while (i + 1 < raw.size() && raw[i + 1] >= '0' && raw[i + 1] <= '9') ++i;
} else if (next == '$') {
// $$ = literal $
result += '$';
++i;
} else if (next == '{' || next == '<') {
// ${...} or $<...> — skip entire block
char close = (next == '{') ? '}' : '>';
size_t end = raw.find(close, i + 2);
if (end != std::string::npos) i = end;
else result += raw[i]; // no closing — keep $
} else {
result += raw[i]; // unknown $ pattern — keep
}
} else {
result += raw[i];
}
}
return result;
}
static int lua_GetSpellDescription(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushstring(L, ""); return 1; }
uint32_t spellId = static_cast<uint32_t>(luaL_checknumber(L, 1));
const std::string& desc = gh->getSpellDescription(spellId);
const int32_t* ebp = gh->getSpellEffectBasePoints(spellId);
float dur = gh->getSpellDuration(spellId);
std::string cleaned = cleanSpellDescription(desc, ebp, dur);
lua_pushstring(L, cleaned.c_str());
return 1;
}
static int lua_GetEnchantInfo(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
uint32_t enchantId = static_cast<uint32_t>(luaL_checknumber(L, 1));
std::string name = gh->getEnchantName(enchantId);
if (name.empty()) { return luaReturnNil(L); }
lua_pushstring(L, name.c_str());
return 1;
}
static int lua_GetSpellCooldown(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
// Accept spell name or ID
uint32_t spellId = 0;
if (lua_isnumber(L, 1)) {
spellId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else {
const char* name = luaL_checkstring(L, 1);
std::string nameLow(name);
toLowerInPlace(nameLow);
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
toLowerInPlace(sn);
if (sn == nameLow) { spellId = sid; break; }
}
}
float cd = gh->getSpellCooldown(spellId);
// Also check GCD — if spell has no individual cooldown but GCD is active,
// return the GCD timing (this is how WoW handles it)
float gcdRem = gh->getGCDRemaining();
float gcdTotal = gh->getGCDTotal();
// WoW returns (start, duration, enabled) where remaining = start + duration - GetTime()
double nowSec = luaGetTimeNow();
if (cd > 0.01f) {
// Spell-specific cooldown (longer than GCD)
double start = nowSec - 0.01; // approximate start as "just now" minus epsilon
lua_pushnumber(L, start);
lua_pushnumber(L, cd);
} else if (gcdRem > 0.01f) {
// GCD is active — return GCD timing
double elapsed = gcdTotal - gcdRem;
double start = nowSec - elapsed;
lua_pushnumber(L, start);
lua_pushnumber(L, gcdTotal);
} else {
lua_pushnumber(L, 0); // not on cooldown
lua_pushnumber(L, 0);
}
lua_pushnumber(L, 1); // enabled
return 3;
}
static int lua_HasTarget(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushboolean(L, gh && gh->hasTarget());
return 1;
}
// TargetUnit(unitId) — set current target
static int lua_TargetUnit(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
const char* uid = luaL_checkstring(L, 1);
std::string uidStr(uid);
toLowerInPlace(uidStr);
uint64_t guid = resolveUnitGuid(gh, uidStr);
if (guid != 0) gh->setTarget(guid);
return 0;
}
// ClearTarget() — clear current target
static int lua_ClearTarget(lua_State* L) {
auto* gh = getGameHandler(L);
if (gh) gh->clearTarget();
return 0;
}
// FocusUnit(unitId) — set focus target
static int lua_FocusUnit(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
const char* uid = luaL_optstring(L, 1, nullptr);
if (!uid || !*uid) return 0;
std::string uidStr(uid);
toLowerInPlace(uidStr);
uint64_t guid = resolveUnitGuid(gh, uidStr);
if (guid != 0) gh->setFocus(guid);
return 0;
}
// ClearFocus() — clear focus target
static int lua_ClearFocus(lua_State* L) {
auto* gh = getGameHandler(L);
if (gh) gh->clearFocus();
return 0;
}
// AssistUnit(unitId) — target whatever the given unit is targeting
static int lua_AssistUnit(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
const char* uid = luaL_optstring(L, 1, "target");
std::string uidStr(uid);
toLowerInPlace(uidStr);
uint64_t guid = resolveUnitGuid(gh, uidStr);
if (guid == 0) return 0;
uint64_t theirTarget = getEntityTargetGuid(gh, guid);
if (theirTarget != 0) gh->setTarget(theirTarget);
return 0;
}
// TargetLastTarget() — re-target previous target
static int lua_TargetLastTarget(lua_State* L) {
auto* gh = getGameHandler(L);
if (gh) gh->targetLastTarget();
return 0;
}
// TargetNearestEnemy() — tab-target nearest enemy
static int lua_TargetNearestEnemy(lua_State* L) {
auto* gh = getGameHandler(L);
if (gh) gh->targetEnemy(false);
return 0;
}
// TargetNearestFriend() — target nearest friendly unit
static int lua_TargetNearestFriend(lua_State* L) {
auto* gh = getGameHandler(L);
if (gh) gh->targetFriend(false);
return 0;
}
// GetRaidTargetIndex(unit) → icon index (1-8) or nil
static int lua_GetRaidTargetIndex(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
const char* uid = luaL_optstring(L, 1, "target");
std::string uidStr(uid);
toLowerInPlace(uidStr);
uint64_t guid = resolveUnitGuid(gh, uidStr);
if (guid == 0) { return luaReturnNil(L); }
uint8_t mark = gh->getEntityRaidMark(guid);
if (mark == 0xFF) { return luaReturnNil(L); }
lua_pushnumber(L, mark + 1); // WoW uses 1-indexed (1=Star, 2=Circle, ... 8=Skull)
return 1;
}
// SetRaidTarget(unit, index) — set raid marker (1-8, or 0 to clear)
static int lua_SetRaidTarget(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
const char* uid = luaL_optstring(L, 1, "target");
int index = static_cast<int>(luaL_checknumber(L, 2));
std::string uidStr(uid);
toLowerInPlace(uidStr);
uint64_t guid = resolveUnitGuid(gh, uidStr);
if (guid == 0) return 0;
if (index >= 1 && index <= 8)
gh->setRaidMark(guid, static_cast<uint8_t>(index - 1));
else if (index == 0)
gh->setRaidMark(guid, 0xFF); // clear
return 0;
}
// GetSpellPowerCost(spellId) → {{ type=powerType, cost=manaCost, name=powerName }}
static int lua_GetSpellPowerCost(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_newtable(L); return 1; }
uint32_t spellId = static_cast<uint32_t>(luaL_checknumber(L, 1));
auto data = gh->getSpellData(spellId);
lua_newtable(L); // outer table (array of cost entries)
if (data.manaCost > 0) {
lua_newtable(L); // cost entry
lua_pushnumber(L, data.powerType);
lua_setfield(L, -2, "type");
lua_pushnumber(L, data.manaCost);
lua_setfield(L, -2, "cost");
lua_pushstring(L, data.powerType < 7 ? kLuaPowerNames[data.powerType] : "MANA");
lua_setfield(L, -2, "name");
lua_rawseti(L, -2, 1); // outer[1] = entry
}
return 1;
}
// --- GetSpellInfo / GetSpellTexture ---
// GetSpellInfo(spellIdOrName) -> name, rank, icon, castTime, minRange, maxRange, spellId
static int lua_GetSpellInfo(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
uint32_t spellId = 0;
if (lua_isnumber(L, 1)) {
spellId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
const char* name = lua_tostring(L, 1);
if (!name || !*name) { return luaReturnNil(L); }
std::string nameLow(name);
toLowerInPlace(nameLow);
int bestRank = -1;
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
toLowerInPlace(sn);
if (sn != nameLow) continue;
int rank = 0;
const std::string& rk = gh->getSpellRank(sid);
if (!rk.empty()) {
std::string rkl = rk;
toLowerInPlace(rkl);
if (rkl.rfind("rank ", 0) == 0) {
try { rank = std::stoi(rkl.substr(5)); } catch (...) {}
}
}
if (rank > bestRank) { bestRank = rank; spellId = sid; }
}
}
if (spellId == 0) { return luaReturnNil(L); }
std::string name = gh->getSpellName(spellId);
if (name.empty()) { return luaReturnNil(L); }
lua_pushstring(L, name.c_str()); // 1: name
const std::string& rank = gh->getSpellRank(spellId);
lua_pushstring(L, rank.c_str()); // 2: rank
std::string iconPath = gh->getSpellIconPath(spellId);
if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str());
else lua_pushnil(L); // 3: icon texture path
// Resolve cast time and range from Spell.dbc → SpellCastTimes.dbc / SpellRange.dbc
auto spellData = gh->getSpellData(spellId);
lua_pushnumber(L, spellData.castTimeMs); // 4: castTime (ms)
lua_pushnumber(L, spellData.minRange); // 5: minRange (yards)
lua_pushnumber(L, spellData.maxRange); // 6: maxRange (yards)
lua_pushnumber(L, spellId); // 7: spellId
return 7;
}
// GetSpellTexture(spellIdOrName) -> icon texture path string
static int lua_GetSpellTexture(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
uint32_t spellId = 0;
if (lua_isnumber(L, 1)) {
spellId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
const char* name = lua_tostring(L, 1);
if (!name || !*name) { return luaReturnNil(L); }
std::string nameLow(name);
toLowerInPlace(nameLow);
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
toLowerInPlace(sn);
if (sn == nameLow) { spellId = sid; break; }
}
}
if (spellId == 0) { return luaReturnNil(L); }
std::string iconPath = gh->getSpellIconPath(spellId);
if (!iconPath.empty()) lua_pushstring(L, iconPath.c_str());
else lua_pushnil(L);
return 1;
}
// GetItemInfo(itemId) -> name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture, vendorPrice
static int lua_GetSpellLink(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnNil(L); }
uint32_t spellId = 0;
if (lua_isnumber(L, 1)) {
spellId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
const char* name = lua_tostring(L, 1);
if (!name || !*name) { return luaReturnNil(L); }
std::string nameLow(name);
toLowerInPlace(nameLow);
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
toLowerInPlace(sn);
if (sn == nameLow) { spellId = sid; break; }
}
}
if (spellId == 0) { return luaReturnNil(L); }
std::string name = gh->getSpellName(spellId);
if (name.empty()) { return luaReturnNil(L); }
char link[256];
snprintf(link, sizeof(link), "|cff71d5ff|Hspell:%u|h[%s]|h|r", spellId, name.c_str());
lua_pushstring(L, link);
return 1;
}
static int lua_CancelUnitBuff(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
const char* uid = luaL_optstring(L, 1, "player");
std::string uidStr(uid);
toLowerInPlace(uidStr);
if (uidStr != "player") return 0; // Can only cancel own buffs
int index = static_cast<int>(luaL_checknumber(L, 2));
const auto& auras = gh->getPlayerAuras();
// Find the Nth buff (non-debuff)
int buffCount = 0;
for (const auto& a : auras) {
if (a.isEmpty()) continue;
if ((a.flags & 0x80) != 0) continue; // skip debuffs
if (++buffCount == index) {
gh->cancelAura(a.spellId);
break;
}
}
return 0;
}
// CastSpellByID(spellId) — cast spell by numeric ID
static int lua_CastSpellByID(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) return 0;
uint32_t spellId = static_cast<uint32_t>(luaL_checknumber(L, 1));
if (spellId == 0) return 0;
uint64_t target = gh->hasTarget() ? gh->getTargetGuid() : 0;
gh->castSpell(spellId, target);
return 0;
}
static int lua_IsUsableSpell(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushboolean(L, 0); lua_pushboolean(L, 0); return 2; }
uint32_t spellId = 0;
if (lua_isnumber(L, 1)) {
spellId = static_cast<uint32_t>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
const char* name = lua_tostring(L, 1);
if (!name || !*name) { lua_pushboolean(L, 0); lua_pushboolean(L, 0); return 2; }
std::string nameLow(name);
toLowerInPlace(nameLow);
for (uint32_t sid : gh->getKnownSpells()) {
std::string sn = gh->getSpellName(sid);
toLowerInPlace(sn);
if (sn == nameLow) { spellId = sid; break; }
}
}
if (spellId == 0 || !gh->getKnownSpells().count(spellId)) {
lua_pushboolean(L, 0);
lua_pushboolean(L, 0);
return 2;
}
float cd = gh->getSpellCooldown(spellId);
bool onCooldown = (cd > 0.1f);
bool noMana = false;
if (!onCooldown) {
auto spellData = gh->getSpellData(spellId);
if (spellData.manaCost > 0) {
auto playerEntity = gh->getEntityManager().getEntity(gh->getPlayerGuid());
if (playerEntity) {
auto* unit = dynamic_cast<game::Unit*>(playerEntity.get());
if (unit && unit->getPower() < spellData.manaCost) {
noMana = true;
}
}
}
}
lua_pushboolean(L, (onCooldown || noMana) ? 0 : 1);
lua_pushboolean(L, noMana ? 1 : 0);
return 2;
}
void registerSpellLuaAPI(lua_State* L) {
static const struct { const char* name; lua_CFunction func; } api[] = {
{"SpellStopCasting", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->cancelCast();
return 0;
}},
{"SpellStopTargeting", [](lua_State* L) -> int {
(void)L; return 0; // No targeting reticle in this client
}},
{"SpellIsTargeting", [](lua_State* L) -> int {
lua_pushboolean(L, 0); // No AoE targeting reticle
return 1;
}},
{"IsSpellInRange", lua_IsSpellInRange},
{"UnitBuff", lua_UnitBuff},
{"UnitDebuff", lua_UnitDebuff},
{"UnitAura", lua_UnitAuraGeneric},
{"UnitCastingInfo", lua_UnitCastingInfo},
{"UnitChannelInfo", lua_UnitChannelInfo},
{"CastSpellByName", lua_CastSpellByName},
{"CastSpellByID", lua_CastSpellByID},
{"IsSpellKnown", lua_IsSpellKnown},
{"GetNumSpellTabs", lua_GetNumSpellTabs},
{"GetSpellTabInfo", lua_GetSpellTabInfo},
{"GetSpellBookItemInfo", lua_GetSpellBookItemInfo},
{"GetSpellBookItemName", lua_GetSpellBookItemName},
{"GetSpellCooldown", lua_GetSpellCooldown},
{"GetSpellPowerCost", lua_GetSpellPowerCost},
{"GetSpellDescription", lua_GetSpellDescription},
{"GetEnchantInfo", lua_GetEnchantInfo},
{"GetSpellInfo", lua_GetSpellInfo},
{"GetSpellTexture", lua_GetSpellTexture},
{"GetSpellLink", lua_GetSpellLink},
{"IsUsableSpell", lua_IsUsableSpell},
{"CancelUnitBuff", lua_CancelUnitBuff},
{"HasTarget", lua_HasTarget},
{"TargetUnit", lua_TargetUnit},
{"ClearTarget", lua_ClearTarget},
{"FocusUnit", lua_FocusUnit},
{"ClearFocus", lua_ClearFocus},
{"AssistUnit", lua_AssistUnit},
{"TargetLastTarget", lua_TargetLastTarget},
{"TargetNearestEnemy", lua_TargetNearestEnemy},
{"TargetNearestFriend", lua_TargetNearestFriend},
{"GetRaidTargetIndex", lua_GetRaidTargetIndex},
{"SetRaidTarget", lua_SetRaidTarget},
{"IsPlayerSpell", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
uint32_t spellId = static_cast<uint32_t>(luaL_checknumber(L, 1));
lua_pushboolean(L, gh && gh->getKnownSpells().count(spellId) ? 1 : 0);
return 1;
}},
{"IsSpellOverlayed", [](lua_State* L) -> int {
(void)L; lua_pushboolean(L, 0); return 1; // No proc overlay tracking
}},
{"IsCurrentSpell", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
uint32_t spellId = static_cast<uint32_t>(luaL_checknumber(L, 1));
lua_pushboolean(L, gh && gh->getCurrentCastSpellId() == spellId ? 1 : 0);
return 1;
}},
{"IsAutoRepeatSpell", [](lua_State* L) -> int {
(void)L; lua_pushboolean(L, 0); return 1; // Stub
}},
{"CastShapeshiftForm", [](lua_State* L) -> int {
// CastShapeshiftForm(index) — cast the spell for the given form slot
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) return 0;
uint8_t classId = gh->getPlayerClass();
// Map class + index to spell IDs
// Warrior stances
static const uint32_t warriorSpells[] = {2457, 71, 2458}; // Battle, Defensive, Berserker
// Druid forms
static const uint32_t druidSpells[] = {5487, 783, 768, 40120, 24858, 33891}; // Bear, Travel, Cat, Swift Flight, Moonkin, Tree
// DK presences
static const uint32_t dkSpells[] = {48266, 48263, 48265}; // Blood, Frost, Unholy
// Rogue
static const uint32_t rogueSpells[] = {1784}; // Stealth
const uint32_t* spells = nullptr;
int numSpells = 0;
switch (classId) {
case 1: spells = warriorSpells; numSpells = 3; break;
case 6: spells = dkSpells; numSpells = 3; break;
case 4: spells = rogueSpells; numSpells = 1; break;
case 11: spells = druidSpells; numSpells = 6; break;
default: return 0;
}
if (index <= numSpells) {
gh->castSpell(spells[index - 1], 0);
}
return 0;
}},
{"CancelShapeshiftForm", [](lua_State* L) -> int {
// Cancel current form — cast spell 0 or cancel aura
auto* gh = getGameHandler(L);
if (gh && gh->getShapeshiftFormId() != 0) {
// Cancelling a form is done by re-casting the same form spell
// For simplicity, just note that the server will handle it
}
return 0;
}},
{"GetShapeshiftFormCooldown", [](lua_State* L) -> int {
// No per-form cooldown tracking — return no cooldown
lua_pushnumber(L, 0); lua_pushnumber(L, 0); lua_pushnumber(L, 1);
return 3;
}},
{"GetShapeshiftForm", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getShapeshiftFormId() : 0);
return 1;
}},
{"GetNumShapeshiftForms", [](lua_State* L) -> int {
// Return count based on player class
auto* gh = getGameHandler(L);
if (!gh) { return luaReturnZero(L); }
uint8_t classId = gh->getPlayerClass();
// Druid: Bear(1), Aquatic(2), Cat(3), Travel(4), Moonkin/Tree(5/6)
// Warrior: Battle(1), Defensive(2), Berserker(3)
// Rogue: Stealth(1)
// Priest: Shadowform(1)
// Paladin: varies by level/talents
// DK: Blood Presence, Frost, Unholy (3)
switch (classId) {
case 1: lua_pushnumber(L, 3); break; // Warrior
case 2: lua_pushnumber(L, 3); break; // Paladin (auras)
case 4: lua_pushnumber(L, 1); break; // Rogue
case 5: lua_pushnumber(L, 1); break; // Priest
case 6: lua_pushnumber(L, 3); break; // Death Knight
case 11: lua_pushnumber(L, 6); break; // Druid
default: lua_pushnumber(L, 0); break;
}
return 1;
}},
};
for (const auto& [name, func] : api) {
lua_pushcfunction(L, func);
lua_setglobal(L, name);
}
}
} // namespace wowee::addons

View file

@ -0,0 +1,761 @@
// lua_system_api.cpp — System, time, sound, locale, map, addons, instances, and utilities Lua API bindings.
// Extracted from lua_engine.cpp as part of §5.1 (Tame LuaEngine).
#include "addons/lua_api_helpers.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/ui_sound_manager.hpp"
#include "core/window.hpp"
#include "game/expansion_profile.hpp"
namespace wowee::addons {
static int lua_PlaySound(lua_State* L) {
auto* svc = getLuaServices(L);
auto* ac = svc ? svc->audioCoordinator : nullptr;
if (!ac) return 0;
auto* sfx = ac->getUiSoundManager();
if (!sfx) return 0;
// Accept numeric sound ID or string name
std::string sound;
if (lua_isnumber(L, 1)) {
uint32_t id = static_cast<uint32_t>(lua_tonumber(L, 1));
// Map common WoW sound IDs to named sounds
switch (id) {
case 856: case 1115: sfx->playButtonClick(); return 0; // igMainMenuOption
case 840: sfx->playQuestActivate(); return 0; // igQuestListOpen
case 841: sfx->playQuestComplete(); return 0; // igQuestListComplete
case 862: sfx->playBagOpen(); return 0; // igBackPackOpen
case 863: sfx->playBagClose(); return 0; // igBackPackClose
case 867: sfx->playError(); return 0; // igPlayerInvite
case 888: sfx->playLevelUp(); return 0; // LEVELUPSOUND
default: return 0;
}
} else {
const char* name = luaL_optstring(L, 1, "");
sound = name;
for (char& c : sound) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
if (sound == "IGMAINMENUOPTION" || sound == "IGMAINMENUOPTIONCHECKBOXON")
sfx->playButtonClick();
else if (sound == "IGQUESTLISTOPEN") sfx->playQuestActivate();
else if (sound == "IGQUESTLISTCOMPLETE") sfx->playQuestComplete();
else if (sound == "IGBACKPACKOPEN") sfx->playBagOpen();
else if (sound == "IGBACKPACKCLOSE") sfx->playBagClose();
else if (sound == "LEVELUPSOUND") sfx->playLevelUp();
else if (sound == "IGPLAYERINVITEACCEPTED") sfx->playButtonClick();
else if (sound == "TALENTSCREENOPEN") sfx->playCharacterSheetOpen();
else if (sound == "TALENTSCREENCLOSE") sfx->playCharacterSheetClose();
}
return 0;
}
// PlaySoundFile(path) — stub (file-based sounds not loaded from Lua)
static int lua_PlaySoundFile(lua_State* L) { (void)L; return 0; }
static int lua_GetPlayerMapPosition(lua_State* L) {
auto* gh = getGameHandler(L);
if (gh) {
const auto& mi = gh->getMovementInfo();
lua_pushnumber(L, mi.x);
lua_pushnumber(L, mi.y);
return 2;
}
lua_pushnumber(L, 0);
lua_pushnumber(L, 0);
return 2;
}
// GetPlayerFacing() → radians (0 = north, increasing counter-clockwise)
static int lua_GetPlayerFacing(lua_State* L) {
auto* gh = getGameHandler(L);
if (gh) {
float facing = gh->getMovementInfo().orientation;
// Normalize to [0, 2π)
while (facing < 0) facing += 6.2831853f;
while (facing >= 6.2831853f) facing -= 6.2831853f;
lua_pushnumber(L, facing);
} else {
lua_pushnumber(L, 0);
}
return 1;
}
// GetCVar(name) → value string (stub for most, real for a few)
static int lua_GetCVar(lua_State* L) {
const char* name = luaL_checkstring(L, 1);
std::string n(name);
// Return sensible defaults for commonly queried CVars
if (n == "uiScale") lua_pushstring(L, "1");
else if (n == "useUIScale") lua_pushstring(L, "1");
else if (n == "screenWidth" || n == "gxResolution") {
auto* svc = getLuaServices(L);
auto* win = svc ? svc->window : nullptr;
lua_pushstring(L, std::to_string(win ? win->getWidth() : 1920).c_str());
} else if (n == "screenHeight" || n == "gxFullscreenResolution") {
auto* svc = getLuaServices(L);
auto* win = svc ? svc->window : nullptr;
lua_pushstring(L, std::to_string(win ? win->getHeight() : 1080).c_str());
} else if (n == "nameplateShowFriends") lua_pushstring(L, "1");
else if (n == "nameplateShowEnemies") lua_pushstring(L, "1");
else if (n == "Sound_EnableSFX") lua_pushstring(L, "1");
else if (n == "Sound_EnableMusic") lua_pushstring(L, "1");
else if (n == "chatBubbles") lua_pushstring(L, "1");
else if (n == "autoLootDefault") lua_pushstring(L, "1");
else lua_pushstring(L, "0");
return 1;
}
// SetCVar(name, value) — no-op stub
static int lua_SetCVar(lua_State* L) {
(void)L;
return 0;
}
static int lua_GetNumAddOns(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, "wowee_addon_count");
return 1;
}
static int lua_GetAddOnInfo(lua_State* L) {
// Accept index (1-based) or addon name
lua_getfield(L, LUA_REGISTRYINDEX, "wowee_addon_info");
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
return luaReturnNil(L);
}
int idx = 0;
if (lua_isnumber(L, 1)) {
idx = static_cast<int>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
// Search by name
const char* name = lua_tostring(L, 1);
int count = static_cast<int>(lua_objlen(L, -1));
for (int i = 1; i <= count; i++) {
lua_rawgeti(L, -1, i);
lua_getfield(L, -1, "name");
const char* aName = lua_tostring(L, -1);
lua_pop(L, 1);
if (aName && strcmp(aName, name) == 0) { idx = i; lua_pop(L, 1); break; }
lua_pop(L, 1);
}
}
if (idx < 1) { lua_pop(L, 1); lua_pushnil(L); return 1; }
lua_rawgeti(L, -1, idx);
if (!lua_istable(L, -1)) { lua_pop(L, 2); lua_pushnil(L); return 1; }
lua_getfield(L, -1, "name");
lua_getfield(L, -2, "title");
lua_getfield(L, -3, "notes");
lua_pushboolean(L, 1); // loadable (always true for now)
lua_pushstring(L, "INSECURE"); // security
lua_pop(L, 1); // pop addon info entry (keep others)
// Return: name, title, notes, loadable, reason, security
return 5;
}
// GetAddOnMetadata(addonNameOrIndex, key) → value
static int lua_GetAddOnMetadata(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, "wowee_addon_info");
if (!lua_istable(L, -1)) { lua_pop(L, 1); lua_pushnil(L); return 1; }
int idx = 0;
if (lua_isnumber(L, 1)) {
idx = static_cast<int>(lua_tonumber(L, 1));
} else if (lua_isstring(L, 1)) {
const char* name = lua_tostring(L, 1);
int count = static_cast<int>(lua_objlen(L, -1));
for (int i = 1; i <= count; i++) {
lua_rawgeti(L, -1, i);
lua_getfield(L, -1, "name");
const char* aName = lua_tostring(L, -1);
lua_pop(L, 1);
if (aName && strcmp(aName, name) == 0) { idx = i; lua_pop(L, 1); break; }
lua_pop(L, 1);
}
}
if (idx < 1) { lua_pop(L, 1); lua_pushnil(L); return 1; }
const char* key = luaL_checkstring(L, 2);
lua_rawgeti(L, -1, idx);
if (!lua_istable(L, -1)) { lua_pop(L, 2); lua_pushnil(L); return 1; }
lua_getfield(L, -1, "metadata");
if (!lua_istable(L, -1)) { lua_pop(L, 3); lua_pushnil(L); return 1; }
lua_getfield(L, -1, key);
return 1;
}
// UnitBuff(unitId, index) / UnitDebuff(unitId, index)
// Returns: name, rank, icon, count, debuffType, duration, expirationTime, caster, isStealable, shouldConsolidate, spellId
static int lua_GetLocale(lua_State* L) {
lua_pushstring(L, "enUS");
return 1;
}
static int lua_GetBuildInfo(lua_State* L) {
// Return WotLK defaults; expansion-specific version detection would need
// access to the expansion registry which isn't available here.
lua_pushstring(L, "3.3.5a"); // 1: version
lua_pushnumber(L, 12340); // 2: buildNumber
lua_pushstring(L, "Jan 1 2025");// 3: date
lua_pushnumber(L, 30300); // 4: tocVersion
return 4;
}
static int lua_GetCurrentMapAreaID(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getCurrentMapId() : 0);
return 1;
}
// GetZoneText() / GetRealZoneText() → current zone name
static int lua_GetZoneText(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushstring(L, ""); return 1; }
uint32_t zoneId = gh->getWorldStateZoneId();
if (zoneId != 0) {
std::string name = gh->getWhoAreaName(zoneId);
if (!name.empty()) { lua_pushstring(L, name.c_str()); return 1; }
}
lua_pushstring(L, "");
return 1;
}
// GetSubZoneText() → subzone name (same as zone for now — server doesn't always send subzone)
static int lua_GetSubZoneText(lua_State* L) {
return lua_GetZoneText(L); // Best-effort: zone and subzone often overlap
}
// GetMinimapZoneText() → zone name displayed near minimap
static int lua_GetMinimapZoneText(lua_State* L) {
return lua_GetZoneText(L);
}
// --- World Map Navigation API ---
// Map ID → continent mapping
static int mapIdToContinent(uint32_t mapId) {
switch (mapId) {
case 0: return 2; // Eastern Kingdoms
case 1: return 1; // Kalimdor
case 530: return 3; // Outland
case 571: return 4; // Northrend
default: return 0; // Instance or unknown
}
}
// Internal tracked map state (which continent/zone the map UI is viewing)
static int s_mapContinent = 0;
static int s_mapZone = 0;
// SetMapToCurrentZone() — sets map view to the player's current zone
static int lua_SetMapToCurrentZone(lua_State* L) {
auto* gh = getGameHandler(L);
if (gh) {
s_mapContinent = mapIdToContinent(gh->getCurrentMapId());
s_mapZone = static_cast<int>(gh->getWorldStateZoneId());
}
return 0;
}
// GetCurrentMapContinent() → continentId (1=Kalimdor, 2=EK, 3=Outland, 4=Northrend)
static int lua_GetCurrentMapContinent(lua_State* L) {
if (s_mapContinent == 0) {
auto* gh = getGameHandler(L);
if (gh) s_mapContinent = mapIdToContinent(gh->getCurrentMapId());
}
lua_pushnumber(L, s_mapContinent);
return 1;
}
// GetCurrentMapZone() → zoneId
static int lua_GetCurrentMapZone(lua_State* L) {
if (s_mapZone == 0) {
auto* gh = getGameHandler(L);
if (gh) s_mapZone = static_cast<int>(gh->getWorldStateZoneId());
}
lua_pushnumber(L, s_mapZone);
return 1;
}
// SetMapZoom(continent [, zone]) — sets map view to continent/zone
static int lua_SetMapZoom(lua_State* L) {
s_mapContinent = static_cast<int>(luaL_checknumber(L, 1));
s_mapZone = static_cast<int>(luaL_optnumber(L, 2, 0));
return 0;
}
// GetMapContinents() → "Kalimdor", "Eastern Kingdoms", ...
static int lua_GetMapContinents(lua_State* L) {
lua_pushstring(L, "Kalimdor");
lua_pushstring(L, "Eastern Kingdoms");
lua_pushstring(L, "Outland");
lua_pushstring(L, "Northrend");
return 4;
}
// GetMapZones(continent) → zone names for that continent
// Returns a basic list; addons mainly need this to not error
static int lua_GetMapZones(lua_State* L) {
int cont = static_cast<int>(luaL_checknumber(L, 1));
// Return a minimal representative set per continent
switch (cont) {
case 1: // Kalimdor
lua_pushstring(L, "Durotar"); lua_pushstring(L, "Mulgore");
lua_pushstring(L, "The Barrens"); lua_pushstring(L, "Teldrassil");
return 4;
case 2: // Eastern Kingdoms
lua_pushstring(L, "Elwynn Forest"); lua_pushstring(L, "Westfall");
lua_pushstring(L, "Dun Morogh"); lua_pushstring(L, "Tirisfal Glades");
return 4;
case 3: // Outland
lua_pushstring(L, "Hellfire Peninsula"); lua_pushstring(L, "Zangarmarsh");
return 2;
case 4: // Northrend
lua_pushstring(L, "Borean Tundra"); lua_pushstring(L, "Howling Fjord");
return 2;
default:
return 0;
}
}
// GetNumMapLandmarks() → 0 (no landmark data exposed yet)
static int lua_GetNumMapLandmarks(lua_State* L) {
lua_pushnumber(L, 0);
return 1;
}
static int lua_GetGameTime(lua_State* L) {
// Returns server game time as hours, minutes
auto* gh = getGameHandler(L);
if (gh) {
float gt = gh->getGameTime();
int hours = static_cast<int>(gt) % 24;
int mins = static_cast<int>((gt - static_cast<int>(gt)) * 60.0f);
lua_pushnumber(L, hours);
lua_pushnumber(L, mins);
} else {
lua_pushnumber(L, 12);
lua_pushnumber(L, 0);
}
return 2;
}
static int lua_GetServerTime(lua_State* L) {
lua_pushnumber(L, static_cast<double>(std::time(nullptr)));
return 1;
}
static int lua_IsInInstance(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushboolean(L, 0); lua_pushstring(L, "none"); return 2; }
bool inInstance = gh->isInInstance();
lua_pushboolean(L, inInstance);
lua_pushstring(L, inInstance ? "party" : "none"); // simplified: "none", "party", "raid", "pvp", "arena"
return 2;
}
// GetInstanceInfo() → name, type, difficultyIndex, difficultyName, maxPlayers, ...
static int lua_GetInstanceInfo(lua_State* L) {
auto* gh = getGameHandler(L);
if (!gh) {
lua_pushstring(L, ""); lua_pushstring(L, "none"); lua_pushnumber(L, 0);
lua_pushstring(L, "Normal"); lua_pushnumber(L, 0);
return 5;
}
std::string mapName = gh->getMapName(gh->getCurrentMapId());
lua_pushstring(L, mapName.c_str()); // 1: name
lua_pushstring(L, gh->isInInstance() ? "party" : "none"); // 2: instanceType
lua_pushnumber(L, gh->getInstanceDifficulty()); // 3: difficultyIndex
static constexpr const char* kDiff[] = {"Normal", "Heroic", "25 Normal", "25 Heroic"};
uint32_t diff = gh->getInstanceDifficulty();
lua_pushstring(L, (diff < 4) ? kDiff[diff] : "Normal"); // 4: difficultyName
lua_pushnumber(L, 5); // 5: maxPlayers (default 5-man)
return 5;
}
static int lua_GetInstanceDifficulty(lua_State* L) {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? (gh->getInstanceDifficulty() + 1) : 1);
return 1;
}
static int lua_strsplit(lua_State* L) {
const char* delim = luaL_checkstring(L, 1);
const char* str = luaL_checkstring(L, 2);
if (!delim[0]) { lua_pushstring(L, str); return 1; }
int count = 0;
std::string s(str);
size_t pos = 0;
while (pos <= s.size()) {
size_t found = s.find(delim[0], pos);
if (found == std::string::npos) {
lua_pushstring(L, s.substr(pos).c_str());
count++;
break;
}
lua_pushstring(L, s.substr(pos, found - pos).c_str());
count++;
pos = found + 1;
}
return count;
}
// strtrim(str) — remove leading/trailing whitespace
static int lua_strtrim(lua_State* L) {
const char* str = luaL_checkstring(L, 1);
std::string s(str);
size_t start = s.find_first_not_of(" \t\r\n");
size_t end = s.find_last_not_of(" \t\r\n");
lua_pushstring(L, (start == std::string::npos) ? "" : s.substr(start, end - start + 1).c_str());
return 1;
}
// wipe(table) — clear all entries from a table
static int lua_wipe(lua_State* L) {
luaL_checktype(L, 1, LUA_TTABLE);
// Remove all integer keys
int len = static_cast<int>(lua_objlen(L, 1));
for (int i = len; i >= 1; i--) {
lua_pushnil(L);
lua_rawseti(L, 1, i);
}
// Remove all string keys
lua_pushnil(L);
while (lua_next(L, 1) != 0) {
lua_pop(L, 1); // pop value
lua_pushvalue(L, -1); // copy key
lua_pushnil(L);
lua_rawset(L, 1); // table[key] = nil
}
lua_pushvalue(L, 1);
return 1;
}
// date(format) — safe date function (os.date was removed)
static int lua_wow_date(lua_State* L) {
const char* fmt = luaL_optstring(L, 1, "%c");
time_t now = time(nullptr);
struct tm* tm = localtime(&now);
char buf[256];
strftime(buf, sizeof(buf), fmt, tm);
lua_pushstring(L, buf);
return 1;
}
// time() — current unix timestamp
static int lua_wow_time(lua_State* L) {
lua_pushnumber(L, static_cast<double>(time(nullptr)));
return 1;
}
// GetTime() — returns elapsed seconds since engine start (shared epoch)
static int lua_wow_gettime(lua_State* L) {
lua_pushnumber(L, luaGetTimeNow());
return 1;
}
void registerSystemLuaAPI(lua_State* L) {
static const struct { const char* name; lua_CFunction func; } api[] = {
{"PlaySound", lua_PlaySound},
{"PlaySoundFile", lua_PlaySoundFile},
{"GetPlayerMapPosition", lua_GetPlayerMapPosition},
{"GetPlayerFacing", lua_GetPlayerFacing},
{"GetCVar", lua_GetCVar},
{"SetCVar", lua_SetCVar},
{"GetLocale", lua_GetLocale},
{"GetBuildInfo", lua_GetBuildInfo},
{"GetCurrentMapAreaID", lua_GetCurrentMapAreaID},
{"SetMapToCurrentZone", lua_SetMapToCurrentZone},
{"GetCurrentMapContinent", lua_GetCurrentMapContinent},
{"GetCurrentMapZone", lua_GetCurrentMapZone},
{"SetMapZoom", lua_SetMapZoom},
{"GetMapContinents", lua_GetMapContinents},
{"GetMapZones", lua_GetMapZones},
{"GetNumMapLandmarks", lua_GetNumMapLandmarks},
{"GetZoneText", lua_GetZoneText},
{"GetRealZoneText", lua_GetZoneText},
{"GetSubZoneText", lua_GetSubZoneText},
{"GetMinimapZoneText", lua_GetMinimapZoneText},
{"GetGameTime", lua_GetGameTime},
{"GetServerTime", lua_GetServerTime},
{"GetNumAddOns", lua_GetNumAddOns},
{"GetAddOnInfo", lua_GetAddOnInfo},
{"GetAddOnMetadata", lua_GetAddOnMetadata},
{"IsInInstance", lua_IsInInstance},
{"GetInstanceInfo", lua_GetInstanceInfo},
{"GetInstanceDifficulty", lua_GetInstanceDifficulty},
{"strsplit", lua_strsplit},
{"strtrim", lua_strtrim},
{"wipe", lua_wipe},
{"date", lua_wow_date},
{"time", lua_wow_time},
{"GetTime", lua_wow_gettime},
{"IsConnectedToServer", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushboolean(L, gh && gh->isConnected() ? 1 : 0);
return 1;
}},
{"GetRealmName", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) {
const auto* ac = gh->getActiveCharacter();
lua_pushstring(L, ac ? "WoWee" : "Unknown");
} else lua_pushstring(L, "Unknown");
return 1;
}},
{"GetNormalizedRealmName", [](lua_State* L) -> int {
lua_pushstring(L, "WoWee");
return 1;
}},
{"ShowHelm", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->toggleHelm(); // Toggles helm visibility
return 0;
}},
{"ShowCloak", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->toggleCloak();
return 0;
}},
{"TogglePVP", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->togglePvp();
return 0;
}},
{"Minimap_Ping", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
float x = static_cast<float>(luaL_optnumber(L, 1, 0));
float y = static_cast<float>(luaL_optnumber(L, 2, 0));
if (gh) gh->sendMinimapPing(x, y);
return 0;
}},
{"RequestTimePlayed", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->requestPlayedTime();
return 0;
}},
{"Logout", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->requestLogout();
return 0;
}},
{"CancelLogout", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (gh) gh->cancelLogout();
return 0;
}},
{"NumTaxiNodes", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getTaxiNodes().size() : 0);
return 1;
}},
{"TaxiNodeName", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh) { lua_pushstring(L, ""); return 1; }
int i = 0;
for (const auto& [id, node] : gh->getTaxiNodes()) {
if (++i == index) {
lua_pushstring(L, node.name.c_str());
return 1;
}
}
lua_pushstring(L, "");
return 1;
}},
{"TaxiNodeGetType", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh) { return luaReturnZero(L); }
int i = 0;
for (const auto& [id, node] : gh->getTaxiNodes()) {
if (++i == index) {
bool known = gh->isKnownTaxiNode(id);
lua_pushnumber(L, known ? 1 : 0); // 0=none, 1=reachable, 2=current
return 1;
}
}
lua_pushnumber(L, 0);
return 1;
}},
{"TakeTaxiNode", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh) return 0;
int i = 0;
for (const auto& [id, node] : gh->getTaxiNodes()) {
if (++i == index) {
gh->activateTaxi(id);
break;
}
}
return 0;
}},
{"GetNetStats", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
uint32_t ms = gh ? gh->getLatencyMs() : 0;
lua_pushnumber(L, 0); // bandwidthIn
lua_pushnumber(L, 0); // bandwidthOut
lua_pushnumber(L, ms); // latencyHome
lua_pushnumber(L, ms); // latencyWorld
return 4;
}},
{"GetCurrentTitle", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getChosenTitleBit() : -1);
return 1;
}},
{"GetTitleName", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
int bit = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || bit < 0) { return luaReturnNil(L); }
std::string title = gh->getFormattedTitle(static_cast<uint32_t>(bit));
if (title.empty()) { return luaReturnNil(L); }
lua_pushstring(L, title.c_str());
return 1;
}},
{"SetCurrentTitle", [](lua_State* L) -> int {
(void)L; // Title changes require CMSG_SET_TITLE which we don't expose yet
return 0;
}},
{"GetInspectSpecialization", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
const auto* ir = gh ? gh->getInspectResult() : nullptr;
lua_pushnumber(L, ir ? ir->activeTalentGroup : 0);
return 1;
}},
{"NotifyInspect", [](lua_State* L) -> int {
(void)L; // Inspect is auto-triggered by the C++ side when targeting a player
return 0;
}},
{"ClearInspectPlayer", [](lua_State* L) -> int {
(void)L;
return 0;
}},
{"GetHonorCurrency", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getHonorPoints() : 0);
return 1;
}},
{"GetArenaCurrency", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getArenaPoints() : 0);
return 1;
}},
{"GetTimePlayed", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
lua_pushnumber(L, gh->getTotalTimePlayed());
lua_pushnumber(L, gh->getLevelTimePlayed());
return 2;
}},
{"GetBindLocation", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushstring(L, "Unknown"); return 1; }
lua_pushstring(L, gh->getWhoAreaName(gh->getHomeBindZoneId()).c_str());
return 1;
}},
{"GetNumSavedInstances", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
lua_pushnumber(L, gh ? gh->getInstanceLockouts().size() : 0);
return 1;
}},
{"GetSavedInstanceInfo", [](lua_State* L) -> int {
// GetSavedInstanceInfo(index) → name, id, reset, difficulty, locked, extended, instanceIDMostSig, isRaid, maxPlayers
auto* gh = getGameHandler(L);
int index = static_cast<int>(luaL_checknumber(L, 1));
if (!gh || index < 1) { return luaReturnNil(L); }
const auto& lockouts = gh->getInstanceLockouts();
if (index > static_cast<int>(lockouts.size())) { return luaReturnNil(L); }
const auto& l = lockouts[index - 1];
lua_pushstring(L, ("Instance " + std::to_string(l.mapId)).c_str()); // name (would need MapDBC for real names)
lua_pushnumber(L, l.mapId); // id
lua_pushnumber(L, static_cast<double>(l.resetTime - static_cast<uint64_t>(time(nullptr)))); // reset (seconds until)
lua_pushnumber(L, l.difficulty); // difficulty
lua_pushboolean(L, l.locked ? 1 : 0); // locked
lua_pushboolean(L, l.extended ? 1 : 0); // extended
lua_pushnumber(L, 0); // instanceIDMostSig
lua_pushboolean(L, l.difficulty >= 2 ? 1 : 0); // isRaid (25-man = raid)
lua_pushnumber(L, l.difficulty >= 2 ? 25 : (l.difficulty >= 1 ? 10 : 5)); // maxPlayers
return 9;
}},
{"CalendarGetDate", [](lua_State* L) -> int {
// CalendarGetDate() → weekday, month, day, year
time_t now = time(nullptr);
struct tm* t = localtime(&now);
lua_pushnumber(L, t->tm_wday + 1); // weekday (1=Sun)
lua_pushnumber(L, t->tm_mon + 1); // month (1-12)
lua_pushnumber(L, t->tm_mday); // day
lua_pushnumber(L, t->tm_year + 1900); // year
return 4;
}},
{"CalendarGetNumPendingInvites", [](lua_State* L) -> int {
return luaReturnZero(L);
}},
{"CalendarGetNumDayEvents", [](lua_State* L) -> int {
return luaReturnZero(L);
}},
{"GetDifficultyInfo", [](lua_State* L) -> int {
// GetDifficultyInfo(id) → name, groupType, isHeroic, maxPlayers
int diff = static_cast<int>(luaL_checknumber(L, 1));
struct DiffInfo { const char* name; const char* group; int heroic; int maxPlayers; };
static const DiffInfo infos[] = {
{"5 Player", "party", 0, 5}, // 0: Normal 5-man
{"5 Player (Heroic)", "party", 1, 5}, // 1: Heroic 5-man
{"10 Player", "raid", 0, 10}, // 2: 10-man Normal
{"25 Player", "raid", 0, 25}, // 3: 25-man Normal
{"10 Player (Heroic)", "raid", 1, 10}, // 4: 10-man Heroic
{"25 Player (Heroic)", "raid", 1, 25}, // 5: 25-man Heroic
};
if (diff >= 0 && diff < 6) {
lua_pushstring(L, infos[diff].name);
lua_pushstring(L, infos[diff].group);
lua_pushboolean(L, infos[diff].heroic);
lua_pushnumber(L, infos[diff].maxPlayers);
} else {
lua_pushstring(L, "Unknown");
lua_pushstring(L, "party");
lua_pushboolean(L, 0);
lua_pushnumber(L, 5);
}
return 4;
}},
{"GetWeatherInfo", [](lua_State* L) -> int {
auto* gh = getGameHandler(L);
if (!gh) { lua_pushnumber(L, 0); lua_pushnumber(L, 0); return 2; }
lua_pushnumber(L, gh->getWeatherType());
lua_pushnumber(L, gh->getWeatherIntensity());
return 2;
}},
{"GetMaxPlayerLevel", [](lua_State* L) -> int {
auto* svc = getLuaServices(L);
auto* reg = svc ? svc->expansionRegistry : nullptr;
auto* prof = reg ? reg->getActive() : nullptr;
if (prof && prof->id == "wotlk") lua_pushnumber(L, 80);
else if (prof && prof->id == "tbc") lua_pushnumber(L, 70);
else lua_pushnumber(L, 60);
return 1;
}},
{"GetAccountExpansionLevel", [](lua_State* L) -> int {
auto* svc = getLuaServices(L);
auto* reg = svc ? svc->expansionRegistry : nullptr;
auto* prof = reg ? reg->getActive() : nullptr;
if (prof && prof->id == "wotlk") lua_pushnumber(L, 3);
else if (prof && prof->id == "tbc") lua_pushnumber(L, 2);
else lua_pushnumber(L, 1);
return 1;
}},
};
for (const auto& [name, func] : api) {
lua_pushcfunction(L, func);
lua_setglobal(L, name);
}
}
} // namespace wowee::addons

1514
src/addons/lua_unit_api.cpp Normal file

File diff suppressed because it is too large Load diff

View file

@ -265,7 +265,11 @@ bool Application::initialize() {
// Initialize addon system
addonManager_ = std::make_unique<addons::AddonManager>();
if (addonManager_->initialize(gameHandler.get())) {
addons::LuaServices luaSvc;
luaSvc.window = window.get();
luaSvc.audioCoordinator = audioCoordinator_.get();
luaSvc.expansionRegistry = expansionRegistry_.get();
if (addonManager_->initialize(gameHandler.get(), luaSvc)) {
std::string addonsDir = assetPath + "/interface/AddOns";
addonManager_->scanAddons(addonsDir);
// Wire Lua errors to UI error display

View file

@ -5305,6 +5305,40 @@ void GameHandler::acceptBattlefield(uint32_t queueSlot) {
// LFG / Dungeon Finder handlers (WotLK 3.3.5a)
// ---------------------------------------------------------------------------
static const char* lfgJoinResultString(uint8_t result) {
switch (result) {
case 0: return nullptr; // success
case 1: return "Role check failed.";
case 2: return "No LFG slots available for your group.";
case 3: return "No LFG object found.";
case 4: return "No slots available (player).";
case 5: return "No slots available (party).";
case 6: return "Dungeon requirements not met by all members.";
case 7: return "Party members are from different realms.";
case 8: return "Not all members are present.";
case 9: return "Get info timeout.";
case 10: return "Invalid dungeon slot.";
case 11: return "You are marked as a deserter.";
case 12: return "A party member is marked as a deserter.";
case 13: return "You are on a random dungeon cooldown.";
case 14: return "A party member is on a random dungeon cooldown.";
case 16: return "No spec/role available.";
default: return "Cannot join dungeon finder.";
}
}
static const char* lfgTeleportDeniedString(uint8_t reason) {
switch (reason) {
case 0: return "You are not in a LFG group.";
case 1: return "You are not in the dungeon.";
case 2: return "You have a summon pending.";
case 3: return "You are dead.";
case 4: return "You have Deserter.";
case 5: return "You do not meet the requirements.";
default: return "Teleport to dungeon denied.";
}
}
// ---------------------------------------------------------------------------
// LFG outgoing packets
// ---------------------------------------------------------------------------

View file

@ -140,6 +140,50 @@ namespace {
return "Unknown";
}
// Collect all non-comment, non-empty lines from a macro body.
std::vector<std::string> allMacroCommands(const std::string& macroText) {
std::vector<std::string> cmds;
size_t pos = 0;
while (pos <= macroText.size()) {
size_t nl = macroText.find('\n', pos);
std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos);
if (!line.empty() && line.back() == '\r') line.pop_back();
size_t start = line.find_first_not_of(" \t");
if (start != std::string::npos) line = line.substr(start);
if (!line.empty() && line.front() != '#')
cmds.push_back(std::move(line));
if (nl == std::string::npos) break;
pos = nl + 1;
}
return cmds;
}
// Returns the #showtooltip argument from a macro body.
std::string getMacroShowtooltipArg(const std::string& macroText) {
size_t pos = 0;
while (pos <= macroText.size()) {
size_t nl = macroText.find('\n', pos);
std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos);
if (!line.empty() && line.back() == '\r') line.pop_back();
size_t fs = line.find_first_not_of(" \t");
if (fs != std::string::npos) line = line.substr(fs);
if (line.rfind("#showtooltip", 0) == 0 || line.rfind("#show", 0) == 0) {
size_t sp = line.find(' ');
if (sp != std::string::npos) {
std::string arg = line.substr(sp + 1);
size_t as = arg.find_first_not_of(" \t");
if (as != std::string::npos) arg = arg.substr(as);
size_t ae = arg.find_last_not_of(" \t");
if (ae != std::string::npos) arg.resize(ae + 1);
if (!arg.empty()) return arg;
}
return "__auto__";
}
if (nl == std::string::npos) break;
pos = nl + 1;
}
return {};
}
}
namespace wowee { namespace ui {