From 6d719f2c52e2f2952c3dff75ce6d3c0ba0fcdc34 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Feb 2026 23:12:24 -0800 Subject: [PATCH] Fix spirit healer resurrection with correct opcodes Corrected death/resurrection opcode values (SMSG_SPIRIT_HEALER_CONFIRM=0x222, CMSG_SPIRIT_HEALER_ACTIVATE=0x21C, SMSG_RESURRECT_REQUEST=0x15B, CMSG_RESURRECT_RESPONSE=0x15C) and added resurrect dialog UI. --- include/game/game_handler.hpp | 8 +++ include/game/opcodes.hpp | 8 +-- include/ui/game_screen.hpp | 1 + src/game/game_handler.cpp | 126 +++++++++++++++++++++------------- src/ui/game_screen.cpp | 69 ++++++++++++++++--- 5 files changed, 150 insertions(+), 62 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 5a8950aa..2f25f9f7 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -404,7 +404,12 @@ public: // Player death state bool isPlayerDead() const { return playerDead_; } + bool isPlayerGhost() const { return releasedSpirit_; } + bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; } + bool showResurrectDialog() const { return resurrectRequestPending_; } void releaseSpirit(); + void acceptResurrect(); + void declineResurrect(); // ---- Phase 4: Group ---- void inviteToGroup(const std::string& playerName); @@ -912,8 +917,11 @@ private: float preMountRunSpeed_ = 0.0f; float serverRunSpeed_ = 7.0f; bool playerDead_ = false; + bool releasedSpirit_ = false; uint64_t pendingSpiritHealerGuid_ = 0; bool resurrectPending_ = false; + bool resurrectRequestPending_ = false; + uint64_t resurrectCasterGuid_ = 0; bool repopPending_ = false; uint64_t lastRepopRequestMs_ = 0; }; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index bc91d98c..1914ff3b 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -241,10 +241,10 @@ enum class Opcode : uint16_t { // ---- Death/Respawn ---- CMSG_REPOP_REQUEST = 0x015A, - CMSG_SPIRIT_HEALER_ACTIVATE = 0x0176, - SMSG_RESURRECT_REQUEST = 0x0222, - CMSG_RESURRECT_RESPONSE = 0x0223, - SMSG_RESURRECT_RESULT = 0x029D, + SMSG_RESURRECT_REQUEST = 0x015B, + CMSG_RESURRECT_RESPONSE = 0x015C, + CMSG_SPIRIT_HEALER_ACTIVATE = 0x021C, + SMSG_SPIRIT_HEALER_CONFIRM = 0x0222, SMSG_RESURRECT_CANCEL = 0x0390, // ---- Teleport / Transfer ---- diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 9e53a719..6e6fdac2 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -147,6 +147,7 @@ private: void renderVendorWindow(game::GameHandler& gameHandler); void renderTaxiWindow(game::GameHandler& gameHandler); void renderDeathScreen(game::GameHandler& gameHandler); + void renderResurrectDialog(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 ee086cc3..fd3057c3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -507,6 +507,19 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_GOSSIP_COMPLETE: handleGossipComplete(packet); break; + case Opcode::SMSG_SPIRIT_HEALER_CONFIRM: { + if (packet.getSize() - packet.getReadPos() < 8) { + LOG_WARNING("SMSG_SPIRIT_HEALER_CONFIRM too short"); + break; + } + uint64_t npcGuid = packet.readUInt64(); + LOG_INFO("Spirit healer confirm from 0x", std::hex, npcGuid, std::dec); + if (npcGuid) { + resurrectCasterGuid_ = npcGuid; + resurrectRequestPending_ = true; + } + break; + } case Opcode::SMSG_RESURRECT_REQUEST: { if (packet.getSize() - packet.getReadPos() < 8) { LOG_WARNING("SMSG_RESURRECT_REQUEST too short"); @@ -514,41 +527,9 @@ void GameHandler::handlePacket(network::Packet& packet) { } uint64_t casterGuid = packet.readUInt64(); LOG_INFO("Resurrect request from 0x", std::hex, casterGuid, std::dec); - if (!playerDead_) { - playerDead_ = true; - LOG_INFO("Marked player dead due to resurrect request"); - } - if (socket && state == WorldState::IN_WORLD) { - uint64_t useGuid = casterGuid ? casterGuid : pendingSpiritHealerGuid_; - if (useGuid == 0) { - LOG_WARNING("Resurrect request received without a valid guid"); - break; - } - if (!playerDead_) { - LOG_WARNING("Resurrect request while playerDead_ is false; proceeding anyway"); - } - auto response = ResurrectResponsePacket::build(useGuid, true); - socket->send(response); - LOG_INFO("Sent resurrect response for 0x", std::hex, useGuid, std::dec); - resurrectPending_ = true; - pendingSpiritHealerGuid_ = 0; - } - break; - } - case Opcode::SMSG_RESURRECT_RESULT: { - if (packet.getSize() - packet.getReadPos() < 1) { - LOG_WARNING("SMSG_RESURRECT_RESULT too short"); - break; - } - uint8_t result = packet.readUInt8(); - LOG_INFO("Resurrect result: ", static_cast(result)); - if (result == 0) { - playerDead_ = false; - LOG_INFO("Player resurrected (result)"); - } - resurrectPending_ = false; - if (!playerDead_) { - repopPending_ = false; + if (casterGuid) { + resurrectCasterGuid_ = casterGuid; + resurrectRequestPending_ = true; } break; } @@ -559,8 +540,8 @@ void GameHandler::handlePacket(network::Packet& packet) { } uint32_t reason = packet.readUInt32(); LOG_INFO("Resurrect cancel reason: ", reason); - playerDead_ = true; resurrectPending_ = false; + resurrectRequestPending_ = false; break; } case Opcode::SMSG_LIST_INVENTORY: @@ -1080,6 +1061,7 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { castTimeRemaining = 0.0f; castTimeTotal = 0.0f; playerDead_ = false; + releasedSpirit_ = false; targetGuid = 0; focusGuid = 0; lastTargetGuid = 0; @@ -1443,6 +1425,17 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { playerDead_ = true; LOG_INFO("Player logged in dead (dynamic flags)"); } + // Detect ghost state on login via PLAYER_FLAGS (field 150) + if (block.guid == playerGuid) { + constexpr uint32_t PLAYER_FLAGS_IDX = 150; // UNIT_END(148) + 2 + constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010; + auto pfIt = block.fields.find(PLAYER_FLAGS_IDX); + if (pfIt != block.fields.end() && (pfIt->second & PLAYER_FLAGS_GHOST) != 0) { + releasedSpirit_ = true; + playerDead_ = true; + LOG_INFO("Player logged in as ghost (PLAYER_FLAGS)"); + } + } // Determine hostility from faction template for online creatures if (unit->getFactionTemplate() != 0) { unit->setHostile(isHostileFaction(unit->getFactionTemplate())); @@ -1534,6 +1527,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { // Player death if (block.guid == playerGuid) { playerDead_ = true; + releasedSpirit_ = false; stopAutoAttack(); LOG_INFO("Player died!"); } @@ -1542,10 +1536,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { npcDeathCallback_(block.guid); } } else if (oldHealth == 0 && val > 0) { - // Player resurrection + // Player resurrection or ghost form if (block.guid == playerGuid) { playerDead_ = false; - LOG_INFO("Player resurrected!"); + if (!releasedSpirit_) { + LOG_INFO("Player resurrected!"); + } else { + LOG_INFO("Player entered ghost form"); + } } // Respawn: health went from 0 to >0, reset animation if (entity->getType() == ObjectType::UNIT && npcRespawnCallback_) { @@ -1566,9 +1564,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0; if (!wasDead && nowDead) { playerDead_ = true; + releasedSpirit_ = false; LOG_INFO("Player died (dynamic flags)"); } else if (wasDead && !nowDead) { playerDead_ = false; + releasedSpirit_ = false; LOG_INFO("Player resurrected (dynamic flags)"); } } @@ -1637,6 +1637,21 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { playerMoneyCopper_ = val; LOG_INFO("Money updated via VALUES: ", val, " copper"); } + else if (key == 150) { // PLAYER_FLAGS (UNIT_END+2) + constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010; + bool wasGhost = releasedSpirit_; + bool nowGhost = (val & PLAYER_FLAGS_GHOST) != 0; + if (!wasGhost && nowGhost) { + releasedSpirit_ = true; + LOG_INFO("Player entered ghost form (PLAYER_FLAGS)"); + } else if (wasGhost && !nowGhost) { + releasedSpirit_ = false; + playerDead_ = false; + repopPending_ = false; + resurrectPending_ = false; + LOG_INFO("Player resurrected (PLAYER_FLAGS ghost cleared)"); + } + } } if (applyInventoryFields(block.fields)) slotsChanged = true; if (slotsChanged) rebuildOnlineInventory(); @@ -2745,9 +2760,9 @@ void GameHandler::releaseSpirit() { if (repopPending_ && now - static_cast(lastRepopRequestMs_) < 1000) { return; } - playerDead_ = true; auto packet = RepopRequestPacket::build(); socket->send(packet); + releasedSpirit_ = true; repopPending_ = true; lastRepopRequestMs_ = static_cast(now); LOG_INFO("Sent CMSG_REPOP_REQUEST (Release Spirit)"); @@ -2757,15 +2772,30 @@ void GameHandler::releaseSpirit() { void GameHandler::activateSpiritHealer(uint64_t npcGuid) { if (state != WorldState::IN_WORLD || !socket) return; pendingSpiritHealerGuid_ = npcGuid; - if (!gossipWindowOpen) { - auto gossipPacket = GossipHelloPacket::build(npcGuid); - socket->send(gossipPacket); - auto questHelloPacket = QuestgiverHelloPacket::build(npcGuid); - socket->send(questHelloPacket); - LOG_INFO("Requested spirit healer confirm from 0x", std::hex, npcGuid, std::dec); - } else { - LOG_INFO("Queued spirit healer confirm for 0x", std::hex, npcGuid, std::dec); - } + auto packet = SpiritHealerActivatePacket::build(npcGuid); + socket->send(packet); + resurrectPending_ = true; + LOG_INFO("Sent CMSG_SPIRIT_HEALER_ACTIVATE for 0x", std::hex, npcGuid, std::dec); +} + +void GameHandler::acceptResurrect() { + if (state != WorldState::IN_WORLD || !socket || !resurrectRequestPending_) return; + // Send spirit healer activate (correct response to SMSG_SPIRIT_HEALER_CONFIRM) + auto activate = SpiritHealerActivatePacket::build(resurrectCasterGuid_); + socket->send(activate); + LOG_INFO("Sent CMSG_SPIRIT_HEALER_ACTIVATE (0x21C) for 0x", + std::hex, resurrectCasterGuid_, std::dec); + resurrectRequestPending_ = false; + resurrectPending_ = true; +} + +void GameHandler::declineResurrect() { + if (state != WorldState::IN_WORLD || !socket || !resurrectRequestPending_) return; + auto resp = ResurrectResponsePacket::build(resurrectCasterGuid_, false); + socket->send(resp); + LOG_INFO("Sent CMSG_RESURRECT_RESPONSE (decline) for 0x", + std::hex, resurrectCasterGuid_, std::dec); + resurrectRequestPending_ = false; } void GameHandler::tabTarget(float playerX, float playerY, float playerZ) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 05faaa19..ff01aae2 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -104,6 +104,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderQuestMarkers(gameHandler); renderMinimapMarkers(gameHandler); renderDeathScreen(gameHandler); + renderResurrectDialog(gameHandler); renderEscapeMenu(); renderSettingsWindow(); @@ -739,7 +740,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { return (name.find("spirit healer") != std::string::npos) || (name.find("spirit guide") != std::string::npos); }; - bool allowSpiritInteract = gameHandler.isPlayerDead() && isSpiritNpc(); + bool allowSpiritInteract = (gameHandler.isPlayerDead() || gameHandler.isPlayerGhost()) && isSpiritNpc(); if (!unit->isHostile() && (unit->isInteractable() || allowSpiritInteract)) { gameHandler.interactWithNpc(target->getGuid()); } @@ -2936,19 +2937,13 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) { char label[256]; snprintf(label, sizeof(label), "%s %s", icon, opt.text.c_str()); if (ImGui::Selectable(label)) { - if (opt.icon == 4) { // Spirit guide - gameHandler.selectGossipOption(opt.id); - gameHandler.activateSpiritHealer(gossip.npcGuid); - gameHandler.closeGossip(); - } else { - gameHandler.selectGossipOption(opt.id); - } + gameHandler.selectGossipOption(opt.id); } ImGui::PopID(); } // Fallback: some spirit healers don't send gossip options. - if (gossip.options.empty() && gameHandler.isPlayerDead()) { + if (gossip.options.empty() && gameHandler.isPlayerGhost()) { bool isSpirit = false; if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) { auto unit = std::static_pointer_cast(npcEntity); @@ -3512,7 +3507,7 @@ void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) { // ============================================================ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { - if (!gameHandler.isPlayerDead()) return; + if (!gameHandler.showDeathDialog()) return; auto* window = core::Application::getInstance().getWindow(); float screenW = window ? static_cast(window->getWidth()) : 1280.0f; @@ -3567,6 +3562,60 @@ void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) { ImGui::PopStyleVar(); } +void GameScreen::renderResurrectDialog(game::GameHandler& gameHandler) { + if (!gameHandler.showResurrectDialog()) 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 = 300.0f; + float dlgH = 110.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.4f, 0.4f, 0.8f, 1.0f)); + + if (ImGui::Begin("##ResurrectDialog", nullptr, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) { + + ImGui::Spacing(); + const char* text = "Return to life?"; + float textW = ImGui::CalcTextSize(text).x; + ImGui::SetCursorPosX((dlgW - textW) / 2); + ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "%s", text); + + ImGui::Spacing(); + ImGui::Spacing(); + + float btnW = 100.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("Accept", ImVec2(btnW, 30))) { + gameHandler.acceptResurrect(); + } + 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("Decline", ImVec2(btnW, 30))) { + gameHandler.declineResurrect(); + } + ImGui::PopStyleColor(2); + } + ImGui::End(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); +} + // ============================================================ // Settings Window // ============================================================