Compare commits

..

No commits in common. "5ee2b55f4bc2c9ea92829aa1e531e7de97292b56" and "22798d1c76f881dac6d172fa2e3af8fd10ffe7a4" have entirely different histories.

12 changed files with 52 additions and 2441 deletions

View file

@ -26,7 +26,6 @@ public:
bool isInitialized() const { return luaEngine_.isInitialized(); }
void saveAllSavedVariables();
void setCharacterName(const std::string& name) { characterName_ = name; }
/// Re-initialize the Lua VM and reload all addons (used by /reload).
bool reload();
@ -39,8 +38,6 @@ private:
bool loadAddon(const TocFile& addon);
std::string getSavedVariablesPath(const TocFile& addon) const;
std::string getSavedVariablesPerCharacterPath(const TocFile& addon) const;
std::string characterName_;
};
} // namespace wowee::addons

View file

@ -1,6 +1,5 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
@ -48,14 +47,9 @@ public:
lua_State* getState() { return L_; }
bool isInitialized() const { return L_ != nullptr; }
// Optional callback for Lua errors (displayed as UI errors to the player)
using LuaErrorCallback = std::function<void(const std::string&)>;
void setLuaErrorCallback(LuaErrorCallback cb) { luaErrorCallback_ = std::move(cb); }
private:
lua_State* L_ = nullptr;
game::GameHandler* gameHandler_ = nullptr;
LuaErrorCallback luaErrorCallback_;
void registerCoreAPI();
void registerEventAPI();

View file

@ -18,7 +18,6 @@ struct TocFile {
std::string getInterface() const;
bool isLoadOnDemand() const;
std::vector<std::string> getSavedVariables() const;
std::vector<std::string> getSavedVariablesPerCharacter() const;
};
std::optional<TocFile> parseTocFile(const std::string& tocPath);

View file

@ -294,21 +294,6 @@ public:
return spellIconPathResolver_ ? spellIconPathResolver_(spellId) : std::string{};
}
// Spell data resolver: spellId -> {castTimeMs, minRange, maxRange}
struct SpellDataInfo { uint32_t castTimeMs = 0; float minRange = 0; float maxRange = 0; uint32_t manaCost = 0; uint8_t powerType = 0; };
using SpellDataResolver = std::function<SpellDataInfo(uint32_t)>;
void setSpellDataResolver(SpellDataResolver r) { spellDataResolver_ = std::move(r); }
SpellDataInfo getSpellData(uint32_t spellId) const {
return spellDataResolver_ ? spellDataResolver_(spellId) : SpellDataInfo{};
}
// Item icon path resolver: displayInfoId -> texture path (e.g., "Interface\\Icons\\INV_Sword_04")
using ItemIconPathResolver = std::function<std::string(uint32_t)>;
void setItemIconPathResolver(ItemIconPathResolver r) { itemIconPathResolver_ = std::move(r); }
std::string getItemIconPath(uint32_t displayInfoId) const {
return itemIconPathResolver_ ? itemIconPathResolver_(displayInfoId) : std::string{};
}
// Random property/suffix name resolver: randomPropertyId -> suffix name (e.g., "of the Eagle")
// Positive IDs → ItemRandomProperties.dbc; negative IDs → ItemRandomSuffix.dbc (abs value)
using RandomPropertyNameResolver = std::function<std::string(int32_t)>;
@ -418,7 +403,7 @@ public:
bool hasFocus() const { return focusGuid != 0; }
// Mouseover targeting — set each frame by the nameplate renderer
void setMouseoverGuid(uint64_t guid);
void setMouseoverGuid(uint64_t guid) { mouseoverGuid_ = guid; }
uint64_t getMouseoverGuid() const { return mouseoverGuid_; }
// Advanced targeting
@ -1243,16 +1228,6 @@ public:
// Player GUID
uint64_t getPlayerGuid() const { return playerGuid; }
// Look up class/race for a player GUID from name query cache. Returns 0 if unknown.
uint8_t lookupPlayerClass(uint64_t guid) const {
auto it = playerClassRaceCache_.find(guid);
return it != playerClassRaceCache_.end() ? it->second.classId : 0;
}
uint8_t lookupPlayerRace(uint64_t guid) const {
auto it = playerClassRaceCache_.find(guid);
return it != playerClassRaceCache_.end() ? it->second.raceId : 0;
}
// Look up a display name for any guid: checks playerNameCache then entity manager.
// Returns empty string if unknown. Used by chat display to resolve names at render time.
const std::string& lookupName(uint64_t guid) const {
@ -2687,8 +2662,6 @@ private:
AddonChatCallback addonChatCallback_;
AddonEventCallback addonEventCallback_;
SpellIconPathResolver spellIconPathResolver_;
ItemIconPathResolver itemIconPathResolver_;
SpellDataResolver spellDataResolver_;
RandomPropertyNameResolver randomPropertyNameResolver_;
EmoteAnimCallback emoteAnimCallback_;
@ -2729,9 +2702,6 @@ private:
// ---- Phase 1: Name caches ----
std::unordered_map<uint64_t, std::string> playerNameCache;
// Class/race cache from SMSG_NAME_QUERY_RESPONSE (guid → {classId, raceId})
struct PlayerClassRace { uint8_t classId = 0; uint8_t raceId = 0; };
std::unordered_map<uint64_t, PlayerClassRace> playerClassRaceCache_;
std::unordered_set<uint64_t> pendingNameQueries;
std::unordered_map<uint32_t, CreatureQueryResponseData> creatureInfoCache;
std::unordered_set<uint32_t> pendingCreatureQueries;

View file

@ -62,10 +62,7 @@ private:
// Populated by the SpellCastFailedCallback; queried during action bar button rendering.
std::unordered_map<uint32_t, float> actionFlashEndTimes_;
// Cached game handler for input callbacks (set each frame in render)
game::GameHandler* cachedGameHandler_ = nullptr;
// Tab-completion state for slash commands and player names
// Tab-completion state for slash commands
std::string chatTabPrefix_; // prefix captured on first Tab press
std::vector<std::string> chatTabMatches_; // matching command list
int chatTabMatchIdx_ = -1; // active match index (-1 = inactive)

View file

@ -68,11 +68,6 @@ std::string AddonManager::getSavedVariablesPath(const TocFile& addon) const {
return addon.basePath + "/" + addon.addonName + ".lua.saved";
}
std::string AddonManager::getSavedVariablesPerCharacterPath(const TocFile& addon) const {
if (characterName_.empty()) return "";
return addon.basePath + "/" + addon.addonName + "." + characterName_ + ".lua.saved";
}
bool AddonManager::loadAddon(const TocFile& addon) {
// Load SavedVariables before addon code (so globals are available at load time)
auto savedVars = addon.getSavedVariables();
@ -81,15 +76,6 @@ bool AddonManager::loadAddon(const TocFile& addon) {
luaEngine_.loadSavedVariables(svPath);
LOG_DEBUG("AddonManager: loaded saved variables for '", addon.addonName, "'");
}
// Load per-character SavedVariables
auto savedVarsPC = addon.getSavedVariablesPerCharacter();
if (!savedVarsPC.empty()) {
std::string svpcPath = getSavedVariablesPerCharacterPath(addon);
if (!svpcPath.empty()) {
luaEngine_.loadSavedVariables(svpcPath);
LOG_DEBUG("AddonManager: loaded per-character saved variables for '", addon.addonName, "'");
}
}
bool success = true;
for (const auto& filename : addon.files) {
@ -134,13 +120,6 @@ void AddonManager::saveAllSavedVariables() {
std::string svPath = getSavedVariablesPath(addon);
luaEngine_.saveSavedVariables(svPath, savedVars);
}
auto savedVarsPC = addon.getSavedVariablesPerCharacter();
if (!savedVarsPC.empty()) {
std::string svpcPath = getSavedVariablesPerCharacterPath(addon);
if (!svpcPath.empty()) {
luaEngine_.saveSavedVariables(svpcPath, savedVarsPC);
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -19,12 +19,17 @@ bool TocFile::isLoadOnDemand() const {
return (it != directives.end()) && it->second == "1";
}
static std::vector<std::string> parseVarList(const std::string& val) {
std::vector<std::string> TocFile::getSavedVariables() const {
std::vector<std::string> result;
auto it = directives.find("SavedVariables");
if (it == directives.end()) return result;
// Parse comma-separated variable names
std::string val = it->second;
size_t pos = 0;
while (pos <= val.size()) {
size_t comma = val.find(',', pos);
std::string name = (comma != std::string::npos) ? val.substr(pos, comma - pos) : val.substr(pos);
// Trim whitespace
size_t start = name.find_first_not_of(" \t");
size_t end = name.find_last_not_of(" \t");
if (start != std::string::npos)
@ -35,16 +40,6 @@ static std::vector<std::string> parseVarList(const std::string& val) {
return result;
}
std::vector<std::string> TocFile::getSavedVariables() const {
auto it = directives.find("SavedVariables");
return (it != directives.end()) ? parseVarList(it->second) : std::vector<std::string>{};
}
std::vector<std::string> TocFile::getSavedVariablesPerCharacter() const {
auto it = directives.find("SavedVariablesPerCharacter");
return (it != directives.end()) ? parseVarList(it->second) : std::vector<std::string>{};
}
std::optional<TocFile> parseTocFile(const std::string& tocPath) {
std::ifstream f(tocPath);
if (!f.is_open()) return std::nullopt;

View file

@ -335,10 +335,6 @@ bool Application::initialize() {
if (addonManager_->initialize(gameHandler.get())) {
std::string addonsDir = assetPath + "/interface/AddOns";
addonManager_->scanAddons(addonsDir);
// Wire Lua errors to UI error display
addonManager_->getLuaEngine()->setLuaErrorCallback([gh = gameHandler.get()](const std::string& err) {
if (gh) gh->addUIError(err);
});
// Wire chat messages to addon event dispatch
gameHandler->setAddonChatCallback([this](const game::MessageChatData& msg) {
if (!addonManager_ || !addonsLoaded_) return;
@ -358,25 +354,6 @@ bool Application::initialize() {
case game::ChatType::CHANNEL: eventName = "CHAT_MSG_CHANNEL"; break;
case game::ChatType::EMOTE:
case game::ChatType::TEXT_EMOTE: eventName = "CHAT_MSG_EMOTE"; break;
case game::ChatType::ACHIEVEMENT: eventName = "CHAT_MSG_ACHIEVEMENT"; break;
case game::ChatType::GUILD_ACHIEVEMENT: eventName = "CHAT_MSG_GUILD_ACHIEVEMENT"; break;
case game::ChatType::WHISPER_INFORM: eventName = "CHAT_MSG_WHISPER_INFORM"; break;
case game::ChatType::RAID_LEADER: eventName = "CHAT_MSG_RAID_LEADER"; break;
case game::ChatType::BATTLEGROUND_LEADER: eventName = "CHAT_MSG_BATTLEGROUND_LEADER"; break;
case game::ChatType::MONSTER_SAY: eventName = "CHAT_MSG_MONSTER_SAY"; break;
case game::ChatType::MONSTER_YELL: eventName = "CHAT_MSG_MONSTER_YELL"; break;
case game::ChatType::MONSTER_EMOTE: eventName = "CHAT_MSG_MONSTER_EMOTE"; break;
case game::ChatType::MONSTER_WHISPER: eventName = "CHAT_MSG_MONSTER_WHISPER"; break;
case game::ChatType::RAID_BOSS_EMOTE: eventName = "CHAT_MSG_RAID_BOSS_EMOTE"; break;
case game::ChatType::RAID_BOSS_WHISPER: eventName = "CHAT_MSG_RAID_BOSS_WHISPER"; break;
case game::ChatType::BG_SYSTEM_NEUTRAL: eventName = "CHAT_MSG_BG_SYSTEM_NEUTRAL"; break;
case game::ChatType::BG_SYSTEM_ALLIANCE: eventName = "CHAT_MSG_BG_SYSTEM_ALLIANCE"; break;
case game::ChatType::BG_SYSTEM_HORDE: eventName = "CHAT_MSG_BG_SYSTEM_HORDE"; break;
case game::ChatType::MONSTER_PARTY: eventName = "CHAT_MSG_MONSTER_PARTY"; break;
case game::ChatType::AFK: eventName = "CHAT_MSG_AFK"; break;
case game::ChatType::DND: eventName = "CHAT_MSG_DND"; break;
case game::ChatType::LOOT: eventName = "CHAT_MSG_LOOT"; break;
case game::ChatType::SKILL: eventName = "CHAT_MSG_SKILL"; break;
default: break;
}
if (eventName) {
@ -436,119 +413,6 @@ bool Application::initialize() {
return pit->second;
});
}
// Wire item icon path resolver: displayInfoId -> "Interface\\Icons\\INV_..."
{
auto iconNames = std::make_shared<std::unordered_map<uint32_t, std::string>>();
auto loaded = std::make_shared<bool>(false);
auto* am = assetManager.get();
gameHandler->setItemIconPathResolver([iconNames, loaded, am](uint32_t displayInfoId) -> std::string {
if (!am || displayInfoId == 0) return {};
if (!*loaded) {
*loaded = true;
auto dbc = am->loadDBC("ItemDisplayInfo.dbc");
const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
if (dbc && dbc->isLoaded()) {
uint32_t iconField = dispL ? (*dispL)["InventoryIcon"] : 5;
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
uint32_t id = dbc->getUInt32(i, 0); // field 0 = ID
std::string name = dbc->getString(i, iconField);
if (id > 0 && !name.empty()) (*iconNames)[id] = name;
}
LOG_INFO("Loaded ", iconNames->size(), " item icon names from ItemDisplayInfo.dbc");
}
}
auto it = iconNames->find(displayInfoId);
if (it == iconNames->end()) return {};
return "Interface\\Icons\\" + it->second;
});
}
// Wire spell data resolver: spellId -> {castTimeMs, minRange, maxRange}
{
auto castTimeMap = std::make_shared<std::unordered_map<uint32_t, uint32_t>>();
auto rangeMap = std::make_shared<std::unordered_map<uint32_t, std::pair<float,float>>>();
auto spellCastIdx = std::make_shared<std::unordered_map<uint32_t, uint32_t>>(); // spellId→castTimeIdx
auto spellRangeIdx = std::make_shared<std::unordered_map<uint32_t, uint32_t>>(); // spellId→rangeIdx
struct SpellCostEntry { uint32_t manaCost = 0; uint8_t powerType = 0; };
auto spellCostMap = std::make_shared<std::unordered_map<uint32_t, SpellCostEntry>>();
auto loaded = std::make_shared<bool>(false);
auto* am = assetManager.get();
gameHandler->setSpellDataResolver([castTimeMap, rangeMap, spellCastIdx, spellRangeIdx, spellCostMap, loaded, am](uint32_t spellId) -> game::GameHandler::SpellDataInfo {
if (!am) return {};
if (!*loaded) {
*loaded = true;
// Load SpellCastTimes.dbc
auto ctDbc = am->loadDBC("SpellCastTimes.dbc");
if (ctDbc && ctDbc->isLoaded()) {
for (uint32_t i = 0; i < ctDbc->getRecordCount(); ++i) {
uint32_t id = ctDbc->getUInt32(i, 0);
int32_t base = static_cast<int32_t>(ctDbc->getUInt32(i, 1));
if (id > 0 && base > 0) (*castTimeMap)[id] = static_cast<uint32_t>(base);
}
}
// Load SpellRange.dbc
const auto* srL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellRange") : nullptr;
uint32_t minRField = srL ? (*srL)["MinRange"] : 1;
uint32_t maxRField = srL ? (*srL)["MaxRange"] : 4;
auto rDbc = am->loadDBC("SpellRange.dbc");
if (rDbc && rDbc->isLoaded()) {
for (uint32_t i = 0; i < rDbc->getRecordCount(); ++i) {
uint32_t id = rDbc->getUInt32(i, 0);
float minR = rDbc->getFloat(i, minRField);
float maxR = rDbc->getFloat(i, maxRField);
if (id > 0) (*rangeMap)[id] = {minR, maxR};
}
}
// Load Spell.dbc: extract castTimeIndex and rangeIndex per spell
auto sDbc = am->loadDBC("Spell.dbc");
const auto* spL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
if (sDbc && sDbc->isLoaded()) {
uint32_t idF = spL ? (*spL)["ID"] : 0;
uint32_t ctF = spL ? (*spL)["CastingTimeIndex"] : 134; // WotLK default
uint32_t rF = spL ? (*spL)["RangeIndex"] : 132;
uint32_t ptF = UINT32_MAX, mcF = UINT32_MAX;
if (spL) {
try { ptF = (*spL)["PowerType"]; } catch (...) {}
try { mcF = (*spL)["ManaCost"]; } catch (...) {}
}
uint32_t fc = sDbc->getFieldCount();
for (uint32_t i = 0; i < sDbc->getRecordCount(); ++i) {
uint32_t id = sDbc->getUInt32(i, idF);
if (id == 0) continue;
uint32_t ct = sDbc->getUInt32(i, ctF);
uint32_t ri = sDbc->getUInt32(i, rF);
if (ct > 0) (*spellCastIdx)[id] = ct;
if (ri > 0) (*spellRangeIdx)[id] = ri;
// Extract power cost
uint32_t mc = (mcF < fc) ? sDbc->getUInt32(i, mcF) : 0;
uint8_t pt = (ptF < fc) ? static_cast<uint8_t>(sDbc->getUInt32(i, ptF)) : 0;
if (mc > 0) (*spellCostMap)[id] = {mc, pt};
}
}
LOG_INFO("SpellDataResolver: loaded ", spellCastIdx->size(), " cast indices, ",
spellRangeIdx->size(), " range indices");
}
game::GameHandler::SpellDataInfo info;
auto ciIt = spellCastIdx->find(spellId);
if (ciIt != spellCastIdx->end()) {
auto ctIt = castTimeMap->find(ciIt->second);
if (ctIt != castTimeMap->end()) info.castTimeMs = ctIt->second;
}
auto riIt = spellRangeIdx->find(spellId);
if (riIt != spellRangeIdx->end()) {
auto rIt = rangeMap->find(riIt->second);
if (rIt != rangeMap->end()) {
info.minRange = rIt->second.first;
info.maxRange = rIt->second.second;
}
}
auto mcIt = spellCostMap->find(spellId);
if (mcIt != spellCostMap->end()) {
info.manaCost = mcIt->second.manaCost;
info.powerType = mcIt->second.powerType;
}
return info;
});
}
// Wire random property/suffix name resolver for item display
{
auto propNames = std::make_shared<std::unordered_map<int32_t, std::string>>();
@ -5318,21 +5182,6 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
// Load addons once per session on first world entry
if (addonManager_ && !addonsLoaded_) {
// Set character name for per-character SavedVariables
if (gameHandler) {
const std::string& charName = gameHandler->lookupName(gameHandler->getPlayerGuid());
if (!charName.empty()) {
addonManager_->setCharacterName(charName);
} else {
// Fallback: find name from character list
for (const auto& c : gameHandler->getCharacters()) {
if (c.guid == gameHandler->getPlayerGuid()) {
addonManager_->setCharacterName(c.name);
break;
}
}
}
}
addonManager_->loadAllAddons();
addonsLoaded_ = true;
addonManager_->fireEvent("VARIABLES_LOADED");

View file

@ -2010,11 +2010,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
pendingItemPushNotifs_.push_back({itemId, count});
}
}
// Fire bag/inventory events for all item receipts (not just chat-visible ones)
if (addonEventCallback_) {
addonEventCallback_("BAG_UPDATE", {});
addonEventCallback_("UNIT_INVENTORY_CHANGED", {"player"});
}
LOG_INFO("Item push: itemId=", itemId, " count=", count,
" showInChat=", static_cast<int>(showInChat));
}
@ -2165,15 +2160,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (auto* unit = dynamic_cast<Unit*>(entity.get())) {
unit->setHealth(hp);
}
if (addonEventCallback_ && guid != 0) {
std::string unitId;
if (guid == playerGuid) unitId = "player";
else if (guid == targetGuid) unitId = "target";
else if (guid == focusGuid) unitId = "focus";
else if (guid == petGuid_) unitId = "pet";
if (!unitId.empty())
addonEventCallback_("UNIT_HEALTH", {unitId});
}
break;
}
case Opcode::SMSG_POWER_UPDATE: {
@ -2191,15 +2177,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (auto* unit = dynamic_cast<Unit*>(entity.get())) {
unit->setPowerByType(powerType, value);
}
if (addonEventCallback_ && guid != 0) {
std::string unitId;
if (guid == playerGuid) unitId = "player";
else if (guid == targetGuid) unitId = "target";
else if (guid == focusGuid) unitId = "focus";
else if (guid == petGuid_) unitId = "pet";
if (!unitId.empty())
addonEventCallback_("UNIT_POWER", {unitId});
}
break;
}
@ -2236,8 +2213,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (pvpHonorCallback_) {
pvpHonorCallback_(honor, victimGuid, rank);
}
if (addonEventCallback_)
addonEventCallback_("CHAT_MSG_COMBAT_HONOR_GAIN", {msg});
}
break;
}
@ -2275,11 +2250,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
mirrorTimers_[type].scale = scale;
mirrorTimers_[type].paused = (paused != 0);
mirrorTimers_[type].active = true;
if (addonEventCallback_)
addonEventCallback_("MIRROR_TIMER_START", {
std::to_string(type), std::to_string(value),
std::to_string(maxV), std::to_string(scale),
paused ? "1" : "0"});
}
break;
}
@ -2290,8 +2260,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (type < 3) {
mirrorTimers_[type].active = false;
mirrorTimers_[type].value = 0;
if (addonEventCallback_)
addonEventCallback_("MIRROR_TIMER_STOP", {std::to_string(type)});
}
break;
}
@ -2336,10 +2304,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
: ("Spell cast failed (error " + std::to_string(castResult) + ")");
addUIError(errMsg);
if (spellCastFailedCallback_) spellCastFailedCallback_(castResultSpellId);
if (addonEventCallback_) {
if (addonEventCallback_)
addonEventCallback_("UNIT_SPELLCAST_FAILED", {"player", std::to_string(castResultSpellId)});
addonEventCallback_("UNIT_SPELLCAST_STOP", {"player", std::to_string(castResultSpellId)});
}
MessageChatData msg;
msg.type = ChatType::SYSTEM;
msg.language = ChatLanguage::UNIVERSAL;
@ -2360,16 +2326,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
: UpdateObjectParser::readPackedGuid(packet);
if (failOtherGuid != 0 && failOtherGuid != playerGuid) {
unitCastStates_.erase(failOtherGuid);
// Fire cast failure events so cast bar addons clear the bar
if (addonEventCallback_) {
std::string unitId;
if (failOtherGuid == targetGuid) unitId = "target";
else if (failOtherGuid == focusGuid) unitId = "focus";
if (!unitId.empty()) {
addonEventCallback_("UNIT_SPELLCAST_FAILED", {unitId});
addonEventCallback_("UNIT_SPELLCAST_STOP", {unitId});
}
}
}
packet.setReadPos(packet.getSize());
break;
@ -2446,8 +2402,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
pendingLootRoll_.rollStartedAt = std::chrono::steady_clock::now();
LOG_INFO("SMSG_LOOT_START_ROLL: item=", itemId, " (", pendingLootRoll_.itemName,
") slot=", slot, " voteMask=0x", std::hex, (int)voteMask, std::dec);
if (addonEventCallback_)
addonEventCallback_("START_LOOT_ROLL", {std::to_string(slot), std::to_string(countdown)});
break;
}
@ -3311,10 +3265,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
sendMovement(Opcode::MSG_MOVE_STOP_TURN);
sendMovement(Opcode::MSG_MOVE_STOP_SWIM);
addSystemChatMessage("Movement disabled by server.");
if (addonEventCallback_) addonEventCallback_("PLAYER_CONTROL_LOST", {});
} else if (changed && allowMovement) {
addSystemChatMessage("Movement re-enabled.");
if (addonEventCallback_) addonEventCallback_("PLAYER_CONTROL_GAINED", {});
}
}
break;
@ -3466,11 +3418,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (failGuid == playerGuid || failGuid == 0) unitId = "player";
else if (failGuid == targetGuid) unitId = "target";
else if (failGuid == focusGuid) unitId = "focus";
else if (failGuid == petGuid_) unitId = "pet";
if (!unitId.empty()) {
if (!unitId.empty())
addonEventCallback_("UNIT_SPELLCAST_INTERRUPTED", {unitId});
addonEventCallback_("UNIT_SPELLCAST_STOP", {unitId});
}
}
if (failGuid == playerGuid || failGuid == 0) {
// Player's own cast failed — clear gather-node loot target so the
@ -3762,10 +3711,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
addUIError("Your party has been disbanded.");
addSystemChatMessage("Your party has been disbanded.");
LOG_INFO("SMSG_GROUP_DESTROYED: party cleared");
if (addonEventCallback_) {
addonEventCallback_("GROUP_ROSTER_UPDATE", {});
addonEventCallback_("PARTY_MEMBERS_CHANGED", {});
}
break;
case Opcode::SMSG_GROUP_CANCEL:
// Group invite was cancelled before being accepted.
@ -3809,8 +3754,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
? "Ready check initiated!"
: readyCheckInitiator_ + " initiated a ready check!");
LOG_INFO("MSG_RAID_READY_CHECK: initiator=", readyCheckInitiator_);
if (addonEventCallback_)
addonEventCallback_("READY_CHECK", {readyCheckInitiator_});
break;
}
case Opcode::MSG_RAID_READY_CHECK_CONFIRM: {
@ -3839,11 +3782,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
std::snprintf(rbuf, sizeof(rbuf), "%s is %s.", rname.c_str(), isReady ? "Ready" : "Not Ready");
addSystemChatMessage(rbuf);
}
if (addonEventCallback_) {
char guidBuf[32];
snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)respGuid);
addonEventCallback_("READY_CHECK_CONFIRM", {guidBuf, isReady ? "1" : "0"});
}
break;
}
case Opcode::MSG_RAID_READY_CHECK_FINISHED: {
@ -3856,8 +3794,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
readyCheckReadyCount_ = 0;
readyCheckNotReadyCount_ = 0;
readyCheckResults_.clear();
if (addonEventCallback_)
addonEventCallback_("READY_CHECK_FINISHED", {});
break;
}
case Opcode::SMSG_RAID_INSTANCE_INFO:
@ -4072,8 +4008,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
resurrectCasterName_ = (nit != playerNameCache.end()) ? nit->second : "";
}
resurrectRequestPending_ = true;
if (addonEventCallback_)
addonEventCallback_("RESURRECT_REQUEST", {resurrectCasterName_});
}
break;
}
@ -4802,7 +4736,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
recentLootMoneyAnnounceCooldowns_[notifyGuid] = 1.5f;
}
}
if (addonEventCallback_) addonEventCallback_("PLAYER_MONEY", {});
}
break;
}
@ -4824,10 +4757,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playDropOnGround();
}
if (addonEventCallback_) {
addonEventCallback_("BAG_UPDATE", {});
addonEventCallback_("PLAYER_MONEY", {});
}
} else {
bool removedPending = false;
auto it = pendingSellToBuyback_.find(itemGuid);
@ -5062,8 +4991,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
}
LOG_DEBUG("MSG_RAID_TARGET_UPDATE: type=", static_cast<int>(rtuType));
if (addonEventCallback_)
addonEventCallback_("RAID_TARGET_UPDATE", {});
break;
}
case Opcode::SMSG_BUY_ITEM: {
@ -5093,10 +5020,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
pendingBuyItemId_ = 0;
pendingBuyItemSlot_ = 0;
if (addonEventCallback_) {
addonEventCallback_("MERCHANT_UPDATE", {});
addonEventCallback_("BAG_UPDATE", {});
}
}
break;
}
@ -5479,10 +5402,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (questProgressCallback_) {
questProgressCallback_(quest.title, creatureName, count, reqCount);
}
if (addonEventCallback_) {
addonEventCallback_("QUEST_WATCH_UPDATE", {std::to_string(questId)});
addonEventCallback_("QUEST_LOG_UPDATE", {});
}
LOG_INFO("Updated kill count for quest ", questId, ": ",
count, "/", reqCount);
@ -5560,10 +5479,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
}
if (addonEventCallback_ && updatedAny) {
addonEventCallback_("QUEST_WATCH_UPDATE", {});
addonEventCallback_("QUEST_LOG_UPDATE", {});
}
LOG_INFO("Quest item update: itemId=", itemId, " count=", count,
" trackedQuestsUpdated=", updatedAny);
}
@ -5627,8 +5542,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
isResting_ = nowResting;
addSystemChatMessage(isResting_ ? "You are now resting."
: "You are no longer resting.");
if (addonEventCallback_)
addonEventCallback_("PLAYER_UPDATE_RESTING", {});
}
break;
}
@ -5795,10 +5708,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (!leaderName.empty())
addSystemChatMessage(leaderName + " is now the group leader.");
LOG_INFO("SMSG_GROUP_SET_LEADER: ", leaderName);
if (addonEventCallback_) {
addonEventCallback_("PARTY_LEADER_CHANGED", {});
addonEventCallback_("GROUP_ROSTER_UPDATE", {});
}
}
break;
}
@ -6163,8 +6072,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
talentWipePending_ = true;
LOG_INFO("MSG_TALENT_WIPE_CONFIRM: npc=0x", std::hex, talentWipeNpcGuid_,
std::dec, " cost=", talentWipeCost_);
if (addonEventCallback_)
addonEventCallback_("CONFIRM_TALENT_WIPE", {std::to_string(talentWipeCost_)});
break;
}
@ -6504,8 +6411,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
isResting_ = (restTrigger > 0);
addSystemChatMessage(isResting_ ? "You are now resting."
: "You are no longer resting.");
if (addonEventCallback_)
addonEventCallback_("PLAYER_UPDATE_RESTING", {});
}
break;
}
@ -7493,7 +7398,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (chanCaster == playerGuid) unitId = "player";
else if (chanCaster == targetGuid) unitId = "target";
else if (chanCaster == focusGuid) unitId = "focus";
else if (chanCaster == petGuid_) unitId = "pet";
if (!unitId.empty())
addonEventCallback_("UNIT_SPELLCAST_CHANNEL_START", {unitId, std::to_string(chanSpellId)});
}
@ -7530,7 +7434,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (chanCaster2 == playerGuid) unitId = "player";
else if (chanCaster2 == targetGuid) unitId = "target";
else if (chanCaster2 == focusGuid) unitId = "focus";
else if (chanCaster2 == petGuid_) unitId = "pet";
if (!unitId.empty())
addonEventCallback_("UNIT_SPELLCAST_CHANNEL_STOP", {unitId});
}
@ -7776,10 +7679,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
LOG_DEBUG("SMSG_REAL_GROUP_UPDATE groupType=", static_cast<int>(newGroupType),
" memberFlags=0x", std::hex, newMemberFlags, std::dec,
" leaderGuid=", newLeaderGuid);
if (addonEventCallback_) {
addonEventCallback_("PARTY_LEADER_CHANGED", {});
addonEventCallback_("GROUP_ROSTER_UPDATE", {});
}
break;
}
@ -8005,7 +7904,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
const std::string& sname = getSpellName(spellId);
addSystemChatMessage("Your pet has learned " + (sname.empty() ? "a new ability." : sname + "."));
LOG_DEBUG("SMSG_PET_LEARNED_SPELL: spellId=", spellId);
if (addonEventCallback_) addonEventCallback_("PET_BAR_UPDATE", {});
}
packet.setReadPos(packet.getSize());
break;
@ -8125,11 +8023,6 @@ void GameHandler::handlePacket(network::Packet& packet) {
LOG_INFO("SMSG_INSPECT (Classic): ", playerName, " has gear in ",
std::count_if(items.begin(), items.end(),
[](uint32_t e) { return e != 0; }), "/19 slots");
if (addonEventCallback_) {
char guidBuf[32];
snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)guid);
addonEventCallback_("INSPECT_READY", {guidBuf});
}
break;
}
@ -11198,13 +11091,6 @@ void GameHandler::sendMovement(Opcode opcode) {
}
}
// Track movement state transition for PLAYER_STARTED/STOPPED_MOVING events
const uint32_t kMoveMask = static_cast<uint32_t>(MovementFlags::FORWARD) |
static_cast<uint32_t>(MovementFlags::BACKWARD) |
static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT);
const bool wasMoving = (movementInfo.flags & kMoveMask) != 0;
// Cancel any timed (non-channeled) cast the moment the player starts moving.
// Channeled spells end via MSG_CHANNEL_UPDATE / SMSG_CHANNEL_NOTIFY from the server.
// Turning (MSG_MOVE_START_TURN_*) is allowed while casting.
@ -11309,15 +11195,6 @@ void GameHandler::sendMovement(Opcode opcode) {
break;
}
// Fire PLAYER_STARTED/STOPPED_MOVING on movement state transitions
{
const bool isMoving = (movementInfo.flags & kMoveMask) != 0;
if (isMoving && !wasMoving && addonEventCallback_)
addonEventCallback_("PLAYER_STARTED_MOVING", {});
else if (!isMoving && wasMoving && addonEventCallback_)
addonEventCallback_("PLAYER_STOPPED_MOVING", {});
}
if (opcode == Opcode::MSG_MOVE_SET_FACING) {
lastFacingSendTimeMs_ = movementInfo.time;
lastFacingSentOrientation_ = movementInfo.orientation;
@ -13657,7 +13534,6 @@ std::shared_ptr<Entity> GameHandler::getTarget() const {
void GameHandler::setFocus(uint64_t guid) {
focusGuid = guid;
if (addonEventCallback_) addonEventCallback_("PLAYER_FOCUS_CHANGED", {});
if (guid != 0) {
auto entity = entityManager.getEntity(guid);
if (entity) {
@ -13683,14 +13559,6 @@ void GameHandler::clearFocus() {
LOG_INFO("Focus cleared");
}
focusGuid = 0;
if (addonEventCallback_) addonEventCallback_("PLAYER_FOCUS_CHANGED", {});
}
void GameHandler::setMouseoverGuid(uint64_t guid) {
if (mouseoverGuid_ != guid) {
mouseoverGuid_ = guid;
if (addonEventCallback_) addonEventCallback_("UPDATE_MOUSEOVER_UNIT", {});
}
}
std::shared_ptr<Entity> GameHandler::getFocus() const {
@ -14364,7 +14232,6 @@ void GameHandler::handleDuelRequested(network::Packet& packet) {
}
LOG_INFO("SMSG_DUEL_REQUESTED: challenger=0x", std::hex, duelChallengerGuid_,
" flag=0x", duelFlagGuid_, std::dec, " name=", duelChallengerName_);
if (addonEventCallback_) addonEventCallback_("DUEL_REQUESTED", {duelChallengerName_});
}
void GameHandler::handleDuelComplete(network::Packet& packet) {
@ -14377,7 +14244,6 @@ void GameHandler::handleDuelComplete(network::Packet& packet) {
addSystemChatMessage("The duel was cancelled.");
}
LOG_INFO("SMSG_DUEL_COMPLETE: started=", static_cast<int>(started));
if (addonEventCallback_) addonEventCallback_("DUEL_FINISHED", {});
}
void GameHandler::handleDuelWinner(network::Packet& packet) {
@ -14916,10 +14782,6 @@ void GameHandler::handleNameQueryResponse(network::Packet& packet) {
if (data.isValid()) {
playerNameCache[data.guid] = data.name;
// Cache class/race from name query for UnitClass/UnitRace fallback
if (data.classId != 0 || data.race != 0) {
playerClassRaceCache_[data.guid] = {data.classId, data.race};
}
// Update entity name
auto entity = entityManager.getEntity(data.guid);
if (entity && entity->getType() == ObjectType::PLAYER) {
@ -14946,16 +14808,6 @@ void GameHandler::handleNameQueryResponse(network::Packet& packet) {
if (friendGuids_.count(data.guid)) {
friendsCache[data.name] = data.guid;
}
// Fire UNIT_NAME_UPDATE so nameplate/unit frame addons know the name is available
if (addonEventCallback_) {
std::string unitId;
if (data.guid == targetGuid) unitId = "target";
else if (data.guid == focusGuid) unitId = "focus";
else if (data.guid == playerGuid) unitId = "player";
if (!unitId.empty())
addonEventCallback_("UNIT_NAME_UPDATE", {unitId});
}
}
}
@ -15331,11 +15183,6 @@ void GameHandler::handleInspectResults(network::Packet& packet) {
LOG_INFO("Inspect results for ", playerName, ": ", totalTalents, " talents, ",
unspentTalents, " unspent, ", (int)talentGroupCount, " specs");
if (addonEventCallback_) {
char guidBuf[32];
snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)guid);
addonEventCallback_("INSPECT_READY", {guidBuf});
}
}
uint64_t GameHandler::resolveOnlineItemGuid(uint32_t itemId) const {
@ -16167,8 +16014,6 @@ void GameHandler::stopAutoAttack() {
socket->send(packet);
}
LOG_INFO("Stopping auto-attack");
if (addonEventCallback_)
addonEventCallback_("PLAYER_LEAVE_COMBAT", {});
}
void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource, uint8_t powerType,
@ -16290,8 +16135,6 @@ void GameHandler::handleAttackStart(network::Packet& packet) {
autoAttacking = true;
autoAttackRetryPending_ = false;
autoAttackTarget = data.victimGuid;
if (addonEventCallback_)
addonEventCallback_("PLAYER_ENTER_COMBAT", {});
} else if (data.victimGuid == playerGuid && data.attackerGuid != 0) {
hostileAttackers_.insert(data.attackerGuid);
autoTargetAttacker(data.attackerGuid);
@ -16850,8 +16693,6 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
LOG_INFO("Battlefield status: unknown (", statusId, ") for ", bgName);
break;
}
if (addonEventCallback_)
addonEventCallback_("UPDATE_BATTLEFIELD_STATUS", {std::to_string(statusId)});
}
void GameHandler::handleBattlefieldList(network::Packet& packet) {
@ -18472,27 +18313,6 @@ void GameHandler::handleMonsterMove(network::Packet& packet) {
creatureMoveCallback_(data.guid,
posCanonical.x, posCanonical.y, posCanonical.z, 0);
}
} else if (data.moveType == 4) {
// FacingAngle without movement — rotate NPC in place
float orientation = core::coords::serverToCanonicalYaw(data.facingAngle);
glm::vec3 posCanonical = core::coords::serverToCanonical(
glm::vec3(data.x, data.y, data.z));
entity->setPosition(posCanonical.x, posCanonical.y, posCanonical.z, orientation);
if (creatureMoveCallback_) {
creatureMoveCallback_(data.guid,
posCanonical.x, posCanonical.y, posCanonical.z, 0);
}
} else if (data.moveType == 3 && data.facingTarget != 0) {
// FacingTarget without movement — rotate NPC to face a target
auto target = entityManager.getEntity(data.facingTarget);
if (target) {
float dx = target->getX() - entity->getX();
float dy = target->getY() - entity->getY();
if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) {
float orientation = std::atan2(-dy, dx);
entity->setOrientation(orientation);
}
}
}
}
@ -18899,13 +18719,6 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
socket->send(packet);
LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec);
// Fire UNIT_SPELLCAST_SENT for cast bar addons (fires on client intent, before server confirms)
if (addonEventCallback_) {
std::string targetName;
if (target != 0) targetName = lookupName(target);
addonEventCallback_("UNIT_SPELLCAST_SENT", {"player", targetName, std::to_string(spellId)});
}
// Optimistically start GCD immediately on cast, but do not restart it while
// already active (prevents timeout animation reset on repeated key presses).
if (!isGCDActive()) {
@ -18934,8 +18747,6 @@ void GameHandler::cancelCast() {
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
if (addonEventCallback_)
addonEventCallback_("UNIT_SPELLCAST_STOP", {"player"});
}
void GameHandler::startCraftQueue(uint32_t spellId, int count) {
@ -18978,7 +18789,6 @@ void GameHandler::handlePetSpells(network::Packet& packet) {
petAutocastSpells_.clear();
memset(petActionSlots_, 0, sizeof(petActionSlots_));
LOG_INFO("SMSG_PET_SPELLS: pet cleared");
if (addonEventCallback_) addonEventCallback_("UNIT_PET", {"player"});
return;
}
@ -18988,7 +18798,6 @@ void GameHandler::handlePetSpells(network::Packet& packet) {
petAutocastSpells_.clear();
memset(petActionSlots_, 0, sizeof(petActionSlots_));
LOG_INFO("SMSG_PET_SPELLS: pet cleared (guid=0)");
if (addonEventCallback_) addonEventCallback_("UNIT_PET", {"player"});
return;
}
@ -19030,10 +18839,6 @@ done:
LOG_INFO("SMSG_PET_SPELLS: petGuid=0x", std::hex, petGuid_, std::dec,
" react=", (int)petReact_, " command=", (int)petCommand_,
" spells=", petSpellList_.size());
if (addonEventCallback_) {
addonEventCallback_("UNIT_PET", {"player"});
addonEventCallback_("PET_BAR_UPDATE", {});
}
}
void GameHandler::sendPetAction(uint32_t action, uint64_t targetGuid) {
@ -19362,7 +19167,6 @@ void GameHandler::handleSpellStart(network::Packet& packet) {
if (data.casterUnit == playerGuid) unitId = "player";
else if (data.casterUnit == targetGuid) unitId = "target";
else if (data.casterUnit == focusGuid) unitId = "focus";
else if (data.casterUnit == petGuid_) unitId = "pet";
if (!unitId.empty())
addonEventCallback_("UNIT_SPELLCAST_START", {unitId, std::to_string(data.spellId)});
}
@ -19442,10 +19246,6 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
spellCastAnimCallback_(playerGuid, false, false);
}
// Fire UNIT_SPELLCAST_STOP — cast bar should disappear
if (addonEventCallback_)
addonEventCallback_("UNIT_SPELLCAST_STOP", {"player", std::to_string(data.spellId)});
// Spell queue: fire the next queued spell now that casting has ended
if (queuedSpellId_ != 0) {
uint32_t nextSpell = queuedSpellId_;
@ -19512,7 +19312,6 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
if (data.casterUnit == playerGuid) unitId = "player";
else if (data.casterUnit == targetGuid) unitId = "target";
else if (data.casterUnit == focusGuid) unitId = "focus";
else if (data.casterUnit == petGuid_) unitId = "pet";
if (!unitId.empty())
addonEventCallback_("UNIT_SPELLCAST_SUCCEEDED", {unitId, std::to_string(data.spellId)});
}
@ -19683,7 +19482,6 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) {
LOG_INFO("Learned spell: ", spellId, alreadyKnown ? " (already known, skipping chat)" : "");
// Check if this spell corresponds to a talent rank
bool isTalentSpell = false;
for (const auto& [talentId, talent] : talentCache_) {
for (int rank = 0; rank < 5; ++rank) {
if (talent.rankSpells[rank] == spellId) {
@ -19692,15 +19490,9 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) {
learnedTalents_[activeTalentSpec_][talentId] = newRank;
LOG_INFO("Talent learned: id=", talentId, " rank=", (int)newRank,
" (spell ", spellId, ") in spec ", (int)activeTalentSpec_);
isTalentSpell = true;
if (addonEventCallback_) {
addonEventCallback_("CHARACTER_POINTS_CHANGED", {});
addonEventCallback_("PLAYER_TALENT_UPDATE", {});
}
break;
return;
}
}
if (isTalentSpell) break;
}
// Fire LEARNED_SPELL_IN_TAB / SPELLS_CHANGED for Lua addons
@ -19709,8 +19501,6 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) {
addonEventCallback_("SPELLS_CHANGED", {});
}
if (isTalentSpell) return; // talent spells don't show chat message
// Show chat message for non-talent spells, but only if not already announced by
// SMSG_TRAINER_BUY_SUCCEEDED (which pre-inserts into knownSpells).
if (!alreadyKnown) {
@ -19888,13 +19678,6 @@ void GameHandler::handleTalentsInfo(network::Packet& packet) {
" groups=", (int)talentGroupCount, " active=", (int)activeTalentGroup,
" learned=", learnedTalents_[activeTalentGroup].size());
// Fire talent-related events for addons
if (addonEventCallback_) {
addonEventCallback_("CHARACTER_POINTS_CHANGED", {});
addonEventCallback_("ACTIVE_TALENT_GROUP_CHANGED", {});
addonEventCallback_("PLAYER_TALENT_UPDATE", {});
}
if (!talentsInitialized_) {
talentsInitialized_ = true;
if (unspentTalents > 0) {
@ -20044,8 +19827,6 @@ void GameHandler::handleGroupInvite(network::Packet& packet) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playTargetSelect();
}
if (addonEventCallback_)
addonEventCallback_("PARTY_INVITE_REQUEST", {data.inviterName});
}
void GameHandler::handleGroupDecline(network::Packet& packet) {
@ -20340,40 +20121,6 @@ void GameHandler::handlePartyMemberStats(network::Packet& packet, bool isFull) {
LOG_DEBUG("Party member stats for ", member->name,
": HP=", member->curHealth, "/", member->maxHealth,
" Level=", member->level);
// Fire addon events for party/raid member health/power/aura changes
if (addonEventCallback_) {
// Resolve unit ID for this member (party1..4 or raid1..40)
std::string unitId;
if (partyData.groupType == 1) {
// Raid: find 1-based index
for (size_t i = 0; i < partyData.members.size(); ++i) {
if (partyData.members[i].guid == memberGuid) {
unitId = "raid" + std::to_string(i + 1);
break;
}
}
} else {
// Party: find 1-based index excluding self
int found = 0;
for (const auto& m : partyData.members) {
if (m.guid == playerGuid) continue;
++found;
if (m.guid == memberGuid) {
unitId = "party" + std::to_string(found);
break;
}
}
}
if (!unitId.empty()) {
if (updateFlags & (0x0002 | 0x0004)) // CUR_HP or MAX_HP
addonEventCallback_("UNIT_HEALTH", {unitId});
if (updateFlags & (0x0010 | 0x0020)) // CUR_POWER or MAX_POWER
addonEventCallback_("UNIT_POWER", {unitId});
if (updateFlags & 0x0200) // AURAS
addonEventCallback_("UNIT_AURA", {unitId});
}
}
}
// ============================================================
@ -20688,7 +20435,6 @@ void GameHandler::handleGuildRoster(network::Packet& packet) {
guildRoster_ = std::move(data);
hasGuildRoster_ = true;
LOG_INFO("Guild roster received: ", guildRoster_.members.size(), " members");
if (addonEventCallback_) addonEventCallback_("GUILD_ROSTER_UPDATE", {});
}
void GameHandler::handleGuildQueryResponse(network::Packet& packet) {
@ -20714,10 +20460,8 @@ void GameHandler::handleGuildQueryResponse(network::Packet& packet) {
guildRankNames_.push_back(data.rankNames[i]);
}
LOG_INFO("Guild name set to: ", guildName_);
if (wasUnknown && !guildName_.empty()) {
if (wasUnknown && !guildName_.empty())
addSystemChatMessage("Guild: <" + guildName_ + ">");
if (addonEventCallback_) addonEventCallback_("PLAYER_GUILD_UPDATE", {});
}
} else {
LOG_INFO("Cached guild name: id=", data.guildId, " name=", data.guildName);
}
@ -20767,7 +20511,6 @@ void GameHandler::handleGuildEvent(network::Packet& packet) {
guildRankNames_.clear();
guildRoster_ = GuildRosterData{};
hasGuildRoster_ = false;
if (addonEventCallback_) addonEventCallback_("PLAYER_GUILD_UPDATE", {});
break;
case GuildEvent::SIGNED_ON:
if (data.numStrings >= 1)
@ -20790,28 +20533,6 @@ void GameHandler::handleGuildEvent(network::Packet& packet) {
addLocalChatMessage(chatMsg);
}
// Fire addon events for guild state changes
if (addonEventCallback_) {
switch (data.eventType) {
case GuildEvent::MOTD:
addonEventCallback_("GUILD_MOTD", {data.numStrings >= 1 ? data.strings[0] : ""});
break;
case GuildEvent::SIGNED_ON:
case GuildEvent::SIGNED_OFF:
case GuildEvent::PROMOTION:
case GuildEvent::DEMOTION:
case GuildEvent::JOINED:
case GuildEvent::LEFT:
case GuildEvent::REMOVED:
case GuildEvent::LEADER_CHANGED:
case GuildEvent::DISBANDED:
addonEventCallback_("GUILD_ROSTER_UPDATE", {});
break;
default:
break;
}
}
// Auto-refresh roster after membership/rank changes
switch (data.eventType) {
case GuildEvent::PROMOTION:
@ -20836,8 +20557,6 @@ void GameHandler::handleGuildInvite(network::Packet& packet) {
pendingGuildInviteGuildName_ = data.guildName;
LOG_INFO("Guild invite from: ", data.inviterName, " to guild: ", data.guildName);
addSystemChatMessage(data.inviterName + " has invited you to join " + data.guildName + ".");
if (addonEventCallback_)
addonEventCallback_("GUILD_INVITE_REQUEST", {data.inviterName, data.guildName});
}
void GameHandler::handleGuildCommandResult(network::Packet& packet) {
@ -20932,7 +20651,6 @@ void GameHandler::lootItem(uint8_t slotIndex) {
void GameHandler::closeLoot() {
if (!lootWindowOpen) return;
lootWindowOpen = false;
if (addonEventCallback_) addonEventCallback_("LOOT_CLOSED", {});
masterLootCandidates_.clear();
if (currentLoot.lootGuid != 0 && targetGuid == currentLoot.lootGuid) {
clearTarget();
@ -21383,7 +21101,6 @@ void GameHandler::handleQuestDetails(network::Packet& packet) {
// Delay opening the window slightly to allow item queries to complete
questDetailsOpenTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
gossipWindowOpen = false;
if (addonEventCallback_) addonEventCallback_("QUEST_DETAIL", {});
}
bool GameHandler::hasQuestInLog(uint32_t questId) const {
@ -21829,7 +21546,6 @@ void GameHandler::handleQuestOfferReward(network::Packet& packet) {
gossipWindowOpen = false;
questDetailsOpen = false;
questDetailsOpenTime = std::chrono::steady_clock::time_point{};
if (addonEventCallback_) addonEventCallback_("QUEST_COMPLETE", {});
// Query item names for reward items
for (const auto& item : data.choiceRewards)
@ -21888,7 +21604,6 @@ void GameHandler::closeQuestOfferReward() {
void GameHandler::closeGossip() {
gossipWindowOpen = false;
if (addonEventCallback_) addonEventCallback_("GOSSIP_CLOSED", {});
currentGossip = GossipMessageData{};
}
@ -22397,7 +22112,6 @@ void GameHandler::handleLootResponse(network::Packet& packet) {
return;
}
lootWindowOpen = true;
if (addonEventCallback_) addonEventCallback_("LOOT_OPENED", {});
lastInteractedGoGuid_ = 0; // loot opened — no need to re-send in handleSpellGo
pendingGameObjectLootOpens_.erase(
std::remove_if(pendingGameObjectLootOpens_.begin(), pendingGameObjectLootOpens_.end(),
@ -22442,7 +22156,6 @@ void GameHandler::handleLootReleaseResponse(network::Packet& packet) {
(void)packet;
localLootState_.erase(currentLoot.lootGuid);
lootWindowOpen = false;
if (addonEventCallback_) addonEventCallback_("LOOT_CLOSED", {});
currentLoot = LootResponseData{};
}
@ -22465,8 +22178,6 @@ void GameHandler::handleLootRemoved(network::Packet& packet) {
sfx->playLootItem();
}
currentLoot.items.erase(it);
if (addonEventCallback_)
addonEventCallback_("LOOT_SLOT_CLEARED", {std::to_string(slotIndex + 1)});
break;
}
}
@ -22478,7 +22189,6 @@ void GameHandler::handleGossipMessage(network::Packet& packet) {
if (!ok) return;
if (questDetailsOpen) return; // Don't reopen gossip while viewing quest
gossipWindowOpen = true;
if (addonEventCallback_) addonEventCallback_("GOSSIP_SHOW", {});
vendorWindowOpen = false; // Close vendor if gossip opens
// Update known quest-log entries based on gossip quests.
@ -22592,7 +22302,6 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) {
currentGossip = std::move(data);
gossipWindowOpen = true;
if (addonEventCallback_) addonEventCallback_("GOSSIP_SHOW", {});
vendorWindowOpen = false;
bool hasAvailableQuest = false;
@ -22643,7 +22352,6 @@ void GameHandler::handleGossipComplete(network::Packet& packet) {
}
gossipWindowOpen = false;
if (addonEventCallback_) addonEventCallback_("GOSSIP_CLOSED", {});
currentGossip = GossipMessageData{};
}
@ -22772,7 +22480,6 @@ void GameHandler::handleTrainerList(network::Packet& packet) {
if (!TrainerListParser::parse(packet, currentTrainerList_, isClassic)) return;
trainerWindowOpen_ = true;
gossipWindowOpen = false;
if (addonEventCallback_) addonEventCallback_("TRAINER_SHOW", {});
LOG_INFO("Trainer list: ", currentTrainerList_.spells.size(), " spells");
LOG_DEBUG("Known spells count: ", knownSpells.size());
@ -22830,7 +22537,6 @@ void GameHandler::trainSpell(uint32_t spellId) {
void GameHandler::closeTrainer() {
trainerWindowOpen_ = false;
if (addonEventCallback_) addonEventCallback_("TRAINER_CLOSED", {});
currentTrainerList_ = TrainerListData{};
trainerTabs_.clear();
}
@ -24386,7 +24092,6 @@ void GameHandler::handleFriendList(network::Packet& packet) {
entry.classId = classId;
contacts_.push_back(std::move(entry));
}
if (addonEventCallback_) addonEventCallback_("FRIENDLIST_UPDATE", {});
}
void GameHandler::handleContactList(network::Packet& packet) {
@ -24450,11 +24155,6 @@ void GameHandler::handleContactList(network::Packet& packet) {
}
LOG_INFO("SMSG_CONTACT_LIST: mask=", lastContactListMask_,
" count=", lastContactListCount_);
if (addonEventCallback_) {
addonEventCallback_("FRIENDLIST_UPDATE", {});
if (lastContactListMask_ & 0x2) // ignore list
addonEventCallback_("IGNORELIST_UPDATE", {});
}
}
void GameHandler::handleFriendStatus(network::Packet& packet) {
@ -24538,7 +24238,6 @@ void GameHandler::handleFriendStatus(network::Packet& packet) {
}
LOG_INFO("Friend status update: ", playerName, " status=", (int)data.status);
if (addonEventCallback_) addonEventCallback_("FRIENDLIST_UPDATE", {});
}
void GameHandler::handleRandomRoll(network::Packet& packet) {
@ -25318,7 +25017,6 @@ void GameHandler::handleMailListResult(network::Packet& packet) {
selectedMailIndex_ = -1;
showMailCompose_ = false;
}
if (addonEventCallback_) addonEventCallback_("MAIL_INBOX_UPDATE", {});
}
void GameHandler::handleSendMailResult(network::Packet& packet) {
@ -25393,7 +25091,6 @@ void GameHandler::handleReceivedMail(network::Packet& packet) {
LOG_INFO("SMSG_RECEIVED_MAIL: New mail arrived!");
hasNewMail_ = true;
addSystemChatMessage("New mail has arrived.");
if (addonEventCallback_) addonEventCallback_("UPDATE_PENDING_MAIL", {});
// If mailbox is open, refresh
if (mailboxOpen_) {
refreshMailList();
@ -25887,8 +25584,6 @@ void GameHandler::handleSummonRequest(network::Packet& packet) {
addSystemChatMessage(msg);
LOG_INFO("SMSG_SUMMON_REQUEST: summoner=", summonerName_,
" zoneId=", zoneId, " timeout=", summonTimeoutSec_, "s");
if (addonEventCallback_)
addonEventCallback_("CONFIRM_SUMMON", {});
}
void GameHandler::acceptSummon() {
@ -25947,7 +25642,6 @@ void GameHandler::handleTradeStatus(network::Packet& packet) {
}
tradeStatus_ = TradeStatus::PendingIncoming;
addSystemChatMessage(tradePeerName_ + " wants to trade with you.");
if (addonEventCallback_) addonEventCallback_("TRADE_REQUEST", {});
break;
}
case 2: // OPEN_WINDOW
@ -25957,27 +25651,22 @@ void GameHandler::handleTradeStatus(network::Packet& packet) {
peerTradeGold_ = 0;
tradeStatus_ = TradeStatus::Open;
addSystemChatMessage("Trade window opened.");
if (addonEventCallback_) addonEventCallback_("TRADE_SHOW", {});
break;
case 3: // CANCELLED
case 12: // CLOSE_WINDOW
resetTradeState();
addSystemChatMessage("Trade cancelled.");
if (addonEventCallback_) addonEventCallback_("TRADE_CLOSED", {});
break;
case 9: // REJECTED — other player clicked Decline
resetTradeState();
addSystemChatMessage("Trade declined.");
if (addonEventCallback_) addonEventCallback_("TRADE_CLOSED", {});
break;
case 4: // ACCEPTED (partner accepted)
tradeStatus_ = TradeStatus::Accepted;
addSystemChatMessage("Trade accepted. Awaiting other player...");
if (addonEventCallback_) addonEventCallback_("TRADE_ACCEPT_UPDATE", {});
break;
case 8: // COMPLETE
addSystemChatMessage("Trade complete!");
if (addonEventCallback_) addonEventCallback_("TRADE_CLOSED", {});
resetTradeState();
break;
case 7: // BACK_TO_TRADE (unaccepted after a change)
@ -26435,8 +26124,6 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) {
LOG_INFO("SMSG_ACHIEVEMENT_EARNED: guid=0x", std::hex, guid, std::dec,
" achievementId=", achievementId, " self=", isSelf,
achName.empty() ? "" : " name=", achName);
if (addonEventCallback_)
addonEventCallback_("ACHIEVEMENT_EARNED", {std::to_string(achievementId)});
}
// ---------------------------------------------------------------------------

View file

@ -1579,26 +1579,12 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
// since we don't have the full combo table — dual-UV effects are rare edge cases.
bgpu.textureUnit = 0;
// Start at full opacity; hide only if texture failed to load.
// Batch is hidden only when its named texture failed to load (avoids white shell artifacts).
// Do NOT bake transparency/color animation tracks here — they animate over time and
// baking the first keyframe value causes legitimate meshes to become invisible.
// Keep terrain clutter visible even when source texture paths are malformed.
bgpu.batchOpacity = (texFailed && !groundDetailModel) ? 0.0f : 1.0f;
// Apply at-rest transparency and color alpha from the M2 animation tracks.
// These provide per-batch opacity for ghosts, ethereal effects, fading doodads, etc.
// Skip zero values: some animated tracks start at 0 and animate up, and baking
// that first keyframe would make the entire batch permanently invisible.
if (bgpu.batchOpacity > 0.0f) {
float animAlpha = 1.0f;
if (batch.colorIndex < model.colorAlphas.size()) {
float ca = model.colorAlphas[batch.colorIndex];
if (ca > 0.001f) animAlpha *= ca;
}
if (batch.transparencyIndex < model.textureWeights.size()) {
float tw = model.textureWeights[batch.transparencyIndex];
if (tw > 0.001f) animAlpha *= tw;
}
bgpu.batchOpacity *= animAlpha;
}
// Compute batch center and radius for glow sprite positioning
if ((bgpu.blendMode >= 3 || bgpu.colorKeyBlack) && batch.indexCount > 0) {
glm::vec3 sum(0.0f);

View file

@ -268,7 +268,6 @@ static std::string evaluateMacroConditionals(const std::string& rawArg,
static std::string getMacroShowtooltipArg(const std::string& macroText);
void GameScreen::render(game::GameHandler& gameHandler) {
cachedGameHandler_ = &gameHandler;
// Set up chat bubble callback (once)
if (!chatBubbleCallbackSet_) {
gameHandler.setChatBubbleCallback([this](uint64_t guid, const std::string& msg, bool isYell) {
@ -2675,107 +2674,6 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
data->DeleteChars(0, data->BufTextLen);
data->InsertChars(0, newBuf.c_str());
}
} else if (data->BufTextLen > 0) {
// Player name tab-completion for commands like /w, /whisper, /invite, /trade, /duel
// Also works for plain text (completes nearby player names)
std::string fullBuf(data->Buf, data->BufTextLen);
size_t spacePos = fullBuf.find(' ');
bool isNameCommand = false;
std::string namePrefix;
size_t replaceStart = 0;
if (fullBuf[0] == '/' && spacePos != std::string::npos) {
std::string cmd = fullBuf.substr(0, spacePos);
for (char& c : cmd) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
// Commands that take a player name as the first argument after the command
if (cmd == "/w" || cmd == "/whisper" || cmd == "/invite" ||
cmd == "/trade" || cmd == "/duel" || cmd == "/follow" ||
cmd == "/inspect" || cmd == "/friend" || cmd == "/removefriend" ||
cmd == "/ignore" || cmd == "/unignore" || cmd == "/who" ||
cmd == "/t" || cmd == "/target" || cmd == "/kick" ||
cmd == "/uninvite" || cmd == "/ginvite" || cmd == "/gkick") {
// Extract the partial name after the space
namePrefix = fullBuf.substr(spacePos + 1);
// Only complete the first word after the command
size_t nameSpace = namePrefix.find(' ');
if (nameSpace == std::string::npos) {
isNameCommand = true;
replaceStart = spacePos + 1;
}
}
}
if (isNameCommand && !namePrefix.empty()) {
std::string lowerPrefix = namePrefix;
for (char& c : lowerPrefix) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (self->chatTabMatchIdx_ < 0 || self->chatTabPrefix_ != lowerPrefix) {
self->chatTabPrefix_ = lowerPrefix;
self->chatTabMatches_.clear();
// Search player name cache and nearby entities
auto* gh = self->cachedGameHandler_;
// Party/raid members
for (const auto& m : gh->getPartyData().members) {
if (m.name.empty()) continue;
std::string lname = m.name;
for (char& c : lname) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (lname.compare(0, lowerPrefix.size(), lowerPrefix) == 0)
self->chatTabMatches_.push_back(m.name);
}
// Friends
for (const auto& c : gh->getContacts()) {
if (!c.isFriend() || c.name.empty()) continue;
std::string lname = c.name;
for (char& cc : lname) cc = static_cast<char>(std::tolower(static_cast<unsigned char>(cc)));
if (lname.compare(0, lowerPrefix.size(), lowerPrefix) == 0) {
// Avoid duplicates from party
bool dup = false;
for (const auto& em : self->chatTabMatches_)
if (em == c.name) { dup = true; break; }
if (!dup) self->chatTabMatches_.push_back(c.name);
}
}
// Nearby visible players
for (const auto& [guid, entity] : gh->getEntityManager().getEntities()) {
if (!entity || entity->getType() != game::ObjectType::PLAYER) continue;
auto player = std::static_pointer_cast<game::Player>(entity);
if (player->getName().empty()) continue;
std::string lname = player->getName();
for (char& cc : lname) cc = static_cast<char>(std::tolower(static_cast<unsigned char>(cc)));
if (lname.compare(0, lowerPrefix.size(), lowerPrefix) == 0) {
bool dup = false;
for (const auto& em : self->chatTabMatches_)
if (em == player->getName()) { dup = true; break; }
if (!dup) self->chatTabMatches_.push_back(player->getName());
}
}
// Last whisper sender
if (!gh->getLastWhisperSender().empty()) {
std::string lname = gh->getLastWhisperSender();
for (char& cc : lname) cc = static_cast<char>(std::tolower(static_cast<unsigned char>(cc)));
if (lname.compare(0, lowerPrefix.size(), lowerPrefix) == 0) {
bool dup = false;
for (const auto& em : self->chatTabMatches_)
if (em == gh->getLastWhisperSender()) { dup = true; break; }
if (!dup) self->chatTabMatches_.insert(self->chatTabMatches_.begin(), gh->getLastWhisperSender());
}
}
self->chatTabMatchIdx_ = 0;
} else {
++self->chatTabMatchIdx_;
if (self->chatTabMatchIdx_ >= static_cast<int>(self->chatTabMatches_.size()))
self->chatTabMatchIdx_ = 0;
}
if (!self->chatTabMatches_.empty()) {
std::string match = self->chatTabMatches_[self->chatTabMatchIdx_];
std::string prefix = fullBuf.substr(0, replaceStart);
std::string newBuf = prefix + match;
if (self->chatTabMatches_.size() == 1) newBuf += ' ';
data->DeleteChars(0, data->BufTextLen);
data->InsertChars(0, newBuf.c_str());
}
}
}
return 0;
}
@ -2889,18 +2787,6 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
gameHandler.closeBank();
} else if (gameHandler.isTrainerWindowOpen()) {
gameHandler.closeTrainer();
} else if (gameHandler.isMailboxOpen()) {
gameHandler.closeMailbox();
} else if (gameHandler.isAuctionHouseOpen()) {
gameHandler.closeAuctionHouse();
} else if (gameHandler.isQuestDetailsOpen()) {
gameHandler.declineQuest();
} else if (gameHandler.isQuestOfferRewardOpen()) {
gameHandler.closeQuestOfferReward();
} else if (gameHandler.isQuestRequestItemsOpen()) {
gameHandler.closeQuestRequestItems();
} else if (gameHandler.isTradeOpen()) {
gameHandler.cancelTrade();
} else if (showWhoWindow_) {
showWhoWindow_ = false;
} else if (showCombatLog_) {