Add transport support, gameobject queries, and fix item use

- Add setInstancePosition() to M2Renderer and WMORenderer for moving
  transport instances at runtime
- Detect UPDATEFLAG_TRANSPORT on gameobjects and track transport GUIDs
- Parse player-on-transport state from movement blocks
- Wire transport move callback in Application to update render positions
- Implement CMSG_GAMEOBJECT_QUERY / SMSG_GAMEOBJECT_QUERY_RESPONSE so
  gameobjects display proper names instead of "Unknown"
- Add name/entry fields to GameObject entity class
- Fix CMSG_USE_ITEM packet: remove extra uint8 that shifted the item
  GUID by one byte, breaking hearthstone and all item usage
- Remove redundant CMSG_LOOT after CMSG_GAMEOBJECT_USE for chests
- Show PvP enabled/disabled state in toggle message
- Relax WMO ramp wall-collision step-up check to allow walking on
  gentle ramps where floor rise per step is under 0.1 units
- Add M2 fallback when WMO group files fail to load for gameobjects
- Handle re-creation of existing gameobject render instances by
  updating position instead of silently ignoring
This commit is contained in:
Kelsi 2026-02-08 00:59:40 -08:00
parent 5610faa958
commit 0ce38cfb99
12 changed files with 391 additions and 65 deletions

View file

@ -662,9 +662,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
}
case Opcode::SMSG_BUY_FAILED:
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
case Opcode::MSG_RAID_TARGET_UPDATE:
break;
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
handleGameObjectQueryResponse(packet);
break;
case Opcode::SMSG_QUESTGIVER_STATUS: {
// uint64 npcGuid + uint8 status
if (packet.getSize() - packet.getReadPos() >= 9) {
@ -731,9 +733,18 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::MSG_MOVE_TELEPORT_ACK:
handleTeleportAck(packet);
break;
case Opcode::SMSG_TRANSFER_PENDING:
LOG_INFO("SMSG_TRANSFER_PENDING received - map transfer incoming");
case Opcode::SMSG_TRANSFER_PENDING: {
// SMSG_TRANSFER_PENDING: uint32 mapId, then optional transport data
uint32_t pendingMapId = packet.readUInt32();
LOG_INFO("SMSG_TRANSFER_PENDING: mapId=", pendingMapId);
// Optional: if remaining data, there's a transport entry + mapId
if (packet.getReadPos() + 8 <= packet.getSize()) {
uint32_t transportEntry = packet.readUInt32();
uint32_t transportMapId = packet.readUInt32();
LOG_INFO(" Transport entry=", transportEntry, " transportMapId=", transportMapId);
}
break;
}
// ---- Taxi / Flight Paths ----
case Opcode::SMSG_SHOWTAXINODES:
@ -1356,6 +1367,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
gameObjectDespawnCallback_(guid);
}
}
transportGuids_.erase(guid);
if (playerTransportGuid_ == guid) {
playerTransportGuid_ = 0;
playerTransportOffset_ = glm::vec3(0.0f);
}
entityManager.removeEntity(guid);
}
}
@ -1400,6 +1416,21 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
if (block.guid == playerGuid && block.runSpeed > 0.1f && block.runSpeed < 100.0f) {
serverRunSpeed_ = block.runSpeed;
}
// Track player-on-transport state
if (block.guid == playerGuid) {
if (block.onTransport) {
playerTransportGuid_ = block.transportGuid;
playerTransportOffset_ = glm::vec3(block.transportX, block.transportY, block.transportZ);
LOG_INFO("Player on transport: 0x", std::hex, playerTransportGuid_, std::dec,
" offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")");
} else {
if (playerTransportGuid_ != 0) {
LOG_INFO("Player left transport");
}
playerTransportGuid_ = 0;
playerTransportOffset_ = glm::vec3(0.0f);
}
}
}
// Set fields
@ -1501,17 +1532,39 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
}
}
// Extract displayId for gameobjects (3.3.5a: GAMEOBJECT_DISPLAYID = field 8)
// Extract displayId and entry for gameobjects (3.3.5a: GAMEOBJECT_DISPLAYID = field 8)
if (block.objectType == ObjectType::GAMEOBJECT) {
auto go = std::static_pointer_cast<GameObject>(entity);
auto itDisp = block.fields.find(8);
if (itDisp != block.fields.end()) {
go->setDisplayId(itDisp->second);
}
// Extract entry and query name (OBJECT_FIELD_ENTRY = index 3)
auto itEntry = block.fields.find(3);
if (itEntry != block.fields.end() && itEntry->second != 0) {
go->setEntry(itEntry->second);
auto cacheIt = gameObjectInfoCache_.find(itEntry->second);
if (cacheIt != gameObjectInfoCache_.end()) {
go->setName(cacheIt->second.name);
}
queryGameObjectInfo(itEntry->second, block.guid);
}
// Detect transport GameObjects via UPDATEFLAG_TRANSPORT (0x0002)
if (block.updateFlags & 0x0002) {
transportGuids_.insert(block.guid);
LOG_INFO("Detected transport GameObject: 0x", std::hex, block.guid, std::dec,
" displayId=", go->getDisplayId(),
" pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")");
}
if (go->getDisplayId() != 0 && gameObjectSpawnCallback_) {
gameObjectSpawnCallback_(block.guid, go->getDisplayId(),
go->getX(), go->getY(), go->getZ(), go->getOrientation());
}
// Fire transport move callback for transports (position update on re-creation)
if (transportGuids_.count(block.guid) && transportMoveCallback_) {
transportMoveCallback_(block.guid,
go->getX(), go->getY(), go->getZ(), go->getOrientation());
}
}
// Track online item objects
if (block.objectType == ObjectType::ITEM) {
@ -1724,6 +1777,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z));
entity->setPosition(pos.x, pos.y, pos.z, block.orientation);
LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec);
// Fire transport move callback if this is a known transport
if (transportGuids_.count(block.guid) && transportMoveCallback_) {
transportMoveCallback_(block.guid, pos.x, pos.y, pos.z, block.orientation);
}
} else {
LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec);
}
@ -2454,7 +2512,19 @@ void GameHandler::togglePvp() {
auto packet = TogglePvpPacket::build();
socket->send(packet);
addSystemChatMessage("PvP flag toggled.");
// Check current PVP state from player's UNIT_FIELD_FLAGS (index 59)
// UNIT_FLAG_PVP = 0x00001000
auto entity = entityManager.getEntity(playerGuid);
bool currentlyPvp = false;
if (entity) {
currentlyPvp = (entity->getField(59) & 0x00001000) != 0;
}
// We're toggling, so report the NEW state
if (currentlyPvp) {
addSystemChatMessage("PvP flag disabled.");
} else {
addSystemChatMessage("PvP flag enabled.");
}
LOG_INFO("Toggled PvP flag");
}
@ -2939,6 +3009,15 @@ void GameHandler::queryCreatureInfo(uint32_t entry, uint64_t guid) {
socket->send(packet);
}
void GameHandler::queryGameObjectInfo(uint32_t entry, uint64_t guid) {
if (gameObjectInfoCache_.count(entry) || pendingGameObjectQueries_.count(entry)) return;
if (state != WorldState::IN_WORLD || !socket) return;
pendingGameObjectQueries_.insert(entry);
auto packet = GameObjectQueryPacket::build(entry, guid);
socket->send(packet);
}
std::string GameHandler::getCachedPlayerName(uint64_t guid) const {
auto it = playerNameCache.find(guid);
return (it != playerNameCache.end()) ? it->second : "";
@ -2986,6 +3065,30 @@ void GameHandler::handleCreatureQueryResponse(network::Packet& packet) {
}
}
// ============================================================
// GameObject Query
// ============================================================
void GameHandler::handleGameObjectQueryResponse(network::Packet& packet) {
GameObjectQueryResponseData data;
if (!GameObjectQueryResponseParser::parse(packet, data)) return;
pendingGameObjectQueries_.erase(data.entry);
if (data.isValid()) {
gameObjectInfoCache_[data.entry] = data;
// Update all gameobject entities with this entry
for (auto& [guid, entity] : entityManager.getEntities()) {
if (entity->getType() == ObjectType::GAMEOBJECT) {
auto go = std::static_pointer_cast<GameObject>(entity);
if (go->getEntry() == data.entry) {
go->setName(data.name);
}
}
}
}
}
// ============================================================
// Item Query
// ============================================================
@ -3959,9 +4062,6 @@ void GameHandler::interactWithGameObject(uint64_t guid) {
if (state != WorldState::IN_WORLD || !socket) return;
auto packet = GameObjectUsePacket::build(guid);
socket->send(packet);
// Many lootable chests require a loot request after use.
auto loot = LootPacket::build(guid);
socket->send(loot);
}
void GameHandler::selectGossipOption(uint32_t optionId) {

View file

@ -624,6 +624,7 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
// Update flags (3.3.5a uses 2 bytes for flags)
uint16_t updateFlags = packet.readUInt16();
block.updateFlags = updateFlags;
LOG_DEBUG(" UpdateFlags: 0x", std::hex, updateFlags, std::dec);
@ -667,14 +668,18 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
// Transport data (if on transport)
if (moveFlags & 0x00000200) { // MOVEMENTFLAG_ONTRANSPORT
/*uint64_t transportGuid =*/ readPackedGuid(packet);
/*float tX =*/ packet.readFloat();
/*float tY =*/ packet.readFloat();
/*float tZ =*/ packet.readFloat();
/*float tO =*/ packet.readFloat();
block.onTransport = true;
block.transportGuid = readPackedGuid(packet);
block.transportX = packet.readFloat();
block.transportY = packet.readFloat();
block.transportZ = packet.readFloat();
block.transportO = packet.readFloat();
/*uint32_t tTime =*/ packet.readUInt32();
/*int8_t tSeat =*/ packet.readUInt8();
LOG_DEBUG(" OnTransport: guid=0x", std::hex, block.transportGuid, std::dec,
" offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")");
if (moveFlags2 & 0x0200) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT
/*uint32_t tTime2 =*/ packet.readUInt32();
}
@ -1584,6 +1589,40 @@ bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryRe
return true;
}
// ---- GameObject Query ----
network::Packet GameObjectQueryPacket::build(uint32_t entry, uint64_t guid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GAMEOBJECT_QUERY));
packet.writeUInt32(entry);
packet.writeUInt64(guid);
LOG_DEBUG("Built CMSG_GAMEOBJECT_QUERY: entry=", entry, " guid=0x", std::hex, guid, std::dec);
return packet;
}
bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQueryResponseData& data) {
data.entry = packet.readUInt32();
// High bit set means gameobject not found
if (data.entry & 0x80000000) {
data.entry &= ~0x80000000;
LOG_DEBUG("GameObject query: entry ", data.entry, " not found");
data.name = "";
return true;
}
data.type = packet.readUInt32(); // GameObjectType
/*uint32_t displayId =*/ packet.readUInt32();
// 4 name strings (only first is usually populated)
data.name = packet.readString();
// name2, name3, name4
packet.readString();
packet.readString();
packet.readString();
LOG_INFO("GameObject query response: ", data.name, " (type=", data.type, " entry=", data.entry, ")");
return true;
}
// ---- Item Query ----
network::Packet ItemQueryPacket::build(uint32_t entry, uint64_t guid) {
@ -2229,9 +2268,8 @@ network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_USE_ITEM));
packet.writeUInt8(bagIndex);
packet.writeUInt8(slotIndex);
packet.writeUInt8(0); // spell index
packet.writeUInt8(0); // cast count
packet.writeUInt32(0); // spell id (unused)
packet.writeUInt8(0); // cast count
packet.writeUInt32(0); // spell id
packet.writeUInt64(itemGuid);
packet.writeUInt32(0); // glyph index
packet.writeUInt8(0); // cast flags