From ea291179ddd3a88dc74548c8b0868a1b0cae5148 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 12:53:05 -0700 Subject: [PATCH] gameplay: fix talent reset and ignore list population on login - SMSG_IGNORE_LIST was silently consumed; now parses guid+name pairs to populate ignoreCache so /unignore works correctly for pre-existing ignores loaded at login. - MSG_TALENT_WIPE_CONFIRM was discarded without responding; now parses the NPC GUID and cost, shows a confirm dialog, and sends the required response packet when the player confirms. Without this, talent reset via Talent Master NPC was completely broken. --- include/game/game_handler.hpp | 8 ++++ include/ui/game_screen.hpp | 1 + src/game/game_handler.cpp | 52 +++++++++++++++++++++--- src/ui/game_screen.cpp | 75 +++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 6 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 2607f216..c333676a 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -835,6 +835,10 @@ public: bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; } bool showResurrectDialog() const { return resurrectRequestPending_; } const std::string& getResurrectCasterName() const { return resurrectCasterName_; } + bool showTalentWipeConfirmDialog() const { return talentWipePending_; } + uint32_t getTalentWipeCost() const { return talentWipeCost_; } + void confirmTalentWipe(); + void cancelTalentWipe() { talentWipePending_ = false; } /** True when ghost is within 40 yards of corpse position (same map). */ bool canReclaimCorpse() const; /** Send CMSG_RECLAIM_CORPSE; noop if not a ghost or not near corpse. */ @@ -2326,6 +2330,10 @@ private: uint64_t pendingSpiritHealerGuid_ = 0; bool resurrectPending_ = false; bool resurrectRequestPending_ = false; + // ---- Talent wipe confirm dialog ---- + bool talentWipePending_ = false; + uint64_t talentWipeNpcGuid_ = 0; + uint32_t talentWipeCost_ = 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 15e098e7..d2b303c8 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -232,6 +232,7 @@ private: void renderDeathScreen(game::GameHandler& gameHandler); void renderReclaimCorpseButton(game::GameHandler& gameHandler); void renderResurrectDialog(game::GameHandler& gameHandler); + void renderTalentWipeConfirmDialog(game::GameHandler& gameHandler); void renderEscapeMenu(); void renderSettingsWindow(); void renderQuestMarkers(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ef41b16e..5751f602 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1530,10 +1530,22 @@ void GameHandler::handlePacket(network::Packet& packet) { // Classic 1.12 and TBC friend list (WotLK uses SMSG_CONTACT_LIST instead) handleFriendList(packet); break; - case Opcode::SMSG_IGNORE_LIST: - // Ignore list: consume to avoid spurious warnings; not parsed. - packet.setReadPos(packet.getSize()); + case Opcode::SMSG_IGNORE_LIST: { + // uint8 count + count × (uint64 guid + string name) + // Populate ignoreCache so /unignore works for pre-existing ignores. + if (packet.getSize() - packet.getReadPos() < 1) break; + uint8_t ignCount = packet.readUInt8(); + for (uint8_t i = 0; i < ignCount; ++i) { + if (packet.getSize() - packet.getReadPos() < 8) break; + uint64_t ignGuid = packet.readUInt64(); + std::string ignName = packet.readString(); + if (!ignName.empty() && ignGuid != 0) { + ignoreCache[ignName] = ignGuid; + } + } + LOG_DEBUG("SMSG_IGNORE_LIST: loaded ", (int)ignCount, " ignored players"); break; + } case Opcode::MSG_RANDOM_ROLL: if (state == WorldState::IN_WORLD) { @@ -4452,10 +4464,20 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::MSG_INSPECT_ARENA_TEAMS: LOG_INFO("Received MSG_INSPECT_ARENA_TEAMS"); break; - case Opcode::MSG_TALENT_WIPE_CONFIRM: - // Talent reset confirmation payload is not needed client-side right now. - packet.setReadPos(packet.getSize()); + case Opcode::MSG_TALENT_WIPE_CONFIRM: { + // Server sends: uint64 npcGuid + uint32 cost + // Client must respond with the same opcode containing uint64 npcGuid to confirm. + if (packet.getSize() - packet.getReadPos() < 12) { + packet.setReadPos(packet.getSize()); + break; + } + talentWipeNpcGuid_ = packet.readUInt64(); + talentWipeCost_ = packet.readUInt32(); + talentWipePending_ = true; + LOG_INFO("MSG_TALENT_WIPE_CONFIRM: npc=0x", std::hex, talentWipeNpcGuid_, + std::dec, " cost=", talentWipeCost_); break; + } // ---- MSG_MOVE_* opcodes (server relays other players' movement) ---- case Opcode::MSG_MOVE_START_FORWARD: @@ -13568,6 +13590,24 @@ void GameHandler::switchTalentSpec(uint8_t newSpec) { addSystemChatMessage(msg); } +void GameHandler::confirmTalentWipe() { + if (!talentWipePending_) return; + talentWipePending_ = false; + + if (state != WorldState::IN_WORLD || !socket) return; + + // Respond to MSG_TALENT_WIPE_CONFIRM with the trainer GUID to trigger the reset. + // Packet: opcode(2) + uint64 npcGuid = 10 bytes. + network::Packet pkt(wireOpcode(Opcode::MSG_TALENT_WIPE_CONFIRM)); + pkt.writeUInt64(talentWipeNpcGuid_); + socket->send(pkt); + + LOG_INFO("confirmTalentWipe: sent confirm for npc=0x", std::hex, talentWipeNpcGuid_, std::dec); + addSystemChatMessage("Talent reset confirmed. The server will update your talents."); + talentWipeNpcGuid_ = 0; + talentWipeCost_ = 0; +} + // ============================================================ // Phase 4: Group/Party // ============================================================ diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 14d78caf..c9c897fc 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -436,6 +436,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderDeathScreen(gameHandler); renderReclaimCorpseButton(gameHandler); renderResurrectDialog(gameHandler); + renderTalentWipeConfirmDialog(gameHandler); renderChatBubbles(gameHandler); renderEscapeMenu(); renderSettingsWindow(); @@ -7525,6 +7526,80 @@ void GameScreen::renderResurrectDialog(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } +// ============================================================ +// Talent Wipe Confirm Dialog +// ============================================================ + +void GameScreen::renderTalentWipeConfirmDialog(game::GameHandler& gameHandler) { + if (!gameHandler.showTalentWipeConfirmDialog()) 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("##TalentWipeDialog", nullptr, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) { + + ImGui::Spacing(); + uint32_t cost = gameHandler.getTalentWipeCost(); + 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 = "Reset your talents for "; + text += costStr; + text += "?"; + 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 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", ImVec2(btnW, 30))) { + gameHandler.confirmTalentWipe(); + } + 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", ImVec2(btnW, 30))) { + gameHandler.cancelTalentWipe(); + } + ImGui::PopStyleColor(2); + } + ImGui::End(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); +} + // ============================================================ // Settings Window // ============================================================