diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 4a1ad26a..e2fab81e 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -494,6 +494,13 @@ public: // GM Ticket void submitGmTicket(const std::string& text); void deleteGmTicket(); + void requestGmTicket(); ///< Send CMSG_GMTICKET_GETTICKET to query open ticket + + // GM ticket status accessors + bool hasActiveGmTicket() const { return gmTicketActive_; } + const std::string& getGmTicketText() const { return gmTicketText_; } + bool isGmSupportAvailable() const { return gmSupportAvailable_; } + float getGmTicketWaitHours() const { return gmTicketWaitHours_; } void queryGuildInfo(uint32_t guildId); void createGuild(const std::string& guildName); void addGuildRank(const std::string& rankName); @@ -3021,6 +3028,12 @@ private: // ---- Quest completion callback ---- QuestCompleteCallback questCompleteCallback_; + + // ---- GM Ticket state (SMSG_GMTICKET_GETTICKET / SMSG_GMTICKET_SYSTEMSTATUS) ---- + bool gmTicketActive_ = false; ///< True when an open ticket exists on the server + std::string gmTicketText_; ///< Text of the open ticket (from SMSG_GMTICKET_GETTICKET) + float gmTicketWaitHours_ = 0.0f; ///< Server-estimated wait time in hours + bool gmSupportAvailable_ = true; ///< GM support system online (SMSG_GMTICKET_SYSTEMSTATUS) }; } // namespace game diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 32428fcc..5ecbd924 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -438,7 +438,8 @@ private: void renderEquipSetWindow(game::GameHandler& gameHandler); // GM Ticket window - bool showGmTicketWindow_ = false; + bool showGmTicketWindow_ = false; + bool gmTicketWindowWasOpen_ = false; ///< Previous frame state; used to fire one-shot query char gmTicketBuf_[2048] = {}; void renderGmTicketWindow(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index fd0f4b56..2534a85a 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -5761,10 +5761,70 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; } - case Opcode::SMSG_GMTICKET_GETTICKET: - case Opcode::SMSG_GMTICKET_SYSTEMSTATUS: + case Opcode::SMSG_GMTICKET_GETTICKET: { + // WotLK 3.3.5a format: + // uint8 status — 1=no ticket, 6=has open ticket, 3=closed, 10=suspended + // If status == 6 (GMTICKET_STATUS_HASTEXT): + // cstring ticketText + // uint32 ticketAge (seconds old) + // uint32 daysUntilOld (days remaining before escalation) + // float waitTimeHours (estimated GM wait time) + if (packet.getSize() - packet.getReadPos() < 1) { packet.setReadPos(packet.getSize()); break; } + uint8_t gmStatus = packet.readUInt8(); + // Status 6 = GMTICKET_STATUS_HASTEXT — open ticket with text + if (gmStatus == 6 && packet.getSize() - packet.getReadPos() >= 1) { + gmTicketText_ = packet.readString(); + uint32_t ageSec = (packet.getSize() - packet.getReadPos() >= 4) ? packet.readUInt32() : 0; + /*uint32_t daysLeft =*/ (packet.getSize() - packet.getReadPos() >= 4) ? packet.readUInt32() : 0; + gmTicketWaitHours_ = (packet.getSize() - packet.getReadPos() >= 4) + ? packet.readFloat() : 0.0f; + gmTicketActive_ = true; + char buf[256]; + if (ageSec < 60) { + std::snprintf(buf, sizeof(buf), + "You have an open GM ticket (submitted %us ago). Estimated wait: %.1f hours.", + ageSec, gmTicketWaitHours_); + } else { + uint32_t ageMin = ageSec / 60; + std::snprintf(buf, sizeof(buf), + "You have an open GM ticket (submitted %um ago). Estimated wait: %.1f hours.", + ageMin, gmTicketWaitHours_); + } + addSystemChatMessage(buf); + LOG_INFO("SMSG_GMTICKET_GETTICKET: open ticket age=", ageSec, + "s wait=", gmTicketWaitHours_, "h"); + } else if (gmStatus == 3) { + gmTicketActive_ = false; + gmTicketText_.clear(); + addSystemChatMessage("Your GM ticket has been closed."); + LOG_INFO("SMSG_GMTICKET_GETTICKET: ticket closed"); + } else if (gmStatus == 10) { + gmTicketActive_ = false; + gmTicketText_.clear(); + addSystemChatMessage("Your GM ticket has been suspended."); + LOG_INFO("SMSG_GMTICKET_GETTICKET: ticket suspended"); + } else { + // Status 1 = no open ticket (default/no ticket) + gmTicketActive_ = false; + gmTicketText_.clear(); + LOG_DEBUG("SMSG_GMTICKET_GETTICKET: no open ticket (status=", (int)gmStatus, ")"); + } packet.setReadPos(packet.getSize()); break; + } + case Opcode::SMSG_GMTICKET_SYSTEMSTATUS: { + // uint32 status: 1 = GM support available, 0 = offline/unavailable + if (packet.getSize() - packet.getReadPos() >= 4) { + uint32_t sysStatus = packet.readUInt32(); + gmSupportAvailable_ = (sysStatus != 0); + addSystemChatMessage(gmSupportAvailable_ + ? "GM support is currently available." + : "GM support is currently unavailable."); + LOG_INFO("SMSG_GMTICKET_SYSTEMSTATUS: available=", gmSupportAvailable_); + } + packet.setReadPos(packet.getSize()); + break; + } // ---- DK rune tracking ---- case Opcode::SMSG_CONVERT_RUNE: { @@ -5975,7 +6035,33 @@ void GameHandler::handlePacket(network::Packet& packet) { packet.setReadPos(packet.getSize()); break; } - case Opcode::SMSG_SPELLINSTAKILLLOG: + case Opcode::SMSG_SPELLINSTAKILLLOG: { + // Sent when a unit is killed by a spell with SPELL_ATTR_EX2_INSTAKILL (e.g. Execute, Obliterate, etc.) + // WotLK: packed_guid caster + packed_guid victim + uint32 spellId + // TBC/Classic: full uint64 caster + full uint64 victim + uint32 spellId + const bool ikTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + auto ik_rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + if (ik_rem() < (ikTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + uint64_t ikCaster = ikTbcLike + ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); + if (ik_rem() < (ikTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + uint64_t ikVictim = ikTbcLike + ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); + uint32_t ikSpell = (ik_rem() >= 4) ? packet.readUInt32() : 0; + // Show kill/death feedback for the local player + if (ikCaster == playerGuid) { + // We killed a target instantly — show a KILL combat text hit + addCombatText(CombatTextEntry::MELEE_DAMAGE, 0, ikSpell, true); + } else if (ikVictim == playerGuid) { + // We were instantly killed — show a large incoming hit + addCombatText(CombatTextEntry::MELEE_DAMAGE, 0, ikSpell, false); + addSystemChatMessage("You were killed by an instant-kill effect."); + } + LOG_DEBUG("SMSG_SPELLINSTAKILLLOG: caster=0x", std::hex, ikCaster, + " victim=0x", ikVictim, std::dec, " spell=", ikSpell); + packet.setReadPos(packet.getSize()); + break; + } case Opcode::SMSG_SPELLLOGEXECUTE: case Opcode::SMSG_SPELL_CHANCE_RESIST_PUSHBACK: case Opcode::SMSG_SPELL_UPDATE_CHAIN_TARGETS: @@ -16153,9 +16239,19 @@ void GameHandler::deleteGmTicket() { if (state != WorldState::IN_WORLD || !socket) return; network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_DELETETICKET)); socket->send(pkt); + gmTicketActive_ = false; + gmTicketText_.clear(); LOG_INFO("Deleting GM ticket"); } +void GameHandler::requestGmTicket() { + if (state != WorldState::IN_WORLD || !socket) return; + // CMSG_GMTICKET_GETTICKET has no payload — server responds with SMSG_GMTICKET_GETTICKET + network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_GETTICKET)); + socket->send(pkt); + LOG_DEBUG("Sent CMSG_GMTICKET_GETTICKET — querying open ticket status"); +} + void GameHandler::queryGuildInfo(uint32_t guildId) { if (state != WorldState::IN_WORLD || !socket) return; auto packet = GuildQueryPacket::build(guildId); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index e5526b09..e90699a3 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -20162,9 +20162,15 @@ void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) { // ─── GM Ticket Window ───────────────────────────────────────────────────────── void GameScreen::renderGmTicketWindow(game::GameHandler& gameHandler) { + // Fire a one-shot query when the window first becomes visible + if (showGmTicketWindow_ && !gmTicketWindowWasOpen_) { + gameHandler.requestGmTicket(); + } + gmTicketWindowWasOpen_ = showGmTicketWindow_; + if (!showGmTicketWindow_) return; - ImGui::SetNextWindowSize(ImVec2(400, 260), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(440, 320), ImGuiCond_FirstUseEver); ImGui::SetNextWindowPos(ImVec2(300, 200), ImGuiCond_FirstUseEver); if (!ImGui::Begin("GM Ticket", &showGmTicketWindow_, @@ -20173,10 +20179,33 @@ void GameScreen::renderGmTicketWindow(game::GameHandler& gameHandler) { return; } + // Show GM support availability + if (!gameHandler.isGmSupportAvailable()) { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "GM support is currently unavailable."); + ImGui::Spacing(); + } + + // Show existing open ticket if any + if (gameHandler.hasActiveGmTicket()) { + ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "You have an open GM ticket."); + const std::string& existingText = gameHandler.getGmTicketText(); + if (!existingText.empty()) { + ImGui::TextWrapped("Current ticket: %s", existingText.c_str()); + } + float waitHours = gameHandler.getGmTicketWaitHours(); + if (waitHours > 0.0f) { + char waitBuf[64]; + std::snprintf(waitBuf, sizeof(waitBuf), "Estimated wait: %.1f hours", waitHours); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.4f, 1.0f), "%s", waitBuf); + } + ImGui::Separator(); + ImGui::Spacing(); + } + ImGui::TextWrapped("Describe your issue and a Game Master will contact you."); ImGui::Spacing(); ImGui::InputTextMultiline("##gmticket_body", gmTicketBuf_, sizeof(gmTicketBuf_), - ImVec2(-1, 160)); + ImVec2(-1, 120)); ImGui::Spacing(); bool hasText = (gmTicketBuf_[0] != '\0'); @@ -20193,8 +20222,11 @@ void GameScreen::renderGmTicketWindow(game::GameHandler& gameHandler) { showGmTicketWindow_ = false; } ImGui::SameLine(); - if (ImGui::Button("Delete Ticket", ImVec2(100, 0))) { - gameHandler.deleteGmTicket(); + if (gameHandler.hasActiveGmTicket()) { + if (ImGui::Button("Delete Ticket", ImVec2(110, 0))) { + gameHandler.deleteGmTicket(); + showGmTicketWindow_ = false; + } } ImGui::End();