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()
# --- HYBRID MODEL RE-IMPLEMENTED ---
last_economy_tick_time = time.time()
TICK_INTERVAL = 10 # seconds
async def trigger_economy_update_and_save():
"""Triggers an economy tick, broadcasts updates concurrently, saves, and resets the timer."""
global last_economy_tick_time
logger.debug("Triggering full economy update and save cycle...")
start_time = time.perf_counter()
economy_engine.tick()
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)
database.save_game_state(game_state)
# Reset the global tick timer after any update
last_economy_tick_time = time.time()
duration = time.perf_counter() - start_time
logger.info(f"Full economy update cycle completed in {duration:.4f} seconds.")
async def economy_loop():
"""Runs periodically to check if a passive economy update is needed."""
global last_economy_tick_time
while True:
# Check if 10 seconds have passed since the last tick (from any source)
if time.time() - last_economy_tick_time > TICK_INTERVAL:
logger.info("Triggering timed economy update for idle players.")
await trigger_economy_update_and_save()
# Check frequently for responsiveness
await asyncio.sleep(1)
@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 hybrid economy loop
task = asyncio.create_task(economy_loop())
logger.info("Hybrid economy loop started.")
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: Trigger an INSTANT economy update on financial action ---
logger.info(f"Player {player_id} action triggered immediate economy update.")
await trigger_economy_update_and_save()
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: Trigger an INSTANT economy update on financial action ---
logger.info(f"Player {player_id} action triggered immediate economy update.")
await trigger_economy_update_and_save()
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"]})