From 549aa69c58e7a40dcaa28497372afc2ee145d0cd Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Thu, 2 Jul 2026 12:21:15 +0200 Subject: [PATCH 1/4] fix: count manual waiters in the target BG's own bracket --- src/BgAutoQueue.cpp | 56 +++++++++++++++++++++++++++++---------------- src/BgAutoQueue.h | 22 +++++++++--------- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/src/BgAutoQueue.cpp b/src/BgAutoQueue.cpp index 65dac1d..1d12b49 100644 --- a/src/BgAutoQueue.cpp +++ b/src/BgAutoQueue.cpp @@ -350,7 +350,7 @@ bool BgAutoQueue::BucketFitsLiveBg(Battleground* bg, BracketBucket const& bucket } BgAutoQueue::QueuedWaiters BgAutoQueue::CountUninvitedWaiters(BattlegroundTypeId bgTypeId, - BattlegroundBracketId bracketId) const + uint32 minLevel, uint32 maxLevel) const { QueuedWaiters waiters; @@ -358,6 +358,20 @@ BgAutoQueue::QueuedWaiters BgAutoQueue::CountUninvitedWaiters(BattlegroundTypeId if (bgQueueTypeId == BATTLEGROUND_QUEUE_NONE) return waiters; + Battleground* bgTemplate = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); + if (!bgTemplate) + return waiters; + + // Bracket indices are numbered per map, so the level range must be resolved + // against the candidate BG's own map. Brackets are contiguous, so the two + // endpoints cover the whole range (0, 1, or 2 distinct ids); a level with no + // bracket on this map cannot enter this BG and contributes nothing. + std::vector bracketIds; + for (uint32 level : { minLevel, maxLevel }) + if (PvPDifficultyEntry const* entry = GetBattlegroundBracketByLevel(bgTemplate->GetMapId(), level)) + if (std::find(bracketIds.begin(), bracketIds.end(), entry->GetBracketId()) == bracketIds.end()) + bracketIds.push_back(entry->GetBracketId()); + BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); static constexpr BattlegroundQueueGroupTypes WAITER_GROUP_TYPES[] = @@ -369,30 +383,32 @@ BgAutoQueue::QueuedWaiters BgAutoQueue::CountUninvitedWaiters(BattlegroundTypeId BG_QUEUE_CFBG }; - for (BattlegroundQueueGroupTypes groupType : WAITER_GROUP_TYPES) + for (BattlegroundBracketId bracketId : bracketIds) { - for (GroupQueueInfo const* gInfo : bgQueue.m_QueuedGroups[bracketId][groupType]) + for (BattlegroundQueueGroupTypes groupType : WAITER_GROUP_TYPES) { - if (gInfo->IsInvitedToBGInstanceGUID != 0) - continue; - - uint32 const count = static_cast(gInfo->Players.size()); - waiters.total += count; - if (gInfo->teamId == TEAM_ALLIANCE) - waiters.alliance += count; - else - waiters.horde += count; + for (GroupQueueInfo const* gInfo : bgQueue.m_QueuedGroups[bracketId][groupType]) + { + if (gInfo->IsInvitedToBGInstanceGUID != 0) + continue; + + uint32 const count = static_cast(gInfo->Players.size()); + waiters.total += count; + if (gInfo->teamId == TEAM_ALLIANCE) + waiters.alliance += count; + else + waiters.horde += count; + } } } return waiters; } -bool BgAutoQueue::IsViable(Battleground* bgTemplate, BracketBucket const& bucket, - BattlegroundBracketId bracketId) const +bool BgAutoQueue::IsViable(Battleground* bgTemplate, BracketBucket const& bucket) const { uint32 const minPerTeam = bgTemplate->GetMinPlayersPerTeam(); - QueuedWaiters const waiters = CountUninvitedWaiters(bgTemplate->GetBgTypeID(), bracketId); + QueuedWaiters const waiters = CountUninvitedWaiters(bgTemplate->GetBgTypeID(), bucket.minLevel, bucket.maxLevel); if (_crossFaction) return (bucket.alliance + bucket.horde + waiters.total) >= (2u * minPerTeam); @@ -401,7 +417,7 @@ bool BgAutoQueue::IsViable(Battleground* bgTemplate, BracketBucket const& bucket && (bucket.horde + waiters.horde) >= minPerTeam; } -BattlegroundTypeId BgAutoQueue::SelectBattlegroundForBracket(BattlegroundBracketId bracketId, BracketBucket const& bucket) const +BattlegroundTypeId BgAutoQueue::SelectBattlegroundForBracket(BracketBucket const& bucket) const { // (a) Live-BG reinforcement (priority; not limited to the pool). std::vector liveTypes; @@ -445,12 +461,12 @@ BattlegroundTypeId BgAutoQueue::SelectBattlegroundForBracket(BattlegroundBracket if (!BucketHasAnyFit(bgTypeId, bucket)) continue; - QueuedWaiters const waiters = CountUninvitedWaiters(bgTypeId, bracketId); + QueuedWaiters const waiters = CountUninvitedWaiters(bgTypeId, bucket.minLevel, bucket.maxLevel); if (waiters.total == 0) continue; Battleground* bgTemplate = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); - if (!bgTemplate || !IsViable(bgTemplate, bucket, bracketId)) + if (!bgTemplate || !IsViable(bgTemplate, bucket)) continue; if (waiters.total > bestWaiters) @@ -487,7 +503,7 @@ BattlegroundTypeId BgAutoQueue::SelectBattlegroundForBracket(BattlegroundBracket for (BattlegroundTypeId bgTypeId : candidates) { Battleground* bgTemplate = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); - if (bgTemplate && IsViable(bgTemplate, bucket, bracketId)) + if (bgTemplate && IsViable(bgTemplate, bucket)) viable.push_back(bgTypeId); } @@ -627,7 +643,7 @@ BgAutoQueue::QueuePassResult BgAutoQueue::RunQueuePass() for (auto const& [bracketId, bucket] : buckets) { - BattlegroundTypeId bgTypeId = SelectBattlegroundForBracket(bracketId, bucket); + BattlegroundTypeId bgTypeId = SelectBattlegroundForBracket(bucket); if (bgTypeId == BATTLEGROUND_TYPE_NONE) { LOG_DEBUG("module", "mod-bg-auto-queue: bracket {} has no eligible battleground, skipping.", static_cast(bracketId)); diff --git a/src/BgAutoQueue.h b/src/BgAutoQueue.h index 1b21105..ccc145c 100644 --- a/src/BgAutoQueue.h +++ b/src/BgAutoQueue.h @@ -105,7 +105,7 @@ class BgAutoQueue std::vector players; uint32 alliance = 0; uint32 horde = 0; - uint32 minLevel = 0; // bracket level range, used for logging purposes + uint32 minLevel = 0; // bracket level range, drives waiter counting and logging uint32 maxLevel = 0; }; @@ -133,24 +133,24 @@ class BgAutoQueue uint32 horde = 0; }; - // Counts uninvited players already sitting in bgTypeId's core queue for this - // bracket, split by faction. Scans every solo/premade/cross-faction group - // bucket because which bucket a manual queuer lands in depends on whether - // mod-cfbg is active. Groups already invited to a forming instance are skipped. + // Counts uninvited players already sitting in bgTypeId's core queue, in the + // candidate BG's own map-relative bracket(s) spanning [minLevel, maxLevel] + // (bracket indices are numbered per map), split by faction. Scans every + // solo/premade/cross-faction group bucket because which bucket a manual + // queuer lands in depends on whether mod-cfbg is active. Groups already + // invited to a forming instance are skipped. QueuedWaiters CountUninvitedWaiters(BattlegroundTypeId bgTypeId, - BattlegroundBracketId bracketId) const; + uint32 minLevel, uint32 maxLevel) const; // Viability per CrossFaction: cross-faction => total >= 2*min; otherwise // each faction tally >= min. Includes uninvited players already queued for - // the candidate BG in this bracket, not just the freshly-gathered batch. - bool IsViable(Battleground* bgTemplate, BracketBucket const& bucket, - BattlegroundBracketId bracketId) const; + // the candidate BG at the bucket's levels, not just the freshly-gathered batch. + bool IsViable(Battleground* bgTemplate, BracketBucket const& bucket) const; // Selects the BG for a populated bracket: live-BG reinforcement first, // then a not-yet-running BG that already has uninvited queuers, then a // random pick from the configured pool with documented fallbacks. - BattlegroundTypeId SelectBattlegroundForBracket(BattlegroundBracketId bracketId, - BracketBucket const& bucket) const; + BattlegroundTypeId SelectBattlegroundForBracket(BracketBucket const& bucket) const; // Queues every player in the bucket into bgTypeId, then schedules a single // queue update for the bracket. Returns the number of players queued. When From b8e430bf84186cb053808f18254dfe96cdf0a2f6 Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Thu, 2 Jul 2026 12:23:26 +0200 Subject: [PATCH 2/4] fix: skip off-bracket players when reinforcing a live BG --- src/BgAutoQueue.cpp | 44 ++++++++++++++++++++++++++++++---------- src/BgAutoQueue.h | 23 +++++++++++++-------- src/cs_bg_auto_queue.cpp | 2 +- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/BgAutoQueue.cpp b/src/BgAutoQueue.cpp index 1d12b49..1a6532d 100644 --- a/src/BgAutoQueue.cpp +++ b/src/BgAutoQueue.cpp @@ -28,6 +28,7 @@ #include #include #include +#include namespace { @@ -417,10 +418,11 @@ bool BgAutoQueue::IsViable(Battleground* bgTemplate, BracketBucket const& bucket && (bucket.horde + waiters.horde) >= minPerTeam; } -BattlegroundTypeId BgAutoQueue::SelectBattlegroundForBracket(BracketBucket const& bucket) const +BattlegroundTypeId BgAutoQueue::SelectBattlegroundForBracket(BracketBucket const& bucket, + Optional& liveBracket) const { // (a) Live-BG reinforcement (priority; not limited to the pool). - std::vector liveTypes; + std::vector> liveTypes; for (BattlegroundTypeId bgTypeId : BG_NORMAL_TYPES) { if (sDisableMgr->IsDisabledFor(DISABLE_TYPE_BATTLEGROUND, bgTypeId, nullptr)) @@ -440,13 +442,17 @@ BattlegroundTypeId BgAutoQueue::SelectBattlegroundForBracket(BracketBucket const if (!BucketFitsLiveBg(bg, bucket)) continue; - liveTypes.push_back(bgTypeId); + liveTypes.push_back({ bgTypeId, bg->GetBracketId() }); break; } } if (!liveTypes.empty()) - return Acore::Containers::SelectRandomContainerElement(liveTypes); + { + auto const& [bgTypeId, bracketId] = Acore::Containers::SelectRandomContainerElement(liveTypes); + liveBracket = bracketId; + return bgTypeId; + } // (a2) Prefer a not-yet-running BG that already has uninvited queuers, so a // manual queuer's chosen BG is reinforced instead of bypassed. Among viable @@ -530,7 +536,8 @@ BattlegroundTypeId BgAutoQueue::SelectBattlegroundForBracket(BracketBucket const return best; } -uint32 BgAutoQueue::QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const& bucket, uint32* skippedAtQueueTime) +uint32 BgAutoQueue::QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const& bucket, + Optional liveBracket, uint32* skippedAtQueueTime) { Battleground* bgTemplate = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); if (!bgTemplate) @@ -543,7 +550,7 @@ uint32 BgAutoQueue::QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); uint32 queued = 0; - BattlegroundBracketId scheduledBracket = BG_BRACKET_ID_FIRST; + std::vector scheduledBrackets; for (ObjectGuid guid : bucket.players) { @@ -577,6 +584,16 @@ uint32 BgAutoQueue::QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const continue; } + // Tier-a reinforcement targets one live game; a player whose own bracket + // differs would sit in a queue list that game never serves. + if (liveBracket && bracketEntry->GetBracketId() != *liveBracket) + { + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: level {} is outside the live game's bracket.", player->GetName(), player->GetLevel()); + if (skippedAtQueueTime) + ++*skippedAtQueueTime; + continue; + } + GroupQueueInfo* ginfo = bgQueue.AddGroup(player, nullptr, bgTypeId, bracketEntry, 0, false, false, 0, 0); uint32 avgWaitTime = bgQueue.GetAverageQueueWaitTime(ginfo); uint32 queueSlot = player->AddBattlegroundQueueId(bgQueueTypeId); @@ -590,15 +607,19 @@ uint32 BgAutoQueue::QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const sScriptMgr->OnPlayerJoinBG(player); - scheduledBracket = bracketEntry->GetBracketId(); + if (std::find(scheduledBrackets.begin(), scheduledBrackets.end(), bracketEntry->GetBracketId()) == scheduledBrackets.end()) + scheduledBrackets.push_back(bracketEntry->GetBracketId()); ++queued; LOG_DEBUG("module", "mod-bg-auto-queue: queued {} into bgTypeId {}.", player->GetName(), static_cast(bgTypeId)); } - // Schedule a single queue update for the bracket, not once per player. - if (queued > 0) + // One queue update per distinct bracket queued into, not once per player. + for (BattlegroundBracketId scheduledBracket : scheduledBrackets) + { sBattlegroundMgr->ScheduleQueueUpdate(0, 0, bgQueueTypeId, bgTypeId, scheduledBracket); + LOG_DEBUG("module", "mod-bg-auto-queue: scheduled queue update for bgTypeId {} bracket {}.", static_cast(bgTypeId), static_cast(scheduledBracket)); + } return queued; } @@ -643,7 +664,8 @@ BgAutoQueue::QueuePassResult BgAutoQueue::RunQueuePass() for (auto const& [bracketId, bucket] : buckets) { - BattlegroundTypeId bgTypeId = SelectBattlegroundForBracket(bucket); + Optional liveBracket; + BattlegroundTypeId bgTypeId = SelectBattlegroundForBracket(bucket, liveBracket); if (bgTypeId == BATTLEGROUND_TYPE_NONE) { LOG_DEBUG("module", "mod-bg-auto-queue: bracket {} has no eligible battleground, skipping.", static_cast(bracketId)); @@ -651,7 +673,7 @@ BgAutoQueue::QueuePassResult BgAutoQueue::RunQueuePass() continue; } - uint32 const queued = QueueBucket(bgTypeId, bucket, &result.skippedAtQueueTime); + uint32 const queued = QueueBucket(bgTypeId, bucket, liveBracket, &result.skippedAtQueueTime); if (queued > 0) { result.players += queued; diff --git a/src/BgAutoQueue.h b/src/BgAutoQueue.h index ccc145c..49452ae 100644 --- a/src/BgAutoQueue.h +++ b/src/BgAutoQueue.h @@ -7,6 +7,7 @@ #include "DBCEnums.h" #include "ObjectGuid.h" +#include "Optional.h" #include "SharedDefines.h" #include @@ -149,15 +150,21 @@ class BgAutoQueue // Selects the BG for a populated bracket: live-BG reinforcement first, // then a not-yet-running BG that already has uninvited queuers, then a - // random pick from the configured pool with documented fallbacks. - BattlegroundTypeId SelectBattlegroundForBracket(BracketBucket const& bucket) const; - - // Queues every player in the bucket into bgTypeId, then schedules a single - // queue update for the bracket. Returns the number of players queued. When - // non-null, skippedAtQueueTime is incremented for each bucket player - // dropped by the queue-time re-check or the BG-specific veto. + // random pick from the configured pool with documented fallbacks. On a + // live-BG pick, liveBracket carries the matched game's own map-relative + // bracket id; it stays empty on every other path. + BattlegroundTypeId SelectBattlegroundForBracket(BracketBucket const& bucket, + Optional& liveBracket) const; + + // 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. uint32 QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const& bucket, - uint32* skippedAtQueueTime = nullptr); + Optional liveBracket, uint32* skippedAtQueueTime = nullptr); void BroadcastWarning() const; diff --git a/src/cs_bg_auto_queue.cpp b/src/cs_bg_auto_queue.cpp index 5c6d3ae..168d2c9 100644 --- a/src/cs_bg_auto_queue.cpp +++ b/src/cs_bg_auto_queue.cpp @@ -121,7 +121,7 @@ class bg_auto_queue_commandscript : public CommandScript handler->PSendSysMessage(" no eligible battleground for their level: {} bracket(s)", result.bracketsWithoutBg); if (result.skippedAtQueueTime > 0) - handler->PSendSysMessage(" dropped at queue time (state change or veto): {} player(s)", result.skippedAtQueueTime); + handler->PSendSysMessage(" dropped at queue time (state change, veto, or off-bracket): {} player(s)", result.skippedAtQueueTime); } }; From 9a54ff4820aa509f3936da47f2a3f0b7db1e938a Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Thu, 2 Jul 2026 13:26:00 +0200 Subject: [PATCH 3/4] fix: use override-aware minimums in viability math IsViable and the tier-c smallest-min fallback read the raw template MinPlayersPerTeam, ignoring Battleground.Override.LowLevels.MinPlayers. Resolve the bucket's bracket on the candidate BG's own map and use the same minimum core matching uses, falling back to the raw template value when no bracket resolves. --- conf/mod-bg-auto-queue.conf.dist | 5 +++++ src/BgAutoQueue.cpp | 17 +++++++++++++++-- src/BgAutoQueue.h | 9 +++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) 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..3ae490b 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" @@ -406,9 +407,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 +538,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; diff --git a/src/BgAutoQueue.h b/src/BgAutoQueue.h index 49452ae..61a8537 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. From 59bcaf82daa8dbb19e6e7a68ea5afff252818c31 Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Thu, 2 Jul 2026 13:27:58 +0200 Subject: [PATCH 4/4] fix: trim odd waves under strict CFBG EvenTeams With CFBG.EvenTeams.Enabled=1 and MaxPlayersThreshold=0 the CFBG queue never invites an odd total, so one player of every odd wave was left in queue forever. Re-validate the bucket first, and when the wave plus the uninvited waiters comes out odd, leave the newest player unqueued for this pass; the next pass picks them up. The trim self-disables when CFBG or EvenTeams is off, the threshold is above 0, or the module runs faction-vs-faction. --- src/BgAutoQueue.cpp | 40 ++++++++++++++++++++++++++++++++++++---- src/BgAutoQueue.h | 13 +++++++++---- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/BgAutoQueue.cpp b/src/BgAutoQueue.cpp index 3ae490b..13ad243 100644 --- a/src/BgAutoQueue.cpp +++ b/src/BgAutoQueue.cpp @@ -111,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); @@ -134,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 @@ -562,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) { @@ -607,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 61a8537..bcbc561 100644 --- a/src/BgAutoQueue.h +++ b/src/BgAutoQueue.h @@ -168,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); @@ -192,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