mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-03 20:03:50 +00:00
chore(lua): refactor addon Lua engine API + progress docs
- Refactor Lua addon integration: - Update CMakeLists.txt for addon build paths - Enhance addons API headers and Lua engine interface - Add new Lua API addon modules (`lua_api_helpers`, `lua_api_registrations`, `lua_services`, `lua_action_api`, `lua_inventory_api`, `lua_quest_api`, `lua_social_api`, `lua_spell_api`, `lua_system_api`, `lua_unit_api`) - Update implementation in addon_manager.cpp, lua_engine.cpp, application.cpp, game_handler.cpp
This commit is contained in:
parent
6e02b4451c
commit
a916270a13
21 changed files with 6183 additions and 6700 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
166
include/addons/lua_api_helpers.hpp
Normal file
166
include/addons/lua_api_helpers.hpp
Normal 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
|
||||
18
include/addons/lua_api_registrations.hpp
Normal file
18
include/addons/lua_api_registrations.hpp
Normal 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
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
17
include/addons/lua_services.hpp
Normal file
17
include/addons/lua_services.hpp
Normal 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
|
||||
|
|
@ -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_);
|
||||
|
|
|
|||
734
src/addons/lua_action_api.cpp
Normal file
734
src/addons/lua_action_api.cpp
Normal 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
892
src/addons/lua_inventory_api.cpp
Normal file
892
src/addons/lua_inventory_api.cpp
Normal 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
|
||||
564
src/addons/lua_quest_api.cpp
Normal file
564
src/addons/lua_quest_api.cpp
Normal 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
|
||||
502
src/addons/lua_social_api.cpp
Normal file
502
src/addons/lua_social_api.cpp
Normal 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
|
||||
968
src/addons/lua_spell_api.cpp
Normal file
968
src/addons/lua_spell_api.cpp
Normal 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
|
||||
761
src/addons/lua_system_api.cpp
Normal file
761
src/addons/lua_system_api.cpp
Normal 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
1514
src/addons/lua_unit_api.cpp
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -94,13 +94,6 @@ bool isAuthCharPipelineOpcode(LogicalOpcode op) {
|
|||
|
||||
namespace {
|
||||
|
||||
bool envFlagEnabled(const char* key, bool defaultValue = false) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw || !*raw) return defaultValue;
|
||||
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
|
||||
raw[0] == 'n' || raw[0] == 'N');
|
||||
}
|
||||
|
||||
int parseEnvIntClamped(const char* key, int defaultValue, int minValue, int maxValue) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw || !*raw) return defaultValue;
|
||||
|
|
@ -126,47 +119,14 @@ float incomingPacketBudgetMs(WorldState state) {
|
|||
return static_cast<float>(state == WorldState::IN_WORLD ? inWorldBudgetMs : loginBudgetMs);
|
||||
}
|
||||
|
||||
int updateObjectBlocksBudgetPerUpdate(WorldState state) {
|
||||
static const int inWorldBudget =
|
||||
parseEnvIntClamped("WOWEE_NET_MAX_UPDATE_OBJECT_BLOCKS", 24, 1, 2048);
|
||||
static const int loginBudget =
|
||||
parseEnvIntClamped("WOWEE_NET_MAX_UPDATE_OBJECT_BLOCKS_LOGIN", 128, 1, 4096);
|
||||
return state == WorldState::IN_WORLD ? inWorldBudget : loginBudget;
|
||||
}
|
||||
|
||||
float slowPacketLogThresholdMs() {
|
||||
static const int thresholdMs =
|
||||
parseEnvIntClamped("WOWEE_NET_SLOW_PACKET_LOG_MS", 10, 1, 60000);
|
||||
return static_cast<float>(thresholdMs);
|
||||
}
|
||||
|
||||
float slowUpdateObjectBlockLogThresholdMs() {
|
||||
static const int thresholdMs =
|
||||
parseEnvIntClamped("WOWEE_NET_SLOW_UPDATE_BLOCK_LOG_MS", 10, 1, 60000);
|
||||
return static_cast<float>(thresholdMs);
|
||||
}
|
||||
|
||||
constexpr size_t kMaxQueuedInboundPackets = 4096;
|
||||
|
||||
CombatTextEntry::Type combatTextTypeFromSpellMissInfo(uint8_t missInfo) {
|
||||
switch (missInfo) {
|
||||
case 0: return CombatTextEntry::MISS;
|
||||
case 1: return CombatTextEntry::DODGE;
|
||||
case 2: return CombatTextEntry::PARRY;
|
||||
case 3: return CombatTextEntry::BLOCK;
|
||||
case 4: return CombatTextEntry::EVADE;
|
||||
case 5: return CombatTextEntry::IMMUNE;
|
||||
case 6: return CombatTextEntry::DEFLECT;
|
||||
case 7: return CombatTextEntry::ABSORB;
|
||||
case 8: return CombatTextEntry::RESIST;
|
||||
case 9: // Some cores encode SPELL_MISS_IMMUNE2 as 9.
|
||||
case 10: // Others encode SPELL_MISS_IMMUNE2 as 10.
|
||||
return CombatTextEntry::IMMUNE;
|
||||
case 11: return CombatTextEntry::REFLECT;
|
||||
default: return CombatTextEntry::MISS;
|
||||
}
|
||||
}
|
||||
|
||||
} // end anonymous namespace
|
||||
|
||||
std::string formatCopperAmount(uint32_t amount) {
|
||||
|
|
@ -192,412 +152,6 @@ std::string formatCopperAmount(uint32_t amount) {
|
|||
return oss.str();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string displaySpellName(GameHandler& handler, uint32_t spellId) {
|
||||
if (spellId == 0) return {};
|
||||
const std::string& name = handler.getSpellName(spellId);
|
||||
if (!name.empty()) return name;
|
||||
return "spell " + std::to_string(spellId);
|
||||
}
|
||||
|
||||
std::string formatSpellNameList(GameHandler& handler,
|
||||
const std::vector<uint32_t>& spellIds,
|
||||
size_t maxShown = 3) {
|
||||
if (spellIds.empty()) return {};
|
||||
|
||||
const size_t shownCount = std::min(spellIds.size(), maxShown);
|
||||
std::ostringstream oss;
|
||||
for (size_t i = 0; i < shownCount; ++i) {
|
||||
if (i > 0) {
|
||||
if (shownCount == 2) {
|
||||
oss << " and ";
|
||||
} else if (i == shownCount - 1) {
|
||||
oss << ", and ";
|
||||
} else {
|
||||
oss << ", ";
|
||||
}
|
||||
}
|
||||
oss << displaySpellName(handler, spellIds[i]);
|
||||
}
|
||||
|
||||
if (spellIds.size() > shownCount) {
|
||||
oss << ", and " << (spellIds.size() - shownCount) << " more";
|
||||
}
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
bool readCStringAt(const std::vector<uint8_t>& data, size_t start, std::string& out, size_t& nextPos) {
|
||||
out.clear();
|
||||
if (start >= data.size()) return false;
|
||||
size_t i = start;
|
||||
while (i < data.size()) {
|
||||
uint8_t b = data[i++];
|
||||
if (b == 0) {
|
||||
nextPos = i;
|
||||
return true;
|
||||
}
|
||||
out.push_back(static_cast<char>(b));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string asciiLower(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return s;
|
||||
}
|
||||
|
||||
std::vector<std::string> splitWowPath(const std::string& wowPath) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
for (char c : wowPath) {
|
||||
if (c == '\\' || c == '/') {
|
||||
if (!cur.empty()) {
|
||||
out.push_back(cur);
|
||||
cur.clear();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
cur.push_back(c);
|
||||
}
|
||||
if (!cur.empty()) out.push_back(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
int pathCaseScore(const std::string& name) {
|
||||
int score = 0;
|
||||
for (unsigned char c : name) {
|
||||
if (std::islower(c)) score += 2;
|
||||
else if (std::isupper(c)) score -= 1;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
std::string resolveCaseInsensitiveDataPath(const std::string& dataRoot, const std::string& wowPath) {
|
||||
if (dataRoot.empty() || wowPath.empty()) return std::string();
|
||||
std::filesystem::path cur(dataRoot);
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(cur, ec) || !std::filesystem::is_directory(cur, ec)) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
for (const std::string& segment : splitWowPath(wowPath)) {
|
||||
std::string wanted = asciiLower(segment);
|
||||
std::filesystem::path bestPath;
|
||||
int bestScore = std::numeric_limits<int>::min();
|
||||
bool found = false;
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(cur, ec)) {
|
||||
if (ec) break;
|
||||
std::string name = entry.path().filename().string();
|
||||
if (asciiLower(name) != wanted) continue;
|
||||
int score = pathCaseScore(name);
|
||||
if (!found || score > bestScore) {
|
||||
found = true;
|
||||
bestScore = score;
|
||||
bestPath = entry.path();
|
||||
}
|
||||
}
|
||||
if (!found) return std::string();
|
||||
cur = bestPath;
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(cur, ec) || std::filesystem::is_directory(cur, ec)) {
|
||||
return std::string();
|
||||
}
|
||||
return cur.string();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> readFileBinary(const std::string& fsPath) {
|
||||
std::ifstream in(fsPath, std::ios::binary);
|
||||
if (!in) return {};
|
||||
in.seekg(0, std::ios::end);
|
||||
std::streamoff size = in.tellg();
|
||||
if (size <= 0) return {};
|
||||
in.seekg(0, std::ios::beg);
|
||||
std::vector<uint8_t> data(static_cast<size_t>(size));
|
||||
in.read(reinterpret_cast<char*>(data.data()), size);
|
||||
if (!in) return {};
|
||||
return data;
|
||||
}
|
||||
|
||||
bool hmacSha1Matches(const uint8_t seedBytes[4], const std::string& text, const uint8_t expected[20]) {
|
||||
uint8_t out[SHA_DIGEST_LENGTH];
|
||||
unsigned int outLen = 0;
|
||||
HMAC(EVP_sha1(),
|
||||
seedBytes, 4,
|
||||
reinterpret_cast<const uint8_t*>(text.data()),
|
||||
static_cast<int>(text.size()),
|
||||
out, &outLen);
|
||||
return outLen == SHA_DIGEST_LENGTH && std::memcmp(out, expected, SHA_DIGEST_LENGTH) == 0;
|
||||
}
|
||||
|
||||
const std::unordered_map<std::string, std::array<uint8_t, 20>>& knownDoorHashes() {
|
||||
static const std::unordered_map<std::string, std::array<uint8_t, 20>> k = {
|
||||
{"world\\lordaeron\\stratholme\\activedoodads\\doors\\nox_door_plague.m2",
|
||||
{0xB4,0x45,0x2B,0x6D,0x95,0xC9,0x8B,0x18,0x6A,0x70,0xB0,0x08,0xFA,0x07,0xBB,0xAE,0xF3,0x0D,0xF7,0xA2}},
|
||||
{"world\\kalimdor\\onyxiaslair\\doors\\onyxiasgate01.m2",
|
||||
{0x75,0x19,0x5E,0x4A,0xED,0xA0,0xBC,0xAF,0x04,0x8C,0xA0,0xE3,0x4D,0x95,0xA7,0x0D,0x4F,0x53,0xC7,0x46}},
|
||||
{"world\\generic\\human\\activedoodads\\doors\\deadminedoor02.m2",
|
||||
{0x3D,0xFF,0x01,0x1B,0x9A,0xB1,0x34,0xF3,0x7F,0x88,0x50,0x97,0xE6,0x95,0x35,0x1B,0x91,0x95,0x35,0x64}},
|
||||
{"world\\kalimdor\\silithus\\activedoodads\\ahnqirajdoor\\ahnqirajdoor02.m2",
|
||||
{0xDB,0xD4,0xF4,0x07,0xC4,0x68,0xCC,0x36,0x13,0x4E,0x62,0x1D,0x16,0x01,0x78,0xFD,0xA4,0xD0,0xD2,0x49}},
|
||||
{"world\\kalimdor\\diremaul\\activedoodads\\doors\\diremaulsmallinstancedoor.m2",
|
||||
{0x0D,0xC8,0xDB,0x46,0xC8,0x55,0x49,0xC0,0xFF,0x1A,0x60,0x0F,0x6C,0x23,0x63,0x57,0xC3,0x05,0x78,0x1A}},
|
||||
};
|
||||
return k;
|
||||
}
|
||||
|
||||
bool isReadableQuestText(const std::string& s, size_t minLen, size_t maxLen) {
|
||||
if (s.size() < minLen || s.size() > maxLen) return false;
|
||||
bool hasAlpha = false;
|
||||
for (unsigned char c : s) {
|
||||
if (c < 0x20 || c > 0x7E) return false;
|
||||
if (std::isalpha(c)) hasAlpha = true;
|
||||
}
|
||||
return hasAlpha;
|
||||
}
|
||||
|
||||
bool isPlaceholderQuestTitle(const std::string& s) {
|
||||
return s.rfind("Quest #", 0) == 0;
|
||||
}
|
||||
|
||||
float mergeCooldownSeconds(float current, float incoming) {
|
||||
constexpr float kEpsilon = 0.05f;
|
||||
if (incoming <= 0.0f) return 0.0f;
|
||||
if (current <= 0.0f) return incoming;
|
||||
// Cooldowns should normally tick down. If a duplicate/late packet reports a
|
||||
// larger value, keep the local remaining time to avoid visible timer resets.
|
||||
if (incoming > current + kEpsilon) return current;
|
||||
return incoming;
|
||||
}
|
||||
|
||||
bool looksLikeQuestDescriptionText(const std::string& s) {
|
||||
int spaces = 0;
|
||||
int commas = 0;
|
||||
for (unsigned char c : s) {
|
||||
if (c == ' ') spaces++;
|
||||
if (c == ',') commas++;
|
||||
}
|
||||
const int words = spaces + 1;
|
||||
if (words > 8) return true;
|
||||
if (commas > 0 && words > 5) return true;
|
||||
if (s.find(". ") != std::string::npos) return true;
|
||||
if (s.find(':') != std::string::npos && words > 5) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isStrongQuestTitle(const std::string& s) {
|
||||
if (!isReadableQuestText(s, 6, 72)) return false;
|
||||
if (looksLikeQuestDescriptionText(s)) return false;
|
||||
unsigned char first = static_cast<unsigned char>(s.front());
|
||||
return std::isupper(first) != 0;
|
||||
}
|
||||
|
||||
int scoreQuestTitle(const std::string& s) {
|
||||
if (!isReadableQuestText(s, 4, 72)) return -1000;
|
||||
if (looksLikeQuestDescriptionText(s)) return -1000;
|
||||
int score = 0;
|
||||
score += static_cast<int>(std::min<size_t>(s.size(), 32));
|
||||
unsigned char first = static_cast<unsigned char>(s.front());
|
||||
if (std::isupper(first)) score += 20;
|
||||
if (std::islower(first)) score -= 20;
|
||||
if (s.find(' ') != std::string::npos) score += 8;
|
||||
if (s.find('.') != std::string::npos) score -= 18;
|
||||
if (s.find('!') != std::string::npos || s.find('?') != std::string::npos) score -= 6;
|
||||
return score;
|
||||
}
|
||||
|
||||
struct QuestQueryTextCandidate {
|
||||
std::string title;
|
||||
std::string objectives;
|
||||
int score = -1000;
|
||||
};
|
||||
|
||||
QuestQueryTextCandidate pickBestQuestQueryTexts(const std::vector<uint8_t>& data, bool classicHint) {
|
||||
QuestQueryTextCandidate best;
|
||||
if (data.size() <= 9) return best;
|
||||
|
||||
std::vector<size_t> seedOffsets;
|
||||
const size_t base = 8;
|
||||
const size_t classicOffset = base + 40u * 4u;
|
||||
const size_t wotlkOffset = base + 55u * 4u;
|
||||
if (classicHint) {
|
||||
seedOffsets.push_back(classicOffset);
|
||||
seedOffsets.push_back(wotlkOffset);
|
||||
} else {
|
||||
seedOffsets.push_back(wotlkOffset);
|
||||
seedOffsets.push_back(classicOffset);
|
||||
}
|
||||
for (size_t off : seedOffsets) {
|
||||
if (off < data.size()) {
|
||||
std::string title;
|
||||
size_t next = off;
|
||||
if (readCStringAt(data, off, title, next)) {
|
||||
QuestQueryTextCandidate c;
|
||||
c.title = title;
|
||||
c.score = scoreQuestTitle(title) + 20; // Prefer expected struct offsets
|
||||
|
||||
std::string s2;
|
||||
size_t n2 = next;
|
||||
if (readCStringAt(data, next, s2, n2) && isReadableQuestText(s2, 8, 600)) {
|
||||
c.objectives = s2;
|
||||
}
|
||||
if (c.score > best.score) best = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: scan packet for best printable C-string title candidate.
|
||||
for (size_t start = 8; start < data.size(); ++start) {
|
||||
std::string title;
|
||||
size_t next = start;
|
||||
if (!readCStringAt(data, start, title, next)) continue;
|
||||
|
||||
QuestQueryTextCandidate c;
|
||||
c.title = title;
|
||||
c.score = scoreQuestTitle(title);
|
||||
if (c.score < 0) continue;
|
||||
|
||||
std::string s2, s3;
|
||||
size_t n2 = next, n3 = next;
|
||||
if (readCStringAt(data, next, s2, n2)) {
|
||||
if (isReadableQuestText(s2, 8, 600)) c.objectives = s2;
|
||||
else if (readCStringAt(data, n2, s3, n3) && isReadableQuestText(s3, 8, 600)) c.objectives = s3;
|
||||
}
|
||||
if (c.score > best.score) best = c;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
// Parse kill/item objectives from SMSG_QUEST_QUERY_RESPONSE raw data.
|
||||
// Returns true if the objective block was found and at least one entry read.
|
||||
//
|
||||
// Format after the fixed integer header (40*4 Classic or 55*4 WotLK bytes post questId+questMethod):
|
||||
// N strings (title, objectives, details, endText; + completedText for WotLK)
|
||||
// 4x { int32 npcOrGoId, uint32 count } -- entity (kill/interact) objectives
|
||||
// 6x { uint32 itemId, uint32 count } -- item collect objectives
|
||||
// 4x cstring -- per-objective display text
|
||||
//
|
||||
// We use the same fixed-offset heuristic as pickBestQuestQueryTexts and then scan past
|
||||
// the string section to reach the objective data.
|
||||
struct QuestQueryObjectives {
|
||||
struct Kill { int32_t npcOrGoId; uint32_t required; };
|
||||
struct Item { uint32_t itemId; uint32_t required; };
|
||||
std::array<Kill, 4> kills{};
|
||||
std::array<Item, 6> items{};
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
static uint32_t readU32At(const std::vector<uint8_t>& d, size_t pos) {
|
||||
return static_cast<uint32_t>(d[pos])
|
||||
| (static_cast<uint32_t>(d[pos + 1]) << 8)
|
||||
| (static_cast<uint32_t>(d[pos + 2]) << 16)
|
||||
| (static_cast<uint32_t>(d[pos + 3]) << 24);
|
||||
}
|
||||
|
||||
// Try to parse objective block starting at `startPos` with `nStrings` strings before it.
|
||||
// Returns a valid QuestQueryObjectives if the data looks plausible, otherwise invalid.
|
||||
static QuestQueryObjectives tryParseQuestObjectivesAt(const std::vector<uint8_t>& data,
|
||||
size_t startPos, int nStrings) {
|
||||
QuestQueryObjectives out;
|
||||
size_t pos = startPos;
|
||||
|
||||
// Scan past each string (null-terminated).
|
||||
for (int si = 0; si < nStrings; ++si) {
|
||||
while (pos < data.size() && data[pos] != 0) ++pos;
|
||||
if (pos >= data.size()) return out; // truncated
|
||||
++pos; // consume null terminator
|
||||
}
|
||||
|
||||
// Read 4 entity objectives: int32 npcOrGoId + uint32 count each.
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (pos + 8 > data.size()) return out;
|
||||
out.kills[i].npcOrGoId = static_cast<int32_t>(readU32At(data, pos)); pos += 4;
|
||||
out.kills[i].required = readU32At(data, pos); pos += 4;
|
||||
}
|
||||
|
||||
// Read 6 item objectives: uint32 itemId + uint32 count each.
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
if (pos + 8 > data.size()) break;
|
||||
out.items[i].itemId = readU32At(data, pos); pos += 4;
|
||||
out.items[i].required = readU32At(data, pos); pos += 4;
|
||||
}
|
||||
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
QuestQueryObjectives extractQuestQueryObjectives(const std::vector<uint8_t>& data, bool classicHint) {
|
||||
if (data.size() < 16) return {};
|
||||
|
||||
// questId(4) + questMethod(4) prefix before the fixed integer header.
|
||||
const size_t base = 8;
|
||||
// Classic/TBC: 40 fixed uint32 fields + 4 strings before objectives.
|
||||
// WotLK: 55 fixed uint32 fields + 5 strings before objectives.
|
||||
const size_t classicStart = base + 40u * 4u;
|
||||
const size_t wotlkStart = base + 55u * 4u;
|
||||
|
||||
// Try the expected layout first, then fall back to the other.
|
||||
if (classicHint) {
|
||||
auto r = tryParseQuestObjectivesAt(data, classicStart, 4);
|
||||
if (r.valid) return r;
|
||||
return tryParseQuestObjectivesAt(data, wotlkStart, 5);
|
||||
} else {
|
||||
auto r = tryParseQuestObjectivesAt(data, wotlkStart, 5);
|
||||
if (r.valid) return r;
|
||||
return tryParseQuestObjectivesAt(data, classicStart, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse quest reward fields from SMSG_QUEST_QUERY_RESPONSE fixed header.
|
||||
// Classic/TBC: 40 fixed fields; WotLK: 55 fixed fields.
|
||||
struct QuestQueryRewards {
|
||||
int32_t rewardMoney = 0;
|
||||
std::array<uint32_t, 4> itemId{};
|
||||
std::array<uint32_t, 4> itemCount{};
|
||||
std::array<uint32_t, 6> choiceItemId{};
|
||||
std::array<uint32_t, 6> choiceItemCount{};
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
static QuestQueryRewards tryParseQuestRewards(const std::vector<uint8_t>& data,
|
||||
bool classicLayout) {
|
||||
const size_t base = 8; // after questId(4) + questMethod(4)
|
||||
const size_t fieldCount = classicLayout ? 40u : 55u;
|
||||
const size_t headerEnd = base + fieldCount * 4u;
|
||||
if (data.size() < headerEnd) return {};
|
||||
|
||||
// Field indices (0-based) for each expansion:
|
||||
// Classic/TBC: rewardMoney=[14], rewardItemId[4]=[20..23], rewardItemCount[4]=[24..27],
|
||||
// rewardChoiceItemId[6]=[28..33], rewardChoiceItemCount[6]=[34..39]
|
||||
// WotLK: rewardMoney=[17], rewardItemId[4]=[30..33], rewardItemCount[4]=[34..37],
|
||||
// rewardChoiceItemId[6]=[38..43], rewardChoiceItemCount[6]=[44..49]
|
||||
const size_t moneyField = classicLayout ? 14u : 17u;
|
||||
const size_t itemIdField = classicLayout ? 20u : 30u;
|
||||
const size_t itemCountField = classicLayout ? 24u : 34u;
|
||||
const size_t choiceIdField = classicLayout ? 28u : 38u;
|
||||
const size_t choiceCntField = classicLayout ? 34u : 44u;
|
||||
|
||||
QuestQueryRewards out;
|
||||
out.rewardMoney = static_cast<int32_t>(readU32At(data, base + moneyField * 4u));
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
out.itemId[i] = readU32At(data, base + (itemIdField + i) * 4u);
|
||||
out.itemCount[i] = readU32At(data, base + (itemCountField + i) * 4u);
|
||||
}
|
||||
for (size_t i = 0; i < 6; ++i) {
|
||||
out.choiceItemId[i] = readU32At(data, base + (choiceIdField + i) * 4u);
|
||||
out.choiceItemCount[i] = readU32At(data, base + (choiceCntField + i) * 4u);
|
||||
}
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
template<typename ManagerGetter, typename Callback>
|
||||
void GameHandler::withSoundManager(ManagerGetter getter, Callback cb) {
|
||||
if (auto* ac = services_.audioCoordinator) {
|
||||
|
|
@ -5747,44 +5301,6 @@ void GameHandler::acceptBattlefield(uint32_t queueSlot) {
|
|||
if (socialHandler_) socialHandler_->acceptBattlefield(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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -5922,16 +5438,6 @@ float GameHandler::getSpellCooldown(uint32_t spellId) const {
|
|||
return 0;
|
||||
}
|
||||
|
||||
static audio::SpellSoundManager::MagicSchool schoolMaskToMagicSchool(uint32_t mask) {
|
||||
if (mask & 0x04) return audio::SpellSoundManager::MagicSchool::FIRE;
|
||||
if (mask & 0x10) return audio::SpellSoundManager::MagicSchool::FROST;
|
||||
if (mask & 0x02) return audio::SpellSoundManager::MagicSchool::HOLY;
|
||||
if (mask & 0x08) return audio::SpellSoundManager::MagicSchool::NATURE;
|
||||
if (mask & 0x20) return audio::SpellSoundManager::MagicSchool::SHADOW;
|
||||
if (mask & 0x40) return audio::SpellSoundManager::MagicSchool::ARCANE;
|
||||
return audio::SpellSoundManager::MagicSchool::ARCANE;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Talents
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -53,23 +53,6 @@ namespace {
|
|||
return s.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
// Format a duration in seconds as compact text: "2h", "3:05", "42"
|
||||
void fmtDurationCompact(char* buf, size_t sz, int secs) {
|
||||
if (secs >= 3600) snprintf(buf, sz, "%dh", secs / 3600);
|
||||
else if (secs >= 60) snprintf(buf, sz, "%d:%02d", secs / 60, secs % 60);
|
||||
else snprintf(buf, sz, "%d", secs);
|
||||
}
|
||||
|
||||
// Render "Remaining: Xs" or "Remaining: Xm Ys" in a tooltip (light gray)
|
||||
void renderAuraRemaining(int remainMs) {
|
||||
if (remainMs <= 0) return;
|
||||
int s = remainMs / 1000;
|
||||
char buf[32];
|
||||
if (s < 60) snprintf(buf, sizeof(buf), "Remaining: %ds", s);
|
||||
else snprintf(buf, sizeof(buf), "Remaining: %dm %ds", s / 60, s % 60);
|
||||
ImGui::TextColored(kLightGray, "%s", buf);
|
||||
}
|
||||
|
||||
std::string toLower(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
|
|
@ -187,12 +170,10 @@ bool ChatPanel::shouldShowMessage(const game::MessageChatData& msg, int tabIndex
|
|||
|
||||
|
||||
// Forward declaration — defined below
|
||||
static std::string firstMacroCommand(const std::string& macroText);
|
||||
static std::vector<std::string> allMacroCommands(const std::string& macroText);
|
||||
static std::string evaluateMacroConditionals(const std::string& rawArg,
|
||||
game::GameHandler& gameHandler,
|
||||
uint64_t& targetOverride);
|
||||
static std::string getMacroShowtooltipArg(const std::string& macroText);
|
||||
|
||||
void ChatPanel::render(game::GameHandler& gameHandler,
|
||||
InventoryScreen& inventoryScreen,
|
||||
|
|
@ -1708,22 +1689,6 @@ void ChatPanel::render(game::GameHandler& gameHandler,
|
|||
}
|
||||
|
||||
|
||||
static std::string firstMacroCommand(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 start = line.find_first_not_of(" \t");
|
||||
if (start != std::string::npos) line = line.substr(start);
|
||||
if (!line.empty() && line.front() != '#')
|
||||
return line;
|
||||
if (nl == std::string::npos) break;
|
||||
pos = nl + 1;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Collect all non-comment, non-empty lines from a macro body.
|
||||
static std::vector<std::string> allMacroCommands(const std::string& macroText) {
|
||||
std::vector<std::string> cmds;
|
||||
|
|
@ -1742,36 +1707,6 @@ static std::vector<std::string> allMacroCommands(const std::string& macroText) {
|
|||
return cmds;
|
||||
}
|
||||
|
||||
// Returns the #showtooltip argument from a macro body:
|
||||
// "#showtooltip Spell" → "Spell"
|
||||
// "#showtooltip" → "__auto__" (derive from first /cast)
|
||||
// (none) → ""
|
||||
static 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 {};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WoW macro conditional evaluator
|
||||
// Parses: [cond1,cond2] Spell1; [cond3] Spell2; DefaultSpell
|
||||
|
|
|
|||
|
|
@ -32,13 +32,6 @@ namespace {
|
|||
constexpr auto& kColorBrightGreen = kBrightGreen;
|
||||
constexpr auto& kColorYellow = kYellow;
|
||||
|
||||
// Format a duration in seconds as compact text: "2h", "3:05", "42"
|
||||
void fmtDurationCompact(char* buf, size_t sz, int secs) {
|
||||
if (secs >= 3600) snprintf(buf, sz, "%dh", secs / 3600);
|
||||
else if (secs >= 60) snprintf(buf, sz, "%d:%02d", secs / 60, secs % 60);
|
||||
else snprintf(buf, sz, "%d", secs);
|
||||
}
|
||||
|
||||
// Render "Remaining: Xs" or "Remaining: Xm Ys" in a tooltip (light gray)
|
||||
void renderAuraRemaining(int remainMs) {
|
||||
if (remainMs <= 0) return;
|
||||
|
|
@ -269,7 +262,6 @@ void CombatUI::renderRaidWarningOverlay(game::GameHandler& gameHandler) {
|
|||
// Walk only the new messages (deque — iterate from back by skipping old ones)
|
||||
size_t toScan = newCount - raidWarnChatSeenCount_;
|
||||
size_t startIdx = newCount > toScan ? newCount - toScan : 0;
|
||||
auto* renderer = services_.renderer;
|
||||
for (size_t i = startIdx; i < newCount; ++i) {
|
||||
const auto& msg = chatHistory[i];
|
||||
if (msg.type == game::ChatType::RAID_WARNING ||
|
||||
|
|
|
|||
|
|
@ -76,24 +76,6 @@ namespace {
|
|||
// Common ImGui window flags for popup dialogs
|
||||
const ImGuiWindowFlags kDialogFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
||||
|
||||
// Build a WoW-format item link string for chat insertion.
|
||||
// Format: |cff<qualHex>|Hitem:<itemId>:0:0:0:0:0:0:0:0|h[<name>]|h|r
|
||||
std::string buildItemChatLink(uint32_t itemId, uint8_t quality, const std::string& name) {
|
||||
static constexpr const char* kQualHex[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"};
|
||||
uint8_t qi = quality < 8 ? quality : 1;
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), "|cff%s|Hitem:%u:0:0:0:0:0:0:0:0|h[%s]|h|r",
|
||||
kQualHex[qi], itemId, name.c_str());
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
size_t first = s.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string::npos) return "";
|
||||
size_t last = s.find_last_not_of(" \t\r\n");
|
||||
return s.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
// Format a duration in seconds as compact text: "2h", "3:05", "42"
|
||||
void fmtDurationCompact(char* buf, size_t sz, int secs) {
|
||||
if (secs >= 3600) snprintf(buf, sz, "%dh", secs / 3600);
|
||||
|
|
@ -111,13 +93,6 @@ namespace {
|
|||
ImGui::TextColored(kLightGray, "%s", buf);
|
||||
}
|
||||
|
||||
std::string toLower(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
// Render gold/silver/copper amounts in WoW-canonical colors on the current ImGui line.
|
||||
// Skips zero-value denominations (except copper, which is always shown when gold=silver=0).
|
||||
// Aliases for shared class color helpers (wowee::ui namespace)
|
||||
|
|
@ -138,40 +113,6 @@ namespace {
|
|||
return wowee::game::getClassName(static_cast<wowee::game::Class>(classId));
|
||||
}
|
||||
|
||||
bool isPortBotTarget(const std::string& target) {
|
||||
std::string t = toLower(trim(target));
|
||||
return t == "portbot" || t == "gmbot" || t == "telebot";
|
||||
}
|
||||
|
||||
std::string buildPortBotCommand(const std::string& rawInput) {
|
||||
std::string input = trim(rawInput);
|
||||
if (input.empty()) return "";
|
||||
|
||||
std::string lower = toLower(input);
|
||||
if (lower == "help" || lower == "?") {
|
||||
return "__help__";
|
||||
}
|
||||
|
||||
if (lower.rfind(".tele ", 0) == 0 || lower.rfind(".go ", 0) == 0) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (lower.rfind("xyz ", 0) == 0) {
|
||||
return ".go " + input;
|
||||
}
|
||||
|
||||
if (lower == "sw" || lower == "stormwind") return ".tele stormwind";
|
||||
if (lower == "if" || lower == "ironforge") return ".tele ironforge";
|
||||
if (lower == "darn" || lower == "darnassus") return ".tele darnassus";
|
||||
if (lower == "org" || lower == "orgrimmar") return ".tele orgrimmar";
|
||||
if (lower == "tb" || lower == "thunderbluff") return ".tele thunderbluff";
|
||||
if (lower == "uc" || lower == "undercity") return ".tele undercity";
|
||||
if (lower == "shatt" || lower == "shattrath") return ".tele shattrath";
|
||||
if (lower == "dal" || lower == "dalaran") return ".tele dalaran";
|
||||
|
||||
return ".tele " + input;
|
||||
}
|
||||
|
||||
bool raySphereIntersect(const wowee::rendering::Ray& ray, const glm::vec3& center, float radius, float& tOut) {
|
||||
glm::vec3 oc = ray.origin - center;
|
||||
float b = glm::dot(oc, ray.direction);
|
||||
|
|
@ -198,51 +139,6 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -43,25 +43,6 @@ namespace {
|
|||
constexpr auto& kColorGray = kGray;
|
||||
constexpr auto& kColorDarkGray = kDarkGray;
|
||||
|
||||
// Render gold/silver/copper amounts in WoW-canonical colors
|
||||
void renderGoldText(uint32_t totalCopper) {
|
||||
uint32_t gold = totalCopper / 10000;
|
||||
uint32_t silver = (totalCopper / 100) % 100;
|
||||
uint32_t copper = totalCopper % 100;
|
||||
bool shown = false;
|
||||
if (gold > 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%ug", gold);
|
||||
shown = true;
|
||||
}
|
||||
if (silver > 0 || shown) {
|
||||
if (shown) { ImGui::SameLine(0, 2); }
|
||||
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f), "%us", silver);
|
||||
shown = true;
|
||||
}
|
||||
if (shown) { ImGui::SameLine(0, 2); }
|
||||
ImGui::TextColored(ImVec4(0.72f, 0.45f, 0.2f, 1.0f), "%uc", copper);
|
||||
}
|
||||
|
||||
// Common ImGui window flags for popup dialogs
|
||||
const ImGuiWindowFlags kDialogFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue