Fix taxi state sync and transport authority; reduce runtime log overhead; restore first-person self-hide

This commit is contained in:
Kelsi 2026-02-11 22:27:02 -08:00
parent 74062aa25f
commit 8bf63b1f06
29 changed files with 529 additions and 360 deletions

View file

@ -18,6 +18,7 @@
#include <fstream>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <cstdlib>
#include <zlib.h>
@ -169,7 +170,8 @@ void GameHandler::update(float deltaTime) {
timeSinceLastPing = 0.0f;
}
if (timeSinceLastMoveHeartbeat_ >= moveHeartbeatInterval_) {
float heartbeatInterval = (onTaxiFlight_ || taxiActivatePending_ || taxiClientActive_) ? 0.25f : moveHeartbeatInterval_;
if (timeSinceLastMoveHeartbeat_ >= heartbeatInterval) {
sendMovement(Opcode::CMSG_MOVE_HEARTBEAT);
timeSinceLastMoveHeartbeat_ = 0.0f;
}
@ -311,7 +313,13 @@ void GameHandler::update(float deltaTime) {
if (taxiActivatePending_) {
taxiActivateTimer_ += deltaTime;
if (!onTaxiFlight_ && taxiActivateTimer_ > 5.0f) {
if (taxiActivateTimer_ > 5.0f) {
// If client taxi simulation is already active, server reply may be missing/late.
// Do not cancel the flight in that case; clear pending state and continue.
if (onTaxiFlight_ || taxiClientActive_ || taxiMountActive_) {
taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f;
} else {
taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f;
if (taxiMountActive_ && mountCallback_) {
@ -323,6 +331,7 @@ void GameHandler::update(float deltaTime) {
taxiClientPath_.clear();
onTaxiFlight_ = false;
LOG_WARNING("Taxi activation timed out");
}
}
}
@ -531,7 +540,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
case Opcode::SMSG_UPDATE_OBJECT:
LOG_INFO("Received SMSG_UPDATE_OBJECT, state=", static_cast<int>(state), " size=", packet.getSize());
LOG_DEBUG("Received SMSG_UPDATE_OBJECT, state=", static_cast<int>(state), " size=", packet.getSize());
// Can be received after entering world
if (state == WorldState::IN_WORLD) {
handleUpdateObject(packet);
@ -539,7 +548,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
case Opcode::SMSG_COMPRESSED_UPDATE_OBJECT:
LOG_INFO("Received SMSG_COMPRESSED_UPDATE_OBJECT, state=", static_cast<int>(state), " size=", packet.getSize());
LOG_DEBUG("Received SMSG_COMPRESSED_UPDATE_OBJECT, state=", static_cast<int>(state), " size=", packet.getSize());
// Compressed version of UPDATE_OBJECT
if (state == WorldState::IN_WORLD) {
handleCompressedUpdateObject(packet);
@ -1243,7 +1252,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
default:
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
// Log each unknown opcode once to avoid log-file I/O spikes in busy areas.
static std::unordered_set<uint16_t> loggedUnhandledOpcodes;
if (loggedUnhandledOpcodes.insert(static_cast<uint16_t>(opcode)).second) {
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
}
break;
}
}
@ -1755,8 +1768,7 @@ void GameHandler::sendMovement(Opcode opcode) {
(opcode == Opcode::CMSG_MOVE_STOP) ||
(opcode == Opcode::CMSG_MOVE_STOP_STRAFE) ||
(opcode == Opcode::CMSG_MOVE_STOP_TURN) ||
(opcode == Opcode::CMSG_MOVE_STOP_SWIM) ||
(opcode == Opcode::CMSG_MOVE_FALL_LAND);
(opcode == Opcode::CMSG_MOVE_STOP_SWIM);
if ((onTaxiFlight_ || taxiMountActive_) && !taxiAllowed) return;
if (resurrectPending_ && !taxiAllowed) return;
@ -1811,6 +1823,10 @@ void GameHandler::sendMovement(Opcode opcode) {
break;
}
if (onTaxiFlight_ || taxiMountActive_ || taxiActivatePending_ || taxiClientActive_) {
sanitizeMovementForTaxi();
}
// Add transport data if player is on a transport
if (isOnTransport()) {
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
@ -1868,6 +1884,29 @@ void GameHandler::sendMovement(Opcode opcode) {
socket->send(packet);
}
void GameHandler::sanitizeMovementForTaxi() {
constexpr uint32_t kClearTaxiFlags =
static_cast<uint32_t>(MovementFlags::FORWARD) |
static_cast<uint32_t>(MovementFlags::BACKWARD) |
static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT) |
static_cast<uint32_t>(MovementFlags::TURN_LEFT) |
static_cast<uint32_t>(MovementFlags::TURN_RIGHT) |
static_cast<uint32_t>(MovementFlags::PITCH_UP) |
static_cast<uint32_t>(MovementFlags::PITCH_DOWN) |
static_cast<uint32_t>(MovementFlags::FALLING) |
static_cast<uint32_t>(MovementFlags::FALLINGFAR) |
static_cast<uint32_t>(MovementFlags::SWIMMING);
movementInfo.flags &= ~kClearTaxiFlags;
movementInfo.fallTime = 0;
movementInfo.jumpVelocity = 0.0f;
movementInfo.jumpSinAngle = 0.0f;
movementInfo.jumpCosAngle = 0.0f;
movementInfo.jumpXYSpeed = 0.0f;
movementInfo.pitch = 0.0f;
}
void GameHandler::forceClearTaxiAndMovementState() {
taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f;
@ -1934,7 +1973,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
continue;
}
LOG_INFO("Entity went out of range: 0x", std::hex, guid, std::dec);
LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec);
// Trigger despawn callbacks before removing entity
auto entity = entityManager.getEntity(guid);
if (entity) {
@ -2092,6 +2131,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
if ((unit->getUnitFlags() & UNIT_FLAG_TAXI_FLIGHT) != 0 && !onTaxiFlight_ && taxiLandingCooldown_ <= 0.0f) {
onTaxiFlight_ = true;
taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f);
sanitizeMovementForTaxi();
applyTaxiMountForCurrentNode();
}
}
@ -2598,7 +2638,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
void GameHandler::handleCompressedUpdateObject(network::Packet& packet) {
LOG_INFO("Handling SMSG_COMPRESSED_UPDATE_OBJECT, packet size: ", packet.getSize());
LOG_DEBUG("Handling SMSG_COMPRESSED_UPDATE_OBJECT, packet size: ", packet.getSize());
// First 4 bytes = decompressed size
if (packet.getSize() < 4) {
@ -2607,7 +2647,7 @@ void GameHandler::handleCompressedUpdateObject(network::Packet& packet) {
}
uint32_t decompressedSize = packet.readUInt32();
LOG_INFO(" Decompressed size: ", decompressedSize);
LOG_DEBUG(" Decompressed size: ", decompressedSize);
if (decompressedSize == 0 || decompressedSize > 1024 * 1024) {
LOG_WARNING("Invalid decompressed size: ", decompressedSize);
@ -6335,6 +6375,7 @@ void GameHandler::startClientTaxiPath(const std::vector<uint32_t>& pathNodes) {
movementInfo.y = start.y;
movementInfo.z = start.z;
movementInfo.orientation = initialOrientation;
sanitizeMovementForTaxi();
auto playerEntity = entityManager.getEntity(playerGuid);
if (playerEntity) {
@ -6495,8 +6536,8 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
}
// Guard against stray/mis-mapped packets being treated as taxi replies.
// Only honor taxi replies when a taxi flow is actually active.
if (!taxiActivatePending_ && !taxiWindowOpen_ && !onTaxiFlight_) {
// We only consume a reply while an activation request is pending.
if (!taxiActivatePending_) {
LOG_DEBUG("Ignoring stray taxi reply: result=", data.result);
return;
}
@ -6509,6 +6550,7 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
}
onTaxiFlight_ = true;
taxiStartGrace_ = std::max(taxiStartGrace_, 2.0f);
sanitizeMovementForTaxi();
taxiWindowOpen_ = false;
taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f;
@ -6518,6 +6560,13 @@ void GameHandler::handleActivateTaxiReply(network::Packet& packet) {
}
LOG_INFO("Taxi flight started!");
} else {
// If local taxi motion already started, treat late failure as stale and ignore.
if (onTaxiFlight_ || taxiClientActive_) {
LOG_WARNING("Ignoring stale taxi failure reply while flight is active: result=", data.result);
taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f;
return;
}
LOG_WARNING("Taxi activation failed, result=", data.result);
addSystemChatMessage("Cannot take that flight path.");
taxiActivatePending_ = false;
@ -6678,6 +6727,7 @@ void GameHandler::activateTaxi(uint32_t destNodeId) {
taxiStartGrace_ = 2.0f;
if (!onTaxiFlight_) {
onTaxiFlight_ = true;
sanitizeMovementForTaxi();
applyTaxiMountForCurrentNode();
}
if (socket) {
@ -6720,6 +6770,11 @@ void GameHandler::activateTaxi(uint32_t destNodeId) {
taxiFlightStartCallback_();
}
startClientTaxiPath(path);
// We run taxi movement locally immediately; don't keep a long-lived pending state.
if (taxiClientActive_) {
taxiActivatePending_ = false;
taxiActivateTimer_ = 0.0f;
}
addSystemChatMessage("Flight started.");

View file

@ -254,84 +254,15 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
}
pathTimeMs = transport.localClockMs % path.durationMs;
} else {
// Server-driven transport without clock sync.
// Do not auto-fallback to local DBC paths; remapped paths can be wrong and cause
// fast sideways movement, diving below water, or despawn-like behavior.
// Instead, briefly dead-reckon from recent authoritative velocity to avoid visual stutter
// when update bursts are sparse.
// Server-driven transport without clock sync:
// stay server-authoritative and never switch to DBC/client animation fallback.
// For short server update gaps, apply lightweight dead-reckoning only when we
// have measured velocity from at least one authoritative delta.
constexpr float kMaxExtrapolationSec = 8.0f;
const float ageSec = elapsedTime_ - transport.lastServerUpdate;
if (transport.hasServerVelocity && ageSec > 0.0f && ageSec <= kMaxExtrapolationSec) {
const float blend = glm::clamp(1.0f - (ageSec / kMaxExtrapolationSec), 0.0f, 1.0f);
transport.position += transport.serverLinearVelocity * (deltaTime * blend);
} else if (transport.serverUpdateCount <= 1 &&
ageSec >= 1.0f &&
path.fromDBC && !path.zOnly && path.durationMs > 0 && path.points.size() > 1 &&
((transport.guid & 0xFFF0000000000000ULL) == 0x1FC0000000000000ULL)) {
// Spawn-only fallback: only for world transport GUIDs (0x1fc...), not all transport-like objects.
glm::vec3 localTarget = transport.position - transport.basePosition;
uint32_t bestTimeMs = 0;
float bestScore = FLT_MAX;
float bestD2 = FLT_MAX;
constexpr uint32_t samples = 600;
for (uint32_t i = 0; i < samples; ++i) {
uint32_t t = static_cast<uint32_t>((static_cast<uint64_t>(i) * path.durationMs) / samples);
glm::vec3 off = evalTimedCatmullRom(path, t);
glm::vec3 d = off - localTarget;
float d2 = glm::dot(d, d);
float score = d2;
if (transport.hasServerYaw) {
constexpr uint32_t kHeadingDtMs = 250;
uint32_t tNext = (t + kHeadingDtMs) % path.durationMs;
glm::vec3 offNext = evalTimedCatmullRom(path, tNext);
glm::vec3 tangent = offNext - off;
if (glm::length2(tangent) > 1e-6f) {
float yaw = std::atan2(tangent.y, tangent.x);
float dyaw = yaw - transport.serverYaw;
while (dyaw > glm::pi<float>()) dyaw -= glm::two_pi<float>();
while (dyaw < -glm::pi<float>()) dyaw += glm::two_pi<float>();
constexpr float kHeadingWeight = 60.0f;
score += (kHeadingWeight * std::abs(dyaw)) * (kHeadingWeight * std::abs(dyaw));
}
}
if (score < bestScore) {
bestScore = score;
bestD2 = d2;
bestTimeMs = t;
}
}
constexpr float kMaxPhaseDrift = 120.0f;
if (bestD2 <= (kMaxPhaseDrift * kMaxPhaseDrift)) {
bool reverse = false;
if (transport.hasServerYaw) {
constexpr uint32_t kYawDtMs = 250;
uint32_t tNext = (bestTimeMs + kYawDtMs) % path.durationMs;
glm::vec3 p0 = evalTimedCatmullRom(path, bestTimeMs);
glm::vec3 p1 = evalTimedCatmullRom(path, tNext);
glm::vec3 d = p1 - p0;
if (glm::length2(d) > 1e-6f) {
float yawFwd = std::atan2(d.y, d.x);
float yawRev = yawFwd + glm::pi<float>();
auto angleDiff = [](float a, float b) -> float {
float d = a - b;
while (d > glm::pi<float>()) d -= glm::two_pi<float>();
while (d < -glm::pi<float>()) d += glm::two_pi<float>();
return std::abs(d);
};
reverse = angleDiff(yawRev, transport.serverYaw) < angleDiff(yawFwd, transport.serverYaw);
}
}
transport.useClientAnimation = true;
transport.localClockMs = bestTimeMs;
transport.clientAnimationReverse = reverse;
LOG_WARNING("TransportManager: No follow-up server updates for world transport 0x", std::hex, transport.guid, std::dec,
" (", ageSec, "s since spawn); enabling guarded fallback at t=", bestTimeMs,
"ms (phaseDrift=", std::sqrt(bestD2), ", reverse=", reverse, ")");
}
}
updateTransformMatrices(transport);

View file

@ -1683,7 +1683,7 @@ bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryRe
// Skip remaining fields (kill credits, display IDs, modifiers, quest items, etc.)
// We've got what we need for display purposes
LOG_INFO("Creature query response: ", data.name, " (type=", data.creatureType,
LOG_DEBUG("Creature query response: ", data.name, " (type=", data.creatureType,
" rank=", data.rank, ")");
return true;
}
@ -1718,7 +1718,7 @@ bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQue
packet.readString();
packet.readString();
LOG_INFO("GameObject query response: ", data.name, " (type=", data.type, " entry=", data.entry, ")");
LOG_DEBUG("GameObject query response: ", data.name, " (type=", data.type, " entry=", data.entry, ")");
return true;
}