mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 15:20:15 +00:00
Compare commits
74 commits
22798d1c76
...
5ee2b55f4b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ee2b55f4b | ||
|
|
774f9bf214 | ||
|
|
c6a6849c86 | ||
|
|
6ab1a189c7 | ||
|
|
d24d12fb8f | ||
|
|
70a50e45f5 | ||
|
|
df79e08788 | ||
|
|
12fb8f73f7 | ||
|
|
b407d5d632 | ||
|
|
5d93108a88 | ||
|
|
de903e363c | ||
|
|
d36172fc90 | ||
|
|
b3ad64099b | ||
|
|
5ab6286f7e | ||
|
|
b04e36aaa4 | ||
|
|
6687c617d9 | ||
|
|
900626f5fe | ||
|
|
19b8d31da2 | ||
|
|
64c0c75bbf | ||
|
|
d20357415b | ||
|
|
2e6400f22e | ||
|
|
2560bd1307 | ||
|
|
b5f7659db5 | ||
|
|
a02e021730 | ||
|
|
7f0d9fe432 | ||
|
|
82d3abe5da | ||
|
|
d4c1eda22b | ||
|
|
494175e2a7 | ||
|
|
f99f4a732a | ||
|
|
74d7e969ab | ||
|
|
1f3e362512 | ||
|
|
8e51754615 | ||
|
|
70a5d3240c | ||
|
|
6a0e86efe8 | ||
|
|
91794f421e | ||
|
|
c7e16646fc | ||
|
|
cfb9e09e1d | ||
|
|
d6a25ca8f2 | ||
|
|
61b54cfa74 | ||
|
|
ec082e029c | ||
|
|
8229a963d1 | ||
|
|
0d49cc8b94 | ||
|
|
a63f980e02 | ||
|
|
7807058f9c | ||
|
|
b2826ce589 | ||
|
|
e64f9f4585 | ||
|
|
a39acd71ba | ||
|
|
4f4c169825 | ||
|
|
b7e5034f27 | ||
|
|
b8d92b5ff2 | ||
|
|
8f2a2dfbb4 | ||
|
|
3b8165cbef | ||
|
|
7105672f06 | ||
|
|
e21f808714 | ||
|
|
0d2fd02dca | ||
|
|
b99bf7021b | ||
|
|
154140f185 | ||
|
|
760c6a2790 | ||
|
|
60904e2e15 | ||
|
|
d75f2c62e5 | ||
|
|
55ef607093 | ||
|
|
0a6fdfb8b1 | ||
|
|
855f00c5b5 | ||
|
|
c20db42479 | ||
|
|
6e863a323a | ||
|
|
45850c5aa9 | ||
|
|
3ae18f03a1 | ||
|
|
00a97aae3f | ||
|
|
ce26284b90 | ||
|
|
8555c80aa2 | ||
|
|
7459f27771 | ||
|
|
74125b7340 | ||
|
|
fe8950bd4b | ||
|
|
32a51aa93d |
12 changed files with 2441 additions and 52 deletions
|
|
@ -26,6 +26,7 @@ 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();
|
||||
|
|
@ -38,6 +39,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -47,9 +48,14 @@ 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();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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);
|
||||
|
|
|
|||
|
|
@ -294,6 +294,21 @@ 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)>;
|
||||
|
|
@ -403,7 +418,7 @@ public:
|
|||
bool hasFocus() const { return focusGuid != 0; }
|
||||
|
||||
// Mouseover targeting — set each frame by the nameplate renderer
|
||||
void setMouseoverGuid(uint64_t guid) { mouseoverGuid_ = guid; }
|
||||
void setMouseoverGuid(uint64_t guid);
|
||||
uint64_t getMouseoverGuid() const { return mouseoverGuid_; }
|
||||
|
||||
// Advanced targeting
|
||||
|
|
@ -1228,6 +1243,16 @@ 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 {
|
||||
|
|
@ -2662,6 +2687,8 @@ private:
|
|||
AddonChatCallback addonChatCallback_;
|
||||
AddonEventCallback addonEventCallback_;
|
||||
SpellIconPathResolver spellIconPathResolver_;
|
||||
ItemIconPathResolver itemIconPathResolver_;
|
||||
SpellDataResolver spellDataResolver_;
|
||||
RandomPropertyNameResolver randomPropertyNameResolver_;
|
||||
EmoteAnimCallback emoteAnimCallback_;
|
||||
|
||||
|
|
@ -2702,6 +2729,9 @@ 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;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,10 @@ private:
|
|||
// Populated by the SpellCastFailedCallback; queried during action bar button rendering.
|
||||
std::unordered_map<uint32_t, float> actionFlashEndTimes_;
|
||||
|
||||
// Tab-completion state for slash commands
|
||||
// Cached game handler for input callbacks (set each frame in render)
|
||||
game::GameHandler* cachedGameHandler_ = nullptr;
|
||||
|
||||
// Tab-completion state for slash commands and player names
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ 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();
|
||||
|
|
@ -76,6 +81,15 @@ 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) {
|
||||
|
|
@ -120,6 +134,13 @@ 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
|
|
@ -19,17 +19,12 @@ bool TocFile::isLoadOnDemand() const {
|
|||
return (it != directives.end()) && it->second == "1";
|
||||
}
|
||||
|
||||
std::vector<std::string> TocFile::getSavedVariables() const {
|
||||
static std::vector<std::string> parseVarList(const std::string& val) {
|
||||
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)
|
||||
|
|
@ -40,6 +35,16 @@ std::vector<std::string> TocFile::getSavedVariables() const {
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -335,6 +335,10 @@ 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;
|
||||
|
|
@ -354,6 +358,25 @@ 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) {
|
||||
|
|
@ -413,6 +436,119 @@ 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>>();
|
||||
|
|
@ -5182,6 +5318,21 @@ 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");
|
||||
|
|
|
|||
|
|
@ -2010,6 +2010,11 @@ 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));
|
||||
}
|
||||
|
|
@ -2160,6 +2165,15 @@ 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: {
|
||||
|
|
@ -2177,6 +2191,15 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -2213,6 +2236,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (pvpHonorCallback_) {
|
||||
pvpHonorCallback_(honor, victimGuid, rank);
|
||||
}
|
||||
if (addonEventCallback_)
|
||||
addonEventCallback_("CHAT_MSG_COMBAT_HONOR_GAIN", {msg});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -2250,6 +2275,11 @@ 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;
|
||||
}
|
||||
|
|
@ -2260,6 +2290,8 @@ 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;
|
||||
}
|
||||
|
|
@ -2304,8 +2336,10 @@ 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;
|
||||
|
|
@ -2326,6 +2360,16 @@ 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;
|
||||
|
|
@ -2402,6 +2446,8 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -3265,8 +3311,10 @@ 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;
|
||||
|
|
@ -3418,8 +3466,11 @@ 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";
|
||||
if (!unitId.empty())
|
||||
else if (failGuid == petGuid_) unitId = "pet";
|
||||
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
|
||||
|
|
@ -3711,6 +3762,10 @@ 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.
|
||||
|
|
@ -3754,6 +3809,8 @@ 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: {
|
||||
|
|
@ -3782,6 +3839,11 @@ 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: {
|
||||
|
|
@ -3794,6 +3856,8 @@ 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:
|
||||
|
|
@ -4008,6 +4072,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
resurrectCasterName_ = (nit != playerNameCache.end()) ? nit->second : "";
|
||||
}
|
||||
resurrectRequestPending_ = true;
|
||||
if (addonEventCallback_)
|
||||
addonEventCallback_("RESURRECT_REQUEST", {resurrectCasterName_});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -4736,6 +4802,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
recentLootMoneyAnnounceCooldowns_[notifyGuid] = 1.5f;
|
||||
}
|
||||
}
|
||||
if (addonEventCallback_) addonEventCallback_("PLAYER_MONEY", {});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -4757,6 +4824,10 @@ 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);
|
||||
|
|
@ -4991,6 +5062,8 @@ 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: {
|
||||
|
|
@ -5020,6 +5093,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
pendingBuyItemId_ = 0;
|
||||
pendingBuyItemSlot_ = 0;
|
||||
if (addonEventCallback_) {
|
||||
addonEventCallback_("MERCHANT_UPDATE", {});
|
||||
addonEventCallback_("BAG_UPDATE", {});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -5402,6 +5479,10 @@ 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);
|
||||
|
|
@ -5479,6 +5560,10 @@ 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);
|
||||
}
|
||||
|
|
@ -5542,6 +5627,8 @@ 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;
|
||||
}
|
||||
|
|
@ -5708,6 +5795,10 @@ 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;
|
||||
}
|
||||
|
|
@ -6072,6 +6163,8 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -6411,6 +6504,8 @@ 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;
|
||||
}
|
||||
|
|
@ -7398,6 +7493,7 @@ 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)});
|
||||
}
|
||||
|
|
@ -7434,6 +7530,7 @@ 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});
|
||||
}
|
||||
|
|
@ -7679,6 +7776,10 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -7904,6 +8005,7 @@ 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;
|
||||
|
|
@ -8023,6 +8125,11 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -11091,6 +11198,13 @@ 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.
|
||||
|
|
@ -11195,6 +11309,15 @@ 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;
|
||||
|
|
@ -13534,6 +13657,7 @@ 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) {
|
||||
|
|
@ -13559,6 +13683,14 @@ 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 {
|
||||
|
|
@ -14232,6 +14364,7 @@ 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) {
|
||||
|
|
@ -14244,6 +14377,7 @@ 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) {
|
||||
|
|
@ -14782,6 +14916,10 @@ 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) {
|
||||
|
|
@ -14808,6 +14946,16 @@ 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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -15183,6 +15331,11 @@ 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 {
|
||||
|
|
@ -16014,6 +16167,8 @@ 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,
|
||||
|
|
@ -16135,6 +16290,8 @@ 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);
|
||||
|
|
@ -16693,6 +16850,8 @@ 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) {
|
||||
|
|
@ -18313,6 +18472,27 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -18719,6 +18899,13 @@ 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()) {
|
||||
|
|
@ -18747,6 +18934,8 @@ void GameHandler::cancelCast() {
|
|||
craftQueueRemaining_ = 0;
|
||||
queuedSpellId_ = 0;
|
||||
queuedSpellTarget_ = 0;
|
||||
if (addonEventCallback_)
|
||||
addonEventCallback_("UNIT_SPELLCAST_STOP", {"player"});
|
||||
}
|
||||
|
||||
void GameHandler::startCraftQueue(uint32_t spellId, int count) {
|
||||
|
|
@ -18789,6 +18978,7 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -18798,6 +18988,7 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -18839,6 +19030,10 @@ 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) {
|
||||
|
|
@ -19167,6 +19362,7 @@ 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)});
|
||||
}
|
||||
|
|
@ -19246,6 +19442,10 @@ 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_;
|
||||
|
|
@ -19312,6 +19512,7 @@ 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)});
|
||||
}
|
||||
|
|
@ -19482,6 +19683,7 @@ 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) {
|
||||
|
|
@ -19490,9 +19692,15 @@ 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_);
|
||||
return;
|
||||
isTalentSpell = true;
|
||||
if (addonEventCallback_) {
|
||||
addonEventCallback_("CHARACTER_POINTS_CHANGED", {});
|
||||
addonEventCallback_("PLAYER_TALENT_UPDATE", {});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isTalentSpell) break;
|
||||
}
|
||||
|
||||
// Fire LEARNED_SPELL_IN_TAB / SPELLS_CHANGED for Lua addons
|
||||
|
|
@ -19501,6 +19709,8 @@ 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) {
|
||||
|
|
@ -19678,6 +19888,13 @@ 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) {
|
||||
|
|
@ -19827,6 +20044,8 @@ 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) {
|
||||
|
|
@ -20121,6 +20340,40 @@ 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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -20435,6 +20688,7 @@ 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) {
|
||||
|
|
@ -20460,8 +20714,10 @@ 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);
|
||||
}
|
||||
|
|
@ -20511,6 +20767,7 @@ 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)
|
||||
|
|
@ -20533,6 +20790,28 @@ 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:
|
||||
|
|
@ -20557,6 +20836,8 @@ 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) {
|
||||
|
|
@ -20651,6 +20932,7 @@ 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();
|
||||
|
|
@ -21101,6 +21383,7 @@ 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 {
|
||||
|
|
@ -21546,6 +21829,7 @@ 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)
|
||||
|
|
@ -21604,6 +21888,7 @@ void GameHandler::closeQuestOfferReward() {
|
|||
|
||||
void GameHandler::closeGossip() {
|
||||
gossipWindowOpen = false;
|
||||
if (addonEventCallback_) addonEventCallback_("GOSSIP_CLOSED", {});
|
||||
currentGossip = GossipMessageData{};
|
||||
}
|
||||
|
||||
|
|
@ -22112,6 +22397,7 @@ 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(),
|
||||
|
|
@ -22156,6 +22442,7 @@ void GameHandler::handleLootReleaseResponse(network::Packet& packet) {
|
|||
(void)packet;
|
||||
localLootState_.erase(currentLoot.lootGuid);
|
||||
lootWindowOpen = false;
|
||||
if (addonEventCallback_) addonEventCallback_("LOOT_CLOSED", {});
|
||||
currentLoot = LootResponseData{};
|
||||
}
|
||||
|
||||
|
|
@ -22178,6 +22465,8 @@ void GameHandler::handleLootRemoved(network::Packet& packet) {
|
|||
sfx->playLootItem();
|
||||
}
|
||||
currentLoot.items.erase(it);
|
||||
if (addonEventCallback_)
|
||||
addonEventCallback_("LOOT_SLOT_CLEARED", {std::to_string(slotIndex + 1)});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -22189,6 +22478,7 @@ 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.
|
||||
|
|
@ -22302,6 +22592,7 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) {
|
|||
|
||||
currentGossip = std::move(data);
|
||||
gossipWindowOpen = true;
|
||||
if (addonEventCallback_) addonEventCallback_("GOSSIP_SHOW", {});
|
||||
vendorWindowOpen = false;
|
||||
|
||||
bool hasAvailableQuest = false;
|
||||
|
|
@ -22352,6 +22643,7 @@ void GameHandler::handleGossipComplete(network::Packet& packet) {
|
|||
}
|
||||
|
||||
gossipWindowOpen = false;
|
||||
if (addonEventCallback_) addonEventCallback_("GOSSIP_CLOSED", {});
|
||||
currentGossip = GossipMessageData{};
|
||||
}
|
||||
|
||||
|
|
@ -22480,6 +22772,7 @@ 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());
|
||||
|
|
@ -22537,6 +22830,7 @@ void GameHandler::trainSpell(uint32_t spellId) {
|
|||
|
||||
void GameHandler::closeTrainer() {
|
||||
trainerWindowOpen_ = false;
|
||||
if (addonEventCallback_) addonEventCallback_("TRAINER_CLOSED", {});
|
||||
currentTrainerList_ = TrainerListData{};
|
||||
trainerTabs_.clear();
|
||||
}
|
||||
|
|
@ -24092,6 +24386,7 @@ 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) {
|
||||
|
|
@ -24155,6 +24450,11 @@ 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) {
|
||||
|
|
@ -24238,6 +24538,7 @@ 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) {
|
||||
|
|
@ -25017,6 +25318,7 @@ void GameHandler::handleMailListResult(network::Packet& packet) {
|
|||
selectedMailIndex_ = -1;
|
||||
showMailCompose_ = false;
|
||||
}
|
||||
if (addonEventCallback_) addonEventCallback_("MAIL_INBOX_UPDATE", {});
|
||||
}
|
||||
|
||||
void GameHandler::handleSendMailResult(network::Packet& packet) {
|
||||
|
|
@ -25091,6 +25393,7 @@ 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();
|
||||
|
|
@ -25584,6 +25887,8 @@ 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() {
|
||||
|
|
@ -25642,6 +25947,7 @@ 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
|
||||
|
|
@ -25651,22 +25957,27 @@ 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)
|
||||
|
|
@ -26124,6 +26435,8 @@ 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)});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1579,12 +1579,26 @@ 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;
|
||||
|
||||
// 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.
|
||||
// Start at full opacity; hide only if texture failed to load.
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -268,6 +268,7 @@ 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) {
|
||||
|
|
@ -2674,6 +2675,107 @@ 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;
|
||||
}
|
||||
|
|
@ -2787,6 +2889,18 @@ 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_) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue