From 2bbd0fdc5f7d57b30b7a07c075e95e54f3cc354a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 19 Feb 2026 16:40:17 -0800 Subject: [PATCH] Fix movement desync: strafe animation and missing SET_FACING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs caused the client to look like a bot to server GMs: 1. Strafe animation played during forward+strafe (W+A) instead of the walk/run animation. Added pureStrafe guard so strafe animations only play when exclusively strafing (no forward key or auto-run active). 2. CMSG_MOVE_SET_FACING was never sent on mouse-look turns. The server predicts movement from the last known facing; without SET_FACING the heartbeat position appeared to teleport each time the player changed direction. Now sent at up to 10 Hz whenever facing changes >3°, skipped while keyboard-turning (handled server-side by TURN flags). --- .gitignore | 1 + include/core/application.hpp | 2 ++ include/rendering/camera_controller.hpp | 1 + src/core/application.cpp | 20 ++++++++++++++++++++ src/rendering/renderer.cpp | 9 +++++++-- 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index aab84ea2..8e047dfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build directories build/ +build-sanitize/ bin/ lib/ diff --git a/include/core/application.hpp b/include/core/application.hpp index a323df16..9f3939ba 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -184,6 +184,8 @@ private: uint32_t loadedMapId_ = 0xFFFFFFFF; // Map ID of currently loaded terrain (0xFFFFFFFF = none) float taxiLandingClampTimer_ = 0.0f; float worldEntryMovementGraceTimer_ = 0.0f; + float facingSendCooldown_ = 0.0f; // Rate-limits CMSG_MOVE_SET_FACING + float lastSentCanonicalYaw_ = 1000.0f; // Sentinel — triggers first send float taxiStreamCooldown_ = 0.0f; bool idleYawned_ = false; diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 3c671b40..400383b1 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -74,6 +74,7 @@ public: bool isMovingBackward() const { return moveBackwardActive; } bool isStrafingLeft() const { return strafeLeftActive; } bool isStrafingRight() const { return strafeRightActive; } + bool isAutoRunning() const { return autoRunning; } bool isRightMouseHeld() const { return rightMouseDown; } bool isSitting() const { return sitting; } bool isSwimming() const { return swimming; } diff --git a/src/core/application.cpp b/src/core/application.cpp index 718a6259..f806c313 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -820,6 +820,26 @@ void Application::update(float deltaTime) { // equivalent canonical yaw is radians(180 - yawDeg). float canonicalYaw = core::coords::normalizeAngleRad(glm::radians(180.0f - yawDeg)); gameHandler->setOrientation(canonicalYaw); + + // Send CMSG_MOVE_SET_FACING when the player changes facing direction + // (e.g. via mouse-look). Without this, the server predicts movement in + // the old facing and position-corrects on the next heartbeat — the + // micro-teleporting the GM observed. + // Skip while keyboard-turning: the server tracks that via TURN_LEFT/RIGHT flags. + facingSendCooldown_ -= deltaTime; + const auto& mi = gameHandler->getMovementInfo(); + constexpr uint32_t kTurnFlags = + static_cast(game::MovementFlags::TURN_LEFT) | + static_cast(game::MovementFlags::TURN_RIGHT); + bool keyboardTurning = (mi.flags & kTurnFlags) != 0; + if (!keyboardTurning && facingSendCooldown_ <= 0.0f) { + float yawDiff = core::coords::normalizeAngleRad(canonicalYaw - lastSentCanonicalYaw_); + if (std::abs(yawDiff) > glm::radians(3.0f)) { + gameHandler->sendMovement(game::Opcode::CMSG_MOVE_SET_FACING); + lastSentCanonicalYaw_ = canonicalYaw; + facingSendCooldown_ = 0.1f; // max 10 Hz + } + } } } diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 589add91..9e2c6dcc 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -954,11 +954,16 @@ void Renderer::updateCharacterAnimation() { CharAnimState newState = charAnimState; bool moving = cameraController->isMoving(); + bool movingForward = cameraController->isMovingForward(); bool movingBackward = cameraController->isMovingBackward(); + bool autoRunning = cameraController->isAutoRunning(); bool strafeLeft = cameraController->isStrafingLeft(); bool strafeRight = cameraController->isStrafingRight(); - bool anyStrafeLeft = strafeLeft && !strafeRight; - bool anyStrafeRight = strafeRight && !strafeLeft; + // Strafe animation only plays during *pure* strafing (no forward/backward/autorun). + // When forward+strafe are both held, the walk/run animation plays — same as the real client. + bool pureStrafe = !movingForward && !movingBackward && !autoRunning; + bool anyStrafeLeft = strafeLeft && !strafeRight && pureStrafe; + bool anyStrafeRight = strafeRight && !strafeLeft && pureStrafe; bool grounded = cameraController->isGrounded(); bool jumping = cameraController->isJumping(); bool sprinting = cameraController->isSprinting();