import asyncio from contextlib import asynccontextmanager from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from pathlib import Path import time from server.logger import logger from server.websocket_manager import WebSocketManager from server.game_state import GameState from server.economy import EconomyEngine from server.database import Database # Global instances game_state = GameState() ws_manager = WebSocketManager(game_state) economy_engine = EconomyEngine(game_state) database = Database() TICK_INTERVAL = 10 # seconds # --- FIX: Reverting to a simple, reliable 10-second loop --- async def economy_loop(): """A simple loop that runs the economy tick every 10 seconds.""" while True: await asyncio.sleep(TICK_INTERVAL) logger.info("Triggering scheduled economy tick.") start_time = time.perf_counter() # 1. Process one economy tick economy_engine.tick() # 2. Broadcast updates concurrently update_tasks = [] for player_id, player in game_state.players.items(): if player_id in ws_manager.active_connections: task = ws_manager.send_to_player(player_id, { "type": "player_stats_update", "player": player.to_dict() }) update_tasks.append(task) if update_tasks: await asyncio.gather(*update_tasks) # 3. Save the new state database.save_game_state(game_state) duration = time.perf_counter() - start_time logger.info(f"Economy tick cycle completed in {duration:.4f} seconds.") @asynccontextmanager async def lifespan(app: FastAPI): """Startup and shutdown events.""" logger.info("Server starting up...") database.init_db() game_state.load_state(database.load_game_state()) # Start the simple economy loop task = asyncio.create_task(economy_loop()) logger.info(f"Economy loop started with a {TICK_INTERVAL}-second interval.") yield logger.info("Server shutting down...") task.cancel() database.save_game_state(game_state) logger.info("Final game state saved.") app = FastAPI(lifespan=lifespan) app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/") async def root(): return FileResponse("static/index.html") @app.websocket("/ws/{nickname}") async def websocket_endpoint(websocket: WebSocket, nickname: str): await ws_manager.connect(websocket, nickname) try: player_id = await ws_manager.get_player_id(websocket) if player_id: player = game_state.get_or_create_player(nickname, player_id) await websocket.send_json({ "type": "init", "player": player.to_dict(), "game_state": game_state.get_state() }) while True: data = await websocket.receive_json() await handle_message(websocket, data) except WebSocketDisconnect: await ws_manager.disconnect(websocket) except Exception as e: player_id = await ws_manager.get_player_id(websocket) logger.error(f"An error occurred in the websocket endpoint for player {player_id}: {e}", exc_info=True) async def handle_message(websocket: WebSocket, data: dict): """Handle incoming WebSocket messages.""" msg_type = data.get("type") player_id = await ws_manager.get_player_id(websocket) if not player_id: logger.warning("Received message from an unknown player, ignoring.") return logger.debug(f"Received '{msg_type}' from player {player_id}") if msg_type == "cursor_move": await ws_manager.broadcast({"type": "cursor_move", "player_id": player_id, "x": data["x"], "y": data["y"]}, exclude=websocket) elif msg_type == "place_building": result = game_state.place_building(player_id, data["building_type"], data["x"], data["y"]) if result["success"]: await ws_manager.broadcast({"type": "building_placed", "building": result["building"]}) # --- CHANGE: Action now only saves, economy is handled by the loop --- database.save_game_state(game_state) else: await websocket.send_json({"type": "error", "message": result["error"]}) elif msg_type == "remove_building": result = game_state.remove_building(player_id, data["x"], data["y"]) if result["success"]: await ws_manager.broadcast({"type": "building_removed", "x": data["x"], "y": data["y"]}) # --- CHANGE: Action now only saves, economy is handled by the loop --- database.save_game_state(game_state) else: await websocket.send_json({"type": "error", "message": result["error"]}) elif msg_type == "edit_building": result = game_state.edit_building_name(player_id, data["x"], data["y"], data["name"]) if result["success"]: await ws_manager.broadcast({"type": "building_updated", "x": data["x"], "y": data["y"], "name": data["name"]}) database.save_game_state(game_state) elif msg_type == "chat": nickname = await ws_manager.get_nickname(websocket) await ws_manager.broadcast({"type": "chat", "nickname": nickname, "message": data["message"], "timestamp": data["timestamp"]})