171 lines
5.2 KiB
Python
171 lines
5.2 KiB
Python
"""
|
|
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}")
|