From 7cd7ac43a943dd4ac3d6c00bcc26b3434934e65f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Feb 2026 21:47:14 -0800 Subject: [PATCH] Refine resurrection flow --- include/game/entity.hpp | 5 ++ include/game/game_handler.hpp | 4 ++ include/game/opcodes.hpp | 5 +- include/game/world_packets.hpp | 6 ++ src/game/game_handler.cpp | 109 ++++++++++++++++++++++++++++----- src/game/world_packets.cpp | 9 ++- src/ui/game_screen.cpp | 2 - 7 files changed, 120 insertions(+), 20 deletions(-) diff --git a/include/game/entity.hpp b/include/game/entity.hpp index 6138a2c9..93170ae1 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -197,6 +197,10 @@ public: uint32_t getUnitFlags() const { return unitFlags; } void setUnitFlags(uint32_t f) { unitFlags = f; } + // Dynamic flags (UNIT_DYNAMIC_FLAGS, index 147) + uint32_t getDynamicFlags() const { return dynamicFlags; } + void setDynamicFlags(uint32_t f) { dynamicFlags = f; } + // NPC flags (UNIT_NPC_FLAGS, index 82) uint32_t getNpcFlags() const { return npcFlags; } void setNpcFlags(uint32_t f) { npcFlags = f; } @@ -222,6 +226,7 @@ protected: uint32_t displayId = 0; uint32_t mountDisplayId = 0; uint32_t unitFlags = 0; + uint32_t dynamicFlags = 0; uint32_t npcFlags = 0; uint32_t factionTemplate = 0; bool hostile = false; diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 0d790d8d..5a8950aa 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -912,6 +912,10 @@ private: float preMountRunSpeed_ = 0.0f; float serverRunSpeed_ = 7.0f; bool playerDead_ = false; + uint64_t pendingSpiritHealerGuid_ = 0; + bool resurrectPending_ = false; + bool repopPending_ = false; + uint64_t lastRepopRequestMs_ = 0; }; } // namespace game diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 56cac724..bc91d98c 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -242,7 +242,10 @@ enum class Opcode : uint16_t { // ---- Death/Respawn ---- CMSG_REPOP_REQUEST = 0x015A, CMSG_SPIRIT_HEALER_ACTIVATE = 0x0176, - SMSG_SPIRIT_HEALER_CONFIRM = 0x0222, + SMSG_RESURRECT_REQUEST = 0x0222, + CMSG_RESURRECT_RESPONSE = 0x0223, + SMSG_RESURRECT_RESULT = 0x029D, + SMSG_RESURRECT_CANCEL = 0x0390, // ---- Teleport / Transfer ---- MSG_MOVE_TELEPORT_ACK = 0x0C7, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index ff8d9ddd..c58f83c3 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1727,5 +1727,11 @@ public: static network::Packet build(uint64_t npcGuid); }; +/** CMSG_RESURRECT_RESPONSE packet builder */ +class ResurrectResponsePacket { +public: + static network::Packet build(uint64_t casterGuid, bool accept); +}; + } // namespace game } // namespace wowee diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index e3ac5247..ee086cc3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -507,18 +507,60 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_GOSSIP_COMPLETE: handleGossipComplete(packet); break; - case Opcode::SMSG_SPIRIT_HEALER_CONFIRM: { + case Opcode::SMSG_RESURRECT_REQUEST: { if (packet.getSize() - packet.getReadPos() < 8) { - LOG_WARNING("SMSG_SPIRIT_HEALER_CONFIRM too short"); + LOG_WARNING("SMSG_RESURRECT_REQUEST too short"); break; } - uint64_t healerGuid = packet.readUInt64(); - LOG_INFO("Spirit healer confirm from 0x", std::hex, healerGuid, std::dec); - if (playerDead_ && socket && state == WorldState::IN_WORLD) { - auto activate = SpiritHealerActivatePacket::build(healerGuid); - socket->send(activate); - LOG_INFO("Confirmed spirit healer activation"); + 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; + } + break; + } + case Opcode::SMSG_RESURRECT_CANCEL: { + if (packet.getSize() - packet.getReadPos() < 4) { + LOG_WARNING("SMSG_RESURRECT_CANCEL too short"); + break; + } + uint32_t reason = packet.readUInt32(); + LOG_INFO("Resurrect cancel reason: ", reason); + playerDead_ = true; + resurrectPending_ = false; break; } case Opcode::SMSG_LIST_INVENTORY: @@ -1179,6 +1221,7 @@ void GameHandler::sendMovement(Opcode opcode) { // Block movement during taxi flight if (onTaxiFlight_) return; + if (resurrectPending_) return; // Use real millisecond timestamp (server validates for anti-cheat) static auto startTime = std::chrono::steady_clock::now(); @@ -1354,6 +1397,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { // Extract health/mana/power from fields (Phase 2) — single pass if (block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) { auto unit = std::static_pointer_cast(entity); + constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008; for (const auto& [key, val] : block.fields) { switch (key) { case 24: @@ -1369,6 +1413,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { case 33: unit->setMaxPower(val); break; case 55: unit->setFactionTemplate(val); break; // UNIT_FIELD_FACTIONTEMPLATE case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS + case 147: unit->setDynamicFlags(val); break; // UNIT_DYNAMIC_FLAGS case 54: unit->setLevel(val); break; case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID case 69: // UNIT_FIELD_MOUNTDISPLAYID @@ -1393,6 +1438,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { default: break; } } + if (block.guid == playerGuid && + (unit->getDynamicFlags() & UNIT_DYNFLAG_DEAD) != 0) { + playerDead_ = true; + LOG_INFO("Player logged in dead (dynamic flags)"); + } // Determine hostility from faction template for online creatures if (unit->getFactionTemplate() != 0) { unit->setHostile(isHostileFaction(unit->getFactionTemplate())); @@ -1470,6 +1520,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { // Update cached health/mana/power values (Phase 2) — single pass if (entity->getType() == ObjectType::UNIT || entity->getType() == ObjectType::PLAYER) { auto unit = std::static_pointer_cast(entity); + constexpr uint32_t UNIT_DYNFLAG_DEAD = 0x0008; for (const auto& [key, val] : block.fields) { switch (key) { case 24: { @@ -1507,6 +1558,22 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { case 32: unit->setMaxHealth(val); break; case 33: unit->setMaxPower(val); break; case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS + case 147: { + uint32_t oldDyn = unit->getDynamicFlags(); + unit->setDynamicFlags(val); + if (block.guid == playerGuid) { + bool wasDead = (oldDyn & UNIT_DYNFLAG_DEAD) != 0; + bool nowDead = (val & UNIT_DYNFLAG_DEAD) != 0; + if (!wasDead && nowDead) { + playerDead_ = true; + LOG_INFO("Player died (dynamic flags)"); + } else if (wasDead && !nowDead) { + playerDead_ = false; + LOG_INFO("Player resurrected (dynamic flags)"); + } + } + break; + } case 54: unit->setLevel(val); break; case 55: // UNIT_FIELD_FACTIONTEMPLATE unit->setFactionTemplate(val); @@ -2672,23 +2739,33 @@ void GameHandler::stopCasting() { } void GameHandler::releaseSpirit() { - if (!playerDead_) return; if (socket && state == WorldState::IN_WORLD) { + auto now = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + if (repopPending_ && now - static_cast(lastRepopRequestMs_) < 1000) { + return; + } + playerDead_ = true; auto packet = RepopRequestPacket::build(); socket->send(packet); + repopPending_ = true; + lastRepopRequestMs_ = static_cast(now); LOG_INFO("Sent CMSG_REPOP_REQUEST (Release Spirit)"); } } void GameHandler::activateSpiritHealer(uint64_t npcGuid) { if (state != WorldState::IN_WORLD || !socket) return; - auto gossipPacket = GossipHelloPacket::build(npcGuid); - socket->send(gossipPacket); - auto questHelloPacket = QuestgiverHelloPacket::build(npcGuid); - socket->send(questHelloPacket); - auto packet = SpiritHealerActivatePacket::build(npcGuid); - socket->send(packet); - LOG_INFO("Sent spirit healer activation sequence to 0x", std::hex, npcGuid, std::dec); + 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); + } } void GameHandler::tabTarget(float playerX, float playerY, float playerZ) { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 8c402aa2..07e75ddf 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2589,7 +2589,7 @@ bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data network::Packet RepopRequestPacket::build() { network::Packet packet(static_cast(Opcode::CMSG_REPOP_REQUEST)); - packet.writeUInt8(0); // auto-release flag (0 = manual) + packet.writeUInt8(1); // request release (1 = manual) return packet; } @@ -2599,6 +2599,13 @@ network::Packet SpiritHealerActivatePacket::build(uint64_t npcGuid) { return packet; } +network::Packet ResurrectResponsePacket::build(uint64_t casterGuid, bool accept) { + network::Packet packet(static_cast(Opcode::CMSG_RESURRECT_RESPONSE)); + packet.writeUInt64(casterGuid); + packet.writeUInt8(accept ? 1 : 0); + return packet; +} + // ============================================================ // Taxi / Flight Paths // ============================================================ diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 4586ab43..05faaa19 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -441,8 +441,6 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - float controlsTopY = ImGui::GetCursorScreenPos().y; - // Lock toggle ImGui::Checkbox("Lock", &chatWindowLocked); ImGui::SameLine();