import asyncio import logging import signal import os import time from dotenv import load_dotenv from telegram import Update from telegram.ext import Application, CommandHandler, CallbackQueryHandler from bot import database, handlers # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) # Quieten down the HTTPX logger, which is very verbose logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # A global event to signal shutdown shutdown_event = asyncio.Event() def signal_handler(sig, frame): """Gracefully handle shutdown signals.""" logger.info("Shutdown signal received. Shutting down gracefully...") shutdown_event.set() async def decay_dropped_items(): """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: 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) if items_removed > 0: logger.info(f"Decayed and removed {items_removed} old items.") async def regenerate_stamina(): """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: logger.info("Running stamina regeneration...") players_updated = await database.regenerate_all_players_stamina() if players_updated > 0: logger.info(f"Regenerated stamina for {players_updated} players.") async def check_combat_timers(): """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: # 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) 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': logger.info(f"Player {combat['player_id']} idle in combat - auto-ending turn") 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}") async def decay_corpses(): """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: 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) 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.") async def main() -> None: """Start the bot and wait for a shutdown signal.""" load_dotenv() TOKEN = os.getenv("TELEGRAM_TOKEN") if not TOKEN or TOKEN == "YOUR_TELEGRAM_BOT_TOKEN_HERE": logger.error("TELEGRAM_TOKEN is not set! Please edit your .env file.") return await database.create_tables() application = Application.builder().token(TOKEN).build() application.add_handler(CommandHandler("start", handlers.start)) application.add_handler(CommandHandler("map", handlers.export_map)) application.add_handler(CommandHandler("spawns", handlers.spawn_stats)) application.add_handler(CallbackQueryHandler(handlers.button_handler)) async with application: await application.start() await application.updater.start_polling(allowed_updates=Update.ALL_TYPES) logger.info("Bot is running and polling for updates...") # Start the spawn manager from bot import spawn_manager await spawn_manager.start_spawn_manager() # Start the background tasks decay_task = asyncio.create_task(decay_dropped_items()) stamina_task = asyncio.create_task(regenerate_stamina()) combat_timer_task = asyncio.create_task(check_combat_timers()) corpse_decay_task = asyncio.create_task(decay_corpses()) await shutdown_event.wait() await application.updater.stop() await application.stop() # Ensure the background tasks are also cancelled on shutdown decay_task.cancel() stamina_task.cancel() combat_timer_task.cancel() corpse_decay_task.cancel() logger.info("Bot has been shut down.") if __name__ == "__main__": signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) try: asyncio.run(main()) except (KeyboardInterrupt, SystemExit): logger.info("Main function interrupted.")