From fcee29eae39f96114b0e8105fe5e95c256f63af3 Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Sun, 24 May 2026 02:58:20 +0200 Subject: [PATCH 1/3] feat: revamp module --- README.md | 82 ++- conf/mod-bg-auto-queue.conf.dist | 113 ++-- .../db-characters/base/mod_bg_auto_queue.sql | 3 +- src/BgAutoQueue.cpp | 505 +++++++++++++----- src/BgAutoQueue.h | 94 ++-- src/PlayerScript_bg_auto_queue.cpp | 27 + src/cs_bg_auto_queue.cpp | 34 +- src/mod_bg_auto_queue_loader.cpp | 6 + 8 files changed, 624 insertions(+), 240 deletions(-) create mode 100644 src/PlayerScript_bg_auto_queue.cpp diff --git a/README.md b/README.md index 58ede71..ddc74f1 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,83 @@ # mod-bg-auto-queue -AzerothCore module that automatically queues players into a battleground on -login. Players are opted in by default and can opt out with an in-game command. +AzerothCore module that periodically auto-queues eligible online players into +battlegrounds, grouped by level bracket, to help battlegrounds reach the +critical mass needed to pop. Players are opted in by default and can opt out +with an in-game command. -## Features +## How it works -- Automatically enqueues eligible players into a battleground on login. -- Configurable level range that gates the auto-join behavior. -- Configurable default battleground: Random Battleground, Warsong Gulch, or - Arathi Basin. Falls back to Warsong Gulch when the chosen battleground is not - available for the player's level. -- Per-character opt-out persisted in the characters database. -- `.bgevents` command to enable, disable, or inspect the auto-join state. +On a configurable interval the module runs a **queue pass**: + +1. It gathers every eligible online player and groups them by PvP level bracket + (10-19, 20-29, …, 70-79). Brackets are never mixed. +2. For each populated bracket it selects one battleground: + - **Live-battleground reinforcement (priority).** If a battleground of any + normal type is already forming or in progress for that bracket and has + free slots, players are queued into it so they reinforce the live match. + This is not limited to the configured pool. + - **Otherwise, a random pick from the configured pool.** With a single + eligible candidate it is used as-is. With several, candidates whose + minimum players per team cannot be met by the bracket's available count + are dropped and one of the rest is chosen at random. If none meet the + threshold, the smallest battleground (typically Warsong Gulch) is chosen + anyway so players are still queued. +3. Every player in the bracket is solo-queued (no premade groups) into the + chosen battleground. + +`WarningLeadTime` seconds before each pass, a generic warning is broadcast to +the players who would be queued, reminding them how to opt out or back in. + +## Behaviour & interactions + +- **Per-bracket matching never mixes brackets**; each bracket gets its own + battleground. +- **Solo queue only** — players are queued individually, never as a premade. +- **Re-queue on decline.** Declining or ignoring the queue popup carries no + penalty (no Deserter debuff — that only applies after entering and leaving a + battleground). The player is simply considered again on the next pass. +- **Reload resets the timer** and re-applies `InitialDelay`. A player who logs + in between the warning and the pass is still queued, just without a warning. +- **Deserter tracking noise.** If `Battleground.TrackDeserters.Enable` is enabled, + players who decline auto-queue invites may appear in the deserter tracking + table. This is informational only, not a player-facing penalty. +- **Queue announcer.** If `Battleground.QueueAnnouncer.Enable` is on in + immediate (non-timed) mode, a mass pass emits one world announcement per + player. Consider `Battleground.QueueAnnouncer.Timed` / + `…PlayerOnly` to avoid spam. + +A player is **eligible** when they are online and in the world, not opted out, +within the configured level range, not in a dungeon/raid, not already in a +battleground, not already in a battleground/arena queue, not a deserter, not +using the LFG system, not a Death Knight still locked to Ebon Hold, and (when +`SkipGameMasters` is on) not a game master. ## Installation 1. Clone this folder into `modules/mod-bg-auto-queue/` of your AzerothCore source. 2. Re-run CMake and rebuild the worldserver. -3. Copy `mod-bg-auto-queue.conf.dist` to `mod-bg-auto-queue.conf` in your worldserver's - configuration directory and adjust as needed. +3. Copy `mod-bg-auto-queue.conf.dist` to `mod-bg-auto-queue.conf` in your + worldserver's configuration directory and adjust as needed. 4. The module installs its characters-database table automatically through the AzerothCore SQL updater on first run. ## Configuration -See `conf/mod-bg-auto-queue.conf.dist` for the full list of options. +All options are documented in `conf/mod-bg-auto-queue.conf.dist`: + +- `BgAutoQueue.Enable` — enable the automatic periodic pass (calling `.bgevents run` works even when disabled). +- `BgAutoQueue.Level.Min` / `BgAutoQueue.Level.Max` — eligible level range. +- `BgAutoQueue.Pool` — CSV of `battleground_template` IDs to pick from. +- `BgAutoQueue.Interval` — minutes between passes (`0` disables the schedule). +- `BgAutoQueue.InitialDelay` — seconds before the first pass after startup/reload. +- `BgAutoQueue.WarningLeadTime` — seconds before a pass to broadcast the warning. +- `BgAutoQueue.BroadcastMessage` — the warning text (empty disables it). +- `BgAutoQueue.CrossFaction` — how the available count is judged against a BG's minimum players per team. +- `BgAutoQueue.SkipGameMasters` — skip GMs in the warning and the queueing. ## Commands -- `.bgevents on` — enable auto-join for the current character (default state). -- `.bgevents off` — disable auto-join for the current character. -- `.bgevents status` — show the current auto-join state. +- `.bgevents on` — opt the current character back into battleground events. +- `.bgevents off` — opt the current character out (future passes only; does not dequeue an existing queue). +- `.bgevents status` — show the opt-in state and the time until the next scheduled pass. +- `.bgevents run` — *(GM, console-capable)* run a queue pass immediately, even when the automatic schedule is disabled. Does not reset the periodic timer. diff --git a/conf/mod-bg-auto-queue.conf.dist b/conf/mod-bg-auto-queue.conf.dist index b9b33c4..61f4ae3 100644 --- a/conf/mod-bg-auto-queue.conf.dist +++ b/conf/mod-bg-auto-queue.conf.dist @@ -8,19 +8,23 @@ # mod-bg-auto-queue # # BgAutoQueue.Enable -# Description: Enable the automatic battleground queue on login. -# When enabled, every eligible player will be enqueued -# automatically into the configured battleground on login. +# Description: Enable the automatic, periodic battleground queue pass. +# When enabled, the module periodically gathers eligible +# online players, groups them by level bracket and queues +# each bracket into a battleground. +# NOTE: This gates ONLY the automatic schedule: even when +# disabled, a GM can still fire a pass on +# demand with ".bgevents run". # Default: 1 - Enabled -# 0 - Disabled +# 0 - Disabled (manual ".bgevents run" only) # BgAutoQueue.Enable = 1 # # BgAutoQueue.Level.Min -# Description: Minimum character level for which the automatic queue is -# applied. Players below this level are skipped. +# Description: Minimum character level eligible for the automatic queue. +# Players below this level are skipped. # Default: 10 # @@ -28,49 +32,96 @@ BgAutoQueue.Level.Min = 10 # # BgAutoQueue.Level.Max -# Description: Maximum character level for which the automatic queue is -# applied. Players above this level are skipped. -# Default: 59 +# Description: Maximum character level eligible for the automatic queue. +# Players above this level are skipped. +# Default: 79 # -BgAutoQueue.Level.Max = 59 +BgAutoQueue.Level.Max = 79 # -# BgAutoQueue.Battleground -# Description: Battleground to enqueue the player into on login. -# If the chosen battleground is unavailable for the -# player's level, the module falls back to Warsong Gulch. -# Default: 0 - Random Battleground -# 1 - Warsong Gulch -# 2 - Arathi Basin -# 3 - Eye of the Storm -# 4 - Alterac Valley -# 5 - Strand of the Ancients -# 6 - Isle of Conquest +# BgAutoQueue.Pool +# Description: Comma-separated list of battleground_template IDs the +# module may randomly pick from for each bracket. The IDs +# are BattlegroundTypeId values: +# 1 = Alterac Valley +# 2 = Warsong Gulch +# 3 = Arathi Basin +# 7 = Eye of the Storm +# 9 = Strand of the Ancients +# 30 = Isle of Conquest +# Invalid IDs, arenas and Random Battleground (32) are +# rejected at load with a warning. If the pool ends up +# empty, only live-battleground reinforcement can queue +# players. +# Default: "2,3,7" - Warsong Gulch, Arathi Basin, Eye of the Storm # -BgAutoQueue.Battleground = 0 +BgAutoQueue.Pool = "2,3,7" # # BgAutoQueue.Interval -# Description: Interval, in minutes, between automatic battleground -# queue passes. On every tick, every eligible online -# player that hasn't opted out is enqueued into the -# configured battleground. -# Set to 0 to disable the periodic pass entirely. -# +# Description: Minutes between automatic queue passes. Set to 0 to +# disable the periodic pass entirely (".bgevents run" still +# works). # Default: 45 # BgAutoQueue.Interval = 45 +# +# BgAutoQueue.InitialDelay +# Description: Seconds before the FIRST pass after startup or a config +# reload. 0 means wait one full Interval before the first +# pass. A small value (e.g. 30) fires the first event +# quickly on a test server. +# Default: 0 +# + +BgAutoQueue.InitialDelay = 0 + +# +# BgAutoQueue.WarningLeadTime +# Description: Seconds before each pass to broadcast the warning message. +# Must be smaller than the Interval (and than InitialDelay +# for the first pass) to fire. Set BroadcastMessage to an +# empty string to disable the warning. +# Default: 60 +# + +BgAutoQueue.WarningLeadTime = 60 + # # BgAutoQueue.BroadcastMessage # Description: System message sent to every eligible online player -# two minutes before the periodic queue pass fires. -# Default: "BG Event starting in 2 minutes, you can opt-out by typing .bgevents off. You can always join manually by pressing H and going to the BG tab" +# WarningLeadTime seconds before a pass. It should remind +# players of both ".bgevents off" (opt out) and +# ".bgevents on" (opt back in). An empty string disables +# the warning. +# Default: "A Battleground event is starting shortly. Type .bgevents off to opt out, or .bgevents on to opt back in. You can also join manually by pressing H." +# + +BgAutoQueue.BroadcastMessage = "A Battleground event is starting shortly. Type .bgevents off to opt out, or .bgevents on to opt back in. You can also join manually by pressing H." + +# +# BgAutoQueue.CrossFaction +# Description: How the available player count is judged against a +# battleground's minimum players per team when several +# pool candidates exist. +# Default: 1 - Cross-faction: total players >= 2 x MinPlayersPerTeam +# 0 - Vanilla: Alliance >= min AND Horde >= min +# + +BgAutoQueue.CrossFaction = 1 + +# +# BgAutoQueue.SkipGameMasters +# Description: Skip game masters in both the warning and the queueing. +# Set to 0 on a test server to allow queueing a GM. +# Default: 1 - Skip GMs +# 0 - Include GMs # -BgAutoQueue.BroadcastMessage = "BG Event starting in 2 minutes, you can opt-out by typing .bgevents off. You can always join manually by pressing H and going to the BG tab" +BgAutoQueue.SkipGameMasters = 1 ################################################################################################### diff --git a/data/sql/db-characters/base/mod_bg_auto_queue.sql b/data/sql/db-characters/base/mod_bg_auto_queue.sql index 36ced34..ce078d4 100644 --- a/data/sql/db-characters/base/mod_bg_auto_queue.sql +++ b/data/sql/db-characters/base/mod_bg_auto_queue.sql @@ -1,6 +1,5 @@ -- --- Table tracking characters that opted out from the automatic battleground --- queue performed by mod-bg-auto-queue. +-- Table tracking characters that opted out from the automatic battleground queue performed by mod-bg-auto-queue -- CREATE TABLE IF NOT EXISTS `mod_bg_auto_queue_optout` ( `guid` INT UNSIGNED NOT NULL, diff --git a/src/BgAutoQueue.cpp b/src/BgAutoQueue.cpp index 436ce07..b007aaa 100644 --- a/src/BgAutoQueue.cpp +++ b/src/BgAutoQueue.cpp @@ -1,25 +1,54 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license + */ + #include "BgAutoQueue.h" +#include "AreaDefines.h" #include "Battleground.h" #include "BattlegroundMgr.h" #include "BattlegroundQueue.h" #include "Chat.h" #include "Config.h" +#include "Containers.h" #include "DBCStores.h" #include "DatabaseEnv.h" +#include "DisableMgr.h" +#include "LFGMgr.h" #include "Log.h" #include "ObjectAccessor.h" #include "Player.h" #include "ScriptMgr.h" +#include "StringConvert.h" +#include "Tokenize.h" +#include "World.h" #include "WorldPacket.h" #include "WorldSession.h" +#include +#include + namespace { - constexpr uint32 BG_AUTO_QUEUE_WARNING_LEAD_MS = 2u * 60u * 1000u; // 2 minutes + // Used as reference (Warsong Gulch spans for every eligible level) + constexpr uint32 BG_BRACKET_REFERENCE_MAP = MAP_WARSONG_GULCH; // 489 + + // Normal (non-arena, non-random) battleground types + constexpr BattlegroundTypeId BG_NORMAL_TYPES[] = + { + BATTLEGROUND_AV, + BATTLEGROUND_WS, + BATTLEGROUND_AB, + BATTLEGROUND_EY, + BATTLEGROUND_SA, + BATTLEGROUND_IC + }; + + // default for BgAutoQueue.BroadcastMessage config constexpr char const* BG_AUTO_QUEUE_DEFAULT_BROADCAST = - "BG Event starting in 2 minutes, you can opt-out by typing \".bgevents off\". " - "You can always join manually by pressing H and going to the BG tab"; + "A Battleground event is starting shortly. Type .bgevents off to opt " + "out, or .bgevents on to opt back in. You can also join manually by " + "pressing H."; } BgAutoQueue* BgAutoQueue::instance() @@ -31,33 +60,75 @@ BgAutoQueue* BgAutoQueue::instance() void BgAutoQueue::LoadConfig() { _enabled = sConfigMgr->GetOption("BgAutoQueue.Enable", true); - _minLevel = sConfigMgr->GetOption("BgAutoQueue.Level.Min", 10); - _maxLevel = sConfigMgr->GetOption("BgAutoQueue.Level.Max", 80); + _levelMin = sConfigMgr->GetOption("BgAutoQueue.Level.Min", 10); + _levelMax = sConfigMgr->GetOption("BgAutoQueue.Level.Max", 79); - uint32 choice = sConfigMgr->GetOption("BgAutoQueue.Battleground", BG_AUTO_QUEUE_WSG); - if (choice >= BG_AUTO_QUEUE_MAX) + if (_levelMin > _levelMax) { - LOG_WARN("module", "BgAutoQueue.Battleground has invalid value {}, defaulting to Warsong Gulch.", choice); - choice = BG_AUTO_QUEUE_WSG; + LOG_WARN("module", "BgAutoQueue level range is inverted ({} > {}), swapping.", _levelMin, _levelMax); + std::swap(_levelMin, _levelMax); } - _defaultChoice = static_cast(choice); - if (_minLevel > _maxLevel) + _pool.clear(); + std::string const poolStr = sConfigMgr->GetOption("BgAutoQueue.Pool", "2,3,7"); + for (std::string_view token : Acore::Tokenize(poolStr, ',', false)) { - LOG_WARN("module", "BgAutoQueue level range is inverted ({} > {}), swapping.", _minLevel, _maxLevel); - std::swap(_minLevel, _maxLevel); + Optional value = Acore::StringTo(token); + if (!value) + { + LOG_WARN("module", "BgAutoQueue.Pool entry '{}' is not a valid number, ignoring.", token); + continue; + } + + BattlegroundTypeId bgTypeId = static_cast(*value); + if (bgTypeId == BATTLEGROUND_RB) + { + LOG_WARN("module", "BgAutoQueue.Pool entry {} is Random Battleground (unsupported), ignoring.", *value); + continue; + } + + Battleground* bgTemplate = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); + if (!bgTemplate) + { + LOG_WARN("module", "BgAutoQueue.Pool entry {} has no battleground template, ignoring.", *value); + continue; + } + + if (bgTemplate->isArena()) + { + LOG_WARN("module", "BgAutoQueue.Pool entry {} is an arena (unsupported), ignoring.", *value); + continue; + } + + _pool.push_back(bgTypeId); } - uint32 minutes = sConfigMgr->GetOption("BgAutoQueue.Interval", 45); - _intervalMs = minutes * 60u * 1000u; - _elapsedMs = 0; - _warningSent = false; + if (_pool.empty()) + LOG_WARN("module", "BgAutoQueue.Pool is empty; the random pick is disabled (only live-BG reinforcement can queue players)."); + + uint32 const intervalMin = sConfigMgr->GetOption("BgAutoQueue.Interval", 45); + _intervalMs = intervalMin * 60u * 1000u; + + uint32 const initialDelaySec = sConfigMgr->GetOption("BgAutoQueue.InitialDelay", 0); + _initialDelayMs = initialDelaySec * 1000u; + + uint32 const warningLeadSec = sConfigMgr->GetOption("BgAutoQueue.WarningLeadTime", 120); + _warningLeadMs = warningLeadSec * 1000u; + + if (_intervalMs > 0 && _warningLeadMs >= _intervalMs) + LOG_WARN("module", "BgAutoQueue.WarningLeadTime ({} s) >= Interval ({} min); the warning will not fire.", warningLeadSec, intervalMin); - _broadcastMessage = sConfigMgr->GetOption("BgAutoQueue.BroadcastMessage", - BG_AUTO_QUEUE_DEFAULT_BROADCAST); + _crossFaction = sConfigMgr->GetOption("BgAutoQueue.CrossFaction", true); + _skipGameMasters = sConfigMgr->GetOption("BgAutoQueue.SkipGameMasters", true); + _broadcastMessage = sConfigMgr->GetOption("BgAutoQueue.BroadcastMessage", BG_AUTO_QUEUE_DEFAULT_BROADCAST); - LOG_INFO("module", "mod-bg-auto-queue: enabled={}, levels=[{}-{}], choice={}, interval={} min ({} ms)", - _enabled, _minLevel, _maxLevel, static_cast(_defaultChoice), minutes, _intervalMs); + // Reset timing on (re)load. Reload re-applies InitialDelay — accepted. + _elapsedMs = 0; + _warningSent = false; + _firstPass = true; + + LOG_INFO("module", "mod-bg-auto-queue: enabled={}, levels=[{}-{}], pool size={}, interval={} min, initialDelay={} s, warningLead={} s, crossFaction={}, skipGM={}.", + _enabled, _levelMin, _levelMax, _pool.size(), intervalMin, initialDelaySec, warningLeadSec, _crossFaction, _skipGameMasters); } void BgAutoQueue::LoadOptOutData() @@ -98,159 +169,306 @@ void BgAutoQueue::SetOptOut(ObjectGuid guid, bool optedOut) } } -bool BgAutoQueue::IsLevelEligible(uint8 level) const +void BgAutoQueue::DeleteOptOut(uint32 guidLow) { - return level >= _minLevel && level <= _maxLevel; + _optedOut.erase(guidLow); + CharacterDatabase.Execute("DELETE FROM mod_bg_auto_queue_optout WHERE guid = {}", guidLow); } -BattlegroundTypeId BgAutoQueue::ResolveBattlegroundFor(Player* player) const +bool BgAutoQueue::IsLevelEligible(uint8 level) const { - if (!player) - return BATTLEGROUND_TYPE_NONE; - - auto canEnter = [player](BattlegroundTypeId bgTypeId) -> bool - { - Battleground* bgt = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); - if (!bgt) - return false; - - if (!player->GetBGAccessByLevel(bgTypeId)) - return false; - - // The PvPDifficulty.dbc brackets are independent of the - // battleground_template MinLvl/MaxLvl columns. A BG without a - // bracket entry for the player's level cannot be queued — the - // queue handler will reject it with a null bracket. Treat such - // cases as ineligible so the WSG fallback can take over. - return GetBattlegroundBracketByLevel(bgt->GetMapId(), player->GetLevel()) != nullptr; - }; - - BattlegroundTypeId desired = BATTLEGROUND_WS; - switch (_defaultChoice) - { - case BG_AUTO_QUEUE_RANDOM: desired = BATTLEGROUND_RB; break; - case BG_AUTO_QUEUE_WSG: desired = BATTLEGROUND_WS; break; - case BG_AUTO_QUEUE_AB: desired = BATTLEGROUND_AB; break; - case BG_AUTO_QUEUE_EY: desired = BATTLEGROUND_EY; break; - case BG_AUTO_QUEUE_AV: desired = BATTLEGROUND_AV; break; - case BG_AUTO_QUEUE_SA: desired = BATTLEGROUND_SA; break; - case BG_AUTO_QUEUE_IC: desired = BATTLEGROUND_IC; break; - case BG_AUTO_QUEUE_MAX: break; - } - - if (canEnter(desired)) - return desired; - - // Fallback: Warsong Gulch is the lowest-level standard battleground. - if (desired != BATTLEGROUND_WS && canEnter(BATTLEGROUND_WS)) - return BATTLEGROUND_WS; - - return BATTLEGROUND_TYPE_NONE; + return level >= _levelMin && level <= _levelMax; } -void BgAutoQueue::AutoQueuePlayer(Player* player) const +bool BgAutoQueue::IsEligible(Player* player) const { - if (!_enabled || !player) - return; + if (!player || !player->IsInWorld()) + return false; std::string const& name = player->GetName(); if (IsOptedOut(player->GetGUID())) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: opted out.", name); - return; + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: opted out.", name); + return false; } if (!IsLevelEligible(player->GetLevel())) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: level {} outside [{}-{}].", - name, player->GetLevel(), _minLevel, _maxLevel); - return; + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: level {} outside [{}-{}].", name, player->GetLevel(), _levelMin, _levelMax); + return false; + } + + if (player->GetMap()->IsDungeon()) + { + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: in a dungeon/raid.", name); + return false; } if (player->InBattleground()) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: already in a battleground.", name); - return; + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: already in a battleground.", name); + return false; } - if (player->InBattlegroundQueue()) + if (player->InBattlegroundQueue() || !player->HasFreeBattlegroundQueueId()) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: already in a battleground/arena queue.", name); - return; + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: already queued or no free queue slot.", name); + return false; } - if (!player->HasFreeBattlegroundQueueId()) + // Deserter check via any standard template (WSG). + if (Battleground* bgTemplate = sBattlegroundMgr->GetBattlegroundTemplate(BATTLEGROUND_WS)) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: no free battleground queue slot.", name); - return; + if (!player->CanJoinToBattleground(bgTemplate)) + { + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: CanJoinToBattleground=false (deserter?).", name); + return false; + } } - BattlegroundTypeId bgTypeId = ResolveBattlegroundFor(player); - if (bgTypeId == BATTLEGROUND_TYPE_NONE) + // LFG: mirror the core BG join handler rule. + lfg::LfgState lfgState = sLFGMgr->GetState(player->GetGUID()); + if (lfgState > lfg::LFG_STATE_NONE + && (lfgState != lfg::LFG_STATE_QUEUED || !sWorld->getBoolConfig(CONFIG_ALLOW_JOIN_BG_AND_LFG))) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: no battleground available for level {} (choice={}).", - name, player->GetLevel(), static_cast(_defaultChoice)); - return; + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: using the LFG system.", name); + return false; } + // Death Knights still locked to Ebon Hold cannot be teleported to a BG yet. + if (player->IsClass(CLASS_DEATH_KNIGHT, CLASS_CONTEXT_TELEPORT) + && player->GetMapId() == MAP_EBON_HOLD + && !player->IsGameMaster() + && !player->HasSpell(50977)) + { + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: Death Knight not yet allowed to leave Ebon Hold.", name); + return false; + } + + if (_skipGameMasters && player->IsGameMaster()) + { + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: game master.", name); + return false; + } + + return true; +} + +bool BgAutoQueue::CanEnter(Player* player, BattlegroundTypeId bgTypeId) const +{ + if (!player) + return false; + Battleground* bgTemplate = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); if (!bgTemplate) + return false; + + if (!player->GetBGAccessByLevel(bgTypeId)) + return false; + + // A BG without a PvP bracket entry for the player's level cannot be queued. + return GetBattlegroundBracketByLevel(bgTemplate->GetMapId(), player->GetLevel()) != nullptr; +} + +bool BgAutoQueue::IsBracketEligible(BattlegroundTypeId bgTypeId, BracketBucket const& bucket) const +{ + for (ObjectGuid guid : bucket.players) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: no template for bgTypeId {}.", - name, static_cast(bgTypeId)); - return; + Player* player = ObjectAccessor::FindPlayer(guid); + if (!player || !CanEnter(player, bgTypeId)) + return false; } - PvPDifficultyEntry const* bracketEntry = - GetBattlegroundBracketByLevel(bgTemplate->GetMapId(), player->GetLevel()); - if (!bracketEntry) + return true; +} + +bool BgAutoQueue::IsViable(Battleground* bgTemplate, BracketBucket const& bucket) const +{ + uint32 const minPerTeam = bgTemplate->GetMinPlayersPerTeam(); + + if (_crossFaction) + return (bucket.alliance + bucket.horde) >= (2u * minPerTeam); + + return bucket.alliance >= minPerTeam && bucket.horde >= minPerTeam; +} + +BattlegroundTypeId BgAutoQueue::SelectBattlegroundForBracket(BattlegroundBracketId bracketId, BracketBucket const& bucket) const +{ + // (a) Live-BG reinforcement (priority; not limited to the pool). + std::vector liveTypes; + for (BattlegroundTypeId bgTypeId : BG_NORMAL_TYPES) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: no PvP bracket for map {} level {}.", - name, bgTemplate->GetMapId(), player->GetLevel()); - return; + if (sDisableMgr->IsDisabledFor(DISABLE_TYPE_BATTLEGROUND, bgTypeId, nullptr)) + continue; + + if (!IsBracketEligible(bgTypeId, bucket)) + continue; + + for (Battleground* bg : sBattlegroundMgr->GetBGFreeSlotQueueStore(bgTypeId)) + { + if (bg->GetBracketId() != bracketId) + continue; + + if (!(bg->GetStatus() > STATUS_WAIT_QUEUE && bg->GetStatus() < STATUS_WAIT_LEAVE)) + continue; + + if (!bg->HasFreeSlots()) + continue; + + liveTypes.push_back(bgTypeId); + break; + } } - BattlegroundQueueTypeId bgQueueTypeId = BattlegroundMgr::BGQueueTypeId(bgTypeId, 0); - if (bgQueueTypeId == BATTLEGROUND_QUEUE_NONE) + if (!liveTypes.empty()) + return Acore::Containers::SelectRandomContainerElement(liveTypes); + + // (b) Random pick from the configured pool. + std::vector candidates; + for (BattlegroundTypeId bgTypeId : _pool) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: no queue type id for bgTypeId {}.", - name, static_cast(bgTypeId)); - return; + if (sDisableMgr->IsDisabledFor(DISABLE_TYPE_BATTLEGROUND, bgTypeId, nullptr)) + continue; + + if (IsBracketEligible(bgTypeId, bucket)) + candidates.push_back(bgTypeId); } - if (player->InBattlegroundQueueForBattlegroundQueueType(bgQueueTypeId)) + if (candidates.empty()) + return BATTLEGROUND_TYPE_NONE; + + if (candidates.size() == 1) + return candidates.front(); + + std::vector viable; + for (BattlegroundTypeId bgTypeId : candidates) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: already queued for this BG type.", name); - return; + Battleground* bgTemplate = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); + if (bgTemplate && IsViable(bgTemplate, bucket)) + viable.push_back(bgTypeId); } - if (!player->CanJoinToBattleground(bgTemplate)) + if (!viable.empty()) + return Acore::Containers::SelectRandomContainerElement(viable); + + // None viable: pick the smallest by MinPlayersPerTeam (ties -> lowest id). + BattlegroundTypeId best = BATTLEGROUND_TYPE_NONE; + uint32 bestMin = std::numeric_limits::max(); + for (BattlegroundTypeId bgTypeId : candidates) { - LOG_INFO("module", "mod-bg-auto-queue: skip {}: CanJoinToBattleground=false (deserter?).", name); - return; + Battleground* bgTemplate = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); + if (!bgTemplate) + continue; + + uint32 const minPerTeam = bgTemplate->GetMinPlayersPerTeam(); + if (minPerTeam < bestMin || (minPerTeam == bestMin && bgTypeId < best)) + { + bestMin = minPerTeam; + best = bgTypeId; + } } - LOG_INFO("module", "mod-bg-auto-queue: queuing {} into bgTypeId {}.", - name, static_cast(bgTypeId)); + return best; +} + +void BgAutoQueue::QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const& bucket) +{ + Battleground* bgTemplate = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId); + if (!bgTemplate) + return; + + BattlegroundQueueTypeId bgQueueTypeId = BattlegroundMgr::BGQueueTypeId(bgTypeId, 0); + if (bgQueueTypeId == BATTLEGROUND_QUEUE_NONE) + return; BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(bgQueueTypeId); - GroupQueueInfo* ginfo = bgQueue.AddGroup(player, nullptr, bgTypeId, bracketEntry, 0, false, false, 0, 0); - uint32 avgWaitTime = bgQueue.GetAverageQueueWaitTime(ginfo); - uint32 queueSlot = player->AddBattlegroundQueueId(bgQueueTypeId); + bool anyQueued = false; + BattlegroundBracketId scheduledBracket = BG_BRACKET_ID_FIRST; + + for (ObjectGuid guid : bucket.players) + { + Player* player = ObjectAccessor::FindPlayer(guid); + if (!player) + continue; + + // Re-validate: state may have changed since the bucket was gathered. + if (!IsEligible(player) || player->InBattlegroundQueueForBattlegroundQueueType(bgQueueTypeId)) + continue; + + // BG-specific veto (can only run at queue time). + GroupJoinBattlegroundResult err = ERR_BATTLEGROUND_NONE; + if (!sScriptMgr->OnPlayerCanJoinInBattlegroundQueue(player, ObjectGuid::Empty, bgTypeId, 0, err)) + { + LOG_DEBUG("module", "mod-bg-auto-queue: skip {}: OnPlayerCanJoinInBattlegroundQueue veto.", player->GetName()); + continue; + } + + PvPDifficultyEntry const* bracketEntry = GetBattlegroundBracketByLevel(bgTemplate->GetMapId(), player->GetLevel()); + if (!bracketEntry) + continue; + + GroupQueueInfo* ginfo = bgQueue.AddGroup(player, nullptr, bgTypeId, bracketEntry, 0, false, false, 0, 0); + uint32 avgWaitTime = bgQueue.GetAverageQueueWaitTime(ginfo); + uint32 queueSlot = player->AddBattlegroundQueueId(bgQueueTypeId); + + if (WorldSession* session = player->GetSession()) + { + WorldPacket data; + sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bgTemplate, queueSlot, STATUS_WAIT_QUEUE, avgWaitTime, 0, 0, TEAM_NEUTRAL); + session->SendPacket(&data); + } + + sScriptMgr->OnPlayerJoinBG(player); + + scheduledBracket = bracketEntry->GetBracketId(); + anyQueued = true; + + LOG_DEBUG("module", "mod-bg-auto-queue: queued {} into bgTypeId {}.", player->GetName(), static_cast(bgTypeId)); + } - if (WorldSession* session = player->GetSession()) + // Schedule a single queue update for the bracket, not once per player. + if (anyQueued) + sBattlegroundMgr->ScheduleQueueUpdate(0, 0, bgQueueTypeId, bgTypeId, scheduledBracket); +} + +void BgAutoQueue::RunQueuePass() +{ + std::unordered_map buckets; + + for (auto const& [guid, player] : ObjectAccessor::GetPlayers()) { - WorldPacket data; - sBattlegroundMgr->BuildBattlegroundStatusPacket(&data, bgTemplate, queueSlot, - STATUS_WAIT_QUEUE, avgWaitTime, 0, 0, TEAM_NEUTRAL); - session->SendPacket(&data); + if (!IsEligible(player)) + continue; + + PvPDifficultyEntry const* bracketEntry = GetBattlegroundBracketByLevel(BG_BRACKET_REFERENCE_MAP, player->GetLevel()); + if (!bracketEntry) + continue; + + BracketBucket& bucket = buckets[bracketEntry->GetBracketId()]; + bucket.players.push_back(player->GetGUID()); + if (player->GetTeamId() == TEAM_ALLIANCE) + ++bucket.alliance; + else + ++bucket.horde; } - sScriptMgr->OnPlayerJoinBG(player); + uint32 bracketsQueued = 0; + for (auto const& [bracketId, bucket] : buckets) + { + BattlegroundTypeId bgTypeId = SelectBattlegroundForBracket(bracketId, bucket); + if (bgTypeId == BATTLEGROUND_TYPE_NONE) + { + LOG_DEBUG("module", "mod-bg-auto-queue: bracket {} has no eligible battleground, skipping.", static_cast(bracketId)); + continue; + } - sBattlegroundMgr->ScheduleQueueUpdate(0, 0, bgQueueTypeId, bgTypeId, bracketEntry->GetBracketId()); + QueueBucket(bgTypeId, bucket); + ++bracketsQueued; + } + + LOG_INFO("module", "mod-bg-auto-queue: queue pass processed {} bracket(s).", bracketsQueued); } void BgAutoQueue::Update(uint32 diff) @@ -260,37 +478,30 @@ void BgAutoQueue::Update(uint32 diff) _elapsedMs += diff; - // Heads-up broadcast 2 minutes before the queue pass. - if (!_warningSent - && _intervalMs > BG_AUTO_QUEUE_WARNING_LEAD_MS - && _intervalMs - _elapsedMs <= BG_AUTO_QUEUE_WARNING_LEAD_MS) + uint32 const target = (_firstPass && _initialDelayMs > 0) ? _initialDelayMs : _intervalMs; + + if (!_warningSent && target > _warningLeadMs && (target - _elapsedMs) <= _warningLeadMs) { BroadcastWarning(); _warningSent = true; } - if (_elapsedMs < _intervalMs) - return; - - _elapsedMs = 0; - _warningSent = false; - - auto const& players = ObjectAccessor::GetPlayers(); - LOG_INFO("module", "mod-bg-auto-queue: interval elapsed, scanning {} player(s).", players.size()); - - uint32 queued = 0; - for (auto const& [guid, player] : players) + if (_elapsedMs >= target) { - if (!player || !player->IsInWorld()) - continue; - - bool wasInQueue = player->InBattlegroundQueue(); - AutoQueuePlayer(player); - if (!wasInQueue && player->InBattlegroundQueue()) - ++queued; + RunQueuePass(); + _elapsedMs = 0; + _warningSent = false; + _firstPass = false; } +} - LOG_INFO("module", "mod-bg-auto-queue: queued {} player(s) this pass.", queued); +uint32 BgAutoQueue::GetTimeUntilNextPass() const +{ + if (!_enabled || _intervalMs == 0) + return 0; + + uint32 const target = (_firstPass && _initialDelayMs > 0) ? _initialDelayMs : _intervalMs; + return target > _elapsedMs ? (target - _elapsedMs) : 0; } void BgAutoQueue::BroadcastWarning() const @@ -301,18 +512,16 @@ void BgAutoQueue::BroadcastWarning() const uint32 sent = 0; for (auto const& [guid, player] : ObjectAccessor::GetPlayers()) { - if (!player || !player->IsInWorld()) - continue; - - if (IsOptedOut(player->GetGUID())) + if (!IsEligible(player)) continue; - if (!IsLevelEligible(player->GetLevel())) + WorldSession* session = player->GetSession(); + if (!session) continue; - ChatHandler(player->GetSession()).SendSysMessage(_broadcastMessage); + ChatHandler(session).SendSysMessage(_broadcastMessage); ++sent; } - LOG_INFO("module", "mod-bg-auto-queue: broadcast warning sent to {} player(s).", sent); + LOG_DEBUG("module", "mod-bg-auto-queue: broadcast warning sent to {} player(s).", sent); } diff --git a/src/BgAutoQueue.h b/src/BgAutoQueue.h index bde6bbc..136eeee 100644 --- a/src/BgAutoQueue.h +++ b/src/BgAutoQueue.h @@ -1,28 +1,22 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license + */ + #ifndef _MOD_BG_AUTO_QUEUE_H_ #define _MOD_BG_AUTO_QUEUE_H_ -#include "Define.h" +#include "DBCEnums.h" #include "ObjectGuid.h" #include "SharedDefines.h" #include #include +#include +class Battleground; class Player; -enum BgAutoQueueChoice : uint8 -{ - BG_AUTO_QUEUE_RANDOM = 0, - BG_AUTO_QUEUE_WSG = 1, - BG_AUTO_QUEUE_AB = 2, - BG_AUTO_QUEUE_EY = 3, - BG_AUTO_QUEUE_AV = 4, - BG_AUTO_QUEUE_SA = 5, - BG_AUTO_QUEUE_IC = 6, - BG_AUTO_QUEUE_MAX -}; - -class AC_GAME_API BgAutoQueue +class BgAutoQueue { public: static BgAutoQueue* instance(); @@ -31,42 +25,78 @@ class AC_GAME_API BgAutoQueue void LoadOptOutData(); bool IsEnabled() const { return _enabled; } - uint32 GetMinLevel() const { return _minLevel; } - uint32 GetMaxLevel() const { return _maxLevel; } - BgAutoQueueChoice GetDefaultChoice() const { return _defaultChoice; } - uint32 GetIntervalMs() const { return _intervalMs; } bool IsOptedOut(ObjectGuid guid) const; void SetOptOut(ObjectGuid guid, bool optedOut); + // Called from PlayerScript::OnPlayerDeleteFromDB with a GUID-low. + void DeleteOptOut(uint32 guidLow); bool IsLevelEligible(uint8 level) const; - // Pick the queue target for a player given the configured default, - // falling back to Warsong Gulch when the chosen battleground is not - // available for the player's level. - BattlegroundTypeId ResolveBattlegroundFor(Player* player) const; - - // Auto-queue the player into the resolved battleground. Safe to call - // for players that are already queued or otherwise ineligible — those - // cases are silently skipped. - void AutoQueuePlayer(Player* player) const; + // Runs a single per-bracket queue pass. Does NOT check _enabled — it is + // invoked both by the periodic Update (enabled path) and by .bgevents run + // (always), so the Enable/Interval gate lives only in Update. + void RunQueuePass(); // Drives the periodic queue pass. Call from WorldScript::OnUpdate. void Update(uint32 diff); + // Milliseconds until the next automatic pass (0 when no pass is scheduled). + uint32 GetTimeUntilNextPass() const; + private: BgAutoQueue() = default; + // Per-bracket bucket of eligible players gathered during a pass. Players + // are stored by GUID and re-resolved at queue time (never store a + // long-lived Player*). + struct BracketBucket + { + std::vector players; + uint32 alliance = 0; + uint32 horde = 0; + }; + + // Shared per-player eligibility used by both the queue pass and the + // warning broadcast. Excludes the BG-specific OnPlayerCanJoinInBattleground + // Queue veto (that runs only at queue time). + bool IsEligible(Player* player) const; + + // True when the player can be queued into bgTypeId at their level. + bool CanEnter(Player* player, BattlegroundTypeId bgTypeId) const; + + // True when every player in the bucket passes CanEnter for bgTypeId. + bool IsBracketEligible(BattlegroundTypeId bgTypeId, BracketBucket const& bucket) const; + + // Viability per CrossFaction: cross-faction => total >= 2*min; otherwise + // each faction tally >= min. + bool IsViable(Battleground* bgTemplate, BracketBucket const& bucket) const; + + // Selects the BG for a populated bracket: live-BG reinforcement first, + // then a random pick from the configured pool with documented fallbacks. + BattlegroundTypeId SelectBattlegroundForBracket(BattlegroundBracketId bracketId, + BracketBucket const& bucket) const; + + // Queues every player in the bucket into bgTypeId, then schedules a single + // queue update for the bracket. + void QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const& bucket); + void BroadcastWarning() const; bool _enabled = true; - uint32 _minLevel = 10; - uint32 _maxLevel = 80; - BgAutoQueueChoice _defaultChoice = BG_AUTO_QUEUE_WSG; - uint32 _intervalMs = 45 * 60 * 1000; - uint32 _elapsedMs = 0; + uint32 _levelMin = 10; + uint32 _levelMax = 79; + std::vector _pool; + uint32 _intervalMs = 45u * 60u * 1000u; + uint32 _initialDelayMs = 0; + uint32 _warningLeadMs = 120u * 1000u; + bool _crossFaction = true; + bool _skipGameMasters = true; std::string _broadcastMessage; + + uint32 _elapsedMs = 0; bool _warningSent = false; + bool _firstPass = true; std::unordered_set _optedOut; // characters guid::low values }; diff --git a/src/PlayerScript_bg_auto_queue.cpp b/src/PlayerScript_bg_auto_queue.cpp new file mode 100644 index 0000000..b8390ab --- /dev/null +++ b/src/PlayerScript_bg_auto_queue.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license + */ + +#include "BgAutoQueue.h" + +#include "ScriptMgr.h" + +class mod_bg_auto_queue_playerscript : public PlayerScript +{ +public: + mod_bg_auto_queue_playerscript() : PlayerScript("mod_bg_auto_queue_playerscript", { + PLAYERHOOK_ON_DELETE_FROM_DB + }) { } + + // guid is the character GUID-low; drop any opt-out row so the table does + // not accumulate orphans when a character is deleted. + void OnPlayerDeleteFromDB(CharacterDatabaseTransaction /*trans*/, uint32 guid) override + { + sBgAutoQueue->DeleteOptOut(guid); + } +}; + +void AddSC_bg_auto_queue_playerscript() +{ + new mod_bg_auto_queue_playerscript(); +} diff --git a/src/cs_bg_auto_queue.cpp b/src/cs_bg_auto_queue.cpp index 59be3d3..e3a4b44 100644 --- a/src/cs_bg_auto_queue.cpp +++ b/src/cs_bg_auto_queue.cpp @@ -7,6 +7,7 @@ #include "Chat.h" #include "Player.h" #include "ScriptMgr.h" +#include "Util.h" using namespace Acore::ChatCommands; @@ -19,9 +20,10 @@ class bg_auto_queue_commandscript : public CommandScript { static ChatCommandTable bgAutoQueueTable = { - { "on", HandleBgAutoQueueOnCommand, SEC_PLAYER, Console::No }, - { "off", HandleBgAutoQueueOffCommand, SEC_PLAYER, Console::No }, - { "status", HandleBgAutoQueueStatusCommand, SEC_PLAYER, Console::No }, + { "on", HandleBgAutoQueueOnCommand, SEC_PLAYER, Console::No }, + { "off", HandleBgAutoQueueOffCommand, SEC_PLAYER, Console::No }, + { "status", HandleBgAutoQueueStatusCommand, SEC_PLAYER, Console::No }, + { "run", HandleBgAutoQueueRunCommand, SEC_GAMEMASTER, Console::Yes }, }; static ChatCommandTable commandTable = @@ -42,7 +44,7 @@ class bg_auto_queue_commandscript : public CommandScript } sBgAutoQueue->SetOptOut(player->GetGUID(), false); - handler->SendSysMessage("Battleground auto-join enabled. You will be queued automatically on next login."); + handler->SendSysMessage("Battleground events enabled for your character."); return true; } @@ -56,7 +58,7 @@ class bg_auto_queue_commandscript : public CommandScript } sBgAutoQueue->SetOptOut(player->GetGUID(), true); - handler->SendSysMessage("Battleground auto-join disabled. You will not be queued automatically on login."); + handler->SendSysMessage("Battleground events disabled for your character. You will not be auto-queued. Use .bgevents on to opt back in."); return true; } @@ -69,17 +71,27 @@ class bg_auto_queue_commandscript : public CommandScript return false; } - bool optedOut = sBgAutoQueue->IsOptedOut(player->GetGUID()); - if (optedOut) - handler->SendSysMessage("Battleground auto-join is currently DISABLED for your character."); + if (sBgAutoQueue->IsOptedOut(player->GetGUID())) + handler->SendSysMessage("Battleground events are currently DISABLED for your character."); else - handler->SendSysMessage("Battleground auto-join is currently ENABLED for your character."); + handler->SendSysMessage("Battleground events are currently ENABLED for your character."); - if (!sBgAutoQueue->IsEnabled()) - handler->SendSysMessage("Note: the auto-join feature is globally disabled by the server configuration."); + uint32 msToNext = sBgAutoQueue->GetTimeUntilNextPass(); + if (sBgAutoQueue->IsEnabled() && msToNext > 0) + handler->PSendSysMessage("Next scheduled event in {}.", secsToTimeString(msToNext / 1000)); + else + handler->SendSysMessage("There are no scheduled events (an administrator may still trigger one)."); return true; } + + static bool HandleBgAutoQueueRunCommand(ChatHandler* handler) + { + // Fires an immediate queue pass regardless of Enable/Interval without touching the periodic timer + sBgAutoQueue->RunQueuePass(); + handler->SendSysMessage("Battleground event queue pass executed."); + return true; + } }; void AddSC_bg_auto_queue_commandscript() diff --git a/src/mod_bg_auto_queue_loader.cpp b/src/mod_bg_auto_queue_loader.cpp index 495d2c5..313a82f 100644 --- a/src/mod_bg_auto_queue_loader.cpp +++ b/src/mod_bg_auto_queue_loader.cpp @@ -1,8 +1,14 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license + */ + void AddSC_mod_bg_auto_queue(); void AddSC_bg_auto_queue_commandscript(); +void AddSC_bg_auto_queue_playerscript(); void Addmod_bg_auto_queueScripts() { AddSC_mod_bg_auto_queue(); AddSC_bg_auto_queue_commandscript(); + AddSC_bg_auto_queue_playerscript(); } From de99c97c1424bb30e8887c26fd81c951324f9c1e Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Sun, 24 May 2026 03:11:35 +0200 Subject: [PATCH 2/3] chore: add ci --- .github/workflows/core-build.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/workflows/core-build.yml diff --git a/.github/workflows/core-build.yml b/.github/workflows/core-build.yml new file mode 100644 index 0000000..921c9eb --- /dev/null +++ b/.github/workflows/core-build.yml @@ -0,0 +1,12 @@ +name: core-build +on: + push: + branches: + - 'master' + pull_request: + +jobs: + build: + uses: azerothcore/reusable-workflows/.github/workflows/core_build_modules.yml@main + with: + module_repo: ${{ github.event.repository.name }} From 6347b2961a404f592ce0efc65480b76ee6aefbe0 Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Sun, 24 May 2026 03:31:49 +0200 Subject: [PATCH 3/3] chore: misc minor improvements --- README.md | 23 +++++++++++++++++++++++ src/BgAutoQueue.cpp | 6 +++--- src/BgAutoQueue.h | 8 +++++--- src/PlayerScript_bg_auto_queue.cpp | 7 ++++--- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ddc74f1..d181215 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,29 @@ battleground, not already in a battleground/arena queue, not a deserter, not using the LFG system, not a Death Knight still locked to Ebon Hold, and (when `SkipGameMasters` is on) not a game master. +## Operational notes + +Things to be aware of when running this on a live server: + +- **A pass queues everyone in one tick (burst).** All eligible players are + queued during a single world update, and a `OnPlayerJoinBG` script hook fires + **once per queued player**. On a high-population server this is a noticeable + burst: any other module that listens on `OnPlayerJoinBG` (announcers, reward + systems, statistics) will fire en masse, and the core BG queue announcer in + immediate mode emits one line per player (see "Queue announcer" above — + prefer its timed / player-only mode). Spreading the burst across multiple + ticks is intentionally not implemented; size your `Interval` accordingly. +- **`.reload config` restarts the timer.** Every config reload resets the + interval and re-applies `InitialDelay`. If you reload more frequently than + `Interval`, the next automatic pass is pushed back each time and may never + fire — use `.bgevents run` to trigger one on demand, or avoid reloading + right before a pass is due. +- **Opt-out state is read once at startup.** The opt-out set is loaded from + `mod_bg_auto_queue_optout` on server startup and is thereafter the in-memory + source of truth (mutated by `.bgevents on`/`off` and kept in sync with the + table). Editing the table directly while the server is running has **no + effect until the next restart**. + ## Installation 1. Clone this folder into `modules/mod-bg-auto-queue/` of your AzerothCore source. diff --git a/src/BgAutoQueue.cpp b/src/BgAutoQueue.cpp index b007aaa..3ef3e79 100644 --- a/src/BgAutoQueue.cpp +++ b/src/BgAutoQueue.cpp @@ -112,7 +112,7 @@ void BgAutoQueue::LoadConfig() uint32 const initialDelaySec = sConfigMgr->GetOption("BgAutoQueue.InitialDelay", 0); _initialDelayMs = initialDelaySec * 1000u; - uint32 const warningLeadSec = sConfigMgr->GetOption("BgAutoQueue.WarningLeadTime", 120); + uint32 const warningLeadSec = sConfigMgr->GetOption("BgAutoQueue.WarningLeadTime", 60); _warningLeadMs = warningLeadSec * 1000u; if (_intervalMs > 0 && _warningLeadMs >= _intervalMs) @@ -169,10 +169,10 @@ void BgAutoQueue::SetOptOut(ObjectGuid guid, bool optedOut) } } -void BgAutoQueue::DeleteOptOut(uint32 guidLow) +void BgAutoQueue::DeleteOptOut(CharacterDatabaseTransaction trans, uint32 guidLow) { _optedOut.erase(guidLow); - CharacterDatabase.Execute("DELETE FROM mod_bg_auto_queue_optout WHERE guid = {}", guidLow); + trans->Append("DELETE FROM mod_bg_auto_queue_optout WHERE guid = {}", guidLow); } bool BgAutoQueue::IsLevelEligible(uint8 level) const diff --git a/src/BgAutoQueue.h b/src/BgAutoQueue.h index 136eeee..4ea3aeb 100644 --- a/src/BgAutoQueue.h +++ b/src/BgAutoQueue.h @@ -6,6 +6,7 @@ #define _MOD_BG_AUTO_QUEUE_H_ #include "DBCEnums.h" +#include "DatabaseEnvFwd.h" #include "ObjectGuid.h" #include "SharedDefines.h" @@ -28,8 +29,9 @@ class BgAutoQueue bool IsOptedOut(ObjectGuid guid) const; void SetOptOut(ObjectGuid guid, bool optedOut); - // Called from PlayerScript::OnPlayerDeleteFromDB with a GUID-low. - void DeleteOptOut(uint32 guidLow); + // Called from PlayerScript::OnPlayerDeleteFromDB. The DELETE is appended to + // the character-deletion transaction so it commits atomically with it. + void DeleteOptOut(CharacterDatabaseTransaction trans, uint32 guidLow); bool IsLevelEligible(uint8 level) const; @@ -89,7 +91,7 @@ class BgAutoQueue std::vector _pool; uint32 _intervalMs = 45u * 60u * 1000u; uint32 _initialDelayMs = 0; - uint32 _warningLeadMs = 120u * 1000u; + uint32 _warningLeadMs = 60u * 1000u; bool _crossFaction = true; bool _skipGameMasters = true; std::string _broadcastMessage; diff --git a/src/PlayerScript_bg_auto_queue.cpp b/src/PlayerScript_bg_auto_queue.cpp index b8390ab..0e416bb 100644 --- a/src/PlayerScript_bg_auto_queue.cpp +++ b/src/PlayerScript_bg_auto_queue.cpp @@ -14,10 +14,11 @@ class mod_bg_auto_queue_playerscript : public PlayerScript }) { } // guid is the character GUID-low; drop any opt-out row so the table does - // not accumulate orphans when a character is deleted. - void OnPlayerDeleteFromDB(CharacterDatabaseTransaction /*trans*/, uint32 guid) override + // not accumulate orphans when a character is deleted. The DELETE joins the + // character-deletion transaction so it commits atomically with it. + void OnPlayerDeleteFromDB(CharacterDatabaseTransaction trans, uint32 guid) override { - sBgAutoQueue->DeleteOptOut(guid); + sBgAutoQueue->DeleteOptOut(trans, guid); } };