From 643611ee79bd03bba25467c37656f7876acd5fd3 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Feb 2026 17:59:40 -0800 Subject: [PATCH] Add mount system and crash mouse-release handler Render mount M2 model under player with seated animation, apply creature skin textures, server-driven speed via SMSG_FORCE_RUN_SPEED_CHANGE, and /dismount command. X11 XUngrabPointer on crash/hang to always release mouse. --- CMakeLists.txt | 3 + include/core/application.hpp | 4 + include/game/entity.hpp | 5 + include/game/game_handler.hpp | 15 +++ include/game/opcodes.hpp | 7 ++ include/rendering/camera_controller.hpp | 6 + include/rendering/renderer.hpp | 11 +- src/core/application.cpp | 149 ++++++++++++++++++++++++ src/game/game_handler.cpp | 71 +++++++++++ src/main.cpp | 27 +++++ src/rendering/camera_controller.cpp | 6 +- src/rendering/renderer.cpp | 55 +++++++++ src/ui/game_screen.cpp | 7 ++ 13 files changed, 363 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a0aa9faf..845e9e76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -272,6 +272,9 @@ if (FFMPEG_LIBRARY_DIRS) endif() # Platform-specific libraries +if(UNIX AND NOT APPLE) + target_link_libraries(wowee PRIVATE X11) +endif() if(WIN32) target_link_libraries(wowee PRIVATE ws2_32) # SDL2main provides WinMain entry point on Windows diff --git a/include/core/application.hpp b/include/core/application.hpp index e8325490..2b754ffb 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -150,6 +150,10 @@ private: std::unordered_map creatureModelIds_; // guid → loaded modelId std::unordered_map displayIdModelCache_; // displayId → modelId (model caching) uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures + + // Mount model tracking + uint32_t mountInstanceId_ = 0; + uint32_t mountModelId_ = 0; bool creatureLookupsBuilt_ = false; // Deferred creature spawn queue (throttles spawning to avoid hangs) diff --git a/include/game/entity.hpp b/include/game/entity.hpp index a6bf1a64..6138a2c9 100644 --- a/include/game/entity.hpp +++ b/include/game/entity.hpp @@ -189,6 +189,10 @@ public: uint32_t getDisplayId() const { return displayId; } void setDisplayId(uint32_t id) { displayId = id; } + // Mount display ID (UNIT_FIELD_MOUNTDISPLAYID, index 69) + uint32_t getMountDisplayId() const { return mountDisplayId; } + void setMountDisplayId(uint32_t id) { mountDisplayId = id; } + // Unit flags (UNIT_FIELD_FLAGS, index 59) uint32_t getUnitFlags() const { return unitFlags; } void setUnitFlags(uint32_t f) { unitFlags = f; } @@ -216,6 +220,7 @@ protected: uint32_t level = 1; uint32_t entry = 0; uint32_t displayId = 0; + uint32_t mountDisplayId = 0; uint32_t unitFlags = 0; uint32_t npcFlags = 0; uint32_t factionTemplate = 0; diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 83ce8370..77862560 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -455,6 +455,13 @@ public: } const std::unordered_map& getNpcQuestStatuses() const { return npcQuestStatus_; } + // Mount state + using MountCallback = std::function; // 0 = dismount + void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); } + bool isMounted() const { return currentMountDisplayId_ != 0; } + float getServerRunSpeed() const { return serverRunSpeed_; } + void dismount(); + // Taxi / Flight Paths bool isTaxiWindowOpen() const { return taxiWindowOpen_; } void closeTaxi(); @@ -630,6 +637,9 @@ private: // ---- Teleport handler ---- void handleTeleportAck(network::Packet& packet); + // ---- Speed change handler ---- + void handleForceRunSpeedChange(network::Packet& packet); + // ---- Taxi handlers ---- void handleShowTaxiNodes(network::Packet& packet); void handleActivateTaxiReply(network::Packet& packet); @@ -844,6 +854,8 @@ private: ShowTaxiNodesData currentTaxiData_; uint64_t taxiNpcGuid_ = 0; bool onTaxiFlight_ = false; + uint32_t knownTaxiMask_[12] = {}; // Track previously known nodes for discovery alerts + bool taxiMaskInitialized_ = false; // First SMSG_SHOWTAXINODES seeds mask without alerts // Vendor bool vendorWindowOpen = false; @@ -877,6 +889,9 @@ private: NpcRespawnCallback npcRespawnCallback_; MeleeSwingCallback meleeSwingCallback_; NpcSwingCallback npcSwingCallback_; + MountCallback mountCallback_; + uint32_t currentMountDisplayId_ = 0; + float serverRunSpeed_ = 7.0f; bool playerDead_ = false; }; diff --git a/include/game/opcodes.hpp b/include/game/opcodes.hpp index c78c61d0..4c705c0c 100644 --- a/include/game/opcodes.hpp +++ b/include/game/opcodes.hpp @@ -241,9 +241,16 @@ enum class Opcode : uint16_t { MSG_MOVE_TELEPORT_ACK = 0x0C7, SMSG_TRANSFER_PENDING = 0x003F, + // ---- Speed Changes ---- + SMSG_FORCE_RUN_SPEED_CHANGE = 0x00E2, + + // ---- Mount ---- + CMSG_CANCEL_MOUNT_AURA = 0x0375, + // ---- Taxi / Flight Paths ---- SMSG_SHOWTAXINODES = 0x01A9, SMSG_ACTIVATETAXIREPLY = 0x01AE, + SMSG_NEW_TAXI_PATH = 0x01AF, CMSG_ACTIVATETAXIEXPRESS = 0x0312, }; diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 1759f679..9c834ebb 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -72,6 +72,8 @@ public: using MovementCallback = std::function; void setMovementCallback(MovementCallback cb) { movementCallback = std::move(cb); } void setUseWoWSpeed(bool use) { useWoWSpeed = use; } + void setRunSpeedOverride(float speed) { runSpeedOverride_ = speed; } + void setMounted(bool m) { mounted_ = m; } // For first-person player hiding void setCharacterRenderer(class CharacterRenderer* cr, uint32_t playerId) { @@ -188,6 +190,10 @@ private: static constexpr float WOW_GRAVITY = -19.29f; static constexpr float WOW_JUMP_VELOCITY = 7.96f; + // Server-driven run speed override (0 = use default WOW_RUN_SPEED) + float runSpeedOverride_ = 0.0f; + bool mounted_ = false; + // Online mode: trust server position, don't prefer outdoors over WMO floors bool onlineMode = false; diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 2e253d2c..c2bb0265 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -125,6 +125,11 @@ public: void triggerMeleeSwing(); void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; } + // Mount rendering + void setMounted(uint32_t mountInstId, float heightOffset); + void clearMount(); + bool isMounted() const { return mountInstanceId_ != 0; } + // Selection circle for targeted entity void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color); void clearSelectionCircle(); @@ -214,7 +219,7 @@ private: float characterYaw = 0.0f; // Character animation state - enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING }; + enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT }; CharAnimState charAnimState = CharAnimState::IDLE; void updateCharacterAnimation(); bool isFootstepAnimationState() const; @@ -259,6 +264,10 @@ private: uint32_t meleeAnimId = 0; uint32_t equippedWeaponInvType_ = 0; + // Mount state + uint32_t mountInstanceId_ = 0; + float mountHeightOffset_ = 0.0f; + bool terrainEnabled = true; bool terrainLoaded = false; diff --git a/src/core/application.cpp b/src/core/application.cpp index 2ecb713c..906f35ec 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -389,6 +389,11 @@ void Application::update(float deltaTime) { npcManager->update(deltaTime, renderer->getCharacterRenderer()); } + // Sync server run speed to camera controller + if (renderer && gameHandler && renderer->getCameraController()) { + renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed()); + } + // Sync character render position → canonical WoW coords each frame if (renderer && gameHandler) { glm::vec3 renderPos = renderer->getCharacterPosition(); @@ -554,6 +559,150 @@ void Application::setupUICallbacks() { despawnOnlineCreature(guid); }); + // Mount callback (online mode) - load/destroy mount model + 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_); + mountInstanceId_ = 0; + } + mountModelId_ = 0; + 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); + }); + // Creature move callback (online mode) - update creature positions gameHandler->setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) { auto it = creatureInstances_.find(guid); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index acd9a081..824e17e1 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -363,6 +363,11 @@ void GameHandler::handlePacket(network::Packet& packet) { handleMonsterMove(packet); break; + // ---- Speed Changes ---- + case Opcode::SMSG_FORCE_RUN_SPEED_CHANGE: + handleForceRunSpeedChange(packet); + break; + // ---- Phase 2: Combat ---- case Opcode::SMSG_ATTACKSTART: handleAttackStart(packet); @@ -648,6 +653,11 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_ACTIVATETAXIREPLY: handleActivateTaxiReply(packet); break; + case Opcode::SMSG_NEW_TAXI_PATH: + // Empty packet - server signals a new flight path was learned + // The actual node details come in the next SMSG_SHOWTAXINODES + addSystemChatMessage("New flight path discovered!"); + break; default: LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec); @@ -1287,6 +1297,14 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { case 59: unit->setUnitFlags(val); break; // UNIT_FIELD_FLAGS case 54: unit->setLevel(val); break; case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID + case 69: // UNIT_FIELD_MOUNTDISPLAYID + if (block.guid == playerGuid) { + uint32_t old = currentMountDisplayId_; + currentMountDisplayId_ = val; + if (val != old && mountCallback_) mountCallback_(val); + } + unit->setMountDisplayId(val); + break; case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS default: break; } @@ -1398,6 +1416,15 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { unit->setFactionTemplate(val); unit->setHostile(isHostileFaction(val)); break; + case 67: unit->setDisplayId(val); break; // UNIT_FIELD_DISPLAYID + case 69: // UNIT_FIELD_MOUNTDISPLAYID + if (block.guid == playerGuid) { + uint32_t old = currentMountDisplayId_; + currentMountDisplayId_ = val; + if (val != old && mountCallback_) mountCallback_(val); + } + unit->setMountDisplayId(val); + break; case 82: unit->setNpcFlags(val); break; // UNIT_NPC_FLAGS default: break; } @@ -2964,6 +2991,27 @@ void GameHandler::handleAttackStop(network::Packet& packet) { } } +void GameHandler::dismount() { + if (!isMounted() || !socket) return; + network::Packet pkt(static_cast(Opcode::CMSG_CANCEL_MOUNT_AURA)); + socket->send(pkt); + LOG_INFO("Sent CMSG_CANCEL_MOUNT_AURA"); +} + +void GameHandler::handleForceRunSpeedChange(network::Packet& packet) { + // Packed GUID + uint64_t guid = UpdateObjectParser::readPackedGuid(packet); + // uint32 counter (ack counter, we ignore) + packet.readUInt32(); + // float newSpeed + float newSpeed = packet.readFloat(); + + if (guid == playerGuid) { + serverRunSpeed_ = newSpeed; + LOG_INFO("Server run speed changed to ", newSpeed); + } +} + void GameHandler::handleMonsterMove(network::Packet& packet) { MonsterMoveData data; if (!MonsterMoveParser::parse(packet, data)) { @@ -3945,6 +3993,29 @@ void GameHandler::handleShowTaxiNodes(network::Packet& packet) { loadTaxiDbc(); + // Detect newly discovered flight paths by comparing with stored mask + if (taxiMaskInitialized_) { + for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) { + uint32_t newBits = data.nodeMask[i] & ~knownTaxiMask_[i]; + if (newBits == 0) continue; + for (uint32_t bit = 0; bit < 32; ++bit) { + if (newBits & (1u << bit)) { + uint32_t nodeId = i * 32 + bit; + auto it = taxiNodes_.find(nodeId); + if (it != taxiNodes_.end()) { + addSystemChatMessage("Discovered flight path: " + it->second.name); + } + } + } + } + } + + // Update stored mask + for (uint32_t i = 0; i < TLK_TAXI_MASK_SIZE; ++i) { + knownTaxiMask_[i] = data.nodeMask[i]; + } + taxiMaskInitialized_ = true; + currentTaxiData_ = data; taxiNpcGuid_ = data.npcGuid; taxiWindowOpen_ = true; diff --git a/src/main.cpp b/src/main.cpp index c3757044..e2d1114a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,33 @@ #include "core/application.hpp" #include "core/logger.hpp" #include +#include +#include +#include + +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); + } +} + +static void crashHandler(int sig) { + releaseMouseGrab(); + std::signal(sig, SIG_DFL); + std::raise(sig); +} int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { + std::signal(SIGSEGV, crashHandler); + std::signal(SIGABRT, crashHandler); + std::signal(SIGFPE, crashHandler); + std::signal(SIGTERM, crashHandler); + std::signal(SIGINT, crashHandler); try { wowee::core::Logger::getInstance().setLogLevel(wowee::core::LogLevel::DEBUG); LOG_INFO("=== Wowee Native Client ==="); @@ -22,10 +47,12 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { return 0; } catch (const std::exception& e) { + releaseMouseGrab(); LOG_FATAL("Unhandled exception: ", e.what()); return 1; } catch (...) { + releaseMouseGrab(); LOG_FATAL("Unknown exception occurred"); return 1; } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 36c85a46..fff370c5 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -191,7 +191,7 @@ void CameraController::update(float deltaTime) { } else if (ctrlDown) { speed = WOW_WALK_SPEED; } else { - speed = WOW_RUN_SPEED; + speed = (runSpeedOverride_ > 0.0f) ? runSpeedOverride_ : WOW_RUN_SPEED; } } else { // Exploration mode (original behavior) @@ -225,10 +225,12 @@ void CameraController::update(float deltaTime) { glm::vec3 right(-std::sin(moveYawRad), std::cos(moveYawRad), 0.0f); // Toggle sit/crouch with X key (edge-triggered) — only when UI doesn't want keyboard + // Blocked while mounted bool xDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_X); - if (xDown && !xKeyWasDown) { + if (xDown && !xKeyWasDown && !mounted_) { sitting = !sitting; } + if (mounted_) sitting = false; xKeyWasDown = xDown; // Update eye height based on crouch state (smooth transition) diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index b698d635..1654f8a0 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -364,6 +364,20 @@ void Renderer::setCharacterFollow(uint32_t instanceId) { } } +void Renderer::setMounted(uint32_t mountInstId, float heightOffset) { + mountInstanceId_ = mountInstId; + mountHeightOffset_ = heightOffset; + charAnimState = CharAnimState::MOUNT; + if (cameraController) cameraController->setMounted(true); +} + +void Renderer::clearMount() { + mountInstanceId_ = 0; + mountHeightOffset_ = 0.0f; + charAnimState = CharAnimState::IDLE; + if (cameraController) cameraController->setMounted(false); +} + uint32_t Renderer::resolveMeleeAnimId() { if (!characterRenderer || characterInstanceId == 0) { meleeAnimId = 0; @@ -473,6 +487,7 @@ void Renderer::updateCharacterAnimation() { constexpr uint32_t ANIM_SITTING = 97; // Hold on same animation (no separate idle) constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle) constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim) + constexpr uint32_t ANIM_MOUNT = 91; // Seated on mount CharAnimState newState = charAnimState; @@ -489,6 +504,42 @@ void Renderer::updateCharacterAnimation() { bool swim = cameraController->isSwimming(); bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim; + // When mounted, force MOUNT state and skip normal transitions + if (isMounted()) { + newState = CharAnimState::MOUNT; + charAnimState = newState; + + // Play seated animation on player + uint32_t currentAnimId = 0; + float currentAnimTimeMs = 0.0f, currentAnimDurationMs = 0.0f; + bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); + if (!haveState || currentAnimId != ANIM_MOUNT) { + characterRenderer->playAnimation(characterInstanceId, ANIM_MOUNT, true); + } + + // Sync mount instance position and rotation + if (mountInstanceId_ > 0) { + characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); + float yawRad = glm::radians(characterYaw); + characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(0.0f, 0.0f, yawRad)); + + // Drive mount model animation: idle when still, run when moving + uint32_t mountAnimId = moving ? ANIM_RUN : ANIM_STAND; + uint32_t curMountAnim = 0; + float curMountTime = 0, curMountDur = 0; + bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur); + if (!haveMountState || curMountAnim != mountAnimId) { + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } + } + + // Offset player Z above mount + glm::vec3 playerPos = characterPosition; + playerPos.z += mountHeightOffset_; + characterRenderer->setInstancePosition(characterInstanceId, playerPos); + return; + } + if (!forceMelee) switch (charAnimState) { case CharAnimState::IDLE: if (swim) { @@ -629,6 +680,9 @@ void Renderer::updateCharacterAnimation() { newState = CharAnimState::IDLE; } break; + + case CharAnimState::MOUNT: + break; // Handled by early return above } if (forceMelee) { @@ -692,6 +746,7 @@ void Renderer::updateCharacterAnimation() { } loop = false; break; + case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break; } uint32_t currentAnimId = 0; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 1a1c7883..f4a1fec7 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1138,6 +1138,13 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /dismount command + if (cmdLower == "dismount") { + gameHandler.dismount(); + chatInputBuffer[0] = '\0'; + return; + } + // /sit command if (cmdLower == "sit") { gameHandler.setStandState(1); // 1 = sit