diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 4c6f325b..ce836072 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -409,6 +409,10 @@ public: void setGuildOfficerNote(const std::string& name, const std::string& note); void acceptGuildInvite(); void declineGuildInvite(); + + // GM Ticket + void submitGmTicket(const std::string& text); + void deleteGmTicket(); void queryGuildInfo(uint32_t guildId); void createGuild(const std::string& guildName); void addGuildRank(const std::string& rankName); diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 7c17820d..1fc31818 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -359,6 +359,11 @@ private: bool showAchievementWindow_ = false; char achievementSearchBuf_[128] = {}; void renderAchievementWindow(game::GameHandler& gameHandler); + + // GM Ticket window + bool showGmTicketWindow_ = false; + char gmTicketBuf_[2048] = {}; + void renderGmTicketWindow(game::GameHandler& gameHandler); uint8_t lfgRoles_ = 0x08; // default: DPS (0x02=tank, 0x04=healer, 0x08=dps) uint32_t lfgSelectedDungeon_ = 861; // default: random dungeon (entry 861 = Random Dungeon WotLK) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 8157d44c..e792f459 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -14968,6 +14968,34 @@ void GameHandler::declineGuildInvite() { LOG_INFO("Declined guild invite"); } +void GameHandler::submitGmTicket(const std::string& text) { + if (state != WorldState::IN_WORLD || !socket) return; + + // CMSG_GMTICKET_CREATE (WotLK 3.3.5a): + // string ticket_text + // float[3] position (server coords) + // float facing + // uint32 mapId + // uint8 need_response (1 = yes) + network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_CREATE)); + pkt.writeString(text); + pkt.writeFloat(movementInfo.x); + pkt.writeFloat(movementInfo.y); + pkt.writeFloat(movementInfo.z); + pkt.writeFloat(movementInfo.orientation); + pkt.writeUInt32(currentMapId_); + pkt.writeUInt8(1); // need_response = yes + socket->send(pkt); + LOG_INFO("Submitted GM ticket: '", text, "'"); +} + +void GameHandler::deleteGmTicket() { + if (state != WorldState::IN_WORLD || !socket) return; + network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_DELETETICKET)); + socket->send(pkt); + LOG_INFO("Deleting GM ticket"); +} + 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 6732ced8..4a77261d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -498,6 +498,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderDungeonFinderWindow(gameHandler); renderInstanceLockouts(gameHandler); renderAchievementWindow(gameHandler); + renderGmTicketWindow(gameHandler); // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now if (showMinimap_) { renderMinimapMarkers(gameHandler); @@ -3161,6 +3162,13 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /ticket command — open GM ticket window + if (cmdLower == "ticket" || cmdLower == "gmticket" || cmdLower == "gm") { + showGmTicketWindow_ = true; + chatInputBuffer[0] = '\0'; + return; + } + // /help command — list available slash commands if (cmdLower == "help" || cmdLower == "?") { static const char* kHelpLines[] = { @@ -3178,7 +3186,7 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { "Movement: /sit /stand /kneel /dismount", "Misc: /played /time /afk [msg] /dnd [msg] /inspect", " /helm /cloak /trade /join /leave ", - " /unstuck /logout /help", + " /unstuck /logout /ticket /help", }; for (const char* line : kHelpLines) { game::MessageChatData helpMsg; @@ -9750,7 +9758,7 @@ void GameScreen::renderEscapeMenu() { ImGuiIO& io = ImGui::GetIO(); float screenW = io.DisplaySize.x; float screenH = io.DisplaySize.y; - ImVec2 size(260.0f, 220.0f); + ImVec2 size(260.0f, 248.0f); ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f); ImGui::SetNextWindowPos(pos, ImGuiCond_Always); @@ -9786,6 +9794,10 @@ void GameScreen::renderEscapeMenu() { showInstanceLockouts_ = true; showEscapeMenu = false; } + if (ImGui::Button("Help / GM Ticket", ImVec2(-1, 0))) { + showGmTicketWindow_ = true; + showEscapeMenu = false; + } ImGui::Spacing(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f)); @@ -14432,4 +14444,44 @@ void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) { ImGui::End(); } +// ─── GM Ticket Window ───────────────────────────────────────────────────────── +void GameScreen::renderGmTicketWindow(game::GameHandler& gameHandler) { + if (!showGmTicketWindow_) return; + + ImGui::SetNextWindowSize(ImVec2(400, 260), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(300, 200), ImGuiCond_FirstUseEver); + + if (!ImGui::Begin("GM Ticket", &showGmTicketWindow_, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::End(); + return; + } + + ImGui::TextWrapped("Describe your issue and a Game Master will contact you."); + ImGui::Spacing(); + ImGui::InputTextMultiline("##gmticket_body", gmTicketBuf_, sizeof(gmTicketBuf_), + ImVec2(-1, 160)); + ImGui::Spacing(); + + bool hasText = (gmTicketBuf_[0] != '\0'); + if (!hasText) ImGui::BeginDisabled(); + if (ImGui::Button("Submit Ticket", ImVec2(160, 0))) { + gameHandler.submitGmTicket(gmTicketBuf_); + gmTicketBuf_[0] = '\0'; + showGmTicketWindow_ = false; + } + if (!hasText) ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(80, 0))) { + showGmTicketWindow_ = false; + } + ImGui::SameLine(); + if (ImGui::Button("Delete Ticket", ImVec2(100, 0))) { + gameHandler.deleteGmTicket(); + } + + ImGui::End(); +} + }} // namespace wowee::ui