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.
This commit is contained in:
Kelsi 2026-03-10 12:53:05 -07:00
parent 9291637977
commit ea291179dd
4 changed files with 130 additions and 6 deletions

View file

@ -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_;

View file

@ -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);

View file

@ -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
// ============================================================

View file

@ -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<float>(window->getWidth()) : 1280.0f;
float screenH = window ? static_cast<float>(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
// ============================================================