From f35c3c4ae9b071868c43fff16791861de84fb004 Mon Sep 17 00:00:00 2001 From: Nico Date: Sun, 20 Sep 2026 12:11:44 +0200 Subject: [PATCH] Enabling solo-dungeon queue and implementing first dungeonhelper prototyp --- src/server/game/DungeonFinding/LFGQueue.cpp | 104 +++++++++++++++++++- src/server/game/DungeonFinding/LFGQueue.h | 4 +- src/server/game/World/World.cpp | 14 +++ src/server/game/World/World.h | 3 + src/server/scripts/World/item_scripts.cpp | 46 +++++++++ 5 files changed, 168 insertions(+), 3 deletions(-) diff --git a/src/server/game/DungeonFinding/LFGQueue.cpp b/src/server/game/DungeonFinding/LFGQueue.cpp index ee2f771f1e..91fc792b71 100644 --- a/src/server/game/DungeonFinding/LFGQueue.cpp +++ b/src/server/game/DungeonFinding/LFGQueue.cpp @@ -23,6 +23,7 @@ #include "Group.h" #include "LFGQueue.h" #include "LFGMgr.h" +#include "World.h" #include "Log.h" namespace lfg @@ -331,6 +332,105 @@ uint8 LFGQueue::FindGroups() else AddToCurrentQueue(frontguid); // Lfg group not found, add this group to the queue. } + + // Keep normal TrinityCore matchmaking authoritative. Only after normal + // matching has failed do we permit an underfilled low-population group. + if (sWorld->getBoolConfig(CONFIG_DUNGEON_FINDER_LOW_POPULATION_ENABLE)) + proposals += FindLowPopulationGroups(); + + return proposals; +} + +GuidList LFGQueue::GetQueueGuidsFromKey(std::string const& key) const +{ + GuidList result; + + for (GuidList::const_iterator itr = currentQueueStore.begin(); itr != currentQueueStore.end(); ++itr) + { + std::ostringstream o; + o << itr->GetRawValue(); + std::string token = o.str(); + + size_t pos = key.find(token); + while (pos != std::string::npos) + { + size_t end = pos + token.length(); + bool leftBoundary = pos == 0 || key[pos - 1] == '|'; + bool rightBoundary = end == key.length() || key[end] == '|'; + + if (leftBoundary && rightBoundary) + { + result.push_back(*itr); + break; + } + + pos = key.find(token, pos + 1); + } + } + + return result; +} + +uint8 LFGQueue::FindLowPopulationGroups() +{ + uint8 proposals = 0; + uint32 const fallbackDelay = + sWorld->getIntConfig(CONFIG_DUNGEON_FINDER_LOW_POPULATION_FALLBACK_DELAY); + uint32 const minPlayers = + sWorld->getIntConfig(CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS); + time_t const now = GameTime::GetGameTime(); + + bool createdProposal = true; + while (createdProposal) + { + createdProposal = false; + GuidList queueSnapshot = currentQueueStore; + + for (GuidList::const_iterator itrGuid = queueSnapshot.begin(); itrGuid != queueSnapshot.end(); ++itrGuid) + { + LfgQueueDataContainer::iterator itrQueue = QueueDataStore.find(*itrGuid); + if (itrQueue == QueueDataStore.end()) + continue; + + LfgQueueData& queueData = itrQueue->second; + + if (now < queueData.joinTime || uint32(now - queueData.joinTime) < fallbackDelay) + continue; + + if (queueData.bestCompatible.empty()) + FindBestCompatibleInQueue(itrQueue); + + if (queueData.bestCompatible.empty()) + continue; + + GuidList check = GetQueueGuidsFromKey(queueData.bestCompatible); + if (check.empty()) + continue; + + uint32 playerCount = 0; + for (GuidList::const_iterator itrCheck = check.begin(); itrCheck != check.end(); ++itrCheck) + { + LfgQueueDataContainer::const_iterator itrData = QueueDataStore.find(*itrCheck); + if (itrData != QueueDataStore.end()) + playerCount += uint32(itrData->second.roles.size()); + } + + if (playerCount < minPlayers) + continue; + + TC_LOG_DEBUG("lfg.queue.lowpopulation", + "Low population fallback after %u seconds: trying partial group (%s) with %u player(s)", + fallbackDelay, ConcatenateGuids(check).c_str(), playerCount); + + if (CheckCompatibility(check, true) == LFG_COMPATIBLES_MATCH) + { + ++proposals; + createdProposal = true; + break; + } + } + } + return proposals; } @@ -379,7 +479,7 @@ LfgCompatibility LFGQueue::FindNewGroups(GuidList& check, GuidList& all) @param[in] check List of guids to check compatibilities @return LfgCompatibility type of compatibility */ -LfgCompatibility LFGQueue::CheckCompatibility(GuidList check) +LfgCompatibility LFGQueue::CheckCompatibility(GuidList check, bool allowIncomplete) { std::string strGuids = ConcatenateGuids(check); LfgProposal proposal; @@ -544,7 +644,7 @@ LfgCompatibility LFGQueue::CheckCompatibility(GuidList check) } // Enough players? - if (numPlayers != dungeon->GetMaxGroupSize()) + if (numPlayers != dungeon->GetMaxGroupSize() && !allowIncomplete) { TC_LOG_DEBUG("lfg.queue.match.compatibility.check", "Guids: (%s) Compatibles but not enough players(%u)", GetDetailedMatchRoles(check).c_str(), numPlayers); LfgCompatibilityData data(LFG_COMPATIBLES_WITH_LESS_PLAYERS); diff --git a/src/server/game/DungeonFinding/LFGQueue.h b/src/server/game/DungeonFinding/LFGQueue.h index 4d01cb7345..04c2a5f158 100644 --- a/src/server/game/DungeonFinding/LFGQueue.h +++ b/src/server/game/DungeonFinding/LFGQueue.h @@ -138,9 +138,11 @@ class TC_GAME_API LFGQueue LfgCompatibilityData* GetCompatibilityData(std::string const& key); void FindBestCompatibleInQueue(LfgQueueDataContainer::iterator itrQueue); void UpdateBestCompatibleInQueue(LfgQueueDataContainer::iterator itrQueue, std::string const& key, LfgRolesMap const& roles); + uint8 FindLowPopulationGroups(); + GuidList GetQueueGuidsFromKey(std::string const& key) const; LfgCompatibility FindNewGroups(GuidList& check, GuidList& all); - LfgCompatibility CheckCompatibility(GuidList check); + LfgCompatibility CheckCompatibility(GuidList check, bool allowIncomplete = false); // Queue LfgQueueDataContainer QueueDataStore; ///< Queued groups diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 01e1391063..b29940e14a 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -1521,6 +1521,20 @@ void World::LoadConfigSettings(bool reload) // Dungeon finder m_int_configs[CONFIG_LFG_OPTIONSMASK] = sConfigMgr->GetIntDefault("DungeonFinder.OptionsMask", 1); + m_bool_configs[CONFIG_DUNGEON_FINDER_LOW_POPULATION_ENABLE] = + sConfigMgr->GetBoolDefault("DungeonFinder.LowPopulation.Enable", false); + m_int_configs[CONFIG_DUNGEON_FINDER_LOW_POPULATION_FALLBACK_DELAY] = + sConfigMgr->GetIntDefault("DungeonFinder.LowPopulation.FallbackDelay", 60); + m_int_configs[CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS] = + sConfigMgr->GetIntDefault("DungeonFinder.LowPopulation.MinPlayers", 1); + + if (m_int_configs[CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS] < 1) + { + TC_LOG_ERROR("server.loading", + "DungeonFinder.LowPopulation.MinPlayers (%u) must be at least 1. Using 1 instead.", + m_int_configs[CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS]); + m_int_configs[CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS] = 1; + } // DBC_ItemAttributes m_bool_configs[CONFIG_DBC_ENFORCE_ITEM_ATTRIBUTES] = sConfigMgr->GetBoolDefault("DBC.EnforceItemAttributes", true); diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index 81426b7e73..af8b5db5b4 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -122,6 +122,7 @@ enum WorldBoolConfigs : uint8 CONFIG_CHAT_PARTY_RAID_WARNINGS, CONFIG_DETECT_POS_COLLISION, CONFIG_RESTRICTED_LFG_CHANNEL, + CONFIG_DUNGEON_FINDER_LOW_POPULATION_ENABLE, CONFIG_CHAT_FAKE_MESSAGE_PREVENTING, CONFIG_DEATH_CORPSE_RECLAIM_DELAY_PVP, CONFIG_DEATH_CORPSE_RECLAIM_DELAY_PVE, @@ -364,6 +365,8 @@ enum WorldIntConfigs : uint8 CONFIG_PRESERVE_CUSTOM_CHANNEL_DURATION, CONFIG_PERSISTENT_CHARACTER_CLEAN_FLAGS, CONFIG_LFG_OPTIONSMASK, + CONFIG_DUNGEON_FINDER_LOW_POPULATION_FALLBACK_DELAY, + CONFIG_DUNGEON_FINDER_LOW_POPULATION_MIN_PLAYERS, CONFIG_MAX_INSTANCES_PER_HOUR, CONFIG_WARDEN_CLIENT_RESPONSE_DELAY, CONFIG_WARDEN_CLIENT_CHECK_HOLDOFF, diff --git a/src/server/scripts/World/item_scripts.cpp b/src/server/scripts/World/item_scripts.cpp index 8786cb2a70..501e329f87 100644 --- a/src/server/scripts/World/item_scripts.cpp +++ b/src/server/scripts/World/item_scripts.cpp @@ -37,6 +37,7 @@ EndContentData */ #include "Spell.h" #include "SpellMgr.h" #include "TemporarySummon.h" +#include "Log.h" namespace ItemScripts { @@ -412,6 +413,50 @@ class item_generic_limit_chance_above_60 : public ItemScript return true; } }; +/*##### +# item_horde_adventurer_horn +#####*/ + +enum HordeAdventurerHorn +{ + NPC_ORGRIMMAR_GRUNT = 3296 +}; + +class item_horde_adventurer_horn : public ItemScript +{ +public: + item_horde_adventurer_horn() : ItemScript("item_horde_adventurer_horn") { } + + bool OnUse(Player* player, Item* item, SpellCastTargets const& /*targets*/) override + { + TC_LOG_INFO("scripts", "Horde adventureer horn OnUse triggered by player %s (item %u)", player->GetName().c_str(), item->GetEntry()); + // The horn may not be used during combat. + if (player->IsInCombat()) + { + player->SendEquipError(EQUIP_ERR_CLIENT_LOCKED_OUT, item, nullptr); + return true; + } + + // The horn may only be used inside a dungeon instance. + if (!player->GetMap()->IsDungeon()) + { + player->SendEquipError(EQUIP_ERR_CLIENT_LOCKED_OUT, item, nullptr); + return true; + } + + player->SummonCreature( + NPC_ORGRIMMAR_GRUNT, + player->GetPositionX() + 2.0f, + player->GetPositionY(), + player->GetPositionZ(), + player->GetOrientation(), + TEMPSUMMON_MANUAL_DESPAWN, + 0 + ); + + return true; + } +}; } void AddSC_item_scripts() @@ -428,4 +473,5 @@ void AddSC_item_scripts() new item_dehta_trap_smasher(); new item_captured_frog(); new item_generic_limit_chance_above_60(); + new item_horde_adventurer_horn(); }