diff --git a/conf/mod-bg-auto-queue.conf.dist b/conf/mod-bg-auto-queue.conf.dist index 149aade..97c91b8 100644 --- a/conf/mod-bg-auto-queue.conf.dist +++ b/conf/mod-bg-auto-queue.conf.dist @@ -40,6 +40,11 @@ BgAutoQueue.Level.Min = 10 # BgAutoQueue.Level.Max # Description: Maximum character level eligible for the automatic queue. # Players above this level are skipped. +# On sub-max brackets, batch viability follows the core +# Battleground.Override.LowLevels.MinPlayers override when +# set. A max-level bracket (reachable only with +# Level.Max = 80) keeps the raw template minimums, +# matching core. # Default: 79 # diff --git a/src/BgAutoQueue.cpp b/src/BgAutoQueue.cpp index 1a6532d..13ad243 100644 --- a/src/BgAutoQueue.cpp +++ b/src/BgAutoQueue.cpp @@ -8,6 +8,7 @@ #include "Battleground.h" #include "BattlegroundMgr.h" #include "BattlegroundQueue.h" +#include "BattlegroundUtils.h" #include "Chat.h" #include "Config.h" #include "Containers.h" @@ -110,6 +111,15 @@ void BgAutoQueue::LoadConfig() LOG_WARN("module", "BgAutoQueue.WarningLeadTime ({} s) >= Interval ({} min); the warning will not fire.", warningLeadSec, intervalMin); _crossFaction = sConfigMgr->GetOption("BgAutoQueue.CrossFaction", true); + + // Mirror mod-cfbg's defaults (CFBG.cpp LoadConfig). Only CFBG on + EvenTeams + // on + threshold 0 pins CFBG's allowedDiff to 0 in both match formation and + // reinforcement; every other combination tolerates odd totals, so the parity + // trim in QueueBucket self-disables (including installs without mod-cfbg). + _evenTeamsStrict = sConfigMgr->GetOption("CFBG.Enable", false) + && sConfigMgr->GetOption("CFBG.EvenTeams.Enabled", false) + && sConfigMgr->GetOption("CFBG.EvenTeams.MaxPlayersThreshold", 0) == 0; + _skipGameMasters = sConfigMgr->GetOption("BgAutoQueue.SkipGameMasters", true); _skipAfk = sConfigMgr->GetOption("BgAutoQueue.SkipAFK", true); _broadcastMessage = sConfigMgr->GetOption("BgAutoQueue.BroadcastMessage", BG_AUTO_QUEUE_DEFAULT_BROADCAST); @@ -133,8 +143,8 @@ void BgAutoQueue::LoadConfig() _warningSent = false; _firstPass = true; - LOG_INFO("module", "mod-bg-auto-queue: enabled={}, levels=[{}-{}], pool size={}, interval={} min, initialDelay={} s, warningLead={} s, crossFaction={}, skipGM={}, skipAFK={}, skipAuras={}.", - _enabled, _levelMin, _levelMax, _poolRaw.size(), intervalMin, initialDelaySec, warningLeadSec, _crossFaction, _skipGameMasters, _skipAfk, _skipAuras.size()); + LOG_INFO("module", "mod-bg-auto-queue: enabled={}, levels=[{}-{}], pool size={}, interval={} min, initialDelay={} s, warningLead={} s, crossFaction={}, evenTeamsStrict={}, skipGM={}, skipAFK={}, skipAuras={}.", + _enabled, _levelMin, _levelMax, _poolRaw.size(), intervalMin, initialDelaySec, warningLeadSec, _crossFaction, _evenTeamsStrict, _skipGameMasters, _skipAfk, _skipAuras.size()); // Opt-out is stored via the core PlayerSettings system, which only persists // across logins when EnablePlayerSettings is on. Without it, .bgevents @@ -406,9 +416,21 @@ BgAutoQueue::QueuedWaiters BgAutoQueue::CountUninvitedWaiters(BattlegroundTypeId return waiters; } +uint32 BgAutoQueue::GetEffectiveMinPlayersPerTeam(Battleground* bgTemplate, BracketBucket const& bucket) const +{ + PvPDifficultyEntry const* bracketEntry = GetBattlegroundBracketByLevel(bgTemplate->GetMapId(), bucket.minLevel); + if (!bracketEntry) + bracketEntry = GetBattlegroundBracketByLevel(bgTemplate->GetMapId(), bucket.maxLevel); + + if (!bracketEntry) + return bgTemplate->GetMinPlayersPerTeam(); + + return ::GetMinPlayersPerTeam(bgTemplate, bracketEntry); +} + bool BgAutoQueue::IsViable(Battleground* bgTemplate, BracketBucket const& bucket) const { - uint32 const minPerTeam = bgTemplate->GetMinPlayersPerTeam(); + uint32 const minPerTeam = GetEffectiveMinPlayersPerTeam(bgTemplate, bucket); QueuedWaiters const waiters = CountUninvitedWaiters(bgTemplate->GetBgTypeID(), bucket.minLevel, bucket.maxLevel); if (_crossFaction) @@ -525,7 +547,7 @@ BattlegroundTypeId BgAutoQueue::SelectBattlegroundForBracket(BracketBucket const if (!bgTemplate) continue; - uint32 const minPerTeam = bgTemplate->GetMinPlayersPerTeam(); + uint32 const minPerTeam = GetEffectiveMinPlayersPerTeam(bgTemplate, bucket); if (minPerTeam < bestMin || (minPerTeam == bestMin && bgTypeId < best)) { bestMin = minPerTeam; @@ -549,8 +571,9 @@ uint32 BgAutoQueue::QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); - uint32 queued = 0; - std::vector scheduledBrackets; + // Phase 1: re-validate and collect. Holding Player* between the two phases + // is safe: both run inside this one synchronous call, no tick boundary. + std::vector> verified; for (ObjectGuid guid : bucket.players) { @@ -594,6 +617,28 @@ uint32 BgAutoQueue::QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const continue; } + verified.push_back({ player, bracketEntry }); + } + + // Strict EvenTeams (allowedDiff 0) can never invite an odd total: one solo + // would be stranded until an external parity break. Leave the newest + // unqueued instead; the next pass picks them up. + if (_crossFaction && _evenTeamsStrict && !verified.empty()) + { + QueuedWaiters const waiters = CountUninvitedWaiters(bgTypeId, bucket.minLevel, bucket.maxLevel); + if ((verified.size() + waiters.total) % 2 != 0) + { + LOG_DEBUG("module", "mod-bg-auto-queue: parity trim: leaving {} unqueued to keep the wave even for strict EvenTeams.", verified.back().first->GetName()); + verified.pop_back(); + } + } + + // Phase 2: queue everyone that survived. + uint32 queued = 0; + std::vector scheduledBrackets; + + for (auto const& [player, bracketEntry] : verified) + { GroupQueueInfo* ginfo = bgQueue.AddGroup(player, nullptr, bgTypeId, bracketEntry, 0, false, false, 0, 0); uint32 avgWaitTime = bgQueue.GetAverageQueueWaitTime(ginfo); uint32 queueSlot = player->AddBattlegroundQueueId(bgQueueTypeId); diff --git a/src/BgAutoQueue.h b/src/BgAutoQueue.h index 49452ae..bcbc561 100644 --- a/src/BgAutoQueue.h +++ b/src/BgAutoQueue.h @@ -143,6 +143,15 @@ class BgAutoQueue QueuedWaiters CountUninvitedWaiters(BattlegroundTypeId bgTypeId, uint32 minLevel, uint32 maxLevel) const; + // Bracket-aware MinPlayersPerTeam for viability math: resolves the bucket's + // bracket on the candidate BG's own map and returns the same override-aware + // minimum core matching uses (GetMinPlayersPerTeam, BattlegroundUtils.h), so + // Battleground.Override.LowLevels.MinPlayers is honoured on sub-max brackets. + // Bucket levels come from the WSG reference map, so the candidate may have no + // bracket at minLevel (hence the maxLevel retry) or none at all, in which + // case the raw template minimum is returned. + uint32 GetEffectiveMinPlayersPerTeam(Battleground* bgTemplate, BracketBucket const& bucket) const; + // Viability per CrossFaction: cross-faction => total >= 2*min; otherwise // each faction tally >= min. Includes uninvited players already queued for // the candidate BG at the bucket's levels, not just the freshly-gathered batch. @@ -159,10 +168,14 @@ class BgAutoQueue // Queues every player in the bucket into bgTypeId, then schedules one // queue update per distinct bracket queued into. When liveBracket is set // (live-BG reinforcement), players whose own bracket differs are skipped — - // that game's queue list would never serve them. Returns the number of - // players queued. When non-null, skippedAtQueueTime is incremented for each - // bucket player dropped by the queue-time re-check, the BG-specific veto, - // or the off-bracket skip. + // that game's queue list would never serve them. Under strict CFBG + // EvenTeams (see _evenTeamsStrict), an odd wave (re-validated players plus + // uninvited waiters) leaves its newest player unqueued for this pass so no + // solo is stranded behind an allowedDiff of 0; the next pass picks them up. + // Returns the number of players queued. When non-null, skippedAtQueueTime + // is incremented for each bucket player dropped by the queue-time re-check, + // the BG-specific veto, or the off-bracket skip (not for the parity trim — + // a deferral, not a drop). uint32 QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const& bucket, Optional liveBracket, uint32* skippedAtQueueTime = nullptr); @@ -183,6 +196,7 @@ class BgAutoQueue uint32 _initialDelayMs = 0; uint32 _warningLeadMs = 60u * 1000u; bool _crossFaction = true; + bool _evenTeamsStrict = false; // mod-cfbg EvenTeams with threshold 0 (see LoadConfig) bool _skipGameMasters = true; bool _skipAfk = true; std::vector _skipAuras; // aura ids that exclude a player from a pass