diff --git a/include/game/entity.hpp b/include/game/entity.hpp index 93170ae1..f8e05735 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -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; }; diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 62946ea6..7a61bf8d 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -5,6 +5,7 @@ #include "game/inventory.hpp" #include "game/spell_defines.hpp" #include "game/group_defines.hpp" +#include #include #include #include @@ -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 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 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 pendingNameQueries; std::unordered_map creatureInfoCache; std::unordered_set pendingCreatureQueries; + std::unordered_map gameObjectInfoCache_; + std::unordered_set pendingGameObjectQueries_; // ---- Friend list cache ---- std::unordered_map 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 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 knownSpells; std::unordered_map spellCooldowns; // spellId -> remaining seconds uint8_t castCount = 0; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index c58f83c3..80cd074d 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -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 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 // ============================================================ diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 817ade3b..2574d5f4 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -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() diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index faf8fe85..d176423b 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -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 diff --git a/src/core/application.cpp b/src/core/application.cpp index 9537060c..8be55821 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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(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(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 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 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; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 02aadfd7..07fadad2 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -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(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(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) { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 93a3356a..db84065d 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -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(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(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 diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 5c885e8d..7a891cb4 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -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; } diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 77fc24ad..d74c7d79 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -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) { diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 6cac3948..bce2dcd7 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -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; }); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 9c099791..d75abdc8 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -46,6 +46,9 @@ namespace { } else if (entity->getType() == wowee::game::ObjectType::UNIT) { auto unit = std::static_pointer_cast(entity); if (!unit->getName().empty()) return unit->getName(); + } else if (entity->getType() == wowee::game::ObjectType::GAMEOBJECT) { + auto go = std::static_pointer_cast(entity); + if (!go->getName().empty()) return go->getName(); } return "Unknown"; }