From e5c48dc9b735fe405d11ec53bb2fa9619fbe1366 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Feb 2026 19:44:03 -0800 Subject: [PATCH] Add gameobject interaction and taxi activation --- include/core/application.hpp | 26 ++++ include/game/game_handler.hpp | 12 ++ include/game/opcodes.hpp | 6 + include/game/world_packets.hpp | 18 ++- src/core/application.cpp | 218 +++++++++++++++++++++++++++++++++ src/game/game_handler.cpp | 109 +++++++++-------- src/game/world_packets.cpp | 14 +++ src/ui/game_screen.cpp | 16 ++- 8 files changed, 361 insertions(+), 58 deletions(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index 44b1e19c..9b6532ee 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -85,6 +85,10 @@ private: void despawnOnlineCreature(uint64_t guid); void buildCreatureDisplayLookups(); std::string getModelPathForDisplayId(uint32_t displayId) const; + void spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation); + void despawnOnlineGameObject(uint64_t guid); + void buildGameObjectDisplayLookups(); + std::string getGameObjectModelPathForDisplayId(uint32_t displayId) const; static Application* instance; @@ -151,6 +155,20 @@ private: std::unordered_map displayIdModelCache_; // displayId → modelId (model caching) uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures + // Online gameobject model spawning + struct GameObjectInstanceInfo { + uint32_t modelId = 0; + uint32_t instanceId = 0; + bool isWmo = false; + }; + std::unordered_map gameObjectDisplayIdToPath_; + std::unordered_map gameObjectDisplayIdModelCache_; // displayId → M2 modelId + std::unordered_map gameObjectDisplayIdWmoCache_; // displayId → WMO modelId + std::unordered_map gameObjectInstances_; // guid → instance info + uint32_t nextGameObjectModelId_ = 20000; + uint32_t nextGameObjectWmoModelId_ = 40000; + bool gameObjectLookupsBuilt_ = false; + // Mount model tracking uint32_t mountInstanceId_ = 0; uint32_t mountModelId_ = 0; @@ -167,6 +185,14 @@ private: std::vector pendingCreatureSpawns_; static constexpr int MAX_SPAWNS_PER_FRAME = 2; void processCreatureSpawnQueue(); + + struct PendingGameObjectSpawn { + uint64_t guid; + uint32_t displayId; + float x, y, z, orientation; + }; + std::vector pendingGameObjectSpawns_; + void processGameObjectSpawnQueue(); }; } // namespace core diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 458858cb..57f300fa 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -378,6 +378,15 @@ public: using CreatureDespawnCallback = std::function; void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); } + // GameObject spawn callback (online mode - triggered when gameobject enters view) + // Parameters: guid, displayId, x, y, z (canonical), orientation + using GameObjectSpawnCallback = std::function; + void setGameObjectSpawnCallback(GameObjectSpawnCallback cb) { gameObjectSpawnCallback_ = std::move(cb); } + + // GameObject despawn callback (online mode - triggered when gameobject leaves view) + using GameObjectDespawnCallback = std::function; + void setGameObjectDespawnCallback(GameObjectDespawnCallback cb) { gameObjectDespawnCallback_ = std::move(cb); } + // Faction hostility map (populated from FactionTemplate.dbc by Application) void setFactionHostileMap(std::unordered_map map) { factionHostileMap_ = std::move(map); } @@ -417,6 +426,7 @@ public: // NPC Gossip void interactWithNpc(uint64_t guid); + void interactWithGameObject(uint64_t guid); void selectGossipOption(uint32_t optionId); void selectGossipQuest(uint32_t questId); void acceptQuest(); @@ -792,6 +802,8 @@ private: CreatureSpawnCallback creatureSpawnCallback_; CreatureDespawnCallback creatureDespawnCallback_; CreatureMoveCallback creatureMoveCallback_; + GameObjectSpawnCallback gameObjectSpawnCallback_; + GameObjectDespawnCallback gameObjectDespawnCallback_; std::vector knownSpells; std::unordered_map spellCooldowns; // spellId -> remaining seconds uint8_t castCount = 0; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index c3b39eb9..b8d89dcc 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -193,6 +193,9 @@ enum class Opcode : uint16_t { SMSG_LOOT_MONEY_NOTIFY = 0x163, SMSG_LOOT_CLEAR_MONEY = 0x165, + // ---- Phase 5: Taxi / Flight Paths ---- + CMSG_ACTIVATETAXI = 0x19D, + // ---- Phase 5: NPC Gossip ---- CMSG_GOSSIP_HELLO = 0x17B, CMSG_GOSSIP_SELECT_OPTION = 0x17C, @@ -200,6 +203,9 @@ enum class Opcode : uint16_t { SMSG_GOSSIP_COMPLETE = 0x17E, SMSG_NPC_TEXT_UPDATE = 0x180, + // ---- Phase 5: GameObject ---- + CMSG_GAMEOBJECT_USE = 0x01B, + // ---- Phase 5: Quests ---- CMSG_QUESTGIVER_STATUS_QUERY = 0x182, SMSG_QUESTGIVER_STATUS = 0x183, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 750e7ce9..643364be 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1665,8 +1665,10 @@ struct ShowTaxiNodesData { uint32_t nearestNode = 0; // Taxi node player is at uint32_t nodeMask[TLK_TAXI_MASK_SIZE] = {}; bool isNodeKnown(uint32_t nodeId) const { - uint32_t idx = nodeId / 32; - uint32_t bit = nodeId % 32; + if (nodeId == 0) return false; + uint32_t bitIndex = nodeId - 1; + uint32_t idx = bitIndex / 32; + uint32_t bit = bitIndex % 32; return idx < TLK_TAXI_MASK_SIZE && (nodeMask[idx] & (1u << bit)); } }; @@ -1694,6 +1696,18 @@ public: static network::Packet build(uint64_t npcGuid, const std::vector& pathNodes); }; +/** CMSG_ACTIVATETAXI packet builder */ +class ActivateTaxiPacket { +public: + static network::Packet build(uint64_t npcGuid, uint32_t srcNode, uint32_t destNode); +}; + +/** CMSG_GAMEOBJECT_USE packet builder */ +class GameObjectUsePacket { +public: + static network::Packet build(uint64_t guid); +}; + /** CMSG_REPOP_REQUEST packet builder */ class RepopRequestPacket { public: diff --git a/src/core/application.cpp b/src/core/application.cpp index 2017122b..43d45702 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -18,6 +18,7 @@ #include "rendering/weather.hpp" #include "rendering/character_renderer.hpp" #include "rendering/wmo_renderer.hpp" +#include "rendering/m2_renderer.hpp" #include "rendering/minimap.hpp" #include "rendering/loading_screen.hpp" #include "audio/music_manager.hpp" @@ -39,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -385,6 +387,7 @@ void Application::update(float deltaTime) { } // Process deferred online creature spawns (throttled) processCreatureSpawnQueue(); + processGameObjectSpawnQueue(); processPendingMount(); if (npcManager && renderer && renderer->getCharacterRenderer()) { npcManager->update(deltaTime, renderer->getCharacterRenderer()); @@ -560,6 +563,16 @@ void Application::setupUICallbacks() { despawnOnlineCreature(guid); }); + // GameObject spawn callback (online mode) - spawn static models (mailboxes, etc.) + gameHandler->setGameObjectSpawnCallback([this](uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { + pendingGameObjectSpawns_.push_back({guid, displayId, x, y, z, orientation}); + }); + + // GameObject despawn callback (online mode) - remove static models + gameHandler->setGameObjectDespawnCallback([this](uint64_t guid) { + despawnOnlineGameObject(guid); + }); + // Mount callback (online mode) - defer heavy model load to next frame gameHandler->setMountCallback([this](uint32_t mountDisplayId) { if (mountDisplayId == 0) { @@ -1663,6 +1676,40 @@ std::string Application::getModelPathForDisplayId(uint32_t displayId) const { return itPath->second; } +void Application::buildGameObjectDisplayLookups() { + if (gameObjectLookupsBuilt_ || !assetManager || !assetManager->isInitialized()) return; + + LOG_INFO("Building gameobject display lookups from DBC files"); + + // GameObjectDisplayInfo.dbc structure (3.3.5a): + // Col 0: ID (displayId) + // Col 1: ModelName + if (auto godi = assetManager->loadDBC("GameObjectDisplayInfo.dbc"); godi && godi->isLoaded()) { + for (uint32_t i = 0; i < godi->getRecordCount(); i++) { + uint32_t displayId = godi->getUInt32(i, 0); + std::string modelName = godi->getString(i, 1); + if (modelName.empty()) continue; + if (modelName.size() >= 4) { + std::string ext = modelName.substr(modelName.size() - 4); + for (char& c : ext) c = static_cast(std::tolower(c)); + if (ext == ".mdx") { + modelName = modelName.substr(0, modelName.size() - 4) + ".m2"; + } + } + gameObjectDisplayIdToPath_[displayId] = modelName; + } + LOG_INFO("Loaded ", gameObjectDisplayIdToPath_.size(), " gameobject display mappings"); + } + + gameObjectLookupsBuilt_ = true; +} + +std::string Application::getGameObjectModelPathForDisplayId(uint32_t displayId) const { + auto it = gameObjectDisplayIdToPath_.find(displayId); + if (it == gameObjectDisplayIdToPath_.end()) return ""; + return it->second; +} + bool Application::getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const { if (!renderer || !renderer->getCharacterRenderer()) return false; uint32_t instanceId = 0; @@ -2142,6 +2189,144 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); } +void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) { + if (!renderer || !assetManager) return; + + if (!gameObjectLookupsBuilt_) { + buildGameObjectDisplayLookups(); + } + if (!gameObjectLookupsBuilt_) return; + + if (gameObjectInstances_.count(guid)) return; + + std::string modelPath = getGameObjectModelPathForDisplayId(displayId); + if (modelPath.empty()) { + LOG_WARNING("No model path for gameobject displayId ", displayId, " (guid 0x", std::hex, guid, std::dec, ")"); + return; + } + + std::string lowerPath = modelPath; + std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + 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 + glm::radians(90.0f); + + if (isWmo) { + auto* wmoRenderer = renderer->getWMORenderer(); + if (!wmoRenderer) return; + + uint32_t modelId = 0; + auto itCache = gameObjectDisplayIdWmoCache_.find(displayId); + if (itCache != gameObjectDisplayIdWmoCache_.end()) { + modelId = itCache->second; + } else { + modelId = nextGameObjectWmoModelId_++; + auto wmoData = assetManager->readFile(modelPath); + if (wmoData.empty()) { + LOG_WARNING("Failed to read gameobject WMO: ", modelPath); + return; + } + + 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); + } + } + } + + 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); + return; + } + + gameObjectInstances_[guid] = {modelId, instanceId, true}; + } else { + auto* m2Renderer = renderer->getM2Renderer(); + if (!m2Renderer) return; + + uint32_t modelId = 0; + auto itCache = gameObjectDisplayIdModelCache_.find(displayId); + if (itCache != gameObjectDisplayIdModelCache_.end()) { + modelId = itCache->second; + } else { + modelId = nextGameObjectModelId_++; + + auto m2Data = assetManager->readFile(modelPath); + if (m2Data.empty()) { + LOG_WARNING("Failed to read gameobject M2: ", modelPath); + return; + } + + pipeline::M2Model model = pipeline::M2Loader::load(m2Data); + if (model.vertices.empty()) { + LOG_WARNING("Failed to parse gameobject M2: ", modelPath); + return; + } + + std::string skinPath = modelPath.substr(0, modelPath.size() - 3) + "00.skin"; + auto skinData = assetManager->readFile(skinPath); + if (!skinData.empty()) { + pipeline::M2Loader::loadSkin(skinData, model); + } + + if (!m2Renderer->loadModel(model, modelId)) { + LOG_WARNING("Failed to load gameobject model: ", modelPath); + return; + } + + gameObjectDisplayIdModelCache_[displayId] = modelId; + } + + uint32_t instanceId = m2Renderer->createInstance(modelId, renderPos, + glm::vec3(0.0f, 0.0f, renderYaw), 1.0f); + if (instanceId == 0) { + LOG_WARNING("Failed to create gameobject instance for guid 0x", std::hex, guid, std::dec); + return; + } + + gameObjectInstances_[guid] = {modelId, instanceId, false}; + } + + LOG_INFO("Spawned gameobject: guid=0x", std::hex, guid, std::dec, + " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); +} + void Application::processCreatureSpawnQueue() { if (pendingCreatureSpawns_.empty()) return; @@ -2154,6 +2339,18 @@ void Application::processCreatureSpawnQueue() { } } +void Application::processGameObjectSpawnQueue() { + if (pendingGameObjectSpawns_.empty()) return; + + int spawned = 0; + while (!pendingGameObjectSpawns_.empty() && spawned < MAX_SPAWNS_PER_FRAME) { + auto& s = pendingGameObjectSpawns_.front(); + spawnOnlineGameObject(s.guid, s.displayId, s.x, s.y, s.z, s.orientation); + pendingGameObjectSpawns_.erase(pendingGameObjectSpawns_.begin()); + spawned++; + } +} + void Application::processPendingMount() { if (pendingMountDisplayId_ == 0) return; uint32_t mountDisplayId = pendingMountDisplayId_; @@ -2312,5 +2509,26 @@ void Application::despawnOnlineCreature(uint64_t guid) { LOG_INFO("Despawned creature: guid=0x", std::hex, guid, std::dec); } +void Application::despawnOnlineGameObject(uint64_t guid) { + auto it = gameObjectInstances_.find(guid); + if (it == gameObjectInstances_.end()) return; + + if (renderer) { + if (it->second.isWmo) { + if (auto* wmoRenderer = renderer->getWMORenderer()) { + wmoRenderer->removeInstance(it->second.instanceId); + } + } else { + if (auto* m2Renderer = renderer->getM2Renderer()) { + m2Renderer->removeInstance(it->second.instanceId); + } + } + } + + gameObjectInstances_.erase(it); + + LOG_INFO("Despawned gameobject: guid=0x", std::hex, guid, std::dec); +} + } // namespace core } // namespace wowee diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0e870b2d..ca4f7382 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -203,7 +203,7 @@ void GameHandler::update(float deltaTime) { } } - // Close vendor/gossip window if player walks too far from NPC + // Close vendor/gossip/taxi window if player walks too far from NPC if (vendorWindowOpen && currentVendorItems.vendorGuid != 0) { auto npc = entityManager.getEntity(currentVendorItems.vendorGuid); if (npc) { @@ -216,6 +216,30 @@ void GameHandler::update(float deltaTime) { } } } + if (gossipWindowOpen && currentGossip.npcGuid != 0) { + auto npc = entityManager.getEntity(currentGossip.npcGuid); + if (npc) { + float dx = movementInfo.x - npc->getX(); + float dy = movementInfo.y - npc->getY(); + float dist = std::sqrt(dx * dx + dy * dy); + if (dist > 15.0f) { + closeGossip(); + LOG_INFO("Gossip closed: walked too far from NPC"); + } + } + } + if (taxiWindowOpen_ && taxiNpcGuid_ != 0) { + auto npc = entityManager.getEntity(taxiNpcGuid_); + if (npc) { + float dx = movementInfo.x - npc->getX(); + float dy = movementInfo.y - npc->getY(); + float dist = std::sqrt(dx * dx + dy * dy); + if (dist > 15.0f) { + closeTaxi(); + LOG_INFO("Taxi window closed: walked too far from NPC"); + } + } + } // Update entity movement interpolation (keeps targeting in sync with visuals) for (auto& [guid, entity] : entityManager.getEntities()) { @@ -1231,9 +1255,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { for (uint64_t guid : data.outOfRangeGuids) { if (entityManager.hasEntity(guid)) { LOG_INFO("Entity went out of range: 0x", std::hex, guid, std::dec); - // Trigger creature despawn callback before removing entity - if (creatureDespawnCallback_) { - creatureDespawnCallback_(guid); + // Trigger despawn callbacks before removing entity + auto entity = entityManager.getEntity(guid); + if (entity) { + if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) { + creatureDespawnCallback_(guid); + } else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) { + gameObjectDespawnCallback_(guid); + } } entityManager.removeEntity(guid); } @@ -1355,6 +1384,18 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } } } + // Extract displayId 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); + } + if (go->getDisplayId() != 0 && gameObjectSpawnCallback_) { + gameObjectSpawnCallback_(block.guid, go->getDisplayId(), + go->getX(), go->getY(), go->getZ(), go->getOrientation()); + } + } // Track online item objects if (block.objectType == ObjectType::ITEM) { auto entryIt = block.fields.find(3); // OBJECT_FIELD_ENTRY @@ -3524,6 +3565,12 @@ void GameHandler::interactWithNpc(uint64_t guid) { socket->send(packet); } +void GameHandler::interactWithGameObject(uint64_t guid) { + if (state != WorldState::IN_WORLD || !socket) return; + auto packet = GameObjectUsePacket::build(guid); + socket->send(packet); +} + void GameHandler::selectGossipOption(uint32_t optionId) { if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return; auto packet = GossipSelectOptionPacket::build(currentGossip.npcGuid, currentGossip.menuId, optionId); @@ -4069,7 +4116,7 @@ void GameHandler::handleShowTaxiNodes(network::Packet& packet) { if (newBits == 0) continue; for (uint32_t bit = 0; bit < 32; ++bit) { if (newBits & (1u << bit)) { - uint32_t nodeId = i * 32 + bit; + uint32_t nodeId = i * 32 + bit + 1; auto it = taxiNodes_.find(nodeId); if (it != taxiNodes_.end()) { addSystemChatMessage("Discovered flight path: " + it->second.name); @@ -4119,13 +4166,11 @@ void GameHandler::buildTaxiCostMap() { uint32_t startNode = currentTaxiData_.nearestNode; if (startNode == 0) return; - // Build adjacency list with costs from known edges + // Build adjacency list with costs from all edges (path may traverse unknown nodes) struct AdjEntry { uint32_t node; uint32_t cost; }; std::unordered_map> adj; for (const auto& edge : taxiPathEdges_) { - if (currentTaxiData_.isNodeKnown(edge.fromNode) && currentTaxiData_.isNodeKnown(edge.toNode)) { - adj[edge.fromNode].push_back({edge.toNode, edge.cost}); - } + adj[edge.fromNode].push_back({edge.toNode, edge.cost}); } // BFS from startNode, accumulating costs along the path @@ -4156,51 +4201,7 @@ void GameHandler::activateTaxi(uint32_t destNodeId) { uint32_t startNode = currentTaxiData_.nearestNode; if (startNode == 0 || destNodeId == 0 || startNode == destNodeId) return; - // BFS to find path from startNode to destNodeId through known nodes - // Build adjacency list from edges where both nodes are known - std::unordered_map> adj; - for (const auto& edge : taxiPathEdges_) { - if (currentTaxiData_.isNodeKnown(edge.fromNode) && currentTaxiData_.isNodeKnown(edge.toNode)) { - adj[edge.fromNode].push_back(edge.toNode); - } - } - - // BFS - std::unordered_map parent; - std::deque queue; - queue.push_back(startNode); - parent[startNode] = startNode; - - bool found = false; - while (!queue.empty()) { - uint32_t cur = queue.front(); - queue.pop_front(); - if (cur == destNodeId) { found = true; break; } - for (uint32_t next : adj[cur]) { - if (parent.find(next) == parent.end()) { - parent[next] = cur; - queue.push_back(next); - } - } - } - - if (!found) { - LOG_WARNING("No taxi path found from node ", startNode, " to ", destNodeId); - addSystemChatMessage("No flight path available to that destination."); - return; - } - - // Reconstruct path - std::vector path; - for (uint32_t n = destNodeId; n != startNode; n = parent[n]) { - path.push_back(n); - } - path.push_back(startNode); - std::reverse(path.begin(), path.end()); - - LOG_INFO("Taxi path: ", path.size(), " nodes, from ", startNode, " to ", destNodeId); - - auto pkt = ActivateTaxiExpressPacket::build(taxiNpcGuid_, path); + auto pkt = ActivateTaxiPacket::build(taxiNpcGuid_, startNode, destNodeId); socket->send(pkt); } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 4c0b95c2..86522bcb 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2634,5 +2634,19 @@ network::Packet ActivateTaxiExpressPacket::build(uint64_t npcGuid, const std::ve return packet; } +network::Packet ActivateTaxiPacket::build(uint64_t npcGuid, uint32_t srcNode, uint32_t destNode) { + network::Packet packet(static_cast(Opcode::CMSG_ACTIVATETAXI)); + packet.writeUInt64(npcGuid); + packet.writeUInt32(srcNode); + packet.writeUInt32(destNode); + return packet; +} + +network::Packet GameObjectUsePacket::build(uint64_t guid) { + network::Packet packet(static_cast(Opcode::CMSG_GAMEOBJECT_USE)); + packet.writeUInt64(guid); + return packet; +} + } // namespace game } // namespace wowee diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 538fad28..9391085c 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -608,7 +608,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { const uint64_t myGuid = gameHandler.getPlayerGuid(); for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { auto t = entity->getType(); - if (t != game::ObjectType::UNIT && t != game::ObjectType::PLAYER) continue; + if (t != game::ObjectType::UNIT && + t != game::ObjectType::PLAYER && + t != game::ObjectType::GAMEOBJECT) continue; if (guid == myGuid) continue; // Don't target self glm::vec3 hitCenter; @@ -625,6 +627,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { hitRadius = 0.5f; heightOffset = 0.3f; } + } else if (t == game::ObjectType::GAMEOBJECT) { + hitRadius = 1.2f; + heightOffset = 0.8f; } hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ())); hitCenter.z += heightOffset; @@ -669,7 +674,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { const uint64_t myGuid = gameHandler.getPlayerGuid(); for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) { auto t = entity->getType(); - if (t != game::ObjectType::UNIT && t != game::ObjectType::PLAYER) continue; + if (t != game::ObjectType::UNIT && + t != game::ObjectType::PLAYER && + t != game::ObjectType::GAMEOBJECT) continue; if (guid == myGuid) continue; glm::vec3 hitCenter; float hitRadius = 0.0f; @@ -683,6 +690,9 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { hitRadius = 0.5f; heightOffset = 0.3f; } + } else if (t == game::ObjectType::GAMEOBJECT) { + hitRadius = 1.2f; + heightOffset = 0.8f; } hitCenter = core::coords::canonicalToRender( glm::vec3(entity->getX(), entity->getY(), entity->getZ())); @@ -717,6 +727,8 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { gameHandler.interactWithNpc(target->getGuid()); } } + } else if (target->getType() == game::ObjectType::GAMEOBJECT) { + gameHandler.interactWithGameObject(target->getGuid()); } else if (target->getType() == game::ObjectType::PLAYER) { // Right-click another player could start attack in PvP context }