Implement WotLK 3.3.5a LFG/Dungeon Finder packet handlers

Add full client-side handling for the Looking For Dungeon system:
- SMSG_LFG_JOIN_RESULT: parse join success/failure, surface error message
- SMSG_LFG_QUEUE_STATUS: track dungeon ID, avg wait time, time in queue
- SMSG_LFG_PROPOSAL_UPDATE: detect proposal state (active/passed/failed)
- SMSG_LFG_ROLE_CHECK_UPDATE: surface role check progress/failure
- SMSG_LFG_UPDATE_PLAYER/PARTY: track queue state transitions
- SMSG_LFG_PLAYER_REWARD: show dungeon completion reward in chat
- SMSG_LFG_BOOT_PROPOSAL_UPDATE: show vote-kick status in chat
- SMSG_LFG_TELEPORT_DENIED: surface reason for teleport failure
- SMSG_LFG_DISABLED/OFFER_CONTINUE and informational packets consumed

Outgoing: lfgJoin(), lfgLeave(), lfgAcceptProposal(), lfgTeleport()
State: LfgState enum + lfgState_/lfgDungeonId_/lfgAvgWaitSec_ members
This commit is contained in:
Kelsi 2026-03-09 13:30:23 -07:00
parent ae5c05e14e
commit b33831d833
2 changed files with 400 additions and 0 deletions

View file

@ -707,6 +707,29 @@ public:
bool hasPendingGroupInvite() const { return pendingGroupInvite; }
const std::string& getPendingInviterName() const { return pendingInviterName; }
// ---- LFG / Dungeon Finder ----
enum class LfgState : uint8_t {
None = 0,
RoleCheck = 1,
Queued = 2,
Proposal = 3,
Boot = 4,
InDungeon = 5,
FinishedDungeon= 6,
RaidBrowser = 7,
};
// roles bitmask: 0x02=tank, 0x04=healer, 0x08=dps; pass LFGDungeonEntry ID
void lfgJoin(uint32_t dungeonId, uint8_t roles);
void lfgLeave();
void lfgAcceptProposal(uint32_t proposalId, bool accept);
void lfgTeleport(bool toLfgDungeon = true);
LfgState getLfgState() const { return lfgState_; }
bool isLfgQueued() const { return lfgState_ == LfgState::Queued; }
bool isLfgInDungeon() const { return lfgState_ == LfgState::InDungeon; }
uint32_t getLfgDungeonId() const { return lfgDungeonId_; }
int32_t getLfgAvgWaitSec() const { return lfgAvgWaitSec_; }
// ---- Phase 5: Loot ----
void lootTarget(uint64_t guid);
void lootItem(uint8_t slotIndex);
@ -1207,6 +1230,16 @@ private:
void loadAreaTriggerDbc();
void checkAreaTriggers();
// ---- LFG / Dungeon Finder handlers ----
void handleLfgJoinResult(network::Packet& packet);
void handleLfgQueueStatus(network::Packet& packet);
void handleLfgProposalUpdate(network::Packet& packet);
void handleLfgRoleCheckUpdate(network::Packet& packet);
void handleLfgUpdatePlayer(network::Packet& packet);
void handleLfgPlayerReward(network::Packet& packet);
void handleLfgBootProposalUpdate(network::Packet& packet);
void handleLfgTeleportDenied(network::Packet& packet);
// ---- Arena / Battleground handlers ----
void handleBattlefieldStatus(network::Packet& packet);
void handleInstanceDifficulty(network::Packet& packet);
@ -1533,6 +1566,12 @@ private:
uint32_t instanceDifficulty_ = 0;
bool instanceIsHeroic_ = false;
// LFG / Dungeon Finder state
LfgState lfgState_ = LfgState::None;
uint32_t lfgDungeonId_ = 0; // current dungeon entry
int32_t lfgAvgWaitSec_ = -1; // estimated wait, -1=unknown
uint32_t lfgTimeInQueueMs_= 0; // ms already in queue
// ---- Phase 4: Group ----
GroupListData partyData;
bool pendingGroupInvite = false;

View file

@ -2799,6 +2799,50 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_INSTANCE_DIFFICULTY:
handleInstanceDifficulty(packet);
break;
// ---- LFG / Dungeon Finder ----
case Opcode::SMSG_LFG_JOIN_RESULT:
handleLfgJoinResult(packet);
break;
case Opcode::SMSG_LFG_QUEUE_STATUS:
handleLfgQueueStatus(packet);
break;
case Opcode::SMSG_LFG_PROPOSAL_UPDATE:
handleLfgProposalUpdate(packet);
break;
case Opcode::SMSG_LFG_ROLE_CHECK_UPDATE:
handleLfgRoleCheckUpdate(packet);
break;
case Opcode::SMSG_LFG_UPDATE_PLAYER:
case Opcode::SMSG_LFG_UPDATE_PARTY:
handleLfgUpdatePlayer(packet);
break;
case Opcode::SMSG_LFG_PLAYER_REWARD:
handleLfgPlayerReward(packet);
break;
case Opcode::SMSG_LFG_BOOT_PROPOSAL_UPDATE:
handleLfgBootProposalUpdate(packet);
break;
case Opcode::SMSG_LFG_TELEPORT_DENIED:
handleLfgTeleportDenied(packet);
break;
case Opcode::SMSG_LFG_DISABLED:
addSystemChatMessage("The Dungeon Finder is currently disabled.");
LOG_INFO("SMSG_LFG_DISABLED received");
break;
case Opcode::SMSG_LFG_OFFER_CONTINUE:
addSystemChatMessage("Dungeon Finder: You may continue your dungeon.");
break;
case Opcode::SMSG_LFG_ROLE_CHOSEN:
case Opcode::SMSG_LFG_UPDATE_SEARCH:
case Opcode::SMSG_UPDATE_LFG_LIST:
case Opcode::SMSG_LFG_PLAYER_INFO:
case Opcode::SMSG_LFG_PARTY_INFO:
case Opcode::SMSG_OPEN_LFG_DUNGEON_FINDER:
// Informational LFG packets not yet surfaced in UI — consume silently.
packet.setReadPos(packet.getSize());
break;
case Opcode::SMSG_ARENA_TEAM_COMMAND_RESULT:
handleArenaTeamCommandResult(packet);
break;
@ -8811,6 +8855,323 @@ void GameHandler::handleInstanceDifficulty(network::Packet& packet) {
LOG_INFO("Instance difficulty: ", instanceDifficulty_, " heroic=", instanceIsHeroic_);
}
// ---------------------------------------------------------------------------
// LFG / Dungeon Finder handlers (WotLK 3.3.5a)
// ---------------------------------------------------------------------------
static const char* lfgJoinResultString(uint8_t result) {
switch (result) {
case 0: return nullptr; // success
case 1: return "Role check failed.";
case 2: return "No LFG slots available for your group.";
case 3: return "No LFG object found.";
case 4: return "No slots available (player).";
case 5: return "No slots available (party).";
case 6: return "Dungeon requirements not met by all members.";
case 7: return "Party members are from different realms.";
case 8: return "Not all members are present.";
case 9: return "Get info timeout.";
case 10: return "Invalid dungeon slot.";
case 11: return "You are marked as a deserter.";
case 12: return "A party member is marked as a deserter.";
case 13: return "You are on a random dungeon cooldown.";
case 14: return "A party member is on a random dungeon cooldown.";
case 16: return "No spec/role available.";
default: return "Cannot join dungeon finder.";
}
}
static const char* lfgTeleportDeniedString(uint8_t reason) {
switch (reason) {
case 0: return "You are not in a LFG group.";
case 1: return "You are not in the dungeon.";
case 2: return "You have a summon pending.";
case 3: return "You are dead.";
case 4: return "You have Deserter.";
case 5: return "You do not meet the requirements.";
default: return "Teleport to dungeon denied.";
}
}
void GameHandler::handleLfgJoinResult(network::Packet& packet) {
size_t remaining = packet.getSize() - packet.getReadPos();
if (remaining < 2) return;
uint8_t result = packet.readUInt8();
uint8_t state = packet.readUInt8();
if (result == 0) {
// Success — state tells us what phase we're entering
lfgState_ = static_cast<LfgState>(state);
LOG_INFO("SMSG_LFG_JOIN_RESULT: success, state=", static_cast<int>(state));
addSystemChatMessage("Dungeon Finder: Joined the queue.");
} else {
const char* msg = lfgJoinResultString(result);
std::string errMsg = std::string("Dungeon Finder: ") + (msg ? msg : "Join failed.");
addSystemChatMessage(errMsg);
LOG_INFO("SMSG_LFG_JOIN_RESULT: result=", static_cast<int>(result),
" state=", static_cast<int>(state));
}
}
void GameHandler::handleLfgQueueStatus(network::Packet& packet) {
size_t remaining = packet.getSize() - packet.getReadPos();
if (remaining < 4 + 6 * 4 + 1 + 4) return; // dungeonId + 6 int32 + uint8 + uint32
lfgDungeonId_ = packet.readUInt32();
int32_t avgWait = static_cast<int32_t>(packet.readUInt32());
int32_t waitTime = static_cast<int32_t>(packet.readUInt32());
/*int32_t waitTimeTank =*/ static_cast<int32_t>(packet.readUInt32());
/*int32_t waitTimeHealer =*/ static_cast<int32_t>(packet.readUInt32());
/*int32_t waitTimeDps =*/ static_cast<int32_t>(packet.readUInt32());
/*uint8_t queuedByNeeded=*/ packet.readUInt8();
lfgTimeInQueueMs_ = packet.readUInt32();
lfgAvgWaitSec_ = (waitTime >= 0) ? (waitTime / 1000) : (avgWait / 1000);
lfgState_ = LfgState::Queued;
LOG_INFO("SMSG_LFG_QUEUE_STATUS: dungeonId=", lfgDungeonId_,
" avgWait=", avgWait, "ms waitTime=", waitTime, "ms");
}
void GameHandler::handleLfgProposalUpdate(network::Packet& packet) {
size_t remaining = packet.getSize() - packet.getReadPos();
if (remaining < 16) return;
uint32_t dungeonId = packet.readUInt32();
uint32_t proposalId = packet.readUInt32();
uint32_t proposalState = packet.readUInt32();
/*uint32_t encounterMask =*/ packet.readUInt32();
if (remaining < 17) return;
/*bool canOverride =*/ packet.readUInt8();
lfgDungeonId_ = dungeonId;
switch (proposalState) {
case 0:
lfgState_ = LfgState::Queued;
addSystemChatMessage("Dungeon Finder: Group proposal failed.");
break;
case 1:
lfgState_ = LfgState::InDungeon;
addSystemChatMessage("Dungeon Finder: Group found! Entering dungeon...");
break;
case 2:
lfgState_ = LfgState::Proposal;
addSystemChatMessage("Dungeon Finder: A group has been found. Accept or decline.");
break;
default:
break;
}
LOG_INFO("SMSG_LFG_PROPOSAL_UPDATE: dungeonId=", dungeonId,
" proposalId=", proposalId, " state=", proposalState);
(void)proposalId;
}
void GameHandler::handleLfgRoleCheckUpdate(network::Packet& packet) {
size_t remaining = packet.getSize() - packet.getReadPos();
if (remaining < 6) return;
/*uint32_t dungeonId =*/ packet.readUInt32();
uint8_t roleCheckState = packet.readUInt8();
/*bool isBeginning =*/ packet.readUInt8();
// roleCheckState: 0=default, 1=finished, 2=initializing, 3=missing_role, 4=wrong_dungeons
if (roleCheckState == 1) {
lfgState_ = LfgState::Queued;
LOG_INFO("LFG role check finished");
} else if (roleCheckState == 3) {
lfgState_ = LfgState::None;
addSystemChatMessage("Dungeon Finder: Role check failed — missing required role.");
} else if (roleCheckState == 2) {
lfgState_ = LfgState::RoleCheck;
addSystemChatMessage("Dungeon Finder: Performing role check...");
}
LOG_INFO("SMSG_LFG_ROLE_CHECK_UPDATE: roleCheckState=", static_cast<int>(roleCheckState));
}
void GameHandler::handleLfgUpdatePlayer(network::Packet& packet) {
// SMSG_LFG_UPDATE_PLAYER and SMSG_LFG_UPDATE_PARTY share the same layout.
size_t remaining = packet.getSize() - packet.getReadPos();
if (remaining < 1) return;
uint8_t updateType = packet.readUInt8();
// LFGUpdateType values that carry no extra payload
// 0=default, 1=leader_unk1, 4=rolecheck_aborted, 8=removed_from_queue,
// 9=proposal_failed, 10=proposal_declined, 15=leave_queue, 17=member_offline, 18=group_disband
bool hasExtra = (updateType != 0 && updateType != 1 && updateType != 15 &&
updateType != 17 && updateType != 18);
if (!hasExtra || packet.getSize() - packet.getReadPos() < 3) {
switch (updateType) {
case 8: lfgState_ = LfgState::None;
addSystemChatMessage("Dungeon Finder: Removed from queue."); break;
case 9: lfgState_ = LfgState::Queued;
addSystemChatMessage("Dungeon Finder: Proposal failed — re-queuing."); break;
case 10: lfgState_ = LfgState::Queued;
addSystemChatMessage("Dungeon Finder: A member declined the proposal."); break;
case 15: lfgState_ = LfgState::None;
addSystemChatMessage("Dungeon Finder: Left the queue."); break;
case 18: lfgState_ = LfgState::None;
addSystemChatMessage("Dungeon Finder: Your group disbanded."); break;
default: break;
}
LOG_INFO("SMSG_LFG_UPDATE_PLAYER/PARTY: updateType=", static_cast<int>(updateType));
return;
}
/*bool queued =*/ packet.readUInt8();
packet.readUInt8(); // unk1
packet.readUInt8(); // unk2
if (packet.getSize() - packet.getReadPos() >= 1) {
uint8_t count = packet.readUInt8();
for (uint8_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 4; ++i) {
uint32_t dungeonEntry = packet.readUInt32();
if (i == 0) lfgDungeonId_ = dungeonEntry;
}
}
switch (updateType) {
case 6: lfgState_ = LfgState::Queued;
addSystemChatMessage("Dungeon Finder: You have joined the queue."); break;
case 11: lfgState_ = LfgState::Proposal;
addSystemChatMessage("Dungeon Finder: A group has been found!"); break;
case 12: lfgState_ = LfgState::Queued;
addSystemChatMessage("Dungeon Finder: Added to queue."); break;
case 13: lfgState_ = LfgState::Proposal;
addSystemChatMessage("Dungeon Finder: Proposal started."); break;
case 14: lfgState_ = LfgState::InDungeon; break;
case 16: addSystemChatMessage("Dungeon Finder: Two members are ready."); break;
default: break;
}
LOG_INFO("SMSG_LFG_UPDATE_PLAYER/PARTY: updateType=", static_cast<int>(updateType));
}
void GameHandler::handleLfgPlayerReward(network::Packet& packet) {
size_t remaining = packet.getSize() - packet.getReadPos();
if (remaining < 4 + 4 + 1 + 4 + 4 + 4) return;
/*uint32_t randomDungeonEntry =*/ packet.readUInt32();
/*uint32_t dungeonEntry =*/ packet.readUInt32();
packet.readUInt8(); // unk
uint32_t money = packet.readUInt32();
uint32_t xp = packet.readUInt32();
std::string rewardMsg = "Dungeon Finder reward: " + std::to_string(money) + "g " +
std::to_string(xp) + " XP";
if (packet.getSize() - packet.getReadPos() >= 4) {
uint32_t rewardCount = packet.readUInt32();
for (uint32_t i = 0; i < rewardCount && packet.getSize() - packet.getReadPos() >= 9; ++i) {
uint32_t itemId = packet.readUInt32();
uint32_t itemCount = packet.readUInt32();
packet.readUInt8(); // unk
if (i == 0) {
rewardMsg += ", item #" + std::to_string(itemId);
if (itemCount > 1) rewardMsg += " x" + std::to_string(itemCount);
}
}
}
addSystemChatMessage(rewardMsg);
lfgState_ = LfgState::FinishedDungeon;
LOG_INFO("SMSG_LFG_PLAYER_REWARD: money=", money, " xp=", xp);
}
void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) {
size_t remaining = packet.getSize() - packet.getReadPos();
if (remaining < 7 + 4 + 4 + 4 + 4) return;
bool inProgress = packet.readUInt8() != 0;
bool myVote = packet.readUInt8() != 0;
bool myAnswer = packet.readUInt8() != 0;
uint32_t totalVotes = packet.readUInt32();
uint32_t bootVotes = packet.readUInt32();
uint32_t timeLeft = packet.readUInt32();
uint32_t votesNeeded = packet.readUInt32();
(void)myVote; (void)totalVotes; (void)bootVotes; (void)timeLeft; (void)votesNeeded;
if (inProgress) {
addSystemChatMessage(
std::string("Dungeon Finder: Vote to kick in progress (") +
std::to_string(timeLeft) + "s remaining).");
} else if (myAnswer) {
addSystemChatMessage("Dungeon Finder: Vote kick passed — member removed.");
} else {
addSystemChatMessage("Dungeon Finder: Vote kick failed.");
}
LOG_INFO("SMSG_LFG_BOOT_PROPOSAL_UPDATE: inProgress=", inProgress,
" bootVotes=", bootVotes, "/", totalVotes);
}
void GameHandler::handleLfgTeleportDenied(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 1) return;
uint8_t reason = packet.readUInt8();
const char* msg = lfgTeleportDeniedString(reason);
addSystemChatMessage(std::string("Dungeon Finder: ") + msg);
LOG_INFO("SMSG_LFG_TELEPORT_DENIED: reason=", static_cast<int>(reason));
}
// ---------------------------------------------------------------------------
// LFG outgoing packets
// ---------------------------------------------------------------------------
void GameHandler::lfgJoin(uint32_t dungeonId, uint8_t roles) {
if (state != WorldState::IN_WORLD || !socket) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_LFG_JOIN));
pkt.writeUInt8(roles);
pkt.writeUInt8(0); // needed
pkt.writeUInt8(0); // unk
pkt.writeUInt8(1); // 1 dungeon in list
pkt.writeUInt32(dungeonId);
pkt.writeString(""); // comment
socket->send(pkt);
LOG_INFO("Sent CMSG_LFG_JOIN: dungeonId=", dungeonId, " roles=", static_cast<int>(roles));
}
void GameHandler::lfgLeave() {
if (!socket) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_LFG_LEAVE));
// CMSG_LFG_LEAVE has an LFG identifier block; send zeroes to leave any active queue.
pkt.writeUInt32(0); // slot
pkt.writeUInt32(0); // unk
pkt.writeUInt32(0); // dungeonId
socket->send(pkt);
lfgState_ = LfgState::None;
LOG_INFO("Sent CMSG_LFG_LEAVE");
}
void GameHandler::lfgAcceptProposal(uint32_t proposalId, bool accept) {
if (!socket) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_LFG_PROPOSAL_RESULT));
pkt.writeUInt32(proposalId);
pkt.writeUInt8(accept ? 1 : 0);
socket->send(pkt);
LOG_INFO("Sent CMSG_LFG_PROPOSAL_RESULT: proposalId=", proposalId, " accept=", accept);
}
void GameHandler::lfgTeleport(bool toLfgDungeon) {
if (!socket) return;
network::Packet pkt(wireOpcode(Opcode::CMSG_LFG_TELEPORT));
pkt.writeUInt8(toLfgDungeon ? 0 : 1); // 0=teleport in, 1=teleport out
socket->send(pkt);
LOG_INFO("Sent CMSG_LFG_TELEPORT: toLfgDungeon=", toLfgDungeon);
}
void GameHandler::loadAreaTriggerDbc() {
if (areaTriggerDbcLoaded_) return;
areaTriggerDbcLoaded_ = true;