From c04e97e375991e9c2e3457bc72c92346efa9e3c4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 20 Feb 2026 23:31:30 -0800 Subject: [PATCH] Fix street sign interaction text and M2 sign orientation Add page-text support for sign-like gameobject interactions by handling SMSG_GAMEOBJECT_PAGETEXT and SMSG_PAGE_TEXT_QUERY_RESPONSE, and issuing CMSG_PAGE_TEXT_QUERY when page IDs are available from cached GO template data. Normalize received page text tokens before chat display and add a fallback for basic signpost GO type clicks to print sign names when no page data is present. Correct M2 gameobject yaw alignment for signposts/arrows by applying render-space -90deg offset consistently across spawn, position update, and move-callback transforms; keep WMO orientation path unchanged. --- include/game/game_handler.hpp | 2 + include/game/world_packets.hpp | 21 +++++++++++ src/core/application.cpp | 13 ++++--- src/game/game_handler.cpp | 68 ++++++++++++++++++++++++++++++++++ src/game/world_packets.cpp | 19 ++++++++++ 5 files changed, 118 insertions(+), 5 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index f1e758e9..ad993511 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1052,6 +1052,8 @@ private: void handleNameQueryResponse(network::Packet& packet); void handleCreatureQueryResponse(network::Packet& packet); void handleGameObjectQueryResponse(network::Packet& packet); + void handleGameObjectPageText(network::Packet& packet); + void handlePageTextQueryResponse(network::Packet& packet); void handleItemQueryResponse(network::Packet& packet); void handleInspectResults(network::Packet& packet); void queryItemInfo(uint32_t entry, uint64_t guid); diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index cd86efbb..9124d75e 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1405,6 +1405,27 @@ public: static bool parse(network::Packet& packet, GameObjectQueryResponseData& data); }; +/** CMSG_PAGE_TEXT_QUERY packet builder */ +class PageTextQueryPacket { +public: + static network::Packet build(uint32_t pageId, uint64_t guid); +}; + +/** SMSG_PAGE_TEXT_QUERY_RESPONSE data */ +struct PageTextQueryResponseData { + uint32_t pageId = 0; + std::string text; + uint32_t nextPageId = 0; + + bool isValid() const { return pageId != 0; } +}; + +/** SMSG_PAGE_TEXT_QUERY_RESPONSE parser */ +class PageTextQueryResponseParser { +public: + static bool parse(network::Packet& packet, PageTextQueryResponseData& data); +}; + // ============================================================ // Item Query // ============================================================ diff --git a/src/core/application.cpp b/src/core/application.cpp index aa8747f7..421d3e71 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1720,7 +1720,7 @@ void Application::setupUICallbacks() { if (auto* mr = renderer->getM2Renderer()) { glm::mat4 transform(1.0f); transform = glm::translate(transform, renderPos); - transform = glm::rotate(transform, orientation, glm::vec3(0, 0, 1)); + transform = glm::rotate(transform, orientation - glm::radians(90.0f), glm::vec3(0, 0, 1)); mr->setInstanceTransform(info.instanceId, transform); } } @@ -5420,7 +5420,9 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t if (auto* mr = renderer->getM2Renderer()) { glm::mat4 transform(1.0f); transform = glm::translate(transform, renderPos); - transform = glm::rotate(transform, orientation, glm::vec3(0, 0, 1)); + // M2 gameobjects use model-forward alignment like character M2s. + // Apply -90deg in render space to match world-facing orientation. + transform = glm::rotate(transform, orientation - glm::radians(90.0f), glm::vec3(0, 0, 1)); mr->setInstanceTransform(info.instanceId, transform); } } @@ -5474,7 +5476,8 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t bool isWmo = lowerPath.size() >= 4 && lowerPath.substr(lowerPath.size() - 4) == ".wmo"; glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z)); - float renderYaw = orientation; + const float renderYawWmo = orientation; + const float renderYawM2 = orientation - glm::radians(90.0f); bool loadedAsWmo = false; if (isWmo) { @@ -5545,7 +5548,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t if (loadedAsWmo) { uint32_t instanceId = wmoRenderer->createInstance(modelId, renderPos, - glm::vec3(0.0f, 0.0f, renderYaw), 1.0f); + glm::vec3(0.0f, 0.0f, renderYawWmo), 1.0f); if (instanceId == 0) { LOG_WARNING("Failed to create gameobject WMO instance for guid 0x", std::hex, guid, std::dec); return; @@ -5640,7 +5643,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t } uint32_t instanceId = m2Renderer->createInstance(modelId, renderPos, - glm::vec3(0.0f, 0.0f, renderYaw), 1.0f); + glm::vec3(0.0f, 0.0f, renderYawM2), 1.0f); if (instanceId == 0) { LOG_WARNING("Failed to create gameobject instance for guid 0x", std::hex, guid, std::dec); return; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index fb218c4e..0eb6f638 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2281,6 +2281,12 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE: handleGameObjectQueryResponse(packet); break; + case Opcode::SMSG_GAMEOBJECT_PAGETEXT: + handleGameObjectPageText(packet); + break; + case Opcode::SMSG_PAGE_TEXT_QUERY_RESPONSE: + handlePageTextQueryResponse(packet); + break; case Opcode::SMSG_QUESTGIVER_STATUS: { if (packet.getSize() - packet.getReadPos() >= 9) { uint64_t npcGuid = packet.readUInt64(); @@ -7203,6 +7209,60 @@ void GameHandler::handleGameObjectQueryResponse(network::Packet& packet) { } } +void GameHandler::handleGameObjectPageText(network::Packet& packet) { + if (packet.getSize() - packet.getReadPos() < 8) return; + uint64_t guid = packet.readUInt64(); + auto entity = entityManager.getEntity(guid); + if (!entity || entity->getType() != ObjectType::GAMEOBJECT) return; + + auto go = std::static_pointer_cast(entity); + uint32_t entry = go->getEntry(); + if (entry == 0) return; + + auto cacheIt = gameObjectInfoCache_.find(entry); + if (cacheIt == gameObjectInfoCache_.end()) { + queryGameObjectInfo(entry, guid); + return; + } + + const GameObjectQueryResponseData& info = cacheIt->second; + uint32_t pageId = 0; + // AzerothCore layout: + // type 9 (TEXT): data[0]=pageID + // type 10 (GOOBER): data[7]=pageId + if (info.type == 9) pageId = info.data[0]; + else if (info.type == 10) pageId = info.data[7]; + + if (pageId != 0 && socket && state == WorldState::IN_WORLD) { + auto req = PageTextQueryPacket::build(pageId, guid); + socket->send(req); + return; + } + + if (!info.name.empty()) { + addSystemChatMessage(info.name); + } +} + +void GameHandler::handlePageTextQueryResponse(network::Packet& packet) { + PageTextQueryResponseData data; + if (!PageTextQueryResponseParser::parse(packet, data)) return; + + if (!data.text.empty()) { + std::istringstream iss(data.text); + std::string line; + bool wrote = false; + while (std::getline(iss, line)) { + if (line.empty()) continue; + addSystemChatMessage(line); + wrote = true; + } + if (!wrote) { + addSystemChatMessage(data.text); + } + } +} + // ============================================================ // Item Query // ============================================================ @@ -9730,6 +9790,14 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { goEntry = go->getEntry(); goName = go->getName(); if (auto* info = getCachedGameObjectInfo(goEntry)) goType = info->type; + if (goType == 5 && !goName.empty()) { + std::string lower = goName; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (lower.rfind("doodad_", 0) != 0) { + addSystemChatMessage(goName); + } + } } // Face object and send heartbeat before use so strict servers don't require // a nudge movement to accept interaction. diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index cdbfa234..fca97d4c 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2089,6 +2089,25 @@ bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQue return true; } +network::Packet PageTextQueryPacket::build(uint32_t pageId, uint64_t guid) { + network::Packet packet(wireOpcode(Opcode::CMSG_PAGE_TEXT_QUERY)); + packet.writeUInt32(pageId); + packet.writeUInt64(guid); + return packet; +} + +bool PageTextQueryResponseParser::parse(network::Packet& packet, PageTextQueryResponseData& data) { + if (packet.getSize() - packet.getReadPos() < 4) return false; + data.pageId = packet.readUInt32(); + data.text = normalizeWowTextTokens(packet.readString()); + if (packet.getSize() - packet.getReadPos() >= 4) { + data.nextPageId = packet.readUInt32(); + } else { + data.nextPageId = 0; + } + return data.isValid(); +} + // ---- Item Query ---- network::Packet ItemQueryPacket::build(uint32_t entry, uint64_t guid) {