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"]})