diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 6db967d2..9a095a5d 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -373,6 +373,8 @@ public: void unstuck(); void setUnstuckGyCallback(UnstuckCallback cb) { unstuckGyCallback_ = std::move(cb); } void unstuckGy(); + using BindPointCallback = std::function; + void setBindPointCallback(BindPointCallback cb) { bindPointCallback_ = std::move(cb); } // Creature spawn callback (online mode - triggered when creature enters view) // Parameters: guid, displayId, x, y, z (canonical), orientation @@ -837,6 +839,7 @@ private: WorldEntryCallback worldEntryCallback_; UnstuckCallback unstuckCallback_; UnstuckCallback unstuckGyCallback_; + BindPointCallback bindPointCallback_; CreatureSpawnCallback creatureSpawnCallback_; CreatureDespawnCallback creatureDespawnCallback_; CreatureMoveCallback creatureMoveCallback_; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 108a1fb2..78b31498 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -126,6 +126,7 @@ enum class Opcode : uint16_t { CMSG_GAMEOBJECT_QUERY = 0x05E, SMSG_GAMEOBJECT_QUERY_RESPONSE = 0x05F, CMSG_SET_ACTIVE_MOVER = 0x26A, + CMSG_BINDER_ACTIVATE = 0x1B2, // ---- XP ---- SMSG_LOG_XPGAIN = 0x1D0, @@ -272,6 +273,7 @@ enum class Opcode : uint16_t { // ---- Battleground ---- SMSG_BATTLEFIELD_PORT_DENIED = 0x014B, SMSG_REMOVED_FROM_PVP_QUEUE = 0x0170, + SMSG_BINDPOINTUPDATE = 0x01B3, CMSG_BATTLEFIELD_LIST = 0x023C, SMSG_BATTLEFIELD_LIST = 0x023D, CMSG_BATTLEFIELD_JOIN = 0x023E, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 0d2cc5b5..91200088 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1558,6 +1558,30 @@ public: static bool parse(network::Packet& packet, GossipMessageData& data); }; +// ============================================================ +// Bind Point (Hearthstone) +// ============================================================ + +struct BindPointUpdateData { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + uint32_t mapId = 0; + uint32_t zoneId = 0; +}; + +/** CMSG_BINDER_ACTIVATE packet builder */ +class BinderActivatePacket { +public: + static network::Packet build(uint64_t npcGuid); +}; + +/** SMSG_BINDPOINTUPDATE parser */ +class BindPointUpdateParser { +public: + static bool parse(network::Packet& packet, BindPointUpdateData& data); +}; + /** CMSG_QUESTGIVER_QUERY_QUEST packet builder */ class QuestgiverQueryQuestPacket { public: diff --git a/src/core/application.cpp b/src/core/application.cpp index 76a022df..04504953 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -645,6 +645,15 @@ void Application::setupUICallbacks() { cc->reset(); }); + // Bind point update (innkeeper) + gameHandler->setBindPointCallback([this](uint32_t mapId, float x, float y, float z) { + if (!renderer || !renderer->getCameraController()) return; + glm::vec3 canonical(x, y, z); + glm::vec3 renderPos = core::coords::canonicalToRender(canonical); + renderer->getCameraController()->setDefaultSpawn(renderPos, 0.0f, 15.0f); + LOG_INFO("Bindpoint set: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")"); + }); + // Faction hostility map is built in buildFactionHostilityMap() when character enters world // Creature spawn callback (online mode) - spawn creature models diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 2afa7acb..9f47d6a8 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -577,6 +577,22 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_GOSSIP_MESSAGE: handleGossipMessage(packet); break; + case Opcode::SMSG_BINDPOINTUPDATE: { + BindPointUpdateData data; + if (BindPointUpdateParser::parse(packet, data)) { + LOG_INFO("Bindpoint updated: mapId=", data.mapId, + " pos=(", data.x, ", ", data.y, ", ", data.z, ")"); + if (bindPointCallback_) { + glm::vec3 canonical = core::coords::serverToCanonical( + glm::vec3(data.x, data.y, data.z)); + bindPointCallback_(data.mapId, canonical.x, canonical.y, canonical.z); + } + addSystemChatMessage("Your home has been set."); + } else { + LOG_WARNING("Failed to parse SMSG_BINDPOINTUPDATE"); + } + break; + } case Opcode::SMSG_GOSSIP_COMPLETE: handleGossipComplete(packet); break; @@ -4194,6 +4210,21 @@ void GameHandler::selectGossipOption(uint32_t optionId) { if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return; auto packet = GossipSelectOptionPacket::build(currentGossip.npcGuid, currentGossip.menuId, optionId); socket->send(packet); + + // If this is an innkeeper "make this inn your home" option, send binder activate. + for (const auto& opt : currentGossip.options) { + if (opt.id != optionId) continue; + std::string text = opt.text; + std::transform(text.begin(), text.end(), text.begin(), + [](unsigned char c){ return static_cast(std::tolower(c)); }); + if (text.find("make this inn your home") != std::string::npos || + text.find("set your home") != std::string::npos) { + auto bindPkt = BinderActivatePacket::build(currentGossip.npcGuid); + socket->send(bindPkt); + LOG_INFO("Sent CMSG_BINDER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec); + } + break; + } } void GameHandler::selectGossipQuest(uint32_t questId) { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index a1833dd2..4835fc2e 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2450,6 +2450,26 @@ bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data return true; } +// ============================================================ +// Bind Point (Hearthstone) +// ============================================================ + +network::Packet BinderActivatePacket::build(uint64_t npcGuid) { + network::Packet pkt(static_cast(Opcode::CMSG_BINDER_ACTIVATE)); + pkt.writeUInt64(npcGuid); + return pkt; +} + +bool BindPointUpdateParser::parse(network::Packet& packet, BindPointUpdateData& data) { + if (packet.getSize() < 20) return false; + data.x = packet.readFloat(); + data.y = packet.readFloat(); + data.z = packet.readFloat(); + data.mapId = packet.readUInt32(); + data.zoneId = packet.readUInt32(); + return true; +} + bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsData& data) { if (packet.getSize() - packet.getReadPos() < 20) return false; data.npcGuid = packet.readUInt64(); diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 9d075725..d0032686 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -26,6 +26,11 @@ std::optional selectReachableFloor(const std::optional& terrainH, if (terrainH && *terrainH <= refZ + maxStepUp) reachTerrain = terrainH; if (wmoH && *wmoH <= refZ + maxStepUp) reachWmo = wmoH; + // Avoid snapping up to higher WMO floors when entering buildings. + if (reachTerrain && reachWmo && *reachWmo > refZ + 2.0f) { + return reachTerrain; + } + if (reachTerrain && reachWmo) { // Both available: prefer the one closest to the player's feet. // This prevents tunnels/caves from snapping the player up to the