diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index d38f67d0..f03ae158 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -234,6 +234,16 @@ public: void leaveChannel(const std::string& channelName); const std::vector& getJoinedChannels() const { return joinedChannels_; } std::string getChannelByIndex(int index) const; + int getChannelIndex(const std::string& channelName) const; + + // Chat auto-join settings (set by UI before autoJoinDefaultChannels) + struct ChatAutoJoin { + bool general = true; + bool trade = true; + bool localDefense = true; + bool lfg = true; + }; + ChatAutoJoin chatAutoJoin; // Chat bubble callback: (senderGuid, message, isYell) using ChatBubbleCallback = std::function; diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 1b74c240..989144ea 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -136,6 +136,18 @@ public: return GuildQueryResponseParser::parse(packet, data); } + // --- Channels --- + + /** Build CMSG_JOIN_CHANNEL */ + virtual network::Packet buildJoinChannel(const std::string& channelName, const std::string& password) { + return JoinChannelPacket::build(channelName, password); + } + + /** Build CMSG_LEAVE_CHANNEL */ + virtual network::Packet buildLeaveChannel(const std::string& channelName) { + return LeaveChannelPacket::build(channelName); + } + // --- Utility --- /** Read a packed GUID from the packet */ @@ -216,6 +228,8 @@ public: bool parseMessageChat(network::Packet& packet, MessageChatData& data) override; bool parseGuildRoster(network::Packet& packet, GuildRosterData& data) override; bool parseGuildQueryResponse(network::Packet& packet, GuildQueryResponseData& data) override; + network::Packet buildJoinChannel(const std::string& channelName, const std::string& password) override; + network::Packet buildLeaveChannel(const std::string& channelName) override; }; /** diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index d01381c8..8e70b36c 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace wowee { namespace game { @@ -653,6 +654,7 @@ struct MessageChatData { std::string message; std::string channelName; // For channel messages uint8_t chatTag = 0; // Player flags (AFK, DND, GM, etc.) + std::chrono::system_clock::time_point timestamp = std::chrono::system_clock::now(); bool isValid() const { return !message.empty(); } }; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 1ca68a04..415f8f6d 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -215,6 +215,17 @@ private: GLuint backpackIconTexture_ = 0; GLuint emptyBagSlotTexture_ = 0; + // Chat settings + bool chatShowTimestamps_ = false; + int chatFontSize_ = 1; // 0=small, 1=medium, 2=large + bool chatAutoJoinGeneral_ = true; + bool chatAutoJoinTrade_ = true; + bool chatAutoJoinLocalDefense_ = true; + bool chatAutoJoinLFG_ = true; + + // Join channel input buffer + char joinChannelBuffer_[128] = ""; + static std::string getSettingsPath(); // Gender placeholder replacement diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index f5543477..52ed41f9 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3912,6 +3912,10 @@ void GameHandler::sendChatMessage(ChatType type, const std::string& message, con echo.type = type; } + if (type == ChatType::CHANNEL) { + echo.channelName = target; + } + addLocalChatMessage(echo); } @@ -4075,14 +4079,16 @@ void GameHandler::handleTextEmote(network::Packet& packet) { void GameHandler::joinChannel(const std::string& channelName, const std::string& password) { if (state != WorldState::IN_WORLD || !socket) return; - auto packet = JoinChannelPacket::build(channelName, password); + auto packet = packetParsers_ ? packetParsers_->buildJoinChannel(channelName, password) + : 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); + auto packet = packetParsers_ ? packetParsers_->buildLeaveChannel(channelName) + : LeaveChannelPacket::build(channelName); socket->send(packet); LOG_INFO("Requesting to leave channel: ", channelName); } @@ -4092,6 +4098,13 @@ std::string GameHandler::getChannelByIndex(int index) const { return joinedChannels_[index - 1]; } +int GameHandler::getChannelIndex(const std::string& channelName) const { + for (int i = 0; i < static_cast(joinedChannels_.size()); ++i) { + if (joinedChannels_[i] == channelName) return i + 1; // 1-based + } + return 0; +} + void GameHandler::handleChannelNotify(network::Packet& packet) { ChannelNotifyData data; if (!ChannelNotifyParser::parse(packet, data)) { @@ -4135,8 +4148,10 @@ void GameHandler::handleChannelNotify(network::Packet& packet) { } void GameHandler::autoJoinDefaultChannels() { - joinChannel("General"); - joinChannel("Trade"); + if (chatAutoJoin.general) joinChannel("General"); + if (chatAutoJoin.trade) joinChannel("Trade"); + if (chatAutoJoin.localDefense) joinChannel("LocalDefense"); + if (chatAutoJoin.lfg) joinChannel("LookingForGroup"); } void GameHandler::setTarget(uint64_t guid) { diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 5e6c5e6e..6c618f4e 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -487,6 +487,26 @@ bool ClassicPacketParsers::parseMessageChat(network::Packet& packet, MessageChat return true; } +// ============================================================================ +// Classic CMSG_JOIN_CHANNEL / CMSG_LEAVE_CHANNEL +// Classic format: just string channelName + string password (no channelId/hasVoice/joinedByZone) +// ============================================================================ + +network::Packet ClassicPacketParsers::buildJoinChannel(const std::string& channelName, const std::string& password) { + network::Packet packet(wireOpcode(Opcode::CMSG_JOIN_CHANNEL)); + packet.writeString(channelName); + packet.writeString(password); + LOG_DEBUG("[Classic] Built CMSG_JOIN_CHANNEL: channel=", channelName); + return packet; +} + +network::Packet ClassicPacketParsers::buildLeaveChannel(const std::string& channelName) { + network::Packet packet(wireOpcode(Opcode::CMSG_LEAVE_CHANNEL)); + packet.writeString(channelName); + LOG_DEBUG("[Classic] Built CMSG_LEAVE_CHANNEL: channel=", channelName); + return packet; +} + // ============================================================================ // Classic guild roster parser // Differences from WotLK: diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 8acc43bf..8dd86db1 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1233,6 +1233,10 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) { for (uint32_t i = 0; i < messageLen; ++i) { data.message[i] = static_cast(packet.readUInt8()); } + // Strip trailing null terminator (servers include it in messageLen) + if (!data.message.empty() && data.message.back() == '\0') { + data.message.pop_back(); + } } // Read chat tag diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 955c2f0e..b52c8abd 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -34,6 +34,8 @@ #include #include #include +#include +#include #include namespace { @@ -200,6 +202,12 @@ void GameScreen::render(game::GameHandler& gameHandler) { } } + // Sync chat auto-join settings to GameHandler + gameHandler.chatAutoJoin.general = chatAutoJoinGeneral_; + gameHandler.chatAutoJoin.trade = chatAutoJoinTrade_; + gameHandler.chatAutoJoin.localDefense = chatAutoJoinLocalDefense_; + gameHandler.chatAutoJoin.lfg = chatAutoJoinLFG_; + // Process targeting input before UI windows processTargetInput(gameHandler); @@ -562,6 +570,10 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { // Chat history const auto& chatHistory = gameHandler.getChatHistory(); + // Apply chat font size scaling + float chatScale = chatFontSize_ == 0 ? 0.85f : (chatFontSize_ == 2 ? 1.2f : 1.0f); + ImGui::SetWindowFontScale(chatScale); + ImGui::BeginChild("ChatHistory", ImVec2(0, -70), true, ImGuiWindowFlags_HorizontalScrollbar); bool chatHistoryHovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); @@ -847,20 +859,54 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImVec4 color = getChatTypeColor(msg.type); + // Optional timestamp prefix + std::string tsPrefix; + if (chatShowTimestamps_) { + auto tt = std::chrono::system_clock::to_time_t(msg.timestamp); + std::tm tm{}; + localtime_r(&tt, &tm); + char tsBuf[16]; + snprintf(tsBuf, sizeof(tsBuf), "[%02d:%02d] ", tm.tm_hour, tm.tm_min); + tsPrefix = tsBuf; + } + if (msg.type == game::ChatType::SYSTEM) { + if (!tsPrefix.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + ImGui::TextWrapped("%s", tsPrefix.c_str()); + ImGui::PopStyleColor(); + ImGui::SameLine(0, 0); + } renderTextWithLinks(msg.message, color); } else if (msg.type == game::ChatType::TEXT_EMOTE) { + if (!tsPrefix.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + ImGui::TextWrapped("%s", tsPrefix.c_str()); + ImGui::PopStyleColor(); + ImGui::SameLine(0, 0); + } renderTextWithLinks(msg.message, color); } else if (!msg.senderName.empty()) { if (msg.type == game::ChatType::MONSTER_SAY || msg.type == game::ChatType::MONSTER_YELL) { - std::string prefix = msg.senderName + " says: "; + std::string prefix = tsPrefix + msg.senderName + " says: "; + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::TextWrapped("%s", prefix.c_str()); + ImGui::PopStyleColor(); + ImGui::SameLine(0, 0); + renderTextWithLinks(msg.message, color); + } else if (msg.type == game::ChatType::CHANNEL && !msg.channelName.empty()) { + int chIdx = gameHandler.getChannelIndex(msg.channelName); + std::string chDisplay = chIdx > 0 + ? "[" + std::to_string(chIdx) + ". " + msg.channelName + "]" + : "[" + msg.channelName + "]"; + std::string prefix = tsPrefix + chDisplay + " [" + msg.senderName + "]: "; ImGui::PushStyleColor(ImGuiCol_Text, color); ImGui::TextWrapped("%s", prefix.c_str()); ImGui::PopStyleColor(); ImGui::SameLine(0, 0); renderTextWithLinks(msg.message, color); } else { - std::string prefix = "[" + std::string(getChatTypeName(msg.type)) + "] " + msg.senderName + ": "; + std::string prefix = tsPrefix + "[" + std::string(getChatTypeName(msg.type)) + "] " + msg.senderName + ": "; ImGui::PushStyleColor(ImGuiCol_Text, color); ImGui::TextWrapped("%s", prefix.c_str()); ImGui::PopStyleColor(); @@ -868,7 +914,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { renderTextWithLinks(msg.message, color); } } else { - std::string prefix = "[" + std::string(getChatTypeName(msg.type)) + "] "; + std::string prefix = tsPrefix + "[" + std::string(getChatTypeName(msg.type)) + "] "; ImGui::PushStyleColor(ImGuiCol_Text, color); ImGui::TextWrapped("%s", prefix.c_str()); ImGui::PopStyleColor(); @@ -884,6 +930,9 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGui::EndChild(); + // Reset font scale after chat history + ImGui::SetWindowFontScale(1.0f); + ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); @@ -5464,6 +5513,59 @@ void GameScreen::renderSettingsWindow() { ImGui::EndTabItem(); } + // ============================================================ + // CHAT TAB + // ============================================================ + if (ImGui::BeginTabItem("Chat")) { + ImGui::Spacing(); + + ImGui::Text("Appearance"); + ImGui::Separator(); + + if (ImGui::Checkbox("Show Timestamps", &chatShowTimestamps_)) { + saveSettings(); + } + ImGui::SetItemTooltip("Show [HH:MM] before each chat message"); + + const char* fontSizes[] = { "Small", "Medium", "Large" }; + if (ImGui::Combo("Chat Font Size", &chatFontSize_, fontSizes, 3)) { + saveSettings(); + } + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Text("Auto-Join Channels"); + ImGui::Separator(); + + if (ImGui::Checkbox("General", &chatAutoJoinGeneral_)) saveSettings(); + if (ImGui::Checkbox("Trade", &chatAutoJoinTrade_)) saveSettings(); + if (ImGui::Checkbox("LocalDefense", &chatAutoJoinLocalDefense_)) saveSettings(); + if (ImGui::Checkbox("LookingForGroup", &chatAutoJoinLFG_)) saveSettings(); + + ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Text("Joined Channels"); + ImGui::Separator(); + + ImGui::TextDisabled("Use /join and /leave commands in chat to manage channels."); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Restore Chat Defaults", ImVec2(-1, 0))) { + chatShowTimestamps_ = false; + chatFontSize_ = 1; + chatAutoJoinGeneral_ = true; + chatAutoJoinTrade_ = true; + chatAutoJoinLocalDefense_ = true; + chatAutoJoinLFG_ = true; + saveSettings(); + } + + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } @@ -5920,6 +6022,12 @@ void GameScreen::saveSettings() { // Chat out << "chat_active_tab=" << activeChatTab_ << "\n"; + out << "chat_timestamps=" << (chatShowTimestamps_ ? 1 : 0) << "\n"; + out << "chat_font_size=" << chatFontSize_ << "\n"; + out << "chat_autojoin_general=" << (chatAutoJoinGeneral_ ? 1 : 0) << "\n"; + out << "chat_autojoin_trade=" << (chatAutoJoinTrade_ ? 1 : 0) << "\n"; + out << "chat_autojoin_localdefense=" << (chatAutoJoinLocalDefense_ ? 1 : 0) << "\n"; + out << "chat_autojoin_lfg=" << (chatAutoJoinLFG_ ? 1 : 0) << "\n"; LOG_INFO("Settings saved to ", path); } @@ -5973,6 +6081,12 @@ void GameScreen::loadSettings() { 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); + else if (key == "chat_timestamps") chatShowTimestamps_ = (std::stoi(val) != 0); + else if (key == "chat_font_size") chatFontSize_ = std::clamp(std::stoi(val), 0, 2); + else if (key == "chat_autojoin_general") chatAutoJoinGeneral_ = (std::stoi(val) != 0); + else if (key == "chat_autojoin_trade") chatAutoJoinTrade_ = (std::stoi(val) != 0); + else if (key == "chat_autojoin_localdefense") chatAutoJoinLocalDefense_ = (std::stoi(val) != 0); + else if (key == "chat_autojoin_lfg") chatAutoJoinLFG_ = (std::stoi(val) != 0); } catch (...) {} } LOG_INFO("Settings loaded from ", path);