From 3acb42b36335c36ef7b3615eff3111c84dfde476 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Feb 2026 15:11:43 -0800 Subject: [PATCH] Resolve emote text from DBC for other players' text emotes Load third-person emote text templates (othersTarget/othersNoTarget) from EmotesText.dbc fields 3 and 7 alongside existing sender text. Build reverse lookup map from dbcId to EmoteInfo for incoming SMSG_TEXT_EMOTE resolution. Other players now show proper emote descriptions like "Player dances with Target" instead of generic "Player performs an emote" text. --- include/game/game_handler.hpp | 5 ++ include/rendering/renderer.hpp | 2 + src/game/game_handler.cpp | 25 +++++-- src/pipeline/dbc_layout.cpp | 5 +- src/rendering/renderer.cpp | 119 ++++++++++++++++++++++++--------- 5 files changed, 118 insertions(+), 38 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index b6848044..3b19e5b6 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -239,6 +239,10 @@ public: using ChatBubbleCallback = std::function; void setChatBubbleCallback(ChatBubbleCallback cb) { chatBubbleCallback_ = std::move(cb); } + // Emote animation callback: (entityGuid, animationId) + using EmoteAnimCallback = std::function; + void setEmoteAnimCallback(EmoteAnimCallback cb) { emoteAnimCallback_ = std::move(cb); } + /** * Get chat history (recent messages) * @param maxMessages Maximum number of messages to return (0 = all) @@ -1049,6 +1053,7 @@ private: size_t maxChatHistory = 100; // Maximum chat messages to keep std::vector joinedChannels_; // Active channel memberships ChatBubbleCallback chatBubbleCallback_; + EmoteAnimCallback emoteAnimCallback_; // Targeting uint64_t targetGuid = 0; diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 5f6a1727..aac9f8b6 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -126,6 +126,8 @@ public: bool isEmoteActive() const { return emoteActive; } static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr); static uint32_t getEmoteDbcId(const std::string& emoteName); + static std::string getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, const std::string* targetName = nullptr); + static uint32_t getEmoteAnimByDbcId(uint32_t dbcId); // Targeting support void setTargetPosition(const glm::vec3* pos); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a026dabd..519d3b57 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6,6 +6,7 @@ #include "game/warden_module.hpp" #include "game/opcodes.hpp" #include "game/update_field_table.hpp" +#include "rendering/renderer.hpp" #include "pipeline/dbc_layout.hpp" #include "network/world_socket.hpp" #include "network/packet.hpp" @@ -4021,23 +4022,35 @@ void GameHandler::handleTextEmote(network::Packet& packet) { queryPlayerName(data.senderGuid); } - // Build emote message text (server sends textEmoteId, we look up the text) - // For now, just display a generic emote message + // Resolve emote text from DBC using third-person "others see" templates + const std::string* targetPtr = data.targetName.empty() ? nullptr : &data.targetName; + std::string emoteText = rendering::Renderer::getEmoteTextByDbcId(data.textEmoteId, senderName, targetPtr); + if (emoteText.empty()) { + // Fallback if DBC lookup fails + emoteText = data.targetName.empty() + ? senderName + " performs an emote." + : senderName + " performs an emote at " + data.targetName + "."; + } + MessageChatData chatMsg; chatMsg.type = ChatType::TEXT_EMOTE; chatMsg.language = ChatLanguage::COMMON; chatMsg.senderGuid = data.senderGuid; chatMsg.senderName = senderName; - chatMsg.message = data.targetName.empty() - ? senderName + " performs an emote." - : senderName + " performs an emote at " + data.targetName + "."; + chatMsg.message = emoteText; chatHistory.push_back(chatMsg); if (chatHistory.size() > maxChatHistory) { chatHistory.erase(chatHistory.begin()); } - LOG_INFO("TEXT_EMOTE from ", senderName, " (emoteId=", data.textEmoteId, ")"); + // Trigger emote animation on sender's entity via callback + uint32_t animId = rendering::Renderer::getEmoteAnimByDbcId(data.textEmoteId); + if (animId != 0 && emoteAnimCallback_) { + emoteAnimCallback_(data.senderGuid, animId); + } + + LOG_INFO("TEXT_EMOTE from ", senderName, " (emoteId=", data.textEmoteId, ", anim=", animId, ")"); } void GameHandler::joinChannel(const std::string& channelName, const std::string& password) { diff --git a/src/pipeline/dbc_layout.cpp b/src/pipeline/dbc_layout.cpp index 67f2f603..750062b0 100644 --- a/src/pipeline/dbc_layout.cpp +++ b/src/pipeline/dbc_layout.cpp @@ -105,8 +105,11 @@ void DBCLayout::loadWotlkDefaults() { layouts_["Emotes"] = {{{ "ID", 0 }, { "AnimID", 2 }}}; // EmotesText.dbc + // Fields 3-18 are 16 EmotesTextData refs: [others+target, target+target, sender+target, ?, + // others+notarget, ?, sender+notarget, ?, female variants...] layouts_["EmotesText"] = {{{ "Command", 1 }, { "EmoteRef", 2 }, - { "SenderTargetTextID", 5 }, { "SenderNoTargetTextID", 9 }}}; + { "OthersTargetTextID", 3 }, { "SenderTargetTextID", 5 }, + { "OthersNoTargetTextID", 7 }, { "SenderNoTargetTextID", 9 }}}; // EmotesTextData.dbc layouts_["EmotesTextData"] = {{{ "ID", 0 }, { "Text", 1 }}}; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 85d4e223..e9276dd9 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -67,12 +67,15 @@ struct EmoteInfo { uint32_t animId = 0; uint32_t dbcId = 0; // EmotesText.dbc record ID (for CMSG_TEXT_EMOTE) bool loop = false; - std::string textNoTarget; - std::string textTarget; + std::string textNoTarget; // sender sees, no target: "You dance." + std::string textTarget; // sender sees, with target: "You dance with %s." + std::string othersNoTarget; // others see, no target: "%s dances." + std::string othersTarget; // others see, with target: "%s dances with %s." std::string command; }; static std::unordered_map EMOTE_TABLE; +static std::unordered_map EMOTE_BY_DBCID; // reverse lookup: dbcId → EmoteInfo* static bool emoteTableLoaded = false; static std::vector parseEmoteCommands(const std::string& raw) { @@ -101,26 +104,27 @@ static bool isLoopingEmote(const std::string& command) { static void loadFallbackEmotes() { if (!EMOTE_TABLE.empty()) return; EMOTE_TABLE = { - {"wave", {67, 0, false, "You wave.", "You wave at %s.", "wave"}}, - {"bow", {66, 0, false, "You bow down graciously.", "You bow down before %s.", "bow"}}, - {"laugh", {70, 0, false, "You laugh.", "You laugh at %s.", "laugh"}}, - {"point", {84, 0, false, "You point over yonder.", "You point at %s.", "point"}}, - {"cheer", {68, 0, false, "You cheer!", "You cheer at %s.", "cheer"}}, - {"dance", {69, 0, true, "You burst into dance.", "You dance with %s.", "dance"}}, - {"kneel", {75, 0, false, "You kneel down.", "You kneel before %s.", "kneel"}}, - {"applaud", {80, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "applaud"}}, - {"shout", {81, 0, false, "You shout.", "You shout at %s.", "shout"}}, + {"wave", {67, 0, false, "You wave.", "You wave at %s.", "%s waves.", "%s waves at %s.", "wave"}}, + {"bow", {66, 0, false, "You bow down graciously.", "You bow down before %s.", "%s bows down graciously.", "%s bows down before %s.", "bow"}}, + {"laugh", {70, 0, false, "You laugh.", "You laugh at %s.", "%s laughs.", "%s laughs at %s.", "laugh"}}, + {"point", {84, 0, false, "You point over yonder.", "You point at %s.", "%s points over yonder.", "%s points at %s.", "point"}}, + {"cheer", {68, 0, false, "You cheer!", "You cheer at %s.", "%s cheers!", "%s cheers at %s.", "cheer"}}, + {"dance", {69, 0, true, "You burst into dance.", "You dance with %s.", "%s bursts into dance.", "%s dances with %s.", "dance"}}, + {"kneel", {75, 0, false, "You kneel down.", "You kneel before %s.", "%s kneels down.", "%s kneels before %s.", "kneel"}}, + {"applaud", {80, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "%s applauds. Bravo!", "%s applauds at %s. Bravo!", "applaud"}}, + {"shout", {81, 0, false, "You shout.", "You shout at %s.", "%s shouts.", "%s shouts at %s.", "shout"}}, {"chicken", {78, 0, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!", - "With arms flapping, you strut around %s. Cluck, Cluck, Chicken!", "chicken"}}, - {"cry", {77, 0, false, "You cry.", "You cry on %s's shoulder.", "cry"}}, - {"kiss", {76, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "kiss"}}, - {"roar", {74, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "roar"}}, - {"salute", {113, 0, false, "You salute.", "You salute %s with respect.", "salute"}}, - {"rude", {73, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "rude"}}, - {"flex", {82, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "flex"}}, - {"shy", {83, 0, false, "You smile shyly.", "You smile shyly at %s.", "shy"}}, - {"beg", {79, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "beg"}}, - {"eat", {61, 0, false, "You begin to eat.", "You begin to eat in front of %s.", "eat"}}, + "With arms flapping, you strut around %s. Cluck, Cluck, Chicken!", + "%s struts around. Cluck, Cluck, Chicken!", "%s struts around %s. Cluck, Cluck, Chicken!", "chicken"}}, + {"cry", {77, 0, false, "You cry.", "You cry on %s's shoulder.", "%s cries.", "%s cries on %s's shoulder.", "cry"}}, + {"kiss", {76, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "%s blows a kiss into the wind.", "%s blows a kiss to %s.", "kiss"}}, + {"roar", {74, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "%s roars with bestial vigor. So fierce!", "%s roars with bestial vigor at %s. So fierce!", "roar"}}, + {"salute", {113, 0, false, "You salute.", "You salute %s with respect.", "%s salutes.", "%s salutes %s with respect.", "salute"}}, + {"rude", {73, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "%s makes a rude gesture.", "%s makes a rude gesture at %s.", "rude"}}, + {"flex", {82, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "%s flexes. Oooooh so strong!", "%s flexes at %s. Oooooh so strong!", "flex"}}, + {"shy", {83, 0, false, "You smile shyly.", "You smile shyly at %s.", "%s smiles shyly.", "%s smiles shyly at %s.", "shy"}}, + {"beg", {79, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "%s begs everyone around. How pathetic.", "%s begs %s. How pathetic.", "beg"}}, + {"eat", {61, 0, false, "You begin to eat.", "You begin to eat in front of %s.", "%s begins to eat.", "%s begins to eat in front of %s.", "eat"}}, }; } @@ -197,17 +201,16 @@ static void loadEmotesFromDbc() { animId = emoteRef; // fallback if EmotesText stores animation id directly } - uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5); // unisex, target, sender - uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9); // unisex, no target, sender + uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5); + uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9); + uint32_t othersTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersTargetTextID"] : 3); + uint32_t othersNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersNoTargetTextID"] : 7); - std::string textTarget; - std::string textNoTarget; - if (auto it = textData.find(senderTargetTextId); it != textData.end()) { - textTarget = it->second; - } - if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) { - textNoTarget = it->second; - } + std::string textTarget, textNoTarget, oTarget, oNoTarget; + if (auto it = textData.find(senderTargetTextId); it != textData.end()) textTarget = it->second; + if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) textNoTarget = it->second; + if (auto it = textData.find(othersTargetTextId); it != textData.end()) oTarget = it->second; + if (auto it = textData.find(othersNoTargetTextId); it != textData.end()) oNoTarget = it->second; for (const std::string& cmd : parseEmoteCommands(cmdRaw)) { if (cmd.empty()) continue; @@ -217,6 +220,8 @@ static void loadEmotesFromDbc() { info.loop = isLoopingEmote(cmd); info.textNoTarget = textNoTarget; info.textTarget = textTarget; + info.othersNoTarget = oNoTarget; + info.othersTarget = oTarget; info.command = cmd; EMOTE_TABLE.emplace(cmd, std::move(info)); } @@ -228,6 +233,14 @@ static void loadEmotesFromDbc() { } else { LOG_INFO("Emotes: loaded ", EMOTE_TABLE.size(), " commands from DBC"); } + + // Build reverse lookup by dbcId (only first command per emote needed) + EMOTE_BY_DBCID.clear(); + for (auto& [cmd, info] : EMOTE_TABLE) { + if (info.dbcId != 0) { + EMOTE_BY_DBCID.emplace(info.dbcId, &info); + } + } } Renderer::Renderer() = default; @@ -1581,6 +1594,50 @@ uint32_t Renderer::getEmoteDbcId(const std::string& emoteName) { return 0; } +std::string Renderer::getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, + const std::string* targetName) { + loadEmotesFromDbc(); + auto it = EMOTE_BY_DBCID.find(dbcId); + if (it == EMOTE_BY_DBCID.end()) return ""; + + const EmoteInfo& info = *it->second; + + // Use "others see" text templates: "%s dances." / "%s dances with %s." + if (targetName && !targetName->empty()) { + if (!info.othersTarget.empty()) { + // Replace first %s with sender, second %s with target + std::string out; + out.reserve(info.othersTarget.size() + senderName.size() + targetName->size()); + bool firstReplaced = false; + for (size_t i = 0; i < info.othersTarget.size(); ++i) { + if (info.othersTarget[i] == '%' && i + 1 < info.othersTarget.size() && info.othersTarget[i + 1] == 's') { + out += firstReplaced ? *targetName : senderName; + firstReplaced = true; + ++i; + } else { + out.push_back(info.othersTarget[i]); + } + } + return out; + } + return senderName + " " + info.command + "s at " + *targetName + "."; + } else { + if (!info.othersNoTarget.empty()) { + return replacePlaceholders(info.othersNoTarget, &senderName); + } + return senderName + " " + info.command + "s."; + } +} + +uint32_t Renderer::getEmoteAnimByDbcId(uint32_t dbcId) { + loadEmotesFromDbc(); + auto it = EMOTE_BY_DBCID.find(dbcId); + if (it != EMOTE_BY_DBCID.end()) { + return it->second->animId; + } + return 0; +} + void Renderer::setTargetPosition(const glm::vec3* pos) { targetPosition = pos; }