From 0874f4f2393b771c829542c31ec222c59fe548ba Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Feb 2026 18:33:14 -0800 Subject: [PATCH] Fix mount stability, speed parsing, combat dismount, and self-targeting - Fix SMSG_FORCE_RUN_SPEED_CHANGE parsing (missing uint32 field caused garbage speed) - Always send speed ACK to prevent server stall, even on invalid values - Defer mount model loading to next frame to avoid render-loop hang - Compute mount height from tight vertex bounds instead of M2 header bounds - Dismount when entering combat or casting spells while mounted - Prevent auto-attacking yourself when self-targeted - Leave combat when 40+ yards from target, close vendor at 15+ yards - Pre-open X11 display for reliable mouse release in signal handlers --- include/core/application.hpp | 2 + include/game/opcodes.hpp | 1 + src/core/application.cpp | 287 +++++++++++++++------------- src/game/game_handler.cpp | 77 +++++++- src/main.cpp | 17 +- src/rendering/camera_controller.cpp | 4 +- 6 files changed, 242 insertions(+), 146 deletions(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index 2b754ffb..44b1e19c 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -154,6 +154,8 @@ private: // Mount model tracking uint32_t mountInstanceId_ = 0; uint32_t mountModelId_ = 0; + uint32_t pendingMountDisplayId_ = 0; // Deferred mount load (0 = none pending) + void processPendingMount(); bool creatureLookupsBuilt_ = false; // Deferred creature spawn queue (throttles spawning to avoid hangs) diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index 4c705c0c..c3b39eb9 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -243,6 +243,7 @@ enum class Opcode : uint16_t { // ---- Speed Changes ---- SMSG_FORCE_RUN_SPEED_CHANGE = 0x00E2, + CMSG_FORCE_RUN_SPEED_CHANGE_ACK = 0x00E3, // ---- Mount ---- CMSG_CANCEL_MOUNT_AURA = 0x0375, diff --git a/src/core/application.cpp b/src/core/application.cpp index 906f35ec..2017122b 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -385,6 +385,7 @@ void Application::update(float deltaTime) { } // Process deferred online creature spawns (throttled) processCreatureSpawnQueue(); + processPendingMount(); if (npcManager && renderer && renderer->getCharacterRenderer()) { npcManager->update(deltaTime, renderer->getCharacterRenderer()); } @@ -559,148 +560,22 @@ void Application::setupUICallbacks() { despawnOnlineCreature(guid); }); - // Mount callback (online mode) - load/destroy mount model + // Mount callback (online mode) - defer heavy model load to next frame gameHandler->setMountCallback([this](uint32_t mountDisplayId) { - if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return; - auto* charRenderer = renderer->getCharacterRenderer(); - if (mountDisplayId == 0) { - // Dismount: remove mount instance and model - if (mountInstanceId_ != 0) { - charRenderer->removeInstance(mountInstanceId_); + // Dismount is instant (no loading needed) + if (renderer && renderer->getCharacterRenderer() && mountInstanceId_ != 0) { + renderer->getCharacterRenderer()->removeInstance(mountInstanceId_); mountInstanceId_ = 0; } mountModelId_ = 0; - renderer->clearMount(); + pendingMountDisplayId_ = 0; + if (renderer) renderer->clearMount(); LOG_INFO("Dismounted"); return; } - - // Mount: load mount model - std::string m2Path = getModelPathForDisplayId(mountDisplayId); - if (m2Path.empty()) { - LOG_WARNING("No model path for mount displayId ", mountDisplayId); - return; - } - - // Check model cache - uint32_t modelId = 0; - bool modelCached = false; - auto cacheIt = displayIdModelCache_.find(mountDisplayId); - if (cacheIt != displayIdModelCache_.end()) { - modelId = cacheIt->second; - modelCached = true; - } else { - modelId = nextCreatureModelId_++; - - auto m2Data = assetManager->readFile(m2Path); - if (m2Data.empty()) { - LOG_WARNING("Failed to read mount M2: ", m2Path); - return; - } - - pipeline::M2Model model = pipeline::M2Loader::load(m2Data); - if (model.vertices.empty()) { - LOG_WARNING("Failed to parse mount M2: ", m2Path); - return; - } - - // Load skin file - std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin"; - auto skinData = assetManager->readFile(skinPath); - if (!skinData.empty()) { - pipeline::M2Loader::loadSkin(skinData, model); - } - - // Load external .anim files - std::string basePath = m2Path.substr(0, m2Path.size() - 3); - for (uint32_t si = 0; si < model.sequences.size(); si++) { - if (!(model.sequences[si].flags & 0x20)) { - char animFileName[256]; - snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim", - basePath.c_str(), model.sequences[si].id, model.sequences[si].variationIndex); - auto animData = assetManager->readFile(animFileName); - if (!animData.empty()) { - pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model); - } - } - } - - if (!charRenderer->loadModel(model, modelId)) { - LOG_WARNING("Failed to load mount model: ", m2Path); - return; - } - - displayIdModelCache_[mountDisplayId] = modelId; - } - - // Apply creature skin textures from CreatureDisplayInfo.dbc - if (!modelCached) { - auto itDisplayData = displayDataMap_.find(mountDisplayId); - if (itDisplayData != displayDataMap_.end()) { - const auto& dispData = itDisplayData->second; - const auto* modelData = charRenderer->getModelData(modelId); - if (modelData) { - std::string modelDir; - size_t lastSlash = m2Path.find_last_of("\\/"); - if (lastSlash != std::string::npos) { - modelDir = m2Path.substr(0, lastSlash + 1); - } - for (size_t ti = 0; ti < modelData->textures.size(); ti++) { - const auto& tex = modelData->textures[ti]; - std::string texPath; - if (tex.type == 11 && !dispData.skin1.empty()) { - texPath = modelDir + dispData.skin1 + ".blp"; - } else if (tex.type == 12 && !dispData.skin2.empty()) { - texPath = modelDir + dispData.skin2 + ".blp"; - } else if (tex.type == 13 && !dispData.skin3.empty()) { - texPath = modelDir + dispData.skin3 + ".blp"; - } - if (!texPath.empty()) { - GLuint skinTex = charRenderer->loadTexture(texPath); - if (skinTex != 0) { - charRenderer->setModelTexture(modelId, static_cast(ti), skinTex); - } - } - } - } - } - } - - mountModelId_ = modelId; - - // Create mount instance at player position - glm::vec3 mountPos = renderer->getCharacterPosition(); - float yawRad = glm::radians(renderer->getCharacterYaw()); - uint32_t instanceId = charRenderer->createInstance(modelId, mountPos, - glm::vec3(0.0f, 0.0f, yawRad), 1.0f); - - if (instanceId == 0) { - LOG_WARNING("Failed to create mount instance"); - return; - } - - mountInstanceId_ = instanceId; - - // Compute height offset — place player above mount's back - const auto* modelData = charRenderer->getModelData(modelId); - float heightOffset = 1.2f; // Default fallback - if (modelData) { - // No coord swizzle in character renderer, so Z is up in model space too. - // Use the top of the bounding box as the saddle height. - float topZ = modelData->boundMax.z; - if (topZ > 0.1f) { - heightOffset = topZ * 0.85f; - } - LOG_INFO("Mount bounds: min=(", modelData->boundMin.x, ",", modelData->boundMin.y, ",", modelData->boundMin.z, - ") max=(", modelData->boundMax.x, ",", modelData->boundMax.y, ",", modelData->boundMax.z, - ") radius=", modelData->boundRadius, " → heightOffset=", heightOffset); - } - - renderer->setMounted(instanceId, heightOffset); - charRenderer->playAnimation(instanceId, 0, true); // Idle animation - - LOG_INFO("Mounted: displayId=", mountDisplayId, " model=", m2Path, " heightOffset=", heightOffset); + // Queue the mount for processing in the next update() frame + pendingMountDisplayId_ = mountDisplayId; }); // Creature move callback (online mode) - update creature positions @@ -2279,6 +2154,150 @@ void Application::processCreatureSpawnQueue() { } } +void Application::processPendingMount() { + if (pendingMountDisplayId_ == 0) return; + uint32_t mountDisplayId = pendingMountDisplayId_; + pendingMountDisplayId_ = 0; + LOG_INFO("processPendingMount: loading displayId ", mountDisplayId); + + if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return; + auto* charRenderer = renderer->getCharacterRenderer(); + + std::string m2Path = getModelPathForDisplayId(mountDisplayId); + if (m2Path.empty()) { + LOG_WARNING("No model path for mount displayId ", mountDisplayId); + return; + } + + // Check model cache + uint32_t modelId = 0; + bool modelCached = false; + auto cacheIt = displayIdModelCache_.find(mountDisplayId); + if (cacheIt != displayIdModelCache_.end()) { + modelId = cacheIt->second; + modelCached = true; + } else { + modelId = nextCreatureModelId_++; + + auto m2Data = assetManager->readFile(m2Path); + if (m2Data.empty()) { + LOG_WARNING("Failed to read mount M2: ", m2Path); + return; + } + + pipeline::M2Model model = pipeline::M2Loader::load(m2Data); + if (model.vertices.empty()) { + LOG_WARNING("Failed to parse mount M2: ", m2Path); + return; + } + + // Load skin file + std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin"; + auto skinData = assetManager->readFile(skinPath); + if (!skinData.empty()) { + pipeline::M2Loader::loadSkin(skinData, model); + } + + // Load external .anim files (only idle + run needed for mounts) + std::string basePath = m2Path.substr(0, m2Path.size() - 3); + for (uint32_t si = 0; si < model.sequences.size(); si++) { + if (!(model.sequences[si].flags & 0x20)) { + uint32_t animId = model.sequences[si].id; + // Only load stand(0), walk(4), run(5) anims to avoid hang + if (animId != 0 && animId != 4 && animId != 5) continue; + char animFileName[256]; + snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim", + basePath.c_str(), animId, model.sequences[si].variationIndex); + auto animData = assetManager->readFile(animFileName); + if (!animData.empty()) { + pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model); + } + } + } + + if (!charRenderer->loadModel(model, modelId)) { + LOG_WARNING("Failed to load mount model: ", m2Path); + return; + } + + displayIdModelCache_[mountDisplayId] = modelId; + } + + // Apply creature skin textures from CreatureDisplayInfo.dbc + if (!modelCached) { + auto itDisplayData = displayDataMap_.find(mountDisplayId); + if (itDisplayData != displayDataMap_.end()) { + const auto& dispData = itDisplayData->second; + const auto* md = charRenderer->getModelData(modelId); + if (md) { + std::string modelDir; + size_t lastSlash = m2Path.find_last_of("\\/"); + if (lastSlash != std::string::npos) { + modelDir = m2Path.substr(0, lastSlash + 1); + } + for (size_t ti = 0; ti < md->textures.size(); ti++) { + const auto& tex = md->textures[ti]; + std::string texPath; + if (tex.type == 11 && !dispData.skin1.empty()) { + texPath = modelDir + dispData.skin1 + ".blp"; + } else if (tex.type == 12 && !dispData.skin2.empty()) { + texPath = modelDir + dispData.skin2 + ".blp"; + } else if (tex.type == 13 && !dispData.skin3.empty()) { + texPath = modelDir + dispData.skin3 + ".blp"; + } + if (!texPath.empty()) { + GLuint skinTex = charRenderer->loadTexture(texPath); + if (skinTex != 0) { + charRenderer->setModelTexture(modelId, static_cast(ti), skinTex); + } + } + } + } + } + } + + mountModelId_ = modelId; + + // Create mount instance at player position + glm::vec3 mountPos = renderer->getCharacterPosition(); + float yawRad = glm::radians(renderer->getCharacterYaw()); + uint32_t instanceId = charRenderer->createInstance(modelId, mountPos, + glm::vec3(0.0f, 0.0f, yawRad), 1.0f); + + if (instanceId == 0) { + LOG_WARNING("Failed to create mount instance"); + return; + } + + mountInstanceId_ = instanceId; + + // Compute height offset — place player above mount's back + // Use tight bounds from actual vertices (M2 header bounds can be inaccurate) + const auto* modelData = charRenderer->getModelData(modelId); + float heightOffset = 1.8f; + if (modelData && !modelData->vertices.empty()) { + float minZ = std::numeric_limits::max(); + float maxZ = -std::numeric_limits::max(); + for (const auto& v : modelData->vertices) { + if (v.position.z < minZ) minZ = v.position.z; + if (v.position.z > maxZ) maxZ = v.position.z; + } + float extentZ = maxZ - minZ; + LOG_INFO("Mount tight bounds: minZ=", minZ, " maxZ=", maxZ, " extentZ=", extentZ); + if (extentZ > 0.5f) { + // Saddle point is roughly 75% up the model, measured from model origin + heightOffset = maxZ * 0.8f; + if (heightOffset < 1.0f) heightOffset = extentZ * 0.75f; + if (heightOffset < 1.0f) heightOffset = 1.8f; + } + } + + renderer->setMounted(instanceId, heightOffset); + charRenderer->playAnimation(instanceId, 0, true); + + LOG_INFO("processPendingMount: DONE displayId=", mountDisplayId, " model=", m2Path, " heightOffset=", heightOffset); +} + void Application::despawnOnlineCreature(uint64_t guid) { auto it = creatureInstances_.find(guid); if (it == creatureInstances_.end()) return; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 824e17e1..cf7989dd 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -189,6 +189,34 @@ void GameHandler::update(float deltaTime) { } } + // Leave combat if auto-attack target is too far away (leash range) + if (autoAttacking && autoAttackTarget != 0) { + auto targetEntity = entityManager.getEntity(autoAttackTarget); + if (targetEntity) { + float dx = movementInfo.x - targetEntity->getX(); + float dy = movementInfo.y - targetEntity->getY(); + float dist = std::sqrt(dx * dx + dy * dy); + if (dist > 40.0f) { + stopAutoAttack(); + LOG_INFO("Left combat: target too far (", dist, " yards)"); + } + } + } + + // Close vendor/gossip window if player walks too far from NPC + if (vendorWindowOpen && currentVendorItems.vendorGuid != 0) { + auto npc = entityManager.getEntity(currentVendorItems.vendorGuid); + 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) { + closeVendor(); + LOG_INFO("Vendor closed: walked too far from NPC"); + } + } + } + // Update entity movement interpolation (keeps targeting in sync with visuals) for (auto& [guid, entity] : entityManager.getEntities()) { entity->updateMovement(deltaTime); @@ -2924,6 +2952,13 @@ void GameHandler::rebuildOnlineInventory() { // ============================================================ void GameHandler::startAutoAttack(uint64_t targetGuid) { + // Can't attack yourself + if (targetGuid == playerGuid) return; + + // Dismount when entering combat + if (isMounted()) { + dismount(); + } autoAttacking = true; autoAttackTarget = targetGuid; autoAttackOutOfRange_ = false; @@ -3001,15 +3036,43 @@ void GameHandler::dismount() { void GameHandler::handleForceRunSpeedChange(network::Packet& packet) { // Packed GUID uint64_t guid = UpdateObjectParser::readPackedGuid(packet); - // uint32 counter (ack counter, we ignore) + // uint32 counter + uint32_t counter = packet.readUInt32(); + // uint32 unknown (TrinityCore/AzerothCore adds this for run speed) packet.readUInt32(); // float newSpeed float newSpeed = packet.readFloat(); - if (guid == playerGuid) { - serverRunSpeed_ = newSpeed; - LOG_INFO("Server run speed changed to ", newSpeed); + LOG_INFO("SMSG_FORCE_RUN_SPEED_CHANGE: guid=0x", std::hex, guid, std::dec, + " counter=", counter, " speed=", newSpeed); + + if (guid != playerGuid) return; + + // Always ACK the speed change to prevent server stall + if (socket) { + network::Packet ack(static_cast(Opcode::CMSG_FORCE_RUN_SPEED_CHANGE_ACK)); + ack.writeUInt64(playerGuid); + ack.writeUInt32(counter); + // MovementInfo (minimal — no flags set means no optional fields) + ack.writeUInt32(0); // moveFlags + ack.writeUInt16(0); // moveFlags2 + ack.writeUInt32(movementTime); + ack.writeFloat(movementInfo.x); + ack.writeFloat(movementInfo.y); + ack.writeFloat(movementInfo.z); + ack.writeFloat(movementInfo.orientation); + ack.writeUInt32(0); // fallTime + ack.writeFloat(newSpeed); + socket->send(ack); } + + // Validate speed - reject garbage/NaN values but still ACK + if (std::isnan(newSpeed) || newSpeed < 0.1f || newSpeed > 100.0f) { + LOG_WARNING("Ignoring invalid run speed: ", newSpeed); + return; + } + + serverRunSpeed_ = newSpeed; } void GameHandler::handleMonsterMove(network::Packet& packet) { @@ -3145,6 +3208,12 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { if (state != WorldState::IN_WORLD || !socket) return; + // Casting any spell while mounted → dismount instead + if (isMounted()) { + dismount(); + return; + } + if (casting) return; // Already casting uint64_t target = targetGuid != 0 ? targetGuid : this->targetGuid; diff --git a/src/main.cpp b/src/main.cpp index e2d1114a..66e398ac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,14 +5,15 @@ #include #include +// Keep a persistent X11 connection for emergency mouse release in signal handlers. +// XOpenDisplay inside a signal handler is unreliable, so we open it once at startup. +static Display* g_emergencyDisplay = nullptr; + static void releaseMouseGrab() { - // Bypass SDL — talk to X11 directly (signal-safe enough for our purposes) - Display* dpy = XOpenDisplay(nullptr); - if (dpy) { - XUngrabPointer(dpy, CurrentTime); - XUngrabKeyboard(dpy, CurrentTime); - XFlush(dpy); - XCloseDisplay(dpy); + if (g_emergencyDisplay) { + XUngrabPointer(g_emergencyDisplay, CurrentTime); + XUngrabKeyboard(g_emergencyDisplay, CurrentTime); + XFlush(g_emergencyDisplay); } } @@ -23,6 +24,7 @@ static void crashHandler(int sig) { } int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { + g_emergencyDisplay = XOpenDisplay(nullptr); std::signal(SIGSEGV, crashHandler); std::signal(SIGABRT, crashHandler); std::signal(SIGFPE, crashHandler); @@ -44,6 +46,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { app.shutdown(); LOG_INFO("Application exited successfully"); + if (g_emergencyDisplay) { XCloseDisplay(g_emergencyDisplay); g_emergencyDisplay = nullptr; } return 0; } catch (const std::exception& e) { diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index fff370c5..bc3bf055 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -190,8 +190,10 @@ void CameraController::update(float deltaTime) { speed = WOW_BACK_SPEED; } else if (ctrlDown) { speed = WOW_WALK_SPEED; + } else if (runSpeedOverride_ > 0.0f && runSpeedOverride_ < 100.0f && !std::isnan(runSpeedOverride_)) { + speed = runSpeedOverride_; } else { - speed = (runSpeedOverride_ > 0.0f) ? runSpeedOverride_ : WOW_RUN_SPEED; + speed = WOW_RUN_SPEED; } } else { // Exploration mode (original behavior)