Add bindpoint support and WMO snap fix

This commit is contained in:
Kelsi 2026-02-08 03:32:00 -08:00
parent 132a6ea3c9
commit 189f4a0a58
7 changed files with 94 additions and 0 deletions

View file

@ -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

View file

@ -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<char>(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) {

View file

@ -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<uint16_t>(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();

View file

@ -26,6 +26,11 @@ std::optional<float> selectReachableFloor(const std::optional<float>& 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