From 6f5bdb2e91598c64251dba1b478e8d64305cd474 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 11 Mar 2026 00:18:23 -0700 Subject: [PATCH] feat: implement WotLK quest POI query to show objective locations on minimap Send CMSG_QUEST_POI_QUERY alongside each CMSG_QUEST_QUERY (WotLK only, gated by questLogStride == 5 and opcode availability). Parse the response to extract POI region centroids and add them as GossipPoi markers so the existing minimap rendering shows quest objective locations as cyan diamonds. Each quest POI region is reduced to its centroid point; markers for the current map only are shown. This gives players visual guidance for where to go for active quests directly on the minimap. --- include/game/game_handler.hpp | 1 + src/game/game_handler.cpp | 77 +++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 946c66b0..f2a83f38 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1618,6 +1618,7 @@ private: void handleGossipMessage(network::Packet& packet); void handleQuestgiverQuestList(network::Packet& packet); void handleGossipComplete(network::Packet& packet); + void handleQuestPoiQueryResponse(network::Packet& packet); void handleQuestDetails(network::Packet& packet); void handleQuestRequestItems(network::Packet& packet); void handleQuestOfferReward(network::Packet& packet); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ce8eb976..4873cef2 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -5767,6 +5767,8 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_RESPOND_INSPECT_ACHIEVEMENTS: case Opcode::SMSG_PLAYER_SKINNED: case Opcode::SMSG_QUEST_POI_QUERY_RESPONSE: + handleQuestPoiQueryResponse(packet); + break; case Opcode::SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA: case Opcode::SMSG_RESET_RANGED_COMBAT_TIMER: case Opcode::SMSG_PROFILEDATA_RESPONSE: @@ -14883,9 +14885,84 @@ bool GameHandler::requestQuestQuery(uint32_t questId, bool force) { pkt.writeUInt32(questId); socket->send(pkt); pendingQuestQueryIds_.insert(questId); + + // WotLK supports CMSG_QUEST_POI_QUERY to get objective map locations. + // Only send if the opcode is mapped (stride==5 means WotLK). + if (packetParsers_ && packetParsers_->questLogStride() == 5) { + const uint32_t wirePoiQuery = wireOpcode(Opcode::CMSG_QUEST_POI_QUERY); + if (wirePoiQuery != 0xFFFF) { + network::Packet poiPkt(static_cast(wirePoiQuery)); + poiPkt.writeUInt32(1); // count = 1 + poiPkt.writeUInt32(questId); + socket->send(poiPkt); + } + } return true; } +void GameHandler::handleQuestPoiQueryResponse(network::Packet& packet) { + // WotLK 3.3.5a SMSG_QUEST_POI_QUERY_RESPONSE format: + // uint32 questCount + // per quest: + // uint32 questId + // uint32 poiCount + // per poi: + // uint32 poiId + // int32 objIndex (-1 = no specific objective) + // uint32 mapId + // uint32 areaId + // uint32 floorId + // uint32 unk1 + // uint32 unk2 + // uint32 pointCount + // per point: int32 x, int32 y + if (packet.getSize() - packet.getReadPos() < 4) return; + const uint32_t questCount = packet.readUInt32(); + for (uint32_t qi = 0; qi < questCount; ++qi) { + if (packet.getSize() - packet.getReadPos() < 8) return; + const uint32_t questId = packet.readUInt32(); + const uint32_t poiCount = packet.readUInt32(); + for (uint32_t pi = 0; pi < poiCount; ++pi) { + if (packet.getSize() - packet.getReadPos() < 28) return; + packet.readUInt32(); // poiId + packet.readUInt32(); // objIndex (int32) + const uint32_t mapId = packet.readUInt32(); + packet.readUInt32(); // areaId + packet.readUInt32(); // floorId + packet.readUInt32(); // unk1 + packet.readUInt32(); // unk2 + const uint32_t pointCount = packet.readUInt32(); + if (pointCount == 0) continue; + if (packet.getSize() - packet.getReadPos() < pointCount * 8) return; + // Compute centroid of the poi region to place a minimap marker. + float sumX = 0.0f, sumY = 0.0f; + for (uint32_t pt = 0; pt < pointCount; ++pt) { + const int32_t px = static_cast(packet.readUInt32()); + const int32_t py = static_cast(packet.readUInt32()); + sumX += static_cast(px); + sumY += static_cast(py); + } + // POI points in WotLK are zone-level coordinates. + // Skip POIs for maps other than the player's current map. + if (mapId != currentMapId_) continue; + // Find the quest title for the marker label. + std::string questTitle; + for (const auto& q : questLog_) { + if (q.questId == questId) { questTitle = q.title; break; } + } + // Add as a GossipPoi so the existing minimap code displays it. + GossipPoi poi; + poi.x = sumX / static_cast(pointCount); // WoW canonical X (north) + poi.y = sumY / static_cast(pointCount); // WoW canonical Y (west) + poi.icon = 6; // generic POI icon + poi.name = questTitle.empty() ? "Quest objective" : questTitle; + gossipPois_.push_back(std::move(poi)); + LOG_DEBUG("Quest POI: questId=", questId, " mapId=", mapId, + " centroid=(", poi.x, ",", poi.y, ") title=", poi.name); + } + } +} + void GameHandler::handleQuestDetails(network::Packet& packet) { QuestDetailsData data; bool ok = packetParsers_ ? packetParsers_->parseQuestDetails(packet, data)