From 8e3e6e05ba69f5893e1ca48e3ab2e123f122bfc5 Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Fri, 10 Jul 2026 12:07:46 +0200 Subject: [PATCH 1/2] feat: better opt-out hint + auto opt-out after repeated declines --- .gitignore | 1 + README.md | 25 ++-- conf/mod-bg-auto-queue.conf.dist | 38 ++++++- docs/how-it-works.md | 31 +++-- src/BgAutoQueue.cpp | 182 +++++++++++++++++++++++++++++- src/BgAutoQueue.h | 36 +++++- src/cs_bg_auto_queue.cpp | 15 ++- src/mod_bg_auto_queue_scripts.cpp | 34 ++++++ 8 files changed, 338 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 0596bc9..cf16772 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .vscode/ .idea/ build/ +.claude diff --git a/README.md b/README.md index 3617a07..b4cb556 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ eligible online player, groups them by PvP level bracket (10-19, 20-29, …, everyone in that bracket into it. A warning is broadcast `WarningLeadTime` seconds before each pass. Players choose whether to accept the queue popup; declining carries no penalty and they are simply considered again next pass. +Declining or ignoring a module invite shows a chat hint about `.bgevents off` +(see `BgAutoQueue.DeclineHint.Enable`), and after +`BgAutoQueue.OptOutAfterDeclines` consecutive declined events the character is +automatically opted out with a notice — reversible with `.bgevents on`; +accepting an event invite resets the count. Manual queues never trigger any of +this. Battleground selection per bracket: - **Live-battleground reinforcement (priority).** If a normal battleground is @@ -86,12 +92,15 @@ Things to be aware of when running this on a live server: fire — use `.bgevents run` to trigger one on demand, or avoid reloading right before a pass is due. - **Opt-out is stored per character via the core PlayerSettings system** - (`source = "mod-bg-auto-queue"`, index 0; `1` = opted out). The core loads it - on login, saves it on logout, and deletes it when the character is deleted — - the module keeps no table of its own. **This requires the core config - `EnablePlayerSettings = 1` for the opt-out to persist across logins.** With it - `0` (the core default), `.bgevents off` still works but only for the current - session, and a warning is logged at startup. + (`source = "mod-bg-auto-queue"`; index 0: `0` = opted in, `1` = manual + opt-out via `.bgevents off`, `2` = automatic opt-out after consecutive + declines; index 1: the consecutive declined-invite counter). The core loads + it on login, saves it on logout, and deletes it when the character is + deleted — the module keeps no table of its own. **This requires the core + config `EnablePlayerSettings = 1` for the opt-out (and the counter) to + persist across logins.** With it `0` (the core default), `.bgevents off` + still works but only for the current session, and a warning is logged at + startup. ## Installation @@ -118,10 +127,12 @@ All options are documented in `conf/mod-bg-auto-queue.conf.dist`: - `BgAutoQueue.SkipGameMasters` — skip GMs in the warning and the queueing. - `BgAutoQueue.SkipAFK` — skip players flagged AFK (on by default; note the client auto-flags idle players). - `BgAutoQueue.SkipAuras` — CSV of aura IDs whose carriers are skipped for the pass. +- `BgAutoQueue.DeclineHint.Enable` — chat hint about `.bgevents off` after a declined/expired module invite (the auto-opt-out notice below is always sent). +- `BgAutoQueue.OptOutAfterDeclines` — consecutive declined module events before the character is automatically opted out (`0` disables the auto-opt-out). ## Commands -- `.bgevents on` — opt the current character back into battleground events. +- `.bgevents on` — opt the current character back into battleground events (also resets the consecutive-decline count). - `.bgevents off` — opt the current character out (future passes only; does not dequeue an existing queue). - `.bgevents` — *(no argument)* 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 da650e0..f87b41f 100644 --- a/conf/mod-bg-auto-queue.conf.dist +++ b/conf/mod-bg-auto-queue.conf.dist @@ -7,9 +7,10 @@ ################################################################################################### # mod-bg-auto-queue # -# DEPENDENCY: per-character ".bgevents" opt-out is stored via the core -# PlayerSettings system, which only persists across logins when the core -# config "EnablePlayerSettings" is set to 1. With it 0 (the core default), +# DEPENDENCY: per-character ".bgevents" opt-out and the consecutive-decline +# counter (see OptOutAfterDeclines) are stored via the core PlayerSettings +# system, which only persists across logins when the core config +# "EnablePlayerSettings" is set to 1. With it 0 (the core default), # ".bgevents off" still works but lasts only for the current session, and a # warning is logged at startup. This module does not change the core config. # @@ -184,4 +185,35 @@ BgAutoQueue.SkipAFK = 1 BgAutoQueue.SkipAuras = "" +# +# BgAutoQueue.DeclineHint.Enable +# Description: When a player declines (or lets expire) a Battleground +# invite created by this module, send them a visible chat +# hint explaining ".bgevents off". Gates ONLY the +# per-decline hint; the automatic opt-out notice (see +# OptOutAfterDeclines) is always sent. Manual queues are +# never affected. +# Default: 1 - Send the hint +# 0 - No hint +# + +BgAutoQueue.DeclineHint.Enable = 1 + +# +# BgAutoQueue.OptOutAfterDeclines +# Description: After this many CONSECUTIVE declined or ignored module +# invites, the character is automatically opted out (as if +# they typed ".bgevents off") and told so. Accepting a +# module invite or typing ".bgevents on" resets the count. +# The count is stored via the core PlayerSettings system, +# so it persists across logins only with +# "EnablePlayerSettings = 1" (same dependency as the +# opt-out itself, see the note at the top of this file). +# Default: 3 +# 0 - Never opt out automatically (hints alone keep firing +# if enabled) +# + +BgAutoQueue.OptOutAfterDeclines = 3 + ################################################################################################### diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 1953c93..e06c517 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -28,16 +28,25 @@ the countdown, so the next *scheduled* event still happens at its usual time. yourself. 3. **You decide.** Click **Enter Battle** to join the match, or just ignore / decline the pop-up if you don't feel like it this time. +4. **If you decline or ignore the pop-up**, a chat message reminds you how to + turn events off for good (`.bgevents off`), in case you'd rather not be + asked at all. That's it. Accepting drops you into the Battleground like any normal queue. ## Declining is totally fine -If you ignore or decline the pop-up, **nothing bad happens** — no Deserter -penalty, no cooldown, nothing. You simply get another chance at the next event. -The Deserter debuff only ever applies if you *enter* a Battleground and *then* +If you ignore or decline the pop-up, there's **no penalty** — no Deserter +debuff, no cooldown — and you get another chance at the next event. The +Deserter debuff only ever applies if you *enter* a Battleground and *then* leave it early, which has nothing to do with this. +One thing to know: if you decline **several events in a row** (a server +setting, normally 3), the server assumes you're not interested and **stops +auto-inviting you**. A chat message tells you when that happens, and +**`.bgevents on`** brings events back whenever you want. Accepting any event +invite resets the count. + ## Which Battleground you get - Normally the event picks one **at random** from a pool — currently **Warsong @@ -68,6 +77,10 @@ If you'd rather never be auto-queued, you can turn it off for your character: - **`.bgevents`** — check whether you're opted in, and see how long until the next event. +The server also opts you out **automatically** after several declined events +in a row (see "Declining is totally fine" above) — `.bgevents on` undoes that +too. + Opting out only affects **future** events; it won't remove you from a queue you're already in. @@ -75,7 +88,8 @@ you're already in. You're skipped for an event (this event only) if you're: -- **opted out** with `.bgevents off`, +- **opted out** — with `.bgevents off`, or automatically after several + declined events in a row, - **outside the eligible level range** (by default below 10 or at max level), - **in a dungeon or raid**, - **already in a Battleground**, @@ -102,10 +116,11 @@ silently ignores a manual re-join of that **same** Battleground — nothing happens when you click. The same applies to **Join as Group**: it silently fails for the whole party if any member holds the event's queue spot. To queue for that Battleground with friends right after an event, first **leave the -event queue** (click the battlefields icon next to the minimap and leave), or -wait for the invite and decline it — then queue as a group normally. Queueing -for a *different* Battleground is unaffected (you can be in two queues at -once). +event queue** (click the battlefields icon next to the minimap and leave) — +then queue as a group normally. You can also wait for the invite and decline +it, but that counts toward the automatic opt-out described above; leaving the +queue before the invite pops doesn't. Queueing for a *different* Battleground +is unaffected (you can be in two queues at once). ## Command reference diff --git a/src/BgAutoQueue.cpp b/src/BgAutoQueue.cpp index d243626..7b9f3e2 100644 --- a/src/BgAutoQueue.cpp +++ b/src/BgAutoQueue.cpp @@ -14,6 +14,7 @@ #include "Containers.h" #include "DBCStores.h" #include "DisableMgr.h" +#include "GameTime.h" #include "LFGMgr.h" #include "Log.h" #include "ObjectAccessor.h" @@ -156,6 +157,9 @@ void BgAutoQueue::LoadConfig() _skipAfk = sConfigMgr->GetOption("BgAutoQueue.SkipAFK", true); _broadcastMessage = sConfigMgr->GetOption("BgAutoQueue.BroadcastMessage", BG_AUTO_QUEUE_DEFAULT_BROADCAST); + _declineHintEnabled = sConfigMgr->GetOption("BgAutoQueue.DeclineHint.Enable", true); + _optOutAfterDeclines = sConfigMgr->GetOption("BgAutoQueue.OptOutAfterDeclines", 3); + _skipAuras.clear(); std::string const skipAurasStr = sConfigMgr->GetOption("BgAutoQueue.SkipAuras", ""); for (std::string_view token : Acore::Tokenize(skipAurasStr, ',', false)) @@ -174,6 +178,9 @@ void BgAutoQueue::LoadConfig() // _pendingDrains survives a reload on purpose: it only nudges the core // queue to serve players a previous pass already queued, and every entry // is re-validated against the current templates before each nudge. + // _trackedQueues survives too: entries self-validate via JoinTime, and + // clearing them would silence the decline hint for anyone queued by a + // pass that ran just before the reload. _elapsedMs = 0; _warningSent = false; _firstPass = true; @@ -182,8 +189,8 @@ void BgAutoQueue::LoadConfig() _deserterWarnLogged = false; _poolResolveLogged = false; - LOG_INFO("module", "mod-bg-auto-queue: enabled={}, levels=[{}-{}], configured pool size={}, interval={} min, initialDelay={} s, warningLead={} s, crossFaction={}, cfbg={}, evenTeamsStrict={}, skipGM={}, skipAFK={}, skipAuras={}.", - _enabled, _levelMin, _levelMax, _poolRaw.size(), intervalMin, initialDelaySec, warningLeadSec, _crossFaction, _cfbgEnabled, _evenTeamsStrict, _skipGameMasters, _skipAfk, _skipAuras.size()); + LOG_INFO("module", "mod-bg-auto-queue: enabled={}, levels=[{}-{}], configured pool size={}, interval={} min, initialDelay={} s, warningLead={} s, crossFaction={}, cfbg={}, evenTeamsStrict={}, skipGM={}, skipAFK={}, skipAuras={}, declineHint={}, optOutAfterDeclines={}.", + _enabled, _levelMin, _levelMax, _poolRaw.size(), intervalMin, initialDelaySec, warningLeadSec, _crossFaction, _cfbgEnabled, _evenTeamsStrict, _skipGameMasters, _skipAfk, _skipAuras.size(), _declineHintEnabled, _optOutAfterDeclines); // Opt-out is stored via the core PlayerSettings system, which only persists // across logins when EnablePlayerSettings is on. Without it, .bgevents @@ -250,12 +257,22 @@ bool BgAutoQueue::IsOptedOut(Player* player) const { // GetPlayerSetting is non-const (it lazily creates a zero-default entry), // but the constness here is on BgAutoQueue, not on the Player* argument. - return player->GetPlayerSetting("mod-bg-auto-queue", BG_AUTO_QUEUE_SETTING_OPT_OUT).IsEnabled(); + // Not IsEnabled(): that is an equality-with-1 check and would miss the + // auto-opt-out value 2. + return player->GetPlayerSetting("mod-bg-auto-queue", BG_AUTO_QUEUE_SETTING_OPT_OUT).value != 0; +} + +bool BgAutoQueue::IsAutoOptedOut(Player* player) const +{ + return player->GetPlayerSetting("mod-bg-auto-queue", BG_AUTO_QUEUE_SETTING_OPT_OUT).value == 2; } void BgAutoQueue::SetOptOut(Player* player, bool optedOut) { player->UpdatePlayerSetting("mod-bg-auto-queue", BG_AUTO_QUEUE_SETTING_OPT_OUT, optedOut ? 1u : 0u); + // The counter describes consecutive declines within the current opt-in + // period only, so any explicit toggle restarts it. + player->UpdatePlayerSetting("mod-bg-auto-queue", BG_AUTO_QUEUE_SETTING_DECLINE_COUNT, 0); } bool BgAutoQueue::IsLevelEligible(uint8 level) const @@ -892,6 +909,11 @@ uint32 BgAutoQueue::QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const uint32 avgWaitTime = bgQueue.GetAverageQueueWaitTime(ginfo); uint32 queueSlot = player->AddBattlegroundQueueId(bgQueueTypeId); + // Overwriting a stale entry is fine: gather-time eligibility rejects + // anyone already in a BG queue, so the player holds no other live + // module entry. + _trackedQueues[player->GetGUID()] = { bgQueueTypeId, ginfo->JoinTime }; + if (WorldSession* session = player->GetSession()) { WorldPacket data; @@ -922,6 +944,160 @@ uint32 BgAutoQueue::QueueBucket(BattlegroundTypeId bgTypeId, BracketBucket const return queued; } +void BgAutoQueue::UntrackPlayer(ObjectGuid guid) +{ + _trackedQueues.erase(guid); +} + +void BgAutoQueue::HandleBattleFieldPort(Player* player, uint8 arenaType, BattlegroundTypeId bgTypeId, uint8 action) +{ + auto itr = _trackedQueues.find(player->GetGUID()); + if (itr == _trackedQueues.end()) + return; + + TrackedQueue const tracked = itr->second; + BattlegroundQueueTypeId const queueTypeId = BattlegroundMgr::BGQueueTypeId(bgTypeId, arenaType); + + if (action == 1) // accept ("Enter Battle") + { + // A successful accept removes the player from ALL other BG queues, so + // the tracking entry is dead whichever queue was accepted. A rare + // failed accept (e.g. freeze aura) untracks early — benign direction: + // a hint is missed, a false hint can never fire. + _trackedQueues.erase(itr); + + GroupQueueInfo ginfo; + if (queueTypeId == tracked.queueTypeId + && sBattlegroundMgr->GetBattlegroundQueue(queueTypeId).GetPlayerGroupInfoData(player->GetGUID(), &ginfo) + && ginfo.JoinTime == tracked.joinTime) + player->UpdatePlayerSetting("mod-bg-auto-queue", BG_AUTO_QUEUE_SETTING_DECLINE_COUNT, 0); + + return; + } + + // action == 0: leave queue / decline. A different queue means a manual + // queue action; the module entry is untouched. + if (queueTypeId != tracked.queueTypeId) + return; + + GroupQueueInfo ginfo; + if (!sBattlegroundMgr->GetBattlegroundQueue(queueTypeId).GetPlayerGroupInfoData(player->GetGUID(), &ginfo) + || ginfo.JoinTime != tracked.joinTime) + { + // Stale: our entry left the queue without any hook (invited BG was + // deleted or ended) and this answers a later manual re-queue. + _trackedQueues.erase(itr); + return; + } + + _trackedQueues.erase(itr); + + // No live invite = pre-invite leave via the queue icon (the documented + // group-play workflow): no message, no count. + if (ginfo.IsInvitedToBGInstanceGUID != 0) + OnModuleInviteDeclined(player); +} + +void BgAutoQueue::HandleInviteExpired(Player* player) +{ + auto itr = _trackedQueues.find(player->GetGUID()); + if (itr == _trackedQueues.end()) + return; + + TrackedQueue const tracked = itr->second; + + GroupQueueInfo ginfo; + if (!sBattlegroundMgr->GetBattlegroundQueue(tracked.queueTypeId).GetPlayerGroupInfoData(player->GetGUID(), &ginfo) + || ginfo.JoinTime != tracked.joinTime) + { + _trackedQueues.erase(itr); + return; + } + + // The expiry hook carries no queue identity, but RemoveInviteTime pins it + // down: the invite that just ran out is due (<= now), while a concurrent + // invite from another (manual) queue still lies in the future. When it + // isn't ours, keep tracking — our entry's own expiry or the player's + // answer lands here later. Same-batch double expiry counts once: the + // first call untracks, the second finds nothing. + if (ginfo.IsInvitedToBGInstanceGUID == 0 + || GameTime::GetGameTimeMS().count() < ginfo.RemoveInviteTime) + return; + + _trackedQueues.erase(itr); + OnModuleInviteDeclined(player); +} + +void BgAutoQueue::OnModuleInviteDeclined(Player* player) +{ + // Opted out while queued (opting out never dequeues): don't nag, don't count. + if (IsOptedOut(player)) + return; + + uint32 count = 0; + if (_optOutAfterDeclines > 0) + { + count = player->GetPlayerSetting("mod-bg-auto-queue", BG_AUTO_QUEUE_SETTING_DECLINE_COUNT).value + 1; + player->UpdatePlayerSetting("mod-bg-auto-queue", BG_AUTO_QUEUE_SETTING_DECLINE_COUNT, count); + + if (count >= _optOutAfterDeclines) + { + // Value 2 marks the opt-out as automatic (distinct .bgevents + // status text). Not SetOptOut: that writes 1 and resets the + // counter. The notice ignores _declineHintEnabled — a silent + // auto-opt-out is never acceptable. + player->UpdatePlayerSetting("mod-bg-auto-queue", BG_AUTO_QUEUE_SETTING_OPT_OUT, 2); + SendOptOutNotice(player); + LOG_DEBUG("module", "mod-bg-auto-queue: {} auto-opted out after {} consecutive declined events.", + player->GetName(), count); + return; + } + } + + if (_declineHintEnabled) + SendDeclineHint(player, _optOutAfterDeclines > 0 ? _optOutAfterDeclines - count : 0); +} + +void BgAutoQueue::SendDeclineHint(Player* player, uint32 remaining) const +{ + WorldSession* session = player->GetSession(); + if (!session) + return; + + std::string text = + "|cffff8800|||r\n" + "|cffff8800|||r If you no longer wish to be automatically invited to Battlegrounds, type: |cff00ccff.bgevents off|r\n"; + + if (remaining == 1) + text += "|cffff8800|||r If you decline one more event, automatic invites will stop on their own.\n"; + else if (remaining > 1) + text += Acore::StringFormat("|cffff8800|||r If you decline {} more events in a row, automatic invites will stop on their own.\n", remaining); + + text += "|cffff8800|||r"; + + ChatHandler handler(session); + handler.SendSysMessage(text); + handler.SendNotification("You can turn off automatic BG invites (see chat)."); +} + +void BgAutoQueue::SendOptOutNotice(Player* player) const +{ + WorldSession* session = player->GetSession(); + if (!session) + return; + + std::string const text = Acore::StringFormat( + "|cffff8800|||r\n" + "|cffff8800|||r You have declined {} Battleground events in a row, so automatic invites are now turned off for this character.\n" + "|cffff8800|||r Type |cff00ccff.bgevents on|r if you want them back.\n" + "|cffff8800|||r", + _optOutAfterDeclines); + + ChatHandler handler(session); + handler.SendSysMessage(text); + handler.SendNotification("Automatic BG invites are now off (see chat)."); +} + void BgAutoQueue::RegisterPendingDrain(BattlegroundQueueTypeId queueTypeId, BattlegroundTypeId bgTypeId, BattlegroundBracketId bracketId) { diff --git a/src/BgAutoQueue.h b/src/BgAutoQueue.h index 95f3860..797c9ad 100644 --- a/src/BgAutoQueue.h +++ b/src/BgAutoQueue.h @@ -12,6 +12,7 @@ #include #include +#include #include class Battleground; @@ -21,7 +22,8 @@ struct PvPDifficultyEntry; // PlayerSettings layout for this module. source = "mod-bg-auto-queue". enum BgAutoQueueSetting { - BG_AUTO_QUEUE_SETTING_OPT_OUT = 0 // value: 0 = opted in (default), 1 = opted out + BG_AUTO_QUEUE_SETTING_OPT_OUT = 0, // 0 = opted in (default), 1 = manual opt-out, 2 = auto opt-out + BG_AUTO_QUEUE_SETTING_DECLINE_COUNT = 1 // consecutive module invites declined/ignored }; class BgAutoQueue @@ -36,8 +38,16 @@ class BgAutoQueue // Opt-out is stored as a per-character core PlayerSetting; these are thin // wrappers over Player::GetPlayerSetting/UpdatePlayerSetting. bool IsOptedOut(Player* player) const; + bool IsAutoOptedOut(Player* player) const; void SetOptOut(Player* player, bool optedOut); + // Decline tracking, driven by the module PlayerScript: every + // CMSG_BATTLEFIELD_PORT answer, the invite-expired desertion hook, and + // logout cleanup. Only queue entries this module created are affected. + void HandleBattleFieldPort(Player* player, uint8 arenaType, BattlegroundTypeId bgTypeId, uint8 action); + void HandleInviteExpired(Player* player); + void UntrackPlayer(ObjectGuid guid); + bool IsLevelEligible(uint8 level) const; // Why an online player was not queued during a pass. Reported by @@ -267,6 +277,26 @@ class BgAutoQueue void BroadcastWarning() const; + // Shared decline path (explicit decline and expired invite): counter, + // auto-opt-out, and the player-facing messages. + void OnModuleInviteDeclined(Player* player); + + // remaining = declines left before the auto-opt-out; 0 omits the + // countdown line (OptOutAfterDeclines disabled). + void SendDeclineHint(Player* player, uint32 remaining) const; + void SendOptOutNotice(Player* player) const; + + // One queue entry the module created for a player. joinTime is the core + // GroupQueueInfo::JoinTime captured at AddGroup: queue removals can happen + // without any hook firing (invited BG deleted/ended before the invite ran + // out), so consumers must match joinTime to be sure an entry is still ours + // and not a later manual re-queue of the same queue type. + struct TrackedQueue + { + BattlegroundQueueTypeId queueTypeId = BATTLEGROUND_QUEUE_NONE; + uint32 joinTime = 0; + }; + // Rebuilds _pool from the configured _poolRaw against the battleground // templates that exist right now. Called at the start of every pass so a // pass always reflects the current templates (templates are not yet loaded @@ -288,6 +318,8 @@ class BgAutoQueue bool _skipAfk = true; std::vector _skipAuras; // aura ids that exclude a player from a pass std::string _broadcastMessage; + bool _declineHintEnabled = true; + uint32 _optOutAfterDeclines = 3; uint32 _elapsedMs = 0; bool _warningSent = false; @@ -306,6 +338,8 @@ class BgAutoQueue std::vector _pendingDrains; uint32 _drainTimerMs = 0; + + std::unordered_map _trackedQueues; }; #define sBgAutoQueue BgAutoQueue::instance() diff --git a/src/cs_bg_auto_queue.cpp b/src/cs_bg_auto_queue.cpp index af9761b..5b6a159 100644 --- a/src/cs_bg_auto_queue.cpp +++ b/src/cs_bg_auto_queue.cpp @@ -59,6 +59,17 @@ class bg_auto_queue_commandscript : public CommandScript BgAutoQueue::SkipReason reason = BgAutoQueue::SkipReason::NotInWorld; bool const eligible = sBgAutoQueue->IsQueueEligible(player, &reason); + // An automatic opt-out (after several declined events) reads + // differently from a manual .bgevents off, so the player knows they + // never asked for it. GetPlayerSkipReasonMessage has no player + // context, hence the special case here. + auto skipReasonMessage = [player](BgAutoQueue::SkipReason skipReason) -> char const* + { + if (skipReason == BgAutoQueue::SkipReason::OptedOut && sBgAutoQueue->IsAutoOptedOut(player)) + return "they were turned off automatically after you declined several events in a row. Use \".bgevents on\" to opt back in."; + return GetPlayerSkipReasonMessage(skipReason); + }; + uint32 const msToNext = sBgAutoQueue->GetTimeUntilNextPass(); bool const eventsScheduled = sBgAutoQueue->IsEnabled() && msToNext > 0; @@ -66,14 +77,14 @@ class bg_auto_queue_commandscript : public CommandScript { handler->SendSysMessage("Battleground automatic events are currently turned OFF on this server. An administrator can still trigger one manually."); if (!eligible) - handler->PSendSysMessage("Battleground events are DISABLED for your character because {}", GetPlayerSkipReasonMessage(reason)); + handler->PSendSysMessage("Battleground events are DISABLED for your character because {}", skipReasonMessage(reason)); return true; } if (eligible) handler->SendSysMessage("Battleground events are ENABLED for your character. You will be auto-queued at the next event."); else - handler->PSendSysMessage("Battleground events are DISABLED for your character because {}", GetPlayerSkipReasonMessage(reason)); + handler->PSendSysMessage("Battleground events are DISABLED for your character because {}", skipReasonMessage(reason)); handler->PSendSysMessage("Next scheduled event in {}.", secsToTimeString(msToNext / 1000)); return true; diff --git a/src/mod_bg_auto_queue_scripts.cpp b/src/mod_bg_auto_queue_scripts.cpp index 5e4b44f..86b0095 100644 --- a/src/mod_bg_auto_queue_scripts.cpp +++ b/src/mod_bg_auto_queue_scripts.cpp @@ -4,6 +4,8 @@ #include "BgAutoQueue.h" +#include "Battleground.h" +#include "Player.h" #include "ScriptMgr.h" class mod_bg_auto_queue_world : public WorldScript @@ -25,7 +27,39 @@ class mod_bg_auto_queue_world : public WorldScript } }; +class mod_bg_auto_queue_player : public PlayerScript +{ +public: + mod_bg_auto_queue_player() : PlayerScript("mod_bg_auto_queue_player", { + PLAYERHOOK_CAN_BATTLEFIELD_PORT, + PLAYERHOOK_ON_BATTLEGROUND_DESERTION, + PLAYERHOOK_ON_LOGOUT + }) { } + + bool OnPlayerCanBattleFieldPort(Player* player, uint8 arenaType, BattlegroundTypeId bgTypeId, uint8 action) override + { + // Observe only; the module never vetoes the port action. + sBgAutoQueue->HandleBattleFieldPort(player, arenaType, bgTypeId, action); + return true; + } + + void OnPlayerBattlegroundDesertion(Player* player, BattlegroundDesertionType const desertionType) override + { + // Fires before the core removes the expired entry from the queue, so + // the tracked entry can still be identity-checked. INVITE_LOGOUT needs + // no handling here — the logout hook covers cleanup. + if (desertionType == BG_DESERTION_TYPE_NO_ENTER_BUTTON) + sBgAutoQueue->HandleInviteExpired(player); + } + + void OnPlayerLogout(Player* player) override + { + sBgAutoQueue->UntrackPlayer(player->GetGUID()); + } +}; + void AddSC_mod_bg_auto_queue() { new mod_bg_auto_queue_world(); + new mod_bg_auto_queue_player(); } From 80e3b721c74fa6e2b0ade6862de4d53503a8d1cd Mon Sep 17 00:00:00 2001 From: FrancescoBorzi Date: Fri, 10 Jul 2026 13:17:15 +0200 Subject: [PATCH 2/2] chore: address review feedback --- src/BgAutoQueue.cpp | 6 +++--- src/BgAutoQueue.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BgAutoQueue.cpp b/src/BgAutoQueue.cpp index 7b9f3e2..6237e87 100644 --- a/src/BgAutoQueue.cpp +++ b/src/BgAutoQueue.cpp @@ -1047,7 +1047,7 @@ void BgAutoQueue::OnModuleInviteDeclined(Player* player) // counter. The notice ignores _declineHintEnabled — a silent // auto-opt-out is never acceptable. player->UpdatePlayerSetting("mod-bg-auto-queue", BG_AUTO_QUEUE_SETTING_OPT_OUT, 2); - SendOptOutNotice(player); + SendOptOutNotice(player, count); LOG_DEBUG("module", "mod-bg-auto-queue: {} auto-opted out after {} consecutive declined events.", player->GetName(), count); return; @@ -1080,7 +1080,7 @@ void BgAutoQueue::SendDeclineHint(Player* player, uint32 remaining) const handler.SendNotification("You can turn off automatic BG invites (see chat)."); } -void BgAutoQueue::SendOptOutNotice(Player* player) const +void BgAutoQueue::SendOptOutNotice(Player* player, uint32 count) const { WorldSession* session = player->GetSession(); if (!session) @@ -1091,7 +1091,7 @@ void BgAutoQueue::SendOptOutNotice(Player* player) const "|cffff8800|||r You have declined {} Battleground events in a row, so automatic invites are now turned off for this character.\n" "|cffff8800|||r Type |cff00ccff.bgevents on|r if you want them back.\n" "|cffff8800|||r", - _optOutAfterDeclines); + count); ChatHandler handler(session); handler.SendSysMessage(text); diff --git a/src/BgAutoQueue.h b/src/BgAutoQueue.h index 797c9ad..437eaa3 100644 --- a/src/BgAutoQueue.h +++ b/src/BgAutoQueue.h @@ -284,7 +284,7 @@ class BgAutoQueue // remaining = declines left before the auto-opt-out; 0 omits the // countdown line (OptOutAfterDeclines disabled). void SendDeclineHint(Player* player, uint32 remaining) const; - void SendOptOutNotice(Player* player) const; + void SendOptOutNotice(Player* player, uint32 count) const; // One queue entry the module created for a player. joinTime is the core // GroupQueueInfo::JoinTime captured at AddGroup: queue removals can happen