fix(instance): Add queue timeout to prevent instance bot accumulation

Problem: Instance bots sitting in BG queue were considered "active" and never
timed out. If the BG never popped (not enough players), bots accumulated
indefinitely, causing "HARD CAP REACHED" errors (563 bots when only 19 requested).

Root cause:
- IsInActiveInstanceState() returns true for bots in BG/LFG queue
- Active bots never trigger idle timeout
- If BG never starts, bots wait in queue forever

Solution: Add a 5-minute queue timeout separate from idle timeout:
- Track _queueAccumulatorMs - time spent waiting in queue
- Track _hasEnteredInstance - whether bot actually entered content
- If bot is in queue for >5 minutes without entering instance → logout

New member variables:
- _instanceBotStartTime: when bot was marked as instance bot
- _queueAccumulatorMs: accumulated queue wait time
- _hasEnteredInstance: true once bot enters dungeon/BG/arena

Constants:
- INSTANCE_BOT_IDLE_TIMEOUT_MS = 60s (existing)
- INSTANCE_BOT_QUEUE_TIMEOUT_MS = 5 minutes (new)

This prevents bot explosion when BGs don't pop due to population imbalance.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: luis <[email protected]>
This commit is contained in:
agatho
2026-02-04 20:20:00 -03:00
committed by luis
co-authored by Claude Opus 4.5
parent 61d7aec8f5
commit ecec2fdf9e
2 changed files with 90 additions and 11 deletions
+74 -11
View File
@@ -3134,15 +3134,19 @@ void BotSession::SetInstanceBot(bool isInstanceBot)
if (isInstanceBot)
{
// Reset idle state when marked as instance bot
// Reset idle and queue state when marked as instance bot
_idleAccumulatorMs.store(0);
_queueAccumulatorMs.store(0);
_wasActiveLastCheck.store(true);
_hasEnteredInstance.store(false);
_instanceBotStartTime.store(GameTime::GetGameTimeMS());
Player* player = GetPlayer();
TC_LOG_INFO("module.playerbot.instance",
"Bot {} marked as INSTANCE BOT - will auto-logout after {}s if idle",
"Bot {} marked as INSTANCE BOT - idle timeout: {}s, queue timeout: {}s",
player ? player->GetName() : "unknown",
INSTANCE_BOT_IDLE_TIMEOUT_MS / 1000);
INSTANCE_BOT_IDLE_TIMEOUT_MS / 1000,
INSTANCE_BOT_QUEUE_TIMEOUT_MS / 1000);
}
}
@@ -3203,23 +3207,82 @@ bool BotSession::UpdateIdleStateAndCheckLogout(uint32 diff)
if (!player)
return false;
bool isActive = IsInActiveInstanceState();
// ========================================================================
// CHECK 1: Has bot entered actual instanced content?
// ========================================================================
// If bot is inside an instance (dungeon/raid/BG/arena), mark it as having
// "entered" - this means it's actively participating, not just waiting.
Map* map = player->GetMap();
bool inInstancedContent = map && (map->IsDungeon() || map->IsRaid() ||
map->IsBattleground() || map->IsBattleArena());
if (isActive)
if (inInstancedContent)
{
// Bot is active - reset idle accumulator
if (_idleAccumulatorMs.load() > 0)
// Bot is actually in instanced content - mark it and reset queue timer
if (!_hasEnteredInstance.exchange(true))
{
TC_LOG_DEBUG("module.playerbot.instance",
"Bot {} became ACTIVE (in queue/group/instance) - resetting idle timer",
TC_LOG_INFO("module.playerbot.instance",
"Bot {} ENTERED instanced content - queue timeout disabled",
player->GetName());
}
_queueAccumulatorMs.store(0);
_idleAccumulatorMs.store(0);
_wasActiveLastCheck.store(true);
return false;
}
// Bot is idle - accumulate idle time
// ========================================================================
// CHECK 2: Is bot in an "active" state (queue/group)?
// ========================================================================
bool isActive = IsInActiveInstanceState();
if (isActive)
{
// Bot is in queue or group but NOT in actual instanced content
// Reset idle timer but accumulate queue time (if never entered instance)
if (_idleAccumulatorMs.load() > 0)
{
TC_LOG_DEBUG("module.playerbot.instance",
"Bot {} became ACTIVE (in queue/group) - resetting idle timer",
player->GetName());
}
_idleAccumulatorMs.store(0);
_wasActiveLastCheck.store(true);
// If bot has never entered instance, accumulate queue time
if (!_hasEnteredInstance.load())
{
uint32 currentQueue = _queueAccumulatorMs.load();
uint32 newQueue = currentQueue + diff;
_queueAccumulatorMs.store(newQueue);
// Check if queue timeout exceeded
if (newQueue >= INSTANCE_BOT_QUEUE_TIMEOUT_MS)
{
TC_LOG_INFO("module.playerbot.instance",
"⏰ Bot {} QUEUE TIMEOUT ({} seconds in queue without content starting) - "
"scheduling logout to prevent accumulation",
player->GetName(), INSTANCE_BOT_QUEUE_TIMEOUT_MS / 1000);
return true;
}
// Log progress every 60 seconds
uint32 oldMinutes = currentQueue / 60000;
uint32 newMinutes = newQueue / 60000;
if (newMinutes > oldMinutes)
{
TC_LOG_DEBUG("module.playerbot.instance",
"Bot {} waiting in queue for {}m / {}m",
player->GetName(), newQueue / 60000, INSTANCE_BOT_QUEUE_TIMEOUT_MS / 60000);
}
}
return false;
}
// ========================================================================
// CHECK 3: Bot is idle (not in queue/group/instance)
// ========================================================================
bool wasActive = _wasActiveLastCheck.exchange(false);
if (wasActive)
{
@@ -3233,7 +3296,7 @@ bool BotSession::UpdateIdleStateAndCheckLogout(uint32 diff)
uint32 newIdle = currentIdle + diff;
_idleAccumulatorMs.store(newIdle);
// Check if timeout exceeded
// Check if idle timeout exceeded
if (newIdle >= INSTANCE_BOT_IDLE_TIMEOUT_MS)
{
TC_LOG_INFO("module.playerbot.instance",
@@ -204,6 +204,11 @@ public:
/// Idle timeout for instance bots (60 seconds = 1 minute)
static constexpr uint32 INSTANCE_BOT_IDLE_TIMEOUT_MS = 60 * 1000;
/// Queue timeout for instance bots (5 minutes)
/// If a bot has been queued for content (BG/LFG) for longer than this without
/// actually getting into the content, it will be logged out to prevent accumulation
static constexpr uint32 INSTANCE_BOT_QUEUE_TIMEOUT_MS = 5 * 60 * 1000;
// Process pending async login operations
void ProcessPendingLogin();
@@ -505,14 +510,25 @@ private:
/// Whether this bot is an instance bot (JIT or warm pool)
std::atomic<bool> _isInstanceBot{false};
/// Time when bot was marked as instance bot (for queue timeout)
std::atomic<uint32> _instanceBotStartTime{0};
/// Accumulated idle time in milliseconds
/// Reset to 0 when bot enters queue or group, incremented when idle
std::atomic<uint32> _idleAccumulatorMs{0};
/// Accumulated queue time in milliseconds (time spent waiting in queue without content starting)
/// This prevents bots from sitting in queue forever when BG/LFG never pops
std::atomic<uint32> _queueAccumulatorMs{0};
/// Whether bot was active (in queue/group) last check
/// Used to detect transition from active to idle
std::atomic<bool> _wasActiveLastCheck{true};
/// Whether bot has ever entered actual instanced content (dungeon/BG/arena)
/// Used to distinguish "waiting in queue" from "actually playing"
std::atomic<bool> _hasEnteredInstance{false};
// Deleted copy operations
BotSession(BotSession const&) = delete;
BotSession& operator=(BotSession const&) = delete;