From 9f19d9fa1a5248ab8c70b776b04779b026e2ab93 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Feb 2026 20:24:25 -0800 Subject: [PATCH] Fix movement, mounts, and terrain seams --- include/game/game_handler.hpp | 1 + include/game/world_packets.hpp | 1 + src/game/game_handler.cpp | 29 ++++++++++++++++++++ src/game/world_packets.cpp | 4 ++- src/pipeline/terrain_mesh.cpp | 9 ++++--- src/rendering/camera_controller.cpp | 21 +-------------- src/rendering/m2_renderer.cpp | 14 +++++++++- src/rendering/renderer.cpp | 41 +++++++++++++++++++++++++++-- 8 files changed, 93 insertions(+), 27 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 57f300fa..7aabc307 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -906,6 +906,7 @@ private: NpcSwingCallback npcSwingCallback_; MountCallback mountCallback_; uint32_t currentMountDisplayId_ = 0; + float preMountRunSpeed_ = 0.0f; float serverRunSpeed_ = 7.0f; bool playerDead_ = false; }; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 643364be..e5f3e82d 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -449,6 +449,7 @@ struct UpdateBlock { // Movement data (for MOVEMENT updates) bool hasMovement = false; float x = 0.0f, y = 0.0f, z = 0.0f, orientation = 0.0f; + float runSpeed = 0.0f; // Field data (for VALUES and CREATE updates) std::map fields; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 06e0a517..caefc5b5 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1305,6 +1305,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z)); entity->setPosition(pos.x, pos.y, pos.z, block.orientation); LOG_DEBUG(" Position: (", pos.x, ", ", pos.y, ", ", pos.z, ")"); + if (block.guid == playerGuid && block.runSpeed > 0.1f && block.runSpeed < 100.0f) { + serverRunSpeed_ = block.runSpeed; + } } // Set fields @@ -1358,6 +1361,16 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (block.guid == playerGuid) { uint32_t old = currentMountDisplayId_; currentMountDisplayId_ = val; + if (old == 0 && val != 0) { + preMountRunSpeed_ = serverRunSpeed_; + } else if (old != 0 && val == 0) { + if (preMountRunSpeed_ > 0.1f && preMountRunSpeed_ < 100.0f) { + serverRunSpeed_ = preMountRunSpeed_; + } else { + serverRunSpeed_ = 7.0f; + } + preMountRunSpeed_ = 0.0f; + } if (val != old && mountCallback_) mountCallback_(val); } unit->setMountDisplayId(val); @@ -1490,6 +1503,16 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (block.guid == playerGuid) { uint32_t old = currentMountDisplayId_; currentMountDisplayId_ = val; + if (old == 0 && val != 0) { + preMountRunSpeed_ = serverRunSpeed_; + } else if (old != 0 && val == 0) { + if (preMountRunSpeed_ > 0.1f && preMountRunSpeed_ < 100.0f) { + serverRunSpeed_ = preMountRunSpeed_; + } else { + serverRunSpeed_ = 7.0f; + } + preMountRunSpeed_ = 0.0f; + } if (val != old && mountCallback_) mountCallback_(val); } unit->setMountDisplayId(val); @@ -1501,6 +1524,9 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } // Update XP / inventory slot / skill fields for player entity if (block.guid == playerGuid) { + if (block.hasMovement && block.runSpeed > 0.1f && block.runSpeed < 100.0f) { + serverRunSpeed_ = block.runSpeed; + } for (const auto& [key, val] : block.fields) { lastPlayerFields_[key] = val; } @@ -3569,6 +3595,9 @@ void GameHandler::interactWithGameObject(uint64_t guid) { if (state != WorldState::IN_WORLD || !socket) return; auto packet = GameObjectUsePacket::build(guid); socket->send(packet); + // Many lootable chests require a loot request after use. + auto loot = LootPacket::build(guid); + socket->send(loot); } void GameHandler::selectGossipOption(uint32_t optionId) { diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 86522bcb..6de69772 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -703,7 +703,7 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock // Speeds (7 speed values) /*float walkSpeed =*/ packet.readFloat(); - /*float runSpeed =*/ packet.readFloat(); + float runSpeed = packet.readFloat(); /*float runBackSpeed =*/ packet.readFloat(); /*float swimSpeed =*/ packet.readFloat(); /*float swimBackSpeed =*/ packet.readFloat(); @@ -712,6 +712,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock /*float turnRate =*/ packet.readFloat(); /*float pitchRate =*/ packet.readFloat(); + block.runSpeed = runSpeed; + // Spline data if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED uint32_t splineFlags = packet.readUInt32(); diff --git a/src/pipeline/terrain_mesh.cpp b/src/pipeline/terrain_mesh.cpp index 7e17191c..0eb5f8fe 100644 --- a/src/pipeline/terrain_mesh.cpp +++ b/src/pipeline/terrain_mesh.cpp @@ -218,9 +218,12 @@ std::vector TerrainMeshGenerator::generateVertices(const MapChunk vertex.texCoord[0] = offsetX / 8.0f; vertex.texCoord[1] = offsetY / 8.0f; - // Layer UV for alpha map sampling (0-1 range per chunk) - vertex.layerUV[0] = offsetX / 8.0f; - vertex.layerUV[1] = offsetY / 8.0f; + // Layer UV for alpha map sampling (0-1 range per chunk). + // Sample at texel centers of the 64x64 alpha map to avoid edge seams. + constexpr float alphaTexels = 64.0f; + constexpr float alphaStep = (alphaTexels - 1.0f) / 8.0f; // 63 texels across 8 quads + vertex.layerUV[0] = (offsetX * alphaStep + 0.5f) / alphaTexels; + vertex.layerUV[1] = (offsetY * alphaStep + 0.5f) / alphaTexels; vertices.push_back(vertex); } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 0a2b65c2..7f7c0eca 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -1259,26 +1259,7 @@ bool CameraController::isMoving() const { if (!enabled || !camera) { return false; } - - if (ImGui::GetIO().WantCaptureKeyboard) { - return false; - } - - auto& input = core::Input::getInstance(); - bool keyW = input.isKeyPressed(SDL_SCANCODE_W); - bool keyS = input.isKeyPressed(SDL_SCANCODE_S); - bool keyA = input.isKeyPressed(SDL_SCANCODE_A); - bool keyD = input.isKeyPressed(SDL_SCANCODE_D); - bool keyQ = input.isKeyPressed(SDL_SCANCODE_Q); - bool keyE = input.isKeyPressed(SDL_SCANCODE_E); - - // In third-person without RMB, A/D are turn keys (not movement). - if (thirdPerson && !rightMouseDown) { - return keyW || keyS || keyQ || keyE || autoRunning; - } - - bool mouseAutorun = leftMouseDown && rightMouseDown; - return keyW || keyS || keyA || keyD || keyQ || keyE || mouseAutorun || autoRunning; + return moveForwardActive || moveBackwardActive || strafeLeftActive || strafeRightActive || autoRunning; } bool CameraController::isSprinting() const { diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index c0e068a1..759d712c 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -647,6 +647,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { tightMax = glm::max(tightMax, v.position); } bool foliageOrTreeLike = false; + bool chestName = false; { std::string lowerName = model.name; std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), @@ -684,9 +685,16 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("box") != std::string::npos) || (lowerName.find("chest") != std::string::npos) || (lowerName.find("barrel") != std::string::npos); + chestName = (lowerName.find("chest") != std::string::npos); bool foliageName = (lowerName.find("bush") != std::string::npos) || (lowerName.find("grass") != std::string::npos) || + (lowerName.find("drygrass") != std::string::npos) || + (lowerName.find("dry_grass") != std::string::npos) || + (lowerName.find("dry-grass") != std::string::npos) || + (lowerName.find("deadgrass") != std::string::npos) || + (lowerName.find("dead_grass") != std::string::npos) || + (lowerName.find("dead-grass") != std::string::npos) || ((lowerName.find("plant") != std::string::npos) && !isPlanter) || (lowerName.find("flower") != std::string::npos) || (lowerName.find("shrub") != std::string::npos) || @@ -694,6 +702,10 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("vine") != std::string::npos) || (lowerName.find("lily") != std::string::npos) || (lowerName.find("weed") != std::string::npos) || + (lowerName.find("pumpkin") != std::string::npos) || + (lowerName.find("firefly") != std::string::npos) || + (lowerName.find("fireflies") != std::string::npos) || + (lowerName.find("fireflys") != std::string::npos) || (lowerName.find("mushroom") != std::string::npos) || (lowerName.find("fungus") != std::string::npos) || (lowerName.find("toadstool") != std::string::npos); @@ -763,7 +775,7 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { break; } } - gpuModel.disableAnimation = foliageOrTreeLike; + gpuModel.disableAnimation = foliageOrTreeLike || chestName; // Flag smoke models for UV scroll animation (particle emitters not implemented) { diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index f601100c..422fcdc8 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -662,7 +662,27 @@ void Renderer::updateCharacterAnimation() { 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; + auto pickMountAnim = [&](std::initializer_list candidates, uint32_t fallback) -> uint32_t { + for (uint32_t id : candidates) { + if (characterRenderer->hasAnimation(mountInstanceId_, id)) { + return id; + } + } + return fallback; + }; + + uint32_t mountAnimId = ANIM_STAND; + if (moving) { + if (anyStrafeLeft) { + mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_LEFT, ANIM_STRAFE_WALK_LEFT, ANIM_RUN}, ANIM_RUN); + } else if (anyStrafeRight) { + mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_RIGHT, ANIM_STRAFE_WALK_RIGHT, ANIM_RUN}, ANIM_RUN); + } else if (movingBackward) { + mountAnimId = pickMountAnim({ANIM_BACKPEDAL}, ANIM_RUN); + } else { + mountAnimId = ANIM_RUN; + } + } uint32_t curMountAnim = 0; float curMountTime = 0, curMountDur = 0; bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur); @@ -827,7 +847,24 @@ void Renderer::updateCharacterAnimation() { break; case CharAnimState::MOUNT: - break; // Handled by early return above + // If we got here, the mount state was cleared externally but the + // animation state hasn't been reset yet. Fall back to normal logic. + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (sitting && grounded) { + newState = CharAnimState::SIT_DOWN; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else { + newState = CharAnimState::IDLE; + } + break; } if (forceMelee) {