From 8b98888dd24cd2ac6fc63cce0343df308c994aaf Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Feb 2026 11:45:35 -0800 Subject: [PATCH] Add quest opcodes, fix gossip select packet, and NPC combat animations Fix CMSG_GOSSIP_SELECT_OPTION missing menuId field (was causing ByteBufferException). Add 12 quest opcodes and clickable quest items in gossip dialog. NPC attack/death animation callbacks now work for both single-player and server-spawned creatures, and SMSG_ATTACKERSTATEUPDATE triggers NPC swing animations. --- include/game/game_handler.hpp | 1 + include/game/opcodes.hpp | 14 ++++++++++++++ include/game/world_packets.hpp | 14 +++++++++++++- src/core/application.cpp | 17 +++++++++++++---- src/game/game_handler.cpp | 17 ++++++++++++++++- src/game/world_packets.cpp | 19 ++++++++++++++++++- src/ui/game_screen.cpp | 10 +++++++++- 7 files changed, 84 insertions(+), 8 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 931adb8e..a1338b22 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -321,6 +321,7 @@ public: // NPC Gossip void interactWithNpc(uint64_t guid); void selectGossipOption(uint32_t optionId); + void selectGossipQuest(uint32_t questId); void closeGossip(); bool isGossipWindowOpen() const { return gossipWindowOpen; } const GossipMessageData& getCurrentGossip() const { return currentGossip; } diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 7cb52b82..0f1cd447 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -133,6 +133,20 @@ enum class Opcode : uint16_t { SMSG_GOSSIP_COMPLETE = 0x17E, SMSG_NPC_TEXT_UPDATE = 0x180, + // ---- Phase 5: Quests ---- + CMSG_QUESTGIVER_STATUS_QUERY = 0x182, + SMSG_QUESTGIVER_STATUS = 0x183, + CMSG_QUESTGIVER_HELLO = 0x184, + CMSG_QUESTGIVER_QUERY_QUEST = 0x186, + SMSG_QUESTGIVER_QUEST_DETAILS = 0x188, + CMSG_QUESTGIVER_ACCEPT_QUEST = 0x189, + CMSG_QUESTGIVER_COMPLETE_QUEST = 0x18A, + SMSG_QUESTGIVER_REQUEST_ITEMS = 0x18B, + CMSG_QUESTGIVER_REQUEST_REWARD = 0x18C, + SMSG_QUESTGIVER_OFFER_REWARD = 0x18D, + CMSG_QUESTGIVER_CHOOSE_REWARD = 0x18E, + SMSG_QUESTGIVER_QUEST_COMPLETE = 0x191, + // ---- Phase 5: Vendor ---- CMSG_LIST_INVENTORY = 0x19E, SMSG_LIST_INVENTORY = 0x19F, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 331d2b54..2460c920 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1161,7 +1161,7 @@ public: /** CMSG_GOSSIP_SELECT_OPTION packet builder */ class GossipSelectOptionPacket { public: - static network::Packet build(uint64_t npcGuid, uint32_t optionId, const std::string& code = ""); + static network::Packet build(uint64_t npcGuid, uint32_t menuId, uint32_t optionId, const std::string& code = ""); }; /** SMSG_GOSSIP_MESSAGE parser */ @@ -1170,6 +1170,18 @@ public: static bool parse(network::Packet& packet, GossipMessageData& data); }; +/** CMSG_QUESTGIVER_QUERY_QUEST packet builder */ +class QuestgiverQueryQuestPacket { +public: + static network::Packet build(uint64_t npcGuid, uint32_t questId); +}; + +/** CMSG_QUESTGIVER_ACCEPT_QUEST packet builder */ +class QuestgiverAcceptQuestPacket { +public: + static network::Packet build(uint64_t npcGuid, uint32_t questId); +}; + // ============================================================ // Phase 5: Vendor // ============================================================ diff --git a/src/core/application.cpp b/src/core/application.cpp index 75383494..1fa1de42 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1125,18 +1125,27 @@ void Application::spawnNpcs() { gameHandler->setPosition(canonical.x, canonical.y, canonical.z); } - // Set NPC death callback for single-player combat - if (singlePlayerMode && gameHandler && npcManager) { + // Set NPC animation callbacks (works for both single-player and online creatures) + if (gameHandler && npcManager) { auto* npcMgr = npcManager.get(); auto* cr = renderer->getCharacterRenderer(); - gameHandler->setNpcDeathCallback([npcMgr, cr](uint64_t guid) { + auto* app = this; + gameHandler->setNpcDeathCallback([npcMgr, cr, app](uint64_t guid) { uint32_t instanceId = npcMgr->findRenderInstanceId(guid); + if (instanceId == 0) { + auto it = app->creatureInstances_.find(guid); + if (it != app->creatureInstances_.end()) instanceId = it->second; + } if (instanceId != 0 && cr) { cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death } }); - gameHandler->setNpcSwingCallback([npcMgr, cr](uint64_t guid) { + gameHandler->setNpcSwingCallback([npcMgr, cr, app](uint64_t guid) { uint32_t instanceId = npcMgr->findRenderInstanceId(guid); + if (instanceId == 0) { + auto it = app->creatureInstances_.find(guid); + if (it != app->creatureInstances_.end()) instanceId = it->second; + } if (instanceId != 0 && cr) { cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1 } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 8af98f75..5b02ed22 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1090,6 +1090,11 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_INVENTORY_CHANGE_FAILURE: case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE: case Opcode::MSG_RAID_TARGET_UPDATE: + case Opcode::SMSG_QUESTGIVER_STATUS: + case Opcode::SMSG_QUESTGIVER_QUEST_DETAILS: + case Opcode::SMSG_QUESTGIVER_REQUEST_ITEMS: + case Opcode::SMSG_QUESTGIVER_OFFER_REWARD: + case Opcode::SMSG_QUESTGIVER_QUEST_COMPLETE: case Opcode::SMSG_GROUP_SET_LEADER: LOG_DEBUG("Ignoring known opcode: 0x", std::hex, opcode, std::dec); break; @@ -3071,6 +3076,9 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) { if (isPlayerAttacker && meleeSwingCallback_) { meleeSwingCallback_(); } + if (!isPlayerAttacker && npcSwingCallback_) { + npcSwingCallback_(data.attackerGuid); + } if (data.isMiss()) { addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker); @@ -3519,10 +3527,17 @@ void GameHandler::interactWithNpc(uint64_t guid) { void GameHandler::selectGossipOption(uint32_t optionId) { if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return; - auto packet = GossipSelectOptionPacket::build(currentGossip.npcGuid, optionId); + auto packet = GossipSelectOptionPacket::build(currentGossip.npcGuid, currentGossip.menuId, optionId); socket->send(packet); } +void GameHandler::selectGossipQuest(uint32_t questId) { + if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return; + auto packet = QuestgiverQueryQuestPacket::build(currentGossip.npcGuid, questId); + socket->send(packet); + gossipWindowOpen = false; +} + void GameHandler::closeGossip() { gossipWindowOpen = false; currentGossip = GossipMessageData{}; diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 66358591..67b2d647 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1808,9 +1808,10 @@ network::Packet GossipHelloPacket::build(uint64_t npcGuid) { return packet; } -network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t optionId, const std::string& code) { +network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t menuId, uint32_t optionId, const std::string& code) { network::Packet packet(static_cast(Opcode::CMSG_GOSSIP_SELECT_OPTION)); packet.writeUInt64(npcGuid); + packet.writeUInt32(menuId); packet.writeUInt32(optionId); if (!code.empty()) { packet.writeString(code); @@ -1818,6 +1819,22 @@ network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t optio return packet; } +network::Packet QuestgiverQueryQuestPacket::build(uint64_t npcGuid, uint32_t questId) { + network::Packet packet(static_cast(Opcode::CMSG_QUESTGIVER_QUERY_QUEST)); + packet.writeUInt64(npcGuid); + packet.writeUInt32(questId); + packet.writeUInt8(1); // isDialogContinued = 1 (from gossip) + return packet; +} + +network::Packet QuestgiverAcceptQuestPacket::build(uint64_t npcGuid, uint32_t questId) { + network::Packet packet(static_cast(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST)); + packet.writeUInt64(npcGuid); + packet.writeUInt32(questId); + packet.writeUInt32(0); // unused + return packet; +} + bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data) { data.npcGuid = packet.readUInt64(); data.menuId = packet.readUInt32(); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 35a4fc6f..3f85ed0f 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1608,7 +1608,15 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) { ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Quests:"); for (const auto& quest : gossip.quests) { - ImGui::BulletText("[%d] %s", quest.questLevel, quest.title.c_str()); + ImGui::PushID(static_cast(quest.questId)); + char qlabel[256]; + snprintf(qlabel, sizeof(qlabel), "[%d] %s", quest.questLevel, quest.title.c_str()); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.3f, 1.0f)); + if (ImGui::Selectable(qlabel)) { + gameHandler.selectGossipQuest(quest.questId); + } + ImGui::PopStyleColor(); + ImGui::PopID(); } }