From 49ba89dfc3c570adb8d2379f15efc4967c9c3d4d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 17 Mar 2026 21:13:27 -0700 Subject: [PATCH] feat: handle SMSG_PET_UNLEARN_CONFIRM with pet talent respec dialog Parses the pet talent wipe confirm packet (petGuid + cost), shows a confirmation dialog matching the player talent reset UX, and sends CMSG_PET_UNLEARN_TALENTS on confirmation. Completes the pet talent respec flow for Hunters/Warlocks on WotLK servers. --- include/game/game_handler.hpp | 9 +++++ include/ui/game_screen.hpp | 1 + src/game/game_handler.cpp | 25 ++++++++++++- src/ui/game_screen.cpp | 69 +++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index f67477d4..ff253b96 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1176,6 +1176,11 @@ public: uint32_t getTalentWipeCost() const { return talentWipeCost_; } void confirmTalentWipe(); void cancelTalentWipe() { talentWipePending_ = false; } + // Pet talent respec confirm + bool showPetUnlearnDialog() const { return petUnlearnPending_; } + uint32_t getPetUnlearnCost() const { return petUnlearnCost_; } + void confirmPetUnlearn(); + void cancelPetUnlearn() { petUnlearnPending_ = false; } /** True when ghost is within 40 yards of corpse position (same map). */ bool canReclaimCorpse() const; /** Distance (yards) from ghost to corpse, or -1 if no corpse data. */ @@ -3305,6 +3310,10 @@ private: bool talentWipePending_ = false; uint64_t talentWipeNpcGuid_ = 0; uint32_t talentWipeCost_ = 0; + // ---- Pet talent respec confirm dialog ---- + bool petUnlearnPending_ = false; + uint64_t petUnlearnGuid_ = 0; + uint32_t petUnlearnCost_ = 0; bool resurrectIsSpiritHealer_ = false; // true = SMSG_SPIRIT_HEALER_CONFIRM, false = SMSG_RESURRECT_REQUEST uint64_t resurrectCasterGuid_ = 0; std::string resurrectCasterName_; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 52bcca78..603aba7d 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -358,6 +358,7 @@ private: void renderReclaimCorpseButton(game::GameHandler& gameHandler); void renderResurrectDialog(game::GameHandler& gameHandler); void renderTalentWipeConfirmDialog(game::GameHandler& gameHandler); + void renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler); void renderEscapeMenu(); void renderSettingsWindow(); void applyGraphicsPreset(GraphicsPreset preset); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 4b052724..40c861d3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7660,7 +7660,16 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_PET_GUIDS: case Opcode::SMSG_PET_DISMISS_SOUND: case Opcode::SMSG_PET_ACTION_SOUND: - case Opcode::SMSG_PET_UNLEARN_CONFIRM: + case Opcode::SMSG_PET_UNLEARN_CONFIRM: { + // uint64 petGuid + uint32 cost (copper) + if (packet.getSize() - packet.getReadPos() >= 12) { + petUnlearnGuid_ = packet.readUInt64(); + petUnlearnCost_ = packet.readUInt32(); + petUnlearnPending_ = true; + } + packet.setReadPos(packet.getSize()); + break; + } case Opcode::SMSG_PET_UPDATE_COMBO_POINTS: packet.setReadPos(packet.getSize()); break; @@ -18867,6 +18876,20 @@ void GameHandler::switchTalentSpec(uint8_t newSpec) { addSystemChatMessage(msg); } +void GameHandler::confirmPetUnlearn() { + if (!petUnlearnPending_) return; + petUnlearnPending_ = false; + if (state != WorldState::IN_WORLD || !socket) return; + + // Respond with CMSG_PET_UNLEARN_TALENTS (no payload in 3.3.5a) + network::Packet pkt(wireOpcode(Opcode::CMSG_PET_UNLEARN_TALENTS)); + socket->send(pkt); + LOG_INFO("confirmPetUnlearn: sent CMSG_PET_UNLEARN_TALENTS"); + addSystemChatMessage("Pet talent reset confirmed."); + petUnlearnGuid_ = 0; + petUnlearnCost_ = 0; +} + void GameHandler::confirmTalentWipe() { if (!talentWipePending_) return; talentWipePending_ = false; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 40fab36e..5eda576d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -741,6 +741,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderReclaimCorpseButton(gameHandler); renderResurrectDialog(gameHandler); renderTalentWipeConfirmDialog(gameHandler); + renderPetUnlearnConfirmDialog(gameHandler); renderChatBubbles(gameHandler); renderEscapeMenu(); renderSettingsWindow(); @@ -15568,6 +15569,74 @@ void GameScreen::renderTalentWipeConfirmDialog(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } +void GameScreen::renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler) { + if (!gameHandler.showPetUnlearnDialog()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + float dlgW = 340.0f; + float dlgH = 130.0f; + ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.3f), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.15f, 0.95f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.8f, 0.7f, 0.2f, 1.0f)); + + if (ImGui::Begin("##PetUnlearnDialog", nullptr, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) { + + ImGui::Spacing(); + uint32_t cost = gameHandler.getPetUnlearnCost(); + uint32_t gold = cost / 10000; + uint32_t silver = (cost % 10000) / 100; + uint32_t copper = cost % 100; + char costStr[64]; + if (gold > 0) + std::snprintf(costStr, sizeof(costStr), "%ug %us %uc", gold, silver, copper); + else if (silver > 0) + std::snprintf(costStr, sizeof(costStr), "%us %uc", silver, copper); + else + std::snprintf(costStr, sizeof(costStr), "%uc", copper); + + std::string text = std::string("Reset your pet's talents for ") + costStr + "?"; + float textW = ImGui::CalcTextSize(text.c_str()).x; + ImGui::SetCursorPosX(std::max(4.0f, (dlgW - textW) / 2)); + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.4f, 1.0f), "%s", text.c_str()); + + ImGui::Spacing(); + ImGui::SetCursorPosX(8.0f); + ImGui::TextDisabled("All pet talent points will be refunded."); + ImGui::Spacing(); + + float btnW = 110.0f; + float spacing = 20.0f; + ImGui::SetCursorPosX((dlgW - btnW * 2 - spacing) / 2); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.3f, 1.0f)); + if (ImGui::Button("Confirm##petunlearn", ImVec2(btnW, 30))) { + gameHandler.confirmPetUnlearn(); + } + ImGui::PopStyleColor(2); + + ImGui::SameLine(0, spacing); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f)); + if (ImGui::Button("Cancel##petunlearn", ImVec2(btnW, 30))) { + gameHandler.cancelPetUnlearn(); + } + ImGui::PopStyleColor(2); + } + ImGui::End(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); +} + // ============================================================ // Settings Window // ============================================================