""" Echoes of the Ashes - Main FastAPI Application Streamlined with modular routers for maintainability """ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from contextlib import asynccontextmanager from pathlib import Path import logging # Import core modules from .core.config import CORS_ORIGINS, IMAGES_DIR from .core.websockets import manager from .core.security import get_current_user # Import database and game data from . import database as db from .world_loader import load_world, World, Location from .items import ItemsManager from . import background_tasks from .redis_manager import redis_manager # Import routers from .routers import auth, characters # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Load game data print("🔄 Loading game world...") WORLD: World = load_world() LOCATIONS = WORLD.locations ITEMS_MANAGER = ItemsManager() print(f"✅ Game world ready: {len(LOCATIONS)} locations, {len(ITEMS_MANAGER.items)} items") @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan manager for startup/shutdown""" # Startup await db.init_db() print("✅ Database initialized") # Connect to Redis await redis_manager.connect() print("✅ Redis connected") # Inject Redis manager into ConnectionManager manager.set_redis_manager(redis_manager) # Subscribe to all location channels + global broadcast location_channels = [f"location:{loc_id}" for loc_id in LOCATIONS.keys()] await redis_manager.subscribe_to_channels(location_channels + ['game:broadcast']) print(f"✅ Subscribed to {len(location_channels)} location channels") # Register this worker await redis_manager.register_worker() print(f"✅ Worker registered: {redis_manager.worker_id}") # Start Redis message listener (background task) redis_manager.start_listener(manager.handle_redis_message) print("✅ Redis listener started") # Start background tasks (distributed via Redis locks) tasks = await background_tasks.start_background_tasks(manager, LOCATIONS) if tasks: print(f"✅ Started {len(tasks)} background tasks in this worker") else: print("⏭️ Background tasks running in another worker") yield # Shutdown await background_tasks.stop_background_tasks(tasks) # Unregister worker await redis_manager.unregister_worker() print(f"🔌 Worker unregistered: {redis_manager.worker_id}") # Disconnect from Redis await redis_manager.disconnect() print("✅ Redis disconnected") # Initialize FastAPI app app = FastAPI( title="Echoes of the Ashes API", version="2.0.0", description="Post-apocalyptic survival RPG API", lifespan=lifespan ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Mount static files for images if IMAGES_DIR.exists(): app.mount("/images", StaticFiles(directory=str(IMAGES_DIR)), name="images") print(f"✅ Mounted images directory: {IMAGES_DIR}") else: print(f"⚠️ Images directory not found: {IMAGES_DIR}") # Include routers app.include_router(auth.router) app.include_router(characters.router) # TODO: Add remaining routers as they are created: # app.include_router(game_routes.router) # app.include_router(combat.router) # app.include_router(equipment.router) # app.include_router(crafting.router) # app.include_router(loot.router) # app.include_router(admin.router) # app.include_router(statistics.router) @app.get("/health") async def health_check(): """Health check endpoint for load balancers""" return {"status": "ok"} @app.websocket("/ws") async def websocket_endpoint( websocket: WebSocket, current_user: dict = Depends(get_current_user) ): """WebSocket endpoint for real-time game updates""" player_id = current_user['id'] username = current_user['name'] await manager.connect(websocket, player_id, username) # Get player's location and register in Redis location_id = current_user.get('location_id') if location_id and redis_manager: await redis_manager.add_player_to_location(location_id, player_id) # Store session data await redis_manager.update_player_session(player_id, { 'username': username, 'location_id': location_id, 'level': current_user.get('level', 1), 'websocket_connected': 'true' }) try: while True: # Keep connection alive data = await websocket.receive_text() # You can handle client messages here if needed logger.debug(f"Received from {username}: {data}") except WebSocketDisconnect: await manager.disconnect(player_id) # Remove from location registry if location_id and redis_manager: await redis_manager.remove_player_from_location(location_id, player_id) print(f"WebSocket disconnected: {username}")