fix: pet opcodes shared unlearn handler despite incompatible formats

SMSG_PET_GUIDS, SMSG_PET_DISMISS_SOUND, and SMSG_PET_ACTION_SOUND were
registered with the same handler as SMSG_PET_UNLEARN_CONFIRM. Their
different formats (GUID lists, sound IDs with position) were misread as
unlearn cost, potentially triggering a bogus unlearn confirmation dialog.

Also extracts resetWardenState() from 13 lines duplicated verbatim
between connect() and disconnect().
This commit is contained in:
Kelsi 2026-03-29 18:39:38 -07:00
parent bed859d8db
commit 6f6571fc7a
2 changed files with 35 additions and 37 deletions

View file

@ -621,6 +621,7 @@ public:
void reportPlayer(uint64_t targetGuid, const std::string& reason); void reportPlayer(uint64_t targetGuid, const std::string& reason);
void stopCasting(); void stopCasting();
void resetCastState(); // force-clear all cast/craft/queue state without sending packets void resetCastState(); // force-clear all cast/craft/queue state without sending packets
void resetWardenState(); // clear all warden module/crypto state for connect/disconnect
void clearUnitCaches(); // clear per-unit cast states and aura caches void clearUnitCaches(); // clear per-unit cast states and aura caches
// ---- Phase 1: Name queries (delegated to EntityController) ---- // ---- Phase 1: Name queries (delegated to EntityController) ----

View file

@ -714,19 +714,7 @@ bool GameHandler::connect(const std::string& host,
// Diagnostic: dump session key for AUTH_REJECT debugging // Diagnostic: dump session key for AUTH_REJECT debugging
LOG_INFO("GameHandler session key (", sessionKey.size(), "): ", LOG_INFO("GameHandler session key (", sessionKey.size(), "): ",
core::toHexString(sessionKey.data(), sessionKey.size())); core::toHexString(sessionKey.data(), sessionKey.size()));
requiresWarden_ = false; resetWardenState();
wardenGateSeen_ = false;
wardenGateElapsed_ = 0.0f;
wardenGateNextStatusLog_ = 2.0f;
wardenPacketsAfterGate_ = 0;
wardenCharEnumBlockedLogged_ = false;
wardenCrypto_.reset();
wardenState_ = WardenState::WAIT_MODULE_USE;
wardenModuleHash_.clear();
wardenModuleKey_.clear();
wardenModuleSize_ = 0;
wardenModuleData_.clear();
wardenLoadedModule_.reset();
// Generate random client seed // Generate random client seed
this->clientSeed = generateClientSeed(); this->clientSeed = generateClientSeed();
@ -755,6 +743,22 @@ bool GameHandler::connect(const std::string& host,
return true; return true;
} }
void GameHandler::resetWardenState() {
requiresWarden_ = false;
wardenGateSeen_ = false;
wardenGateElapsed_ = 0.0f;
wardenGateNextStatusLog_ = 2.0f;
wardenPacketsAfterGate_ = 0;
wardenCharEnumBlockedLogged_ = false;
wardenCrypto_.reset();
wardenState_ = WardenState::WAIT_MODULE_USE;
wardenModuleHash_.clear();
wardenModuleKey_.clear();
wardenModuleSize_ = 0;
wardenModuleData_.clear();
wardenLoadedModule_.reset();
}
void GameHandler::disconnect() { void GameHandler::disconnect() {
if (onTaxiFlight_) { if (onTaxiFlight_) {
taxiRecoverPending_ = true; taxiRecoverPending_ = true;
@ -771,19 +775,7 @@ void GameHandler::disconnect() {
friendGuids_.clear(); friendGuids_.clear();
contacts_.clear(); contacts_.clear();
transportAttachments_.clear(); transportAttachments_.clear();
requiresWarden_ = false; resetWardenState();
wardenGateSeen_ = false;
wardenGateElapsed_ = 0.0f;
wardenGateNextStatusLog_ = 2.0f;
wardenPacketsAfterGate_ = 0;
wardenCharEnumBlockedLogged_ = false;
wardenCrypto_.reset();
wardenState_ = WardenState::WAIT_MODULE_USE;
wardenModuleHash_.clear();
wardenModuleKey_.clear();
wardenModuleSize_ = 0;
wardenModuleData_.clear();
wardenLoadedModule_.reset();
pendingIncomingPackets_.clear(); pendingIncomingPackets_.clear();
// Fire despawn callbacks so the renderer releases M2/character model resources. // Fire despawn callbacks so the renderer releases M2/character model resources.
for (const auto& [guid, entity] : entityController_->getEntityManager().getEntities()) { for (const auto& [guid, entity] : entityController_->getEntityManager().getEntities()) {
@ -3126,17 +3118,22 @@ void GameHandler::registerOpcodeHandlers() {
packet.skipAll(); packet.skipAll();
}; };
// uint64 petGuid + uint32 cost (copper) // SMSG_PET_UNLEARN_CONFIRM: uint64 petGuid + uint32 cost (copper).
for (auto op : { Opcode::SMSG_PET_GUIDS, Opcode::SMSG_PET_DISMISS_SOUND, Opcode::SMSG_PET_ACTION_SOUND, Opcode::SMSG_PET_UNLEARN_CONFIRM }) { // The other pet opcodes have different formats and must NOT set unlearn state.
dispatchTable_[op] = [this](network::Packet& packet) { dispatchTable_[Opcode::SMSG_PET_UNLEARN_CONFIRM] = [this](network::Packet& packet) {
// uint64 petGuid + uint32 cost (copper) if (packet.hasRemaining(12)) {
if (packet.hasRemaining(12)) { petUnlearnGuid_ = packet.readUInt64();
petUnlearnGuid_ = packet.readUInt64(); petUnlearnCost_ = packet.readUInt32();
petUnlearnCost_ = packet.readUInt32(); petUnlearnPending_ = true;
petUnlearnPending_ = true; }
} packet.skipAll();
packet.skipAll(); };
}; // These pet opcodes have incompatible formats — just consume the packet.
// Previously they shared the unlearn handler, which misinterpreted sound IDs
// or GUID lists as unlearn costs and could trigger a bogus unlearn dialog.
for (auto op : { Opcode::SMSG_PET_GUIDS, Opcode::SMSG_PET_DISMISS_SOUND,
Opcode::SMSG_PET_ACTION_SOUND }) {
dispatchTable_[op] = [](network::Packet& packet) { packet.skipAll(); };
} }
// Server signals that the pet can now be named (first tame) // Server signals that the pet can now be named (first tame)
dispatchTable_[Opcode::SMSG_PET_RENAMEABLE] = [this](network::Packet& packet) { dispatchTable_[Opcode::SMSG_PET_RENAMEABLE] = [this](network::Packet& packet) {