""" Background tasks for the bot. Handles periodic maintenance, regeneration, and processing. """ import asyncio import logging import time from bot import database logger = logging.getLogger(__name__) async def decay_dropped_items(shutdown_event): """A background task that periodically cleans up old dropped items.""" while not shutdown_event.is_set(): try: # Wait for 5 minutes before the next cleanup await asyncio.wait_for(shutdown_event.wait(), timeout=300) except asyncio.TimeoutError: start_time = time.time() logger.info("Running item decay task...") # Set decay time to 1 hour (3600 seconds) decay_seconds = 3600 timestamp_limit = int(time.time()) - decay_seconds items_removed = await database.remove_expired_dropped_items(timestamp_limit) elapsed = time.time() - start_time if items_removed > 0: logger.info(f"Decayed and removed {items_removed} old items in {elapsed:.2f}s") async def regenerate_stamina(shutdown_event): """A background task that periodically regenerates stamina for all players.""" while not shutdown_event.is_set(): try: # Wait for 5 minutes before the next regeneration cycle await asyncio.wait_for(shutdown_event.wait(), timeout=300) except asyncio.TimeoutError: start_time = time.time() logger.info("Running stamina regeneration...") players_updated = await database.regenerate_all_players_stamina() elapsed = time.time() - start_time if players_updated > 0: logger.info(f"Regenerated stamina for {players_updated} players in {elapsed:.2f}s") # Alert if regeneration is taking too long (potential scaling issue) if elapsed > 5.0: logger.warning(f"⚠️ Stamina regeneration took {elapsed:.2f}s (threshold: 5s) - check database load!") async def check_combat_timers(shutdown_event): """A background task that checks for idle combat turns and auto-attacks.""" while not shutdown_event.is_set(): try: # Wait for 30 seconds before next check await asyncio.wait_for(shutdown_event.wait(), timeout=30) except asyncio.TimeoutError: start_time = time.time() # Check for combats idle for more than 5 minutes (300 seconds) idle_threshold = time.time() - 300 idle_combats = await database.get_all_idle_combats(idle_threshold) if idle_combats: logger.info(f"Processing {len(idle_combats)} idle combats...") for combat in idle_combats: try: from bot import combat as combat_logic # Force end player's turn and let NPC attack if combat['turn'] == 'player': await database.update_combat(combat['player_id'], { 'turn': 'npc', 'turn_started_at': time.time() }) # NPC attacks await combat_logic.npc_attack(combat['player_id']) except Exception as e: logger.error(f"Error processing idle combat: {e}") # Log performance for monitoring if idle_combats: elapsed = time.time() - start_time logger.info(f"Processed {len(idle_combats)} idle combats in {elapsed:.2f}s") # Warn if taking too long (potential scaling issue) if elapsed > 10.0: logger.warning(f"⚠️ Combat timer check took {elapsed:.2f}s (threshold: 10s) - consider batching!") async def decay_corpses(shutdown_event): """A background task that removes old corpses.""" while not shutdown_event.is_set(): try: # Wait for 10 minutes before next cleanup await asyncio.wait_for(shutdown_event.wait(), timeout=600) except asyncio.TimeoutError: start_time = time.time() logger.info("Running corpse decay...") # Player corpses decay after 24 hours player_corpse_limit = time.time() - (24 * 3600) player_corpses_removed = await database.remove_expired_player_corpses(player_corpse_limit) # NPC corpses decay after 2 hours npc_corpse_limit = time.time() - (2 * 3600) npc_corpses_removed = await database.remove_expired_npc_corpses(npc_corpse_limit) elapsed = time.time() - start_time if player_corpses_removed > 0 or npc_corpses_removed > 0: logger.info(f"Decayed {player_corpses_removed} player corpses and {npc_corpses_removed} NPC corpses in {elapsed:.2f}s") async def process_status_effects(shutdown_event): """ A background task that applies damage from persistent status effects. Runs every 5 minutes to process status effect ticks. """ while not shutdown_event.is_set(): try: # Wait for 5 minutes before next processing cycle await asyncio.wait_for(shutdown_event.wait(), timeout=300) except asyncio.TimeoutError: start_time = time.time() logger.info("Running status effects processor...") try: # Decrement all status effect ticks and get affected players affected_players = await database.decrement_all_status_effect_ticks() if not affected_players: elapsed = time.time() - start_time logger.info(f"No active status effects to process ({elapsed:.3f}s)") continue # Process each affected player deaths = 0 damage_dealt = 0 for player_id in affected_players: try: # Get current status effects (after decrement) effects = await database.get_player_status_effects(player_id) if not effects: continue # Calculate total damage from bot.status_utils import calculate_status_damage total_damage = calculate_status_damage(effects) if total_damage > 0: damage_dealt += total_damage player = await database.get_player(player_id) if not player or player['is_dead']: continue new_hp = max(0, player['hp'] - total_damage) # Check if player died from status effects if new_hp <= 0: await database.update_player(player_id, {'hp': 0, 'is_dead': True}) deaths += 1 # Create player corpse inventory = await database.get_inventory(player_id) await database.create_player_corpse( player_name=player['name'], location_id=player['location_id'], items=inventory ) # Remove status effects from dead player await database.remove_all_status_effects(player_id) logger.info(f"Player {player['name']} (ID: {player_id}) died from status effects") else: # Apply damage await database.update_player(player_id, {'hp': new_hp}) except Exception as e: logger.error(f"Error processing status effects for player {player_id}: {e}") elapsed = time.time() - start_time logger.info( f"Processed status effects for {len(affected_players)} players " f"({damage_dealt} total damage, {deaths} deaths) in {elapsed:.3f}s" ) # Warn if taking too long (potential scaling issue) if elapsed > 5.0: logger.warning( f"⚠️ Status effects processing took {elapsed:.3f}s (threshold: 5s) " f"- {len(affected_players)} players affected" ) except Exception as e: logger.error(f"Error in status effects processor: {e}")