From 1a9cd340b7296851bddd746416d896fdfbd648ca Mon Sep 17 00:00:00 2001 From: Nico Date: Sun, 20 Sep 2026 14:25:44 +0200 Subject: [PATCH] cleanup fixes of solo dungeon queue --- .../game/DungeonFinding/LFGGroupData.cpp | 31 +++++- src/server/game/DungeonFinding/LFGGroupData.h | 10 ++ src/server/game/DungeonFinding/LFGMgr.cpp | 94 ++++++++++++++++++- src/server/game/DungeonFinding/LFGMgr.h | 10 ++ src/server/game/DungeonFinding/LFGScripts.cpp | 31 +++++- src/server/game/Groups/Group.cpp | 2 +- src/server/game/Handlers/LFGHandler.cpp | 15 ++- src/server/game/World/World.cpp | 2 + src/server/game/World/World.h | 1 + 9 files changed, 188 insertions(+), 8 deletions(-) diff --git a/src/server/game/DungeonFinding/LFGGroupData.cpp b/src/server/game/DungeonFinding/LFGGroupData.cpp index 181ce3e081..cf2bb2be29 100644 --- a/src/server/game/DungeonFinding/LFGGroupData.cpp +++ b/src/server/game/DungeonFinding/LFGGroupData.cpp @@ -22,7 +22,8 @@ namespace lfg { LfgGroupData::LfgGroupData(): m_State(LFG_STATE_NONE), m_OldState(LFG_STATE_NONE), - m_Leader(), m_Dungeon(0), m_KicksLeft(LFG_GROUP_MAX_KICKS), m_VoteKickActive(false) + m_Leader(), m_Dungeon(0), m_LowPopulation(false), m_LowPopulationOutsideSince(0), + m_KicksLeft(LFG_GROUP_MAX_KICKS), m_VoteKickActive(false) { } LfgGroupData::~LfgGroupData() @@ -83,6 +84,34 @@ void LfgGroupData::SetDungeon(uint32 dungeon) m_Dungeon = dungeon; } +// LOWPOP_GROUP_LIFECYCLE +void LfgGroupData::SetLowPopulation(bool enabled) +{ + m_LowPopulation = enabled; + if (!enabled) + m_LowPopulationOutsideSince = 0; +} + +bool LfgGroupData::IsLowPopulation() const +{ + return m_LowPopulation; +} + +void LfgGroupData::SetLowPopulationOutsideSince(time_t timestamp) +{ + m_LowPopulationOutsideSince = timestamp; +} + +time_t LfgGroupData::GetLowPopulationOutsideSince() const +{ + return m_LowPopulationOutsideSince; +} + +void LfgGroupData::ClearLowPopulationOutsideSince() +{ + m_LowPopulationOutsideSince = 0; +} + void LfgGroupData::DecreaseKicksLeft() { if (m_KicksLeft) diff --git a/src/server/game/DungeonFinding/LFGGroupData.h b/src/server/game/DungeonFinding/LFGGroupData.h index 092c8b0a44..f5617d0863 100644 --- a/src/server/game/DungeonFinding/LFGGroupData.h +++ b/src/server/game/DungeonFinding/LFGGroupData.h @@ -50,6 +50,13 @@ class TC_GAME_API LfgGroupData // Dungeon void SetDungeon(uint32 dungeon); + // Low-population dungeon groups + void SetLowPopulation(bool enabled); + bool IsLowPopulation() const; + void SetLowPopulationOutsideSince(time_t timestamp); + time_t GetLowPopulationOutsideSince() const; + void ClearLowPopulationOutsideSince(); + // VoteKick void DecreaseKicksLeft(); @@ -77,6 +84,9 @@ class TC_GAME_API LfgGroupData GuidSet m_Players; ///< Players in group // Dungeon uint32 m_Dungeon; ///< Dungeon entry + // Low-population dungeon group + bool m_LowPopulation; ///< Group was started below normal dungeon group size + time_t m_LowPopulationOutsideSince; ///< Time the last player left the dungeon map, 0 if inside // Vote Kick uint8 m_KicksLeft; ///< Number of kicks left bool m_VoteKickActive; diff --git a/src/server/game/DungeonFinding/LFGMgr.cpp b/src/server/game/DungeonFinding/LFGMgr.cpp index 9eab3973ed..da959db76b 100644 --- a/src/server/game/DungeonFinding/LFGMgr.cpp +++ b/src/server/game/DungeonFinding/LFGMgr.cpp @@ -300,6 +300,55 @@ void LFGMgr::Update(uint32 diff) time_t currTime = GameTime::GetGameTime(); + // LOWPOP_GROUP_LIFECYCLE + // A solo low-population LFG group may remain alive outside the dungeon for a + // configurable grace period. A timeout of 0 disables automatic cleanup. + int32 const lowPopulationOutsideTimeout = + sWorld->getIntConfig(CONFIG_DUNGEON_FINDER_LOW_POPULATION_SOLO_GROUP_OUTSIDE_TIMEOUT); + + if (lowPopulationOutsideTimeout > 0) + { + GuidList expiredLowPopulationGroups; + + for (LfgGroupDataContainer::const_iterator it = GroupsStore.begin(); it != GroupsStore.end(); ++it) + { + LfgGroupData const& groupData = it->second; + time_t const outsideSince = groupData.GetLowPopulationOutsideSince(); + + if (!groupData.IsLowPopulation() || groupData.GetPlayerCount() != 1 || !outsideSince) + continue; + + if (currTime >= outsideSince + lowPopulationOutsideTimeout) + expiredLowPopulationGroups.push_back(it->first); + } + + // Disband only after iterating GroupsStore because OnDisband removes + // the group's LFG data from this same container. + for (GuidList::const_iterator it = expiredLowPopulationGroups.begin(); it != expiredLowPopulationGroups.end(); ++it) + { + ObjectGuid gguid = *it; + Group* group = sGroupMgr->GetGroupByGUID(gguid.GetCounter()); + + if (!group) + { + RemoveGroupData(gguid); + continue; + } + + if (group->GetMembersCount() != 1) + { + ClearLowPopulationOutsideTimer(gguid); + continue; + } + + TC_LOG_DEBUG("lfg", "Low-population solo LFG group %s expired after %u seconds outside the dungeon.", + gguid.ToString().c_str(), uint32(lowPopulationOutsideTimeout)); + + LeaveLfg(gguid); + group->Disband(); + } + } + // Remove obsolete role checks for (LfgRoleCheckContainer::iterator it = RoleChecksStore.begin(); it != RoleChecksStore.end();) { @@ -1013,6 +1062,10 @@ void LFGMgr::MakeNewGroup(LfgProposal const& proposal) ASSERT(dungeon); Group* grp = proposal.group ? sGroupMgr->GetGroupByGUID(proposal.group.GetCounter()) : nullptr; + bool const lowPopulation = proposal.players.size() < dungeon->GetMaxGroupSize(); + if (grp) + SetLowPopulationGroup(proposal.group, lowPopulation); + for (GuidList::const_iterator it = players.begin(); it != players.end(); ++it) { ObjectGuid pguid = (*it); @@ -1035,6 +1088,7 @@ void LFGMgr::MakeNewGroup(LfgProposal const& proposal) ObjectGuid gguid = grp->GetGUID(); SetState(gguid, LFG_STATE_PROPOSAL); sGroupMgr->AddGroup(grp); + SetLowPopulationGroup(gguid, lowPopulation); } else if (group != grp) grp->AddMember(player); @@ -1053,7 +1107,10 @@ void LFGMgr::MakeNewGroup(LfgProposal const& proposal) { uint32 rDungeonId = (*dungeons.begin()); LFGDungeonEntry const* dungeon = sLFGDungeonStore.LookupEntry(rDungeonId); - if (dungeon && dungeon->TypeID == LFG_TYPE_RANDOM) + // LOWPOP_CLIENT_STATUS + // Low-population fallback groups deliberately skip the normal + // random-dungeon requeue cooldown. + if (dungeon && dungeon->TypeID == LFG_TYPE_RANDOM && !lowPopulation) player->CastSpell(player, LFG_SPELL_DUNGEON_COOLDOWN, false); } } @@ -1941,6 +1998,12 @@ void LFGMgr::SetTicket(ObjectGuid guid, WorldPackets::LFG::RideTicket const& tic PlayersStore[guid].SetTicket(ticket); } +// LOWPOP_LEAVE_CLEANUP +void LFGMgr::ClearTicket(ObjectGuid guid) +{ + PlayersStore[guid].SetTicket(WorldPackets::LFG::RideTicket()); +} + void LFGMgr::RemovePlayerData(ObjectGuid guid) { TC_LOG_TRACE("lfg.data.player.remove", "Player: %s", guid.ToString().c_str()); @@ -1949,6 +2012,35 @@ void LFGMgr::RemovePlayerData(ObjectGuid guid) PlayersStore.erase(it); } +// LOWPOP_GROUP_LIFECYCLE +void LFGMgr::SetLowPopulationGroup(ObjectGuid gguid, bool enabled) +{ + GroupsStore[gguid].SetLowPopulation(enabled); +} + +bool LFGMgr::IsLowPopulationGroup(ObjectGuid gguid) +{ + LfgGroupDataContainer::const_iterator it = GroupsStore.find(gguid); + return it != GroupsStore.end() && it->second.IsLowPopulation(); +} + +void LFGMgr::StartLowPopulationOutsideTimer(ObjectGuid gguid) +{ + LfgGroupDataContainer::iterator it = GroupsStore.find(gguid); + if (it == GroupsStore.end() || !it->second.IsLowPopulation()) + return; + + if (!it->second.GetLowPopulationOutsideSince()) + it->second.SetLowPopulationOutsideSince(GameTime::GetGameTime()); +} + +void LFGMgr::ClearLowPopulationOutsideTimer(ObjectGuid gguid) +{ + LfgGroupDataContainer::iterator it = GroupsStore.find(gguid); + if (it != GroupsStore.end()) + it->second.ClearLowPopulationOutsideSince(); +} + void LFGMgr::RemoveGroupData(ObjectGuid guid) { TC_LOG_TRACE("lfg.data.group.remove", "Group: %s", guid.ToString().c_str()); diff --git a/src/server/game/DungeonFinding/LFGMgr.h b/src/server/game/DungeonFinding/LFGMgr.h index 18ec0f9075..a448521079 100644 --- a/src/server/game/DungeonFinding/LFGMgr.h +++ b/src/server/game/DungeonFinding/LFGMgr.h @@ -395,6 +395,14 @@ class TC_GAME_API LFGMgr uint8 RemovePlayerFromGroup(ObjectGuid gguid, ObjectGuid guid); /// Adds player to group void AddPlayerToGroup(ObjectGuid gguid, ObjectGuid guid); + /// Marks or clears a group as a low-population dungeon group + void SetLowPopulationGroup(ObjectGuid gguid, bool enabled); + /// Returns true if the group was started through the low-population fallback + bool IsLowPopulationGroup(ObjectGuid gguid); + /// Starts the outside-dungeon cleanup timer for a solo low-population group + void StartLowPopulationOutsideTimer(ObjectGuid gguid); + /// Clears the outside-dungeon cleanup timer + void ClearLowPopulationOutsideTimer(ObjectGuid gguid); // LFGHandler /// Get locked dungeons @@ -427,6 +435,8 @@ class TC_GAME_API LFGMgr void LeaveLfg(ObjectGuid guid, bool disconnected = false); /// Gets unique join queue data WorldPackets::LFG::RideTicket const* GetTicket(ObjectGuid guid) const; + /// Clears the player's LFG ride ticket + void ClearTicket(ObjectGuid guid); // LfgQueue /// Get last lfg state (NONE, DUNGEON or FINISHED_DUNGEON) diff --git a/src/server/game/DungeonFinding/LFGScripts.cpp b/src/server/game/DungeonFinding/LFGScripts.cpp index 8222836ee4..dc044a4f3c 100644 --- a/src/server/game/DungeonFinding/LFGScripts.cpp +++ b/src/server/game/DungeonFinding/LFGScripts.cpp @@ -92,6 +92,9 @@ void LFGPlayerScript::OnMapChanged(Player* player) return; } + if (sLFGMgr->IsLowPopulationGroup(group->GetGUID())) + sLFGMgr->ClearLowPopulationOutsideTimer(group->GetGUID()); + for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) if (Player* member = itr->GetSource()) player->GetSession()->SendNameQueryOpcode(member->GetGUID()); @@ -104,10 +107,19 @@ void LFGPlayerScript::OnMapChanged(Player* player) Group* group = player->GetGroup(); if (group && group->GetMembersCount() == 1) { - sLFGMgr->LeaveLfg(group->GetGUID()); - group->Disband(); - TC_LOG_DEBUG("lfg", "LFGPlayerScript::OnMapChanged, Player %s(%s) is last in the lfggroup so we disband the group.", - player->GetName().c_str(), player->GetGUID().ToString().c_str()); + if (sLFGMgr->IsLowPopulationGroup(group->GetGUID())) + { + sLFGMgr->StartLowPopulationOutsideTimer(group->GetGUID()); + TC_LOG_DEBUG("lfg", "LFGPlayerScript::OnMapChanged, Player %s(%s) left the dungeon as the last member of a low-population LFG group. Starting outside timer.", + player->GetName().c_str(), player->GetGUID().ToString().c_str()); + } + else + { + sLFGMgr->LeaveLfg(group->GetGUID()); + group->Disband(); + TC_LOG_DEBUG("lfg", "LFGPlayerScript::OnMapChanged, Player %s(%s) is last in the lfggroup so we disband the group.", + player->GetName().c_str(), player->GetGUID().ToString().c_str()); + } } player->RemoveAurasDueToSpell(LFG_SPELL_LUCK_OF_THE_DRAW); } @@ -155,6 +167,7 @@ void LFGGroupScript::OnRemoveMember(Group* group, ObjectGuid guid, RemoveMethod gguid.ToString().c_str(), guid.ToString().c_str(), method, kicker.ToString().c_str(), (reason ? reason : "")); bool isLFG = group->isLFGGroup(); + bool lowPopulation = isLFG && sLFGMgr->IsLowPopulationGroup(gguid); if (isLFG && method == GROUP_REMOVEMETHOD_KICK) // Player have been kicked { @@ -183,6 +196,16 @@ void LFGGroupScript::OnRemoveMember(Group* group, ObjectGuid guid, RemoveMethod if (Player* player = ObjectAccessor::FindPlayer(guid)) { + // LOWPOP_LEAVE_CLEANUP + // Every voluntary leaver from a low-population LFG group gets a fully + // cleared personal LFG ticket/status. Remaining group members stay. + if (lowPopulation && method == GROUP_REMOVEMETHOD_LEAVE) + { + sLFGMgr->ClearTicket(guid); + player->GetSession()->SendLfgUpdateStatus(LfgUpdateData(LFG_UPDATETYPE_REMOVED_FROM_QUEUE), true); + player->GetSession()->SendLfgUpdateStatus(LfgUpdateData(LFG_UPDATETYPE_REMOVED_FROM_QUEUE), false); + } + if (method == GROUP_REMOVEMETHOD_LEAVE && state == LFG_STATE_DUNGEON && players >= LFG_GROUP_KICK_VOTES_NEEDED) player->CastSpell(player, LFG_SPELL_DUNGEON_DESERTER, true); diff --git a/src/server/game/Groups/Group.cpp b/src/server/game/Groups/Group.cpp index 93d5e8b838..2dc9d2ea84 100644 --- a/src/server/game/Groups/Group.cpp +++ b/src/server/game/Groups/Group.cpp @@ -671,7 +671,7 @@ bool Group::RemoveMember(ObjectGuid guid, const RemoveMethod& method /*= GROUP_R SendRaidMarkerUpdateToPlayer(guid, true); - if (isLFGGroup() && GetMembersCount() == 1) + if (isLFGGroup() && GetMembersCount() == 1 && !sLFGMgr->IsLowPopulationGroup(GetGUID())) { Player* leader = ObjectAccessor::FindConnectedPlayer(GetLeaderGUID()); uint32 mapId = sLFGMgr->GetDungeonMapId(GetGUID()); diff --git a/src/server/game/Handlers/LFGHandler.cpp b/src/server/game/Handlers/LFGHandler.cpp index d8c3a57845..528d09ffea 100644 --- a/src/server/game/Handlers/LFGHandler.cpp +++ b/src/server/game/Handlers/LFGHandler.cpp @@ -354,10 +354,23 @@ void WorldSession::SendLfgUpdateStatus(lfg::LfgUpdateData const& updateData, boo join = true; break; case lfg::LFG_UPDATETYPE_UPDATE_STATUS: + { + // LOWPOP_CLIENT_STATUS + // A low-population LFG group can legitimately remain active with + // one player outside the dungeon. Keep it client-side joined so + // the normal LFG dungeon controls remain available. + bool lowPopulationDungeon = false; + if (Group* group = GetPlayer()->GetGroup()) + lowPopulationDungeon = group->isLFGGroup() && sLFGMgr->IsLowPopulationGroup(group->GetGUID()); + join = updateData.state != lfg::LFG_STATE_ROLECHECK && updateData.state != lfg::LFG_STATE_NONE && - updateData.state != lfg::LFG_STATE_DUNGEON && updateData.state != lfg::LFG_STATE_FINISHED_DUNGEON; + (lowPopulationDungeon || + (updateData.state != lfg::LFG_STATE_DUNGEON && + updateData.state != lfg::LFG_STATE_FINISHED_DUNGEON)); + queued = updateData.state == lfg::LFG_STATE_QUEUED || updateData.state == lfg::LFG_STATE_RAIDBROWSER; break; + } default: break; } diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index b29940e14a..4e8664fe89 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -1527,6 +1527,8 @@ void World::LoadConfigSettings(bool reload) sConfigMgr->GetIntDefault("DungeonFinder.LowPopulation.FallbackDelay", 60); m_int_configs[CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS] = sConfigMgr->GetIntDefault("DungeonFinder.LowPopulation.MinPlayers", 1); + m_int_configs[CONFIG_DUNGEON_FINDER_LOW_POPULATION_SOLO_GROUP_OUTSIDE_TIMEOUT] = + sConfigMgr->GetIntDefault("DungeonFinder.LowPopulation.SoloGroupOutsideTimeout", 300); if (m_int_configs[CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS] < 1) { diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index af8b5db5b4..9a05f0882d 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -367,6 +367,7 @@ enum WorldIntConfigs : uint8 CONFIG_LFG_OPTIONSMASK, CONFIG_DUNGEON_FINDER_LOW_POPULATION_FALLBACK_DELAY, CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS, + CONFIG_DUNGEON_FINDER_LOW_POPULATION_SOLO_GROUP_OUTSIDE_TIMEOUT, CONFIG_MAX_INSTANCES_PER_HOUR, CONFIG_WARDEN_CLIENT_RESPONSE_DELAY, CONFIG_WARDEN_CLIENT_CHECK_HOLDOFF,