Fix movement desync: strafe animation and missing SET_FACING

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).
This commit is contained in:
Kelsi 2026-02-19 16:40:17 -08:00
parent 4e5d424b34
commit 2bbd0fdc5f
5 changed files with 31 additions and 2 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
# Build directories
build/
build-sanitize/
bin/
lib/

View file

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

View file

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

View file

@ -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<uint32_t>(game::MovementFlags::TURN_LEFT) |
static_cast<uint32_t>(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
}
}
}
}

View file

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