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.
This commit is contained in:
Kelsi 2026-02-20 23:31:30 -08:00
parent ace24e8ccc
commit c04e97e375
5 changed files with 118 additions and 5 deletions

View file

@ -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);

View file

@ -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
// ============================================================

View file

@ -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;

View file

@ -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<GameObject>(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<char>(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.

View file

@ -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) {