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.
This commit is contained in:
Josh Anderson
2026-07-13 06:09:07 -05:00
parent a7e3e2e992
commit ae62001736
2 changed files with 32 additions and 0 deletions

View File

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

View File

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