mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add chat tabs, networked text emotes, channel system, and chat bubbles
Chat tabs filter messages into General/Combat/Whispers/Trade tabs. Text emotes now send CMSG_TEXT_EMOTE to server and display incoming emotes from other players. Channel system auto-joins General/Trade on login with /join, /leave, and /1-/9 shortcuts. Chat bubbles render as ImGui overlays above entities for SAY/YELL messages with fade-out animation.
This commit is contained in:
parent
ca3150e43d
commit
9bcead6a0f
14 changed files with 670 additions and 23 deletions
|
|
@ -228,5 +228,14 @@
|
|||
"MSG_BATTLEGROUND_PLAYER_POSITIONS": "0x2E9",
|
||||
"SMSG_BATTLEGROUND_PLAYER_JOINED": "0x2EC",
|
||||
"SMSG_BATTLEGROUND_PLAYER_LEFT": "0x2ED",
|
||||
"CMSG_BATTLEMASTER_JOIN": "0x2EE"
|
||||
"CMSG_BATTLEMASTER_JOIN": "0x2EE",
|
||||
"CMSG_EMOTE": "0x102",
|
||||
"SMSG_EMOTE": "0x103",
|
||||
"CMSG_TEXT_EMOTE": "0x104",
|
||||
"SMSG_TEXT_EMOTE": "0x105",
|
||||
"CMSG_JOIN_CHANNEL": "0x097",
|
||||
"CMSG_LEAVE_CHANNEL": "0x098",
|
||||
"SMSG_CHANNEL_NOTIFY": "0x099",
|
||||
"CMSG_CHANNEL_LIST": "0x09A",
|
||||
"SMSG_CHANNEL_LIST": "0x09B"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,5 +257,14 @@
|
|||
"CMSG_TAXINODE_STATUS_QUERY": "0x1AA",
|
||||
"SMSG_TAXINODE_STATUS": "0x1AB",
|
||||
"SMSG_INIT_EXTRA_AURA_INFO": "0x3A3",
|
||||
"SMSG_SET_EXTRA_AURA_INFO": "0x3A4"
|
||||
"SMSG_SET_EXTRA_AURA_INFO": "0x3A4",
|
||||
"CMSG_EMOTE": "0x102",
|
||||
"SMSG_EMOTE": "0x103",
|
||||
"CMSG_TEXT_EMOTE": "0x104",
|
||||
"SMSG_TEXT_EMOTE": "0x105",
|
||||
"CMSG_JOIN_CHANNEL": "0x097",
|
||||
"CMSG_LEAVE_CHANNEL": "0x098",
|
||||
"SMSG_CHANNEL_NOTIFY": "0x099",
|
||||
"CMSG_CHANNEL_LIST": "0x09A",
|
||||
"SMSG_CHANNEL_LIST": "0x09B"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,5 +228,14 @@
|
|||
"MSG_BATTLEGROUND_PLAYER_POSITIONS": "0x2E9",
|
||||
"SMSG_BATTLEGROUND_PLAYER_JOINED": "0x2EC",
|
||||
"SMSG_BATTLEGROUND_PLAYER_LEFT": "0x2ED",
|
||||
"CMSG_BATTLEMASTER_JOIN": "0x2EE"
|
||||
"CMSG_BATTLEMASTER_JOIN": "0x2EE",
|
||||
"CMSG_EMOTE": "0x102",
|
||||
"SMSG_EMOTE": "0x103",
|
||||
"CMSG_TEXT_EMOTE": "0x104",
|
||||
"SMSG_TEXT_EMOTE": "0x105",
|
||||
"CMSG_JOIN_CHANNEL": "0x097",
|
||||
"CMSG_LEAVE_CHANNEL": "0x098",
|
||||
"SMSG_CHANNEL_NOTIFY": "0x099",
|
||||
"CMSG_CHANNEL_LIST": "0x09A",
|
||||
"SMSG_CHANNEL_LIST": "0x09B"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,5 +257,14 @@
|
|||
"CMSG_BATTLEMASTER_JOIN_ARENA": "0x0358",
|
||||
"SMSG_ARENA_TEAM_STATS": "0x035B",
|
||||
"SMSG_ARENA_ERROR": "0x0376",
|
||||
"MSG_INSPECT_ARENA_TEAMS": "0x0377"
|
||||
"MSG_INSPECT_ARENA_TEAMS": "0x0377",
|
||||
"CMSG_EMOTE": "0x102",
|
||||
"SMSG_EMOTE": "0x103",
|
||||
"CMSG_TEXT_EMOTE": "0x104",
|
||||
"SMSG_TEXT_EMOTE": "0x105",
|
||||
"CMSG_JOIN_CHANNEL": "0x097",
|
||||
"CMSG_LEAVE_CHANNEL": "0x098",
|
||||
"SMSG_CHANNEL_NOTIFY": "0x099",
|
||||
"CMSG_CHANNEL_LIST": "0x09A",
|
||||
"SMSG_CHANNEL_LIST": "0x09B"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,6 +229,15 @@ public:
|
|||
* @param target Target name (for whispers, empty otherwise)
|
||||
*/
|
||||
void sendChatMessage(ChatType type, const std::string& message, const std::string& target = "");
|
||||
void sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid = 0);
|
||||
void joinChannel(const std::string& channelName, const std::string& password = "");
|
||||
void leaveChannel(const std::string& channelName);
|
||||
const std::vector<std::string>& getJoinedChannels() const { return joinedChannels_; }
|
||||
std::string getChannelByIndex(int index) const;
|
||||
|
||||
// Chat bubble callback: (senderGuid, message, isYell)
|
||||
using ChatBubbleCallback = std::function<void(uint64_t, const std::string&, bool)>;
|
||||
void setChatBubbleCallback(ChatBubbleCallback cb) { chatBubbleCallback_ = std::move(cb); }
|
||||
|
||||
/**
|
||||
* Get chat history (recent messages)
|
||||
|
|
@ -845,6 +854,9 @@ private:
|
|||
* Handle SMSG_MESSAGECHAT from server
|
||||
*/
|
||||
void handleMessageChat(network::Packet& packet);
|
||||
void handleTextEmote(network::Packet& packet);
|
||||
void handleChannelNotify(network::Packet& packet);
|
||||
void autoJoinDefaultChannels();
|
||||
|
||||
// ---- Phase 1 handlers ----
|
||||
void handleNameQueryResponse(network::Packet& packet);
|
||||
|
|
@ -1031,6 +1043,8 @@ private:
|
|||
// Chat
|
||||
std::deque<MessageChatData> chatHistory; // Recent chat messages
|
||||
size_t maxChatHistory = 100; // Maximum chat messages to keep
|
||||
std::vector<std::string> joinedChannels_; // Active channel memberships
|
||||
ChatBubbleCallback chatBubbleCallback_;
|
||||
|
||||
// Targeting
|
||||
uint64_t targetGuid = 0;
|
||||
|
|
|
|||
|
|
@ -351,6 +351,19 @@ enum class LogicalOpcode : uint16_t {
|
|||
SMSG_ARENA_ERROR,
|
||||
MSG_INSPECT_ARENA_TEAMS,
|
||||
|
||||
// ---- Emotes ----
|
||||
CMSG_EMOTE,
|
||||
SMSG_EMOTE,
|
||||
CMSG_TEXT_EMOTE,
|
||||
SMSG_TEXT_EMOTE,
|
||||
|
||||
// ---- Channels ----
|
||||
CMSG_JOIN_CHANNEL,
|
||||
CMSG_LEAVE_CHANNEL,
|
||||
SMSG_CHANNEL_NOTIFY,
|
||||
CMSG_CHANNEL_LIST,
|
||||
SMSG_CHANNEL_LIST,
|
||||
|
||||
// Sentinel
|
||||
COUNT
|
||||
};
|
||||
|
|
|
|||
|
|
@ -670,6 +670,115 @@ public:
|
|||
*/
|
||||
const char* getChatTypeString(ChatType type);
|
||||
|
||||
// ============================================================
|
||||
// Text Emotes
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* CMSG_TEXT_EMOTE packet builder
|
||||
*/
|
||||
class TextEmotePacket {
|
||||
public:
|
||||
static network::Packet build(uint32_t textEmoteId, uint64_t targetGuid = 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* SMSG_TEXT_EMOTE data
|
||||
*/
|
||||
struct TextEmoteData {
|
||||
uint64_t senderGuid = 0;
|
||||
uint32_t textEmoteId = 0;
|
||||
uint32_t emoteNum = 0;
|
||||
std::string targetName;
|
||||
|
||||
bool isValid() const { return senderGuid != 0; }
|
||||
};
|
||||
|
||||
/**
|
||||
* SMSG_TEXT_EMOTE parser
|
||||
*/
|
||||
class TextEmoteParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, TextEmoteData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Channel System
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* CMSG_JOIN_CHANNEL packet builder
|
||||
*/
|
||||
class JoinChannelPacket {
|
||||
public:
|
||||
static network::Packet build(const std::string& channelName, const std::string& password = "");
|
||||
};
|
||||
|
||||
/**
|
||||
* CMSG_LEAVE_CHANNEL packet builder
|
||||
*/
|
||||
class LeaveChannelPacket {
|
||||
public:
|
||||
static network::Packet build(const std::string& channelName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Channel notification types
|
||||
*/
|
||||
enum class ChannelNotifyType : uint8_t {
|
||||
YOU_JOINED = 0x00,
|
||||
YOU_LEFT = 0x01,
|
||||
WRONG_PASSWORD = 0x02,
|
||||
NOT_MEMBER = 0x03,
|
||||
NOT_MODERATOR = 0x04,
|
||||
PASSWORD_CHANGED = 0x05,
|
||||
OWNER_CHANGED = 0x06,
|
||||
PLAYER_NOT_FOUND = 0x07,
|
||||
NOT_OWNER = 0x08,
|
||||
CHANNEL_OWNER = 0x09,
|
||||
MODE_CHANGE = 0x0A,
|
||||
ANNOUNCEMENTS_ON = 0x0B,
|
||||
ANNOUNCEMENTS_OFF = 0x0C,
|
||||
MODERATION_ON = 0x0D,
|
||||
MODERATION_OFF = 0x0E,
|
||||
MUTED = 0x0F,
|
||||
PLAYER_KICKED = 0x10,
|
||||
BANNED = 0x11,
|
||||
PLAYER_BANNED = 0x12,
|
||||
PLAYER_UNBANNED = 0x13,
|
||||
PLAYER_NOT_BANNED = 0x14,
|
||||
PLAYER_ALREADY_MEMBER = 0x15,
|
||||
INVITE = 0x16,
|
||||
INVITE_WRONG_FACTION = 0x17,
|
||||
WRONG_FACTION = 0x18,
|
||||
INVALID_NAME = 0x19,
|
||||
NOT_MODERATED = 0x1A,
|
||||
PLAYER_INVITED = 0x1B,
|
||||
PLAYER_INVITE_BANNED = 0x1C,
|
||||
THROTTLED = 0x1D,
|
||||
NOT_IN_AREA = 0x1E,
|
||||
NOT_IN_LFG = 0x1F,
|
||||
};
|
||||
|
||||
/**
|
||||
* SMSG_CHANNEL_NOTIFY data
|
||||
*/
|
||||
struct ChannelNotifyData {
|
||||
ChannelNotifyType notifyType = ChannelNotifyType::YOU_JOINED;
|
||||
std::string channelName;
|
||||
uint64_t senderGuid = 0;
|
||||
|
||||
bool isValid() const { return !channelName.empty(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* SMSG_CHANNEL_NOTIFY parser
|
||||
*/
|
||||
class ChannelNotifyParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, ChannelNotifyData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Server Info Commands
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ public:
|
|||
void cancelEmote();
|
||||
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);
|
||||
|
||||
// Targeting support
|
||||
void setTargetPosition(const glm::vec3* pos);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,16 @@ private:
|
|||
int lastChatType = 0; // Track chat type changes
|
||||
bool chatInputMoveCursorToEnd = false;
|
||||
|
||||
// Chat tabs
|
||||
int activeChatTab_ = 0;
|
||||
struct ChatTab {
|
||||
std::string name;
|
||||
uint32_t typeMask; // bitmask of ChatType values to show
|
||||
};
|
||||
std::vector<ChatTab> chatTabs_;
|
||||
void initChatTabs();
|
||||
bool shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const;
|
||||
|
||||
// UI state
|
||||
bool showEntityWindow = false;
|
||||
bool showChatWindow = true;
|
||||
|
|
@ -170,6 +180,7 @@ private:
|
|||
void renderMinimapMarkers(game::GameHandler& gameHandler);
|
||||
void renderGuildRoster(game::GameHandler& gameHandler);
|
||||
void renderGuildInvitePopup(game::GameHandler& gameHandler);
|
||||
void renderChatBubbles(game::GameHandler& gameHandler);
|
||||
|
||||
/**
|
||||
* Inventory screen
|
||||
|
|
@ -209,6 +220,17 @@ private:
|
|||
// Gender placeholder replacement
|
||||
std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler& gameHandler);
|
||||
|
||||
// Chat bubbles
|
||||
struct ChatBubble {
|
||||
uint64_t senderGuid = 0;
|
||||
std::string message;
|
||||
float timeRemaining = 0.0f;
|
||||
float totalDuration = 0.0f;
|
||||
bool isYell = false;
|
||||
};
|
||||
std::vector<ChatBubble> chatBubbles_;
|
||||
bool chatBubbleCallbackSet_ = false;
|
||||
|
||||
// Left-click targeting: distinguish click from camera drag
|
||||
glm::vec2 leftClickPressPos_ = glm::vec2(0.0f);
|
||||
bool leftClickWasPress_ = false;
|
||||
|
|
|
|||
|
|
@ -737,6 +737,18 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_TEXT_EMOTE:
|
||||
if (state == WorldState::IN_WORLD) {
|
||||
handleTextEmote(packet);
|
||||
}
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_CHANNEL_NOTIFY:
|
||||
if (state == WorldState::IN_WORLD) {
|
||||
handleChannelNotify(packet);
|
||||
}
|
||||
break;
|
||||
|
||||
case Opcode::SMSG_QUERY_TIME_RESPONSE:
|
||||
if (state == WorldState::IN_WORLD) {
|
||||
handleQueryTimeResponse(packet);
|
||||
|
|
@ -1948,6 +1960,9 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
worldEntryCallback_(data.mapId, data.x, data.y, data.z);
|
||||
}
|
||||
|
||||
// Auto-join default chat channels
|
||||
autoJoinDefaultChannels();
|
||||
|
||||
// Auto-query guild info on login
|
||||
const Character* activeChar = getActiveCharacter();
|
||||
if (activeChar && activeChar->hasGuild() && socket) {
|
||||
|
|
@ -3939,6 +3954,15 @@ void GameHandler::handleMessageChat(network::Packet& packet) {
|
|||
lastWhisperSender_ = data.senderName;
|
||||
}
|
||||
|
||||
// Trigger chat bubble for SAY/YELL messages from others
|
||||
if (chatBubbleCallback_ && data.senderGuid != 0) {
|
||||
if (data.type == ChatType::SAY || data.type == ChatType::YELL ||
|
||||
data.type == ChatType::MONSTER_SAY || data.type == ChatType::MONSTER_YELL) {
|
||||
bool isYell = (data.type == ChatType::YELL || data.type == ChatType::MONSTER_YELL);
|
||||
chatBubbleCallback_(data.senderGuid, data.message, isYell);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the message
|
||||
std::string senderInfo;
|
||||
if (!data.senderName.empty()) {
|
||||
|
|
@ -3961,6 +3985,126 @@ void GameHandler::handleMessageChat(network::Packet& packet) {
|
|||
LOG_INFO("========================================");
|
||||
}
|
||||
|
||||
void GameHandler::sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = TextEmotePacket::build(textEmoteId, targetGuid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::handleTextEmote(network::Packet& packet) {
|
||||
TextEmoteData data;
|
||||
if (!TextEmoteParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_TEXT_EMOTE");
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip our own text emotes (we already have local echo)
|
||||
if (data.senderGuid == playerGuid && data.senderGuid != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve sender name
|
||||
std::string senderName;
|
||||
auto nameIt = playerNameCache.find(data.senderGuid);
|
||||
if (nameIt != playerNameCache.end()) {
|
||||
senderName = nameIt->second;
|
||||
} else {
|
||||
auto entity = entityManager.getEntity(data.senderGuid);
|
||||
if (entity) {
|
||||
auto unit = std::dynamic_pointer_cast<Unit>(entity);
|
||||
if (unit) senderName = unit->getName();
|
||||
}
|
||||
}
|
||||
if (senderName.empty()) {
|
||||
senderName = "Unknown";
|
||||
queryPlayerName(data.senderGuid);
|
||||
}
|
||||
|
||||
// Build emote message text (server sends textEmoteId, we look up the text)
|
||||
// For now, just display a generic emote message
|
||||
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 + ".";
|
||||
|
||||
chatHistory.push_back(chatMsg);
|
||||
if (chatHistory.size() > maxChatHistory) {
|
||||
chatHistory.erase(chatHistory.begin());
|
||||
}
|
||||
|
||||
LOG_INFO("TEXT_EMOTE from ", senderName, " (emoteId=", data.textEmoteId, ")");
|
||||
}
|
||||
|
||||
void GameHandler::joinChannel(const std::string& channelName, const std::string& password) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = JoinChannelPacket::build(channelName, password);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Requesting to join channel: ", channelName);
|
||||
}
|
||||
|
||||
void GameHandler::leaveChannel(const std::string& channelName) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = LeaveChannelPacket::build(channelName);
|
||||
socket->send(packet);
|
||||
LOG_INFO("Requesting to leave channel: ", channelName);
|
||||
}
|
||||
|
||||
std::string GameHandler::getChannelByIndex(int index) const {
|
||||
if (index < 1 || index > static_cast<int>(joinedChannels_.size())) return "";
|
||||
return joinedChannels_[index - 1];
|
||||
}
|
||||
|
||||
void GameHandler::handleChannelNotify(network::Packet& packet) {
|
||||
ChannelNotifyData data;
|
||||
if (!ChannelNotifyParser::parse(packet, data)) {
|
||||
LOG_WARNING("Failed to parse SMSG_CHANNEL_NOTIFY");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.notifyType) {
|
||||
case ChannelNotifyType::YOU_JOINED: {
|
||||
// Add to active channels if not already present
|
||||
bool found = false;
|
||||
for (const auto& ch : joinedChannels_) {
|
||||
if (ch == data.channelName) { found = true; break; }
|
||||
}
|
||||
if (!found) {
|
||||
joinedChannels_.push_back(data.channelName);
|
||||
}
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.message = "Joined channel: " + data.channelName;
|
||||
addLocalChatMessage(msg);
|
||||
LOG_INFO("Joined channel: ", data.channelName);
|
||||
break;
|
||||
}
|
||||
case ChannelNotifyType::YOU_LEFT: {
|
||||
joinedChannels_.erase(
|
||||
std::remove(joinedChannels_.begin(), joinedChannels_.end(), data.channelName),
|
||||
joinedChannels_.end());
|
||||
MessageChatData msg;
|
||||
msg.type = ChatType::SYSTEM;
|
||||
msg.message = "Left channel: " + data.channelName;
|
||||
addLocalChatMessage(msg);
|
||||
LOG_INFO("Left channel: ", data.channelName);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG_DEBUG("Channel notify type ", static_cast<int>(data.notifyType),
|
||||
" for channel ", data.channelName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::autoJoinDefaultChannels() {
|
||||
joinChannel("General");
|
||||
joinChannel("Trade");
|
||||
}
|
||||
|
||||
void GameHandler::setTarget(uint64_t guid) {
|
||||
if (guid == targetGuid) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -281,6 +281,15 @@ static const OpcodeNameEntry kOpcodeNames[] = {
|
|||
{"SMSG_ARENA_TEAM_STATS", LogicalOpcode::SMSG_ARENA_TEAM_STATS},
|
||||
{"SMSG_ARENA_ERROR", LogicalOpcode::SMSG_ARENA_ERROR},
|
||||
{"MSG_INSPECT_ARENA_TEAMS", LogicalOpcode::MSG_INSPECT_ARENA_TEAMS},
|
||||
{"CMSG_EMOTE", LogicalOpcode::CMSG_EMOTE},
|
||||
{"SMSG_EMOTE", LogicalOpcode::SMSG_EMOTE},
|
||||
{"CMSG_TEXT_EMOTE", LogicalOpcode::CMSG_TEXT_EMOTE},
|
||||
{"SMSG_TEXT_EMOTE", LogicalOpcode::SMSG_TEXT_EMOTE},
|
||||
{"CMSG_JOIN_CHANNEL", LogicalOpcode::CMSG_JOIN_CHANNEL},
|
||||
{"CMSG_LEAVE_CHANNEL", LogicalOpcode::CMSG_LEAVE_CHANNEL},
|
||||
{"SMSG_CHANNEL_NOTIFY", LogicalOpcode::SMSG_CHANNEL_NOTIFY},
|
||||
{"CMSG_CHANNEL_LIST", LogicalOpcode::CMSG_CHANNEL_LIST},
|
||||
{"SMSG_CHANNEL_LIST", LogicalOpcode::SMSG_CHANNEL_LIST},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
|
@ -563,6 +572,15 @@ void OpcodeTable::loadWotlkDefaults() {
|
|||
{LogicalOpcode::SMSG_ARENA_TEAM_STATS, 0x035B},
|
||||
{LogicalOpcode::SMSG_ARENA_ERROR, 0x0376},
|
||||
{LogicalOpcode::MSG_INSPECT_ARENA_TEAMS, 0x0377},
|
||||
{LogicalOpcode::CMSG_EMOTE, 0x102},
|
||||
{LogicalOpcode::SMSG_EMOTE, 0x103},
|
||||
{LogicalOpcode::CMSG_TEXT_EMOTE, 0x104},
|
||||
{LogicalOpcode::SMSG_TEXT_EMOTE, 0x105},
|
||||
{LogicalOpcode::CMSG_JOIN_CHANNEL, 0x097},
|
||||
{LogicalOpcode::CMSG_LEAVE_CHANNEL, 0x098},
|
||||
{LogicalOpcode::SMSG_CHANNEL_NOTIFY, 0x099},
|
||||
{LogicalOpcode::CMSG_CHANNEL_LIST, 0x09A},
|
||||
{LogicalOpcode::SMSG_CHANNEL_LIST, 0x09B},
|
||||
};
|
||||
|
||||
logicalToWire_.clear();
|
||||
|
|
|
|||
|
|
@ -1293,6 +1293,77 @@ const char* getChatTypeString(ChatType type) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Text Emotes
|
||||
// ============================================================
|
||||
|
||||
network::Packet TextEmotePacket::build(uint32_t textEmoteId, uint64_t targetGuid) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_TEXT_EMOTE));
|
||||
packet.writeUInt32(textEmoteId);
|
||||
packet.writeUInt32(0); // emoteNum (unused)
|
||||
packet.writeUInt64(targetGuid);
|
||||
LOG_DEBUG("Built CMSG_TEXT_EMOTE: emoteId=", textEmoteId, " target=0x", std::hex, targetGuid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool TextEmoteParser::parse(network::Packet& packet, TextEmoteData& data) {
|
||||
size_t bytesLeft = packet.getSize() - packet.getReadPos();
|
||||
if (bytesLeft < 20) {
|
||||
LOG_WARNING("SMSG_TEXT_EMOTE too short: ", bytesLeft, " bytes");
|
||||
return false;
|
||||
}
|
||||
data.senderGuid = packet.readUInt64();
|
||||
data.textEmoteId = packet.readUInt32();
|
||||
data.emoteNum = packet.readUInt32();
|
||||
uint32_t nameLen = packet.readUInt32();
|
||||
if (nameLen > 0 && nameLen <= 256) {
|
||||
data.targetName = packet.readString();
|
||||
} else if (nameLen > 0) {
|
||||
// Skip garbage
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Channel System
|
||||
// ============================================================
|
||||
|
||||
network::Packet JoinChannelPacket::build(const std::string& channelName, const std::string& password) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_JOIN_CHANNEL));
|
||||
packet.writeUInt32(0); // channelId (unused)
|
||||
packet.writeUInt8(0); // hasVoice
|
||||
packet.writeUInt8(0); // joinedByZone
|
||||
packet.writeString(channelName);
|
||||
packet.writeString(password);
|
||||
LOG_DEBUG("Built CMSG_JOIN_CHANNEL: channel=", channelName);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet LeaveChannelPacket::build(const std::string& channelName) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_LEAVE_CHANNEL));
|
||||
packet.writeUInt32(0); // channelId (unused)
|
||||
packet.writeString(channelName);
|
||||
LOG_DEBUG("Built CMSG_LEAVE_CHANNEL: channel=", channelName);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool ChannelNotifyParser::parse(network::Packet& packet, ChannelNotifyData& data) {
|
||||
size_t bytesLeft = packet.getSize() - packet.getReadPos();
|
||||
if (bytesLeft < 2) {
|
||||
LOG_WARNING("SMSG_CHANNEL_NOTIFY too short");
|
||||
return false;
|
||||
}
|
||||
data.notifyType = static_cast<ChannelNotifyType>(packet.readUInt8());
|
||||
data.channelName = packet.readString();
|
||||
// Some notification types have additional fields (guid, etc.)
|
||||
bytesLeft = packet.getSize() - packet.getReadPos();
|
||||
if (bytesLeft >= 8) {
|
||||
data.senderGuid = packet.readUInt64();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Foundation — Targeting, Name Queries
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ namespace rendering {
|
|||
|
||||
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;
|
||||
|
|
@ -100,26 +101,26 @@ static bool isLoopingEmote(const std::string& command) {
|
|||
static void loadFallbackEmotes() {
|
||||
if (!EMOTE_TABLE.empty()) return;
|
||||
EMOTE_TABLE = {
|
||||
{"wave", {67, false, "You wave.", "You wave at %s.", "wave"}},
|
||||
{"bow", {66, false, "You bow down graciously.", "You bow down before %s.", "bow"}},
|
||||
{"laugh", {70, false, "You laugh.", "You laugh at %s.", "laugh"}},
|
||||
{"point", {84, false, "You point over yonder.", "You point at %s.", "point"}},
|
||||
{"cheer", {68, false, "You cheer!", "You cheer at %s.", "cheer"}},
|
||||
{"dance", {69, true, "You burst into dance.", "You dance with %s.", "dance"}},
|
||||
{"kneel", {75, false, "You kneel down.", "You kneel before %s.", "kneel"}},
|
||||
{"applaud", {80, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "applaud"}},
|
||||
{"shout", {81, false, "You shout.", "You shout at %s.", "shout"}},
|
||||
{"chicken", {78, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!",
|
||||
{"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"}},
|
||||
{"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, false, "You cry.", "You cry on %s's shoulder.", "cry"}},
|
||||
{"kiss", {76, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "kiss"}},
|
||||
{"roar", {74, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "roar"}},
|
||||
{"salute", {113, false, "You salute.", "You salute %s with respect.", "salute"}},
|
||||
{"rude", {73, false, "You make a rude gesture.", "You make a rude gesture at %s.", "rude"}},
|
||||
{"flex", {82, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "flex"}},
|
||||
{"shy", {83, false, "You smile shyly.", "You smile shyly at %s.", "shy"}},
|
||||
{"beg", {79, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "beg"}},
|
||||
{"eat", {61, false, "You begin to eat.", "You begin to eat in front of %s.", "eat"}},
|
||||
{"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"}},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +184,7 @@ static void loadEmotesFromDbc() {
|
|||
EMOTE_TABLE.clear();
|
||||
EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount());
|
||||
for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) {
|
||||
uint32_t recordId = emotesTextDbc->getUInt32(r, etL ? (*etL)["ID"] : 0);
|
||||
std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1);
|
||||
if (cmdRaw.empty()) continue;
|
||||
|
||||
|
|
@ -211,6 +213,7 @@ static void loadEmotesFromDbc() {
|
|||
if (cmd.empty()) continue;
|
||||
EmoteInfo info;
|
||||
info.animId = animId;
|
||||
info.dbcId = recordId;
|
||||
info.loop = isLoopingEmote(cmd);
|
||||
info.textNoTarget = textNoTarget;
|
||||
info.textTarget = textTarget;
|
||||
|
|
@ -1569,6 +1572,15 @@ std::string Renderer::getEmoteText(const std::string& emoteName, const std::stri
|
|||
return "";
|
||||
}
|
||||
|
||||
uint32_t Renderer::getEmoteDbcId(const std::string& emoteName) {
|
||||
loadEmotesFromDbc();
|
||||
auto it = EMOTE_TABLE.find(emoteName);
|
||||
if (it != EMOTE_TABLE.end()) {
|
||||
return it->second.dbcId;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Renderer::setTargetPosition(const glm::vec3* pos) {
|
||||
targetPosition = pos;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,9 +117,71 @@ namespace wowee { namespace ui {
|
|||
|
||||
GameScreen::GameScreen() {
|
||||
loadSettings();
|
||||
initChatTabs();
|
||||
}
|
||||
|
||||
void GameScreen::initChatTabs() {
|
||||
chatTabs_.clear();
|
||||
// General tab: shows everything
|
||||
chatTabs_.push_back({"General", 0xFFFFFFFF});
|
||||
// Combat tab: system + loot messages
|
||||
chatTabs_.push_back({"Combat", (1u << static_cast<uint8_t>(game::ChatType::SYSTEM)) |
|
||||
(1u << static_cast<uint8_t>(game::ChatType::LOOT))});
|
||||
// Whispers tab
|
||||
chatTabs_.push_back({"Whispers", (1u << static_cast<uint8_t>(game::ChatType::WHISPER)) |
|
||||
(1u << static_cast<uint8_t>(game::ChatType::WHISPER_INFORM))});
|
||||
// Trade/LFG tab: channel messages
|
||||
chatTabs_.push_back({"Trade/LFG", (1u << static_cast<uint8_t>(game::ChatType::CHANNEL))});
|
||||
}
|
||||
|
||||
bool GameScreen::shouldShowMessage(const game::MessageChatData& msg, int tabIndex) const {
|
||||
if (tabIndex < 0 || tabIndex >= static_cast<int>(chatTabs_.size())) return true;
|
||||
const auto& tab = chatTabs_[tabIndex];
|
||||
if (tab.typeMask == 0xFFFFFFFF) return true; // General tab shows all
|
||||
|
||||
uint32_t typeBit = 1u << static_cast<uint8_t>(msg.type);
|
||||
|
||||
// For Trade/LFG tab, also filter by channel name
|
||||
if (tabIndex == 3 && msg.type == game::ChatType::CHANNEL) {
|
||||
const std::string& ch = msg.channelName;
|
||||
if (ch.find("Trade") == std::string::npos &&
|
||||
ch.find("General") == std::string::npos &&
|
||||
ch.find("LookingForGroup") == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return (tab.typeMask & typeBit) != 0;
|
||||
}
|
||||
|
||||
void GameScreen::render(game::GameHandler& gameHandler) {
|
||||
// Set up chat bubble callback (once)
|
||||
if (!chatBubbleCallbackSet_) {
|
||||
gameHandler.setChatBubbleCallback([this](uint64_t guid, const std::string& msg, bool isYell) {
|
||||
float duration = 8.0f + static_cast<float>(msg.size()) * 0.06f;
|
||||
if (isYell) duration += 2.0f;
|
||||
if (duration > 15.0f) duration = 15.0f;
|
||||
|
||||
// Replace existing bubble for same sender
|
||||
for (auto& b : chatBubbles_) {
|
||||
if (b.senderGuid == guid) {
|
||||
b.message = msg;
|
||||
b.timeRemaining = duration;
|
||||
b.totalDuration = duration;
|
||||
b.isYell = isYell;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Evict oldest if too many
|
||||
if (chatBubbles_.size() >= 10) {
|
||||
chatBubbles_.erase(chatBubbles_.begin());
|
||||
}
|
||||
chatBubbles_.push_back({guid, msg, duration, duration, isYell});
|
||||
});
|
||||
chatBubbleCallbackSet_ = true;
|
||||
}
|
||||
|
||||
// Apply UI transparency setting
|
||||
float prevAlpha = ImGui::GetStyle().Alpha;
|
||||
ImGui::GetStyle().Alpha = uiOpacity_;
|
||||
|
|
@ -185,6 +247,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderMinimapMarkers(gameHandler);
|
||||
renderDeathScreen(gameHandler);
|
||||
renderResurrectDialog(gameHandler);
|
||||
renderChatBubbles(gameHandler);
|
||||
renderEscapeMenu();
|
||||
renderSettingsWindow();
|
||||
|
||||
|
|
@ -485,6 +548,17 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
chatWindowPos_ = ImGui::GetWindowPos();
|
||||
}
|
||||
|
||||
// Chat tabs
|
||||
if (ImGui::BeginTabBar("ChatTabs")) {
|
||||
for (int i = 0; i < static_cast<int>(chatTabs_.size()); ++i) {
|
||||
if (ImGui::BeginTabItem(chatTabs_[i].name.c_str())) {
|
||||
activeChatTab_ = i;
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
// Chat history
|
||||
const auto& chatHistory = gameHandler.getChatHistory();
|
||||
|
||||
|
|
@ -546,6 +620,8 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
|
|||
};
|
||||
|
||||
for (const auto& msg : chatHistory) {
|
||||
if (!shouldShowMessage(msg, activeChatTab_)) continue;
|
||||
|
||||
ImVec4 color = getChatTypeColor(msg.type);
|
||||
|
||||
if (msg.type == game::ChatType::SYSTEM) {
|
||||
|
|
@ -1955,6 +2031,45 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
message = (spacePos != std::string::npos) ? command.substr(spacePos + 1) : "";
|
||||
isChannelCommand = true;
|
||||
switchChatType = 9;
|
||||
} else if (cmdLower == "join") {
|
||||
// /join ChannelName [password]
|
||||
if (spacePos != std::string::npos) {
|
||||
std::string rest = command.substr(spacePos + 1);
|
||||
size_t pwStart = rest.find(' ');
|
||||
std::string channelName = (pwStart != std::string::npos) ? rest.substr(0, pwStart) : rest;
|
||||
std::string password = (pwStart != std::string::npos) ? rest.substr(pwStart + 1) : "";
|
||||
gameHandler.joinChannel(channelName, password);
|
||||
}
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
} else if (cmdLower == "leave") {
|
||||
// /leave ChannelName
|
||||
if (spacePos != std::string::npos) {
|
||||
std::string channelName = command.substr(spacePos + 1);
|
||||
gameHandler.leaveChannel(channelName);
|
||||
}
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
} else if (cmdLower.size() == 1 && cmdLower[0] >= '1' && cmdLower[0] <= '9') {
|
||||
// /1 msg, /2 msg — channel shortcuts
|
||||
int channelIdx = cmdLower[0] - '0';
|
||||
std::string channelName = gameHandler.getChannelByIndex(channelIdx);
|
||||
if (!channelName.empty() && spacePos != std::string::npos) {
|
||||
message = command.substr(spacePos + 1);
|
||||
type = game::ChatType::CHANNEL;
|
||||
target = channelName;
|
||||
isChannelCommand = true;
|
||||
} else if (channelName.empty()) {
|
||||
game::MessageChatData errMsg;
|
||||
errMsg.type = game::ChatType::SYSTEM;
|
||||
errMsg.message = "You are not in channel " + std::to_string(channelIdx) + ".";
|
||||
gameHandler.addLocalChatMessage(errMsg);
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
} else {
|
||||
chatInputBuffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
} else if (cmdLower == "w" || cmdLower == "whisper" || cmdLower == "tell" || cmdLower == "t") {
|
||||
switchChatType = 4;
|
||||
if (spacePos != std::string::npos) {
|
||||
|
|
@ -2003,6 +2118,13 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
|||
renderer->playEmote(cmdLower);
|
||||
}
|
||||
|
||||
// Send CMSG_TEXT_EMOTE to server
|
||||
uint32_t dbcId = rendering::Renderer::getEmoteDbcId(cmdLower);
|
||||
if (dbcId != 0) {
|
||||
uint64_t targetGuid = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
||||
gameHandler.sendTextEmote(dbcId, targetGuid);
|
||||
}
|
||||
|
||||
// Add local chat message
|
||||
game::MessageChatData msg;
|
||||
msg.type = game::ChatType::TEXT_EMOTE;
|
||||
|
|
@ -5440,6 +5562,86 @@ std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game:
|
|||
return result;
|
||||
}
|
||||
|
||||
void GameScreen::renderChatBubbles(game::GameHandler& gameHandler) {
|
||||
if (chatBubbles_.empty()) return;
|
||||
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
||||
if (!camera) return;
|
||||
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
||||
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
||||
|
||||
// Get delta time from ImGui
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
|
||||
glm::mat4 viewProj = camera->getProjectionMatrix() * camera->getViewMatrix();
|
||||
|
||||
// Update and render bubbles
|
||||
for (int i = static_cast<int>(chatBubbles_.size()) - 1; i >= 0; --i) {
|
||||
auto& bubble = chatBubbles_[i];
|
||||
bubble.timeRemaining -= dt;
|
||||
if (bubble.timeRemaining <= 0.0f) {
|
||||
chatBubbles_.erase(chatBubbles_.begin() + i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get entity position
|
||||
auto entity = gameHandler.getEntityManager().getEntity(bubble.senderGuid);
|
||||
if (!entity) continue;
|
||||
|
||||
// Convert canonical → render coordinates, offset up by 2.5 units for bubble above head
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ() + 2.5f);
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
||||
|
||||
// Project to screen
|
||||
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
||||
if (clipPos.w <= 0.0f) continue; // Behind camera
|
||||
|
||||
glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
|
||||
float screenX = (ndc.x * 0.5f + 0.5f) * screenW;
|
||||
float screenY = (1.0f - (ndc.y * 0.5f + 0.5f)) * screenH; // Flip Y
|
||||
|
||||
// Skip if off-screen
|
||||
if (screenX < -200.0f || screenX > screenW + 200.0f ||
|
||||
screenY < -100.0f || screenY > screenH + 100.0f) continue;
|
||||
|
||||
// Fade alpha over last 2 seconds
|
||||
float alpha = 1.0f;
|
||||
if (bubble.timeRemaining < 2.0f) {
|
||||
alpha = bubble.timeRemaining / 2.0f;
|
||||
}
|
||||
|
||||
// Draw bubble window
|
||||
std::string winId = "##ChatBubble" + std::to_string(bubble.senderGuid);
|
||||
ImGui::SetNextWindowPos(ImVec2(screenX, screenY), ImGuiCond_Always, ImVec2(0.5f, 1.0f));
|
||||
ImGui::SetNextWindowBgAlpha(0.7f * alpha);
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoInputs |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 4));
|
||||
|
||||
ImGui::Begin(winId.c_str(), nullptr, flags);
|
||||
|
||||
ImVec4 textColor = bubble.isYell
|
||||
? ImVec4(1.0f, 0.2f, 0.2f, alpha)
|
||||
: ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, textColor);
|
||||
ImGui::PushTextWrapPos(200.0f);
|
||||
ImGui::TextWrapped("%s", bubble.message.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
}
|
||||
|
||||
void GameScreen::saveSettings() {
|
||||
std::string path = getSettingsPath();
|
||||
std::filesystem::path dir = std::filesystem::path(path).parent_path();
|
||||
|
|
@ -5475,6 +5677,9 @@ void GameScreen::saveSettings() {
|
|||
out << "mouse_sensitivity=" << pendingMouseSensitivity << "\n";
|
||||
out << "invert_mouse=" << (pendingInvertMouse ? 1 : 0) << "\n";
|
||||
|
||||
// Chat
|
||||
out << "chat_active_tab=" << activeChatTab_ << "\n";
|
||||
|
||||
LOG_INFO("Settings saved to ", path);
|
||||
}
|
||||
|
||||
|
|
@ -5525,6 +5730,8 @@ void GameScreen::loadSettings() {
|
|||
// Controls
|
||||
else if (key == "mouse_sensitivity") pendingMouseSensitivity = std::clamp(std::stof(val), 0.05f, 1.0f);
|
||||
else if (key == "invert_mouse") pendingInvertMouse = (std::stoi(val) != 0);
|
||||
// Chat
|
||||
else if (key == "chat_active_tab") activeChatTab_ = std::clamp(std::stoi(val), 0, 3);
|
||||
} catch (...) {}
|
||||
}
|
||||
LOG_INFO("Settings loaded from ", path);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue