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

@ -256,10 +256,18 @@ public:
GameObject() { type = ObjectType::GAMEOBJECT; }
explicit GameObject(uint64_t guid) : Entity(guid) { type = ObjectType::GAMEOBJECT; }
const std::string& getName() const { return name; }
void setName(const std::string& n) { name = n; }
uint32_t getEntry() const { return entry; }
void setEntry(uint32_t e) { entry = e; }
uint32_t getDisplayId() const { return displayId; }
void setDisplayId(uint32_t id) { displayId = id; }
protected:
std::string name;
uint32_t entry = 0;
uint32_t displayId = 0;
};

View file

@ -5,6 +5,7 @@
#include "game/inventory.hpp"
#include "game/spell_defines.hpp"
#include "game/group_defines.hpp"
#include <glm/glm.hpp>
#include <memory>
#include <string>
#include <vector>
@ -297,6 +298,7 @@ public:
// ---- Phase 1: Name queries ----
void queryPlayerName(uint64_t guid);
void queryCreatureInfo(uint32_t entry, uint64_t guid);
void queryGameObjectInfo(uint32_t entry, uint64_t guid);
std::string getCachedPlayerName(uint64_t guid) const;
std::string getCachedCreatureName(uint32_t entry) const;
@ -395,6 +397,16 @@ public:
using CreatureMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, uint32_t durationMs)>;
void setCreatureMoveCallback(CreatureMoveCallback cb) { creatureMoveCallback_ = std::move(cb); }
// Transport move callback (online mode - triggered when transport position updates)
// Parameters: guid, x, y, z (canonical), orientation
using TransportMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, float orientation)>;
void setTransportMoveCallback(TransportMoveCallback cb) { transportMoveCallback_ = std::move(cb); }
// Transport state for player-on-transport
bool isOnTransport() const { return playerTransportGuid_ != 0; }
uint64_t getPlayerTransportGuid() const { return playerTransportGuid_; }
glm::vec3 getPlayerTransportOffset() const { return playerTransportOffset_; }
// Cooldowns
float getSpellCooldown(uint32_t spellId) const;
@ -600,6 +612,7 @@ private:
// ---- Phase 1 handlers ----
void handleNameQueryResponse(network::Packet& packet);
void handleCreatureQueryResponse(network::Packet& packet);
void handleGameObjectQueryResponse(network::Packet& packet);
void handleItemQueryResponse(network::Packet& packet);
void queryItemInfo(uint32_t entry, uint64_t guid);
void rebuildOnlineInventory();
@ -766,6 +779,8 @@ private:
std::unordered_set<uint64_t> pendingNameQueries;
std::unordered_map<uint32_t, CreatureQueryResponseData> creatureInfoCache;
std::unordered_set<uint32_t> pendingCreatureQueries;
std::unordered_map<uint32_t, GameObjectQueryResponseData> gameObjectInfoCache_;
std::unordered_set<uint32_t> pendingGameObjectQueries_;
// ---- Friend list cache ----
std::unordered_map<std::string, uint64_t> friendsCache; // name -> guid
@ -818,8 +833,14 @@ private:
CreatureSpawnCallback creatureSpawnCallback_;
CreatureDespawnCallback creatureDespawnCallback_;
CreatureMoveCallback creatureMoveCallback_;
TransportMoveCallback transportMoveCallback_;
GameObjectSpawnCallback gameObjectSpawnCallback_;
GameObjectDespawnCallback gameObjectDespawnCallback_;
// Transport tracking
std::unordered_set<uint64_t> transportGuids_; // GUIDs of known transport GameObjects
uint64_t playerTransportGuid_ = 0; // Transport the player is riding (0 = none)
glm::vec3 playerTransportOffset_ = glm::vec3(0.0f); // Player offset on transport
std::vector<uint32_t> knownSpells;
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
uint8_t castCount = 0;

View file

@ -451,6 +451,14 @@ struct UpdateBlock {
float x = 0.0f, y = 0.0f, z = 0.0f, orientation = 0.0f;
float runSpeed = 0.0f;
// Update flags from movement block (for detecting transports, etc.)
uint16_t updateFlags = 0;
// Transport data from LIVING movement block (MOVEMENTFLAG_ONTRANSPORT)
bool onTransport = false;
uint64_t transportGuid = 0;
float transportX = 0.0f, transportY = 0.0f, transportZ = 0.0f, transportO = 0.0f;
// Field data (for VALUES and CREATE updates)
std::map<uint16_t, uint32_t> fields;
};
@ -1050,6 +1058,31 @@ public:
static bool parse(network::Packet& packet, CreatureQueryResponseData& data);
};
// ============================================================
// GameObject Query
// ============================================================
/** CMSG_GAMEOBJECT_QUERY packet builder */
class GameObjectQueryPacket {
public:
static network::Packet build(uint32_t entry, uint64_t guid);
};
/** SMSG_GAMEOBJECT_QUERY_RESPONSE data */
struct GameObjectQueryResponseData {
uint32_t entry = 0;
std::string name;
uint32_t type = 0; // GameObjectType (e.g. 3=chest, 2=questgiver)
bool isValid() const { return entry != 0 && !name.empty(); }
};
/** SMSG_GAMEOBJECT_QUERY_RESPONSE parser */
class GameObjectQueryResponseParser {
public:
static bool parse(network::Packet& packet, GameObjectQueryResponseData& data);
};
// ============================================================
// Item Query
// ============================================================

View file

@ -208,6 +208,13 @@ public:
*/
void renderM2Particles(const glm::mat4& view, const glm::mat4& proj);
/**
* Update the world position of an existing instance (e.g., for transports)
* @param instanceId Instance ID returned by createInstance()
* @param position New world position
*/
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
/**
* Remove a specific instance by ID
* @param instanceId Instance ID returned by createInstance()

View file

@ -75,6 +75,13 @@ public:
const glm::vec3& rotation = glm::vec3(0.0f),
float scale = 1.0f);
/**
* Update the world position of an existing instance (e.g., for transports)
* @param instanceId Instance to update
* @param position New world position
*/
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
/**
* Remove WMO instance
* @param instanceId Instance to remove

View file

@ -606,6 +606,39 @@ void Application::setupUICallbacks() {
}
});
// Transport move callback (online mode) - update transport gameobject positions
gameHandler->setTransportMoveCallback([this](uint64_t guid, float x, float y, float z, float /*orientation*/) {
auto it = gameObjectInstances_.find(guid);
if (it == gameObjectInstances_.end()) return;
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
if (renderer) {
if (it->second.isWmo) {
if (auto* wmoRenderer = renderer->getWMORenderer()) {
wmoRenderer->setInstancePosition(it->second.instanceId, renderPos);
}
} else {
if (auto* m2Renderer = renderer->getM2Renderer()) {
m2Renderer->setInstancePosition(it->second.instanceId, renderPos);
}
}
// Move player with transport if riding it
if (gameHandler && gameHandler->isOnTransport() && gameHandler->getPlayerTransportGuid() == guid) {
auto* cc = renderer->getCameraController();
if (cc) {
glm::vec3* ft = cc->getFollowTargetMutable();
if (ft) {
// Transport offset is in server/canonical coords — convert to render
glm::vec3 offset = gameHandler->getPlayerTransportOffset();
glm::vec3 canonicalPlayerPos = glm::vec3(x + offset.x, y + offset.y, z + offset.z);
glm::vec3 playerRenderPos = core::coords::canonicalToRender(canonicalPlayerPos);
*ft = playerRenderPos;
}
}
}
}
});
// NPC death callback (online mode) - play death animation
gameHandler->setNpcDeathCallback([this](uint64_t guid) {
auto it = creatureInstances_.find(guid);
@ -2202,7 +2235,21 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float
}
if (!gameObjectLookupsBuilt_) return;
if (gameObjectInstances_.count(guid)) return;
if (gameObjectInstances_.count(guid)) {
// Already have a render instance — update its position (e.g. transport re-creation)
auto& info = gameObjectInstances_[guid];
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
if (renderer) {
if (info.isWmo) {
if (auto* wr = renderer->getWMORenderer())
wr->setInstancePosition(info.instanceId, renderPos);
} else {
if (auto* mr = renderer->getM2Renderer())
mr->setInstancePosition(info.instanceId, renderPos);
}
}
return;
}
std::string modelPath = getGameObjectModelPathForDisplayId(displayId);
if (modelPath.empty()) {
@ -2218,6 +2265,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
float renderYaw = orientation + glm::radians(90.0f);
bool loadedAsWmo = false;
if (isWmo) {
auto* wmoRenderer = renderer->getWMORenderer();
if (!wmoRenderer) return;
@ -2226,62 +2274,84 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float
auto itCache = gameObjectDisplayIdWmoCache_.find(displayId);
if (itCache != gameObjectDisplayIdWmoCache_.end()) {
modelId = itCache->second;
loadedAsWmo = true;
} else {
modelId = nextGameObjectWmoModelId_++;
auto wmoData = assetManager->readFile(modelPath);
if (wmoData.empty()) {
LOG_WARNING("Failed to read gameobject WMO: ", modelPath);
return;
}
if (!wmoData.empty()) {
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
LOG_INFO("Gameobject WMO root loaded: ", modelPath, " nGroups=", wmoModel.nGroups);
int loadedGroups = 0;
if (wmoModel.nGroups > 0) {
std::string basePath = modelPath;
std::string extension;
if (basePath.size() > 4) {
extension = basePath.substr(basePath.size() - 4);
std::string extLower = extension;
for (char& c : extLower) c = static_cast<char>(std::tolower(c));
if (extLower == ".wmo") {
basePath = basePath.substr(0, basePath.size() - 4);
}
}
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
if (wmoModel.nGroups > 0) {
std::string basePath = modelPath;
std::string extension;
if (basePath.size() > 4) {
extension = basePath.substr(basePath.size() - 4);
std::string extLower = extension;
for (char& c : extLower) c = static_cast<char>(std::tolower(c));
if (extLower == ".wmo") {
basePath = basePath.substr(0, basePath.size() - 4);
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
char groupSuffix[16];
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
std::string groupPath = basePath + groupSuffix;
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
if (groupData.empty()) {
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
groupData = assetManager->readFile(basePath + groupSuffix);
}
if (groupData.empty()) {
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
groupData = assetManager->readFile(basePath + groupSuffix);
}
if (!groupData.empty()) {
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
loadedGroups++;
} else {
LOG_WARNING(" Failed to load WMO group ", gi, " for: ", basePath);
}
}
}
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
char groupSuffix[16];
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
std::string groupPath = basePath + groupSuffix;
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
if (groupData.empty()) {
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
groupData = assetManager->readFile(basePath + groupSuffix);
}
if (groupData.empty()) {
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
groupData = assetManager->readFile(basePath + groupSuffix);
}
if (!groupData.empty()) {
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
if (loadedGroups > 0 || wmoModel.nGroups == 0) {
modelId = nextGameObjectWmoModelId_++;
if (wmoRenderer->loadModel(wmoModel, modelId)) {
gameObjectDisplayIdWmoCache_[displayId] = modelId;
loadedAsWmo = true;
} else {
LOG_WARNING("Failed to load gameobject WMO model: ", modelPath);
}
} else {
LOG_WARNING("No WMO groups loaded for gameobject: ", modelPath,
" — falling back to M2");
}
} else {
LOG_WARNING("Failed to read gameobject WMO: ", modelPath, " — falling back to M2");
}
if (!wmoRenderer->loadModel(wmoModel, modelId)) {
LOG_WARNING("Failed to load gameobject WMO: ", modelPath);
return;
}
gameObjectDisplayIdWmoCache_[displayId] = modelId;
}
uint32_t instanceId = wmoRenderer->createInstance(modelId, renderPos,
glm::vec3(0.0f, 0.0f, renderYaw), 1.0f);
if (instanceId == 0) {
LOG_WARNING("Failed to create gameobject WMO instance for guid 0x", std::hex, guid, std::dec);
if (loadedAsWmo) {
uint32_t instanceId = wmoRenderer->createInstance(modelId, renderPos,
glm::vec3(0.0f, 0.0f, renderYaw), 1.0f);
if (instanceId == 0) {
LOG_WARNING("Failed to create gameobject WMO instance for guid 0x", std::hex, guid, std::dec);
return;
}
gameObjectInstances_[guid] = {modelId, instanceId, true};
LOG_INFO("Spawned gameobject WMO: guid=0x", std::hex, guid, std::dec,
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
return;
}
gameObjectInstances_[guid] = {modelId, instanceId, true};
} else {
// WMO failed — fall through to try as M2
// Convert .wmo path to .m2 for fallback
modelPath = modelPath.substr(0, modelPath.size() - 4) + ".m2";
}
{
auto* m2Renderer = renderer->getM2Renderer();
if (!m2Renderer) return;

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

View file

@ -453,16 +453,16 @@ void CameraController::update(float deltaTime) {
if (wmoRenderer) {
glm::vec3 adjusted;
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
// Before blocking, check if there's a floor at the
// destination above current feet (stair step-up).
// Before blocking, check if there's a walkable floor at the
// destination (stair step-up or ramp continuation).
float feetZ = stepPos.z;
float probeZ = feetZ + 2.5f;
auto floorH = wmoRenderer->getFloorHeight(
candidate.x, candidate.y, probeZ);
bool isStair = floorH &&
*floorH > feetZ + 0.1f &&
*floorH <= feetZ + 1.6f;
if (!isStair) {
bool walkable = floorH &&
*floorH >= feetZ - 0.5f &&
*floorH <= feetZ + 1.6f;
if (!walkable) {
candidate.x = adjusted.x;
candidate.y = adjusted.y;
}

View file

@ -2148,6 +2148,21 @@ void M2Renderer::renderSmokeParticles(const Camera& /*camera*/, const glm::mat4&
glEnable(GL_CULL_FACE);
}
void M2Renderer::setInstancePosition(uint32_t instanceId, const glm::vec3& position) {
auto idxIt = instanceIndexById.find(instanceId);
if (idxIt == instanceIndexById.end()) return;
auto& inst = instances[idxIt->second];
inst.position = position;
inst.updateModelMatrix();
auto modelIt = models.find(inst.modelId);
if (modelIt != models.end()) {
glm::vec3 localMin, localMax;
getTightCollisionBounds(modelIt->second, localMin, localMax);
transformAABB(inst.modelMatrix, localMin, localMax, inst.worldBoundsMin, inst.worldBoundsMax);
}
rebuildSpatialIndex();
}
void M2Renderer::removeInstance(uint32_t instanceId) {
for (auto it = instances.begin(); it != instances.end(); ++it) {
if (it->id == instanceId) {

View file

@ -528,6 +528,30 @@ uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position
return instance.id;
}
void WMORenderer::setInstancePosition(uint32_t instanceId, const glm::vec3& position) {
auto idxIt = instanceIndexById.find(instanceId);
if (idxIt == instanceIndexById.end()) return;
auto& inst = instances[idxIt->second];
inst.position = position;
inst.updateModelMatrix();
auto modelIt = loadedModels.find(inst.modelId);
if (modelIt != loadedModels.end()) {
const ModelData& model = modelIt->second;
transformAABB(inst.modelMatrix, model.boundingBoxMin, model.boundingBoxMax,
inst.worldBoundsMin, inst.worldBoundsMax);
inst.worldGroupBounds.clear();
inst.worldGroupBounds.reserve(model.groups.size());
for (const auto& group : model.groups) {
glm::vec3 gMin, gMax;
transformAABB(inst.modelMatrix, group.boundingBoxMin, group.boundingBoxMax, gMin, gMax);
gMin -= glm::vec3(0.5f);
gMax += glm::vec3(0.5f);
inst.worldGroupBounds.emplace_back(gMin, gMax);
}
}
rebuildSpatialIndex();
}
void WMORenderer::removeInstance(uint32_t instanceId) {
auto it = std::find_if(instances.begin(), instances.end(),
[instanceId](const WMOInstance& inst) { return inst.id == instanceId; });

View file

@ -46,6 +46,9 @@ namespace {
} else if (entity->getType() == wowee::game::ObjectType::UNIT) {
auto unit = std::static_pointer_cast<wowee::game::Unit>(entity);
if (!unit->getName().empty()) return unit->getName();
} else if (entity->getType() == wowee::game::ObjectType::GAMEOBJECT) {
auto go = std::static_pointer_cast<wowee::game::GameObject>(entity);
if (!go->getName().empty()) return go->getName();
}
return "Unknown";
}