From ae620017364ddb695e730c20da69ddeebb18c8d1 Mon Sep 17 00:00:00 2001 From: Josh Anderson Date: Mon, 13 Jul 2026 06:09:07 -0500 Subject: [PATCH] fix(taxi): time out a taxi activation reply that never arrives Live-found while validating the activation-race fix against the TBC server: a CMSG_ACTIVATETAXI the server can't make sense of (e.g. a stale start node vs. the player's actual position - reproduced by requesting from a stale cached nearestNode after moving) can go completely unanswered, no SMSG_ACTIVATETAXIREPLY at all in either direction. Before activation was deferred to the reply, taxiActivatePending_ being permanently stuck true didn't matter - the speculative flight had already started regardless. Now that activation correctly waits for the reply, a dropped one left taxiActivatePending_ true forever, silently no-op'ing every future activateTaxi() call (the "already pending" guard) - a permanent soft-lock of the character's taxi system until relog. Live-confirmed the fix: forced the drop scenario, the warning fired at exactly 8.011s (kTaxiActivateReplyTimeoutSeconds=8.0f), pending state cleared, and a subsequent request flew normally. --- include/game/movement_handler.hpp | 3 +++ src/game/movement_handler.cpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/include/game/movement_handler.hpp b/include/game/movement_handler.hpp index 6c71df1b..c53aa03c 100644 --- a/include/game/movement_handler.hpp +++ b/include/game/movement_handler.hpp @@ -291,6 +291,9 @@ private: uint32_t taxiMountDisplayId_ = 0; bool taxiActivatePending_ = false; float taxiActivateTimer_ = 0.0f; + // How long to wait for SMSG_ACTIVATETAXIREPLY before giving up - see + // updateClientTaxi()'s comment on why a dropped reply needs this now. + static constexpr float kTaxiActivateReplyTimeoutSeconds = 8.0f; bool taxiClientActive_ = false; float taxiLandingCooldown_ = 0.0f; float taxiStartGrace_ = 0.0f; diff --git a/src/game/movement_handler.cpp b/src/game/movement_handler.cpp index a4307c14..65bec1d7 100644 --- a/src/game/movement_handler.cpp +++ b/src/game/movement_handler.cpp @@ -2426,6 +2426,35 @@ void MovementHandler::finishClientTaxiFlight(bool snapToFinalWaypoint) { } void MovementHandler::updateClientTaxi(float deltaTime) { + // Live-confirmed: a CMSG_ACTIVATETAXI the server can't make sense of (e.g. + // a stale start node vs. the player's actual position) can go completely + // unanswered - no SMSG_ACTIVATETAXIREPLY at all, success or failure. Before + // activation was deferred to the reply (see activateTaxi()'s comment), the + // speculative flight start accidentally "recovered" from this since it + // never waited on a reply in the first place. Now that it correctly does, + // a dropped reply left taxiActivatePending_ true forever - every future + // activateTaxi() call silently no-ops on its pending-guard, permanently + // soft-locking the player's taxi system until relog. Time out and clear + // pending state the same way an explicit failure reply would. + if (taxiActivatePending_) { + taxiActivateTimer_ += deltaTime; + if (taxiActivateTimer_ > kTaxiActivateReplyTimeoutSeconds) { + LOG_WARNING("Taxi activation reply timed out after ", + kTaxiActivateReplyTimeoutSeconds, "s - clearing pending state"); + taxiActivatePending_ = false; + taxiActivateTimer_ = 0.0f; + taxiClientPath_.clear(); + taxiClientIndex_ = 0; + taxiClientSegmentProgress_ = 0.0f; + if (taxiMountActive_ && owner_.mountCallbackRef()) { + owner_.mountCallbackRef()(0); + } + taxiMountActive_ = false; + taxiMountDisplayId_ = 0; + onTaxiFlight_ = false; + } + } + if (!taxiClientActive_ || taxiClientPath_.size() < 2) return; auto playerEntity = owner_.getEntityManager().getEntity(owner_.getPlayerGuid());