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.
This commit is contained in:
Kelsi 2026-02-14 15:11:43 -08:00
parent a90c130d6e
commit 3acb42b363
5 changed files with 118 additions and 38 deletions

View file

@ -239,6 +239,10 @@ public:
using ChatBubbleCallback = std::function<void(uint64_t, const std::string&, bool)>;
void setChatBubbleCallback(ChatBubbleCallback cb) { chatBubbleCallback_ = std::move(cb); }
// Emote animation callback: (entityGuid, animationId)
using EmoteAnimCallback = std::function<void(uint64_t, uint32_t)>;
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<std::string> joinedChannels_; // Active channel memberships
ChatBubbleCallback chatBubbleCallback_;
EmoteAnimCallback emoteAnimCallback_;
// Targeting
uint64_t targetGuid = 0;

View file

@ -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);

View file

@ -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) {

View file

@ -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 }}};

View file

@ -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<std::string, EmoteInfo> EMOTE_TABLE;
static std::unordered_map<uint32_t, const EmoteInfo*> EMOTE_BY_DBCID; // reverse lookup: dbcId → EmoteInfo*
static bool emoteTableLoaded = false;
static std::vector<std::string> 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;
}