190 lines
7.4 KiB
Python
Raw Normal View History

2025-10-04 20:40:44 +02:00
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
2025-10-04 20:40:44 +02:00
from server.logger import logger
2025-10-04 20:40:44 +02:00
from server.websocket_manager import WebSocketManager
from server.game_state import GameState
from server.economy import EconomyEngine
from server.database import Database
2025-10-05 02:25:37 +02:00
# Global instances - can be overridden for testing
game_state = None
ws_manager = None
economy_engine = None
database = None
def initialize_components(test_db_path=None):
"""Initialize server components with optional test database"""
global game_state, ws_manager, economy_engine, database
2025-10-05 06:13:39 +02:00
# Initialize database first
database = Database(test_db_path) if test_db_path else Database()
database.init_db()
# Create game state and load data from database
2025-10-05 02:25:37 +02:00
game_state = GameState()
2025-10-05 06:13:39 +02:00
game_state.load_state(database.load_game_state())
# Now create WebSocketManager (it will rebuild nickname mapping from loaded players)
2025-10-05 02:25:37 +02:00
ws_manager = WebSocketManager(game_state)
economy_engine = EconomyEngine(game_state)
return game_state, ws_manager, economy_engine, database
2025-10-04 20:40:44 +02:00
2025-10-04 23:46:44 +02:00
# --- HYBRID MODEL RE-IMPLEMENTED ---
last_economy_tick_time = time.time()
TICK_INTERVAL = 10 # seconds
2025-10-04 23:46:44 +02:00
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():
2025-10-04 23:46:44 +02:00
"""Runs periodically to check if a passive economy update is needed."""
global last_economy_tick_time
2025-10-04 20:40:44 +02:00
while True:
2025-10-04 23:46:44 +02:00
# 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()
2025-10-04 23:46:44 +02:00
# Check frequently for responsiveness
await asyncio.sleep(1)
2025-10-04 20:40:44 +02:00
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Startup and shutdown events."""
2025-10-05 02:25:37 +02:00
global game_state, ws_manager, economy_engine, database
# Initialize components if not already done (for testing)
if not all([game_state, ws_manager, economy_engine, database]):
initialize_components()
logger.info("Server starting up...")
2025-10-05 06:13:39 +02:00
# Database and game state are already initialized in initialize_components()
2025-10-04 20:40:44 +02:00
2025-10-04 23:46:44 +02:00
# Start the hybrid economy loop
task = asyncio.create_task(economy_loop())
2025-10-04 23:46:44 +02:00
logger.info("Hybrid economy loop started.")
2025-10-04 20:40:44 +02:00
yield
logger.info("Server shutting down...")
2025-10-04 20:40:44 +02:00
task.cancel()
database.save_game_state(game_state)
logger.info("Final game state saved.")
2025-10-04 20:40:44 +02:00
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)
2025-10-05 03:31:45 +02:00
# Trigger economy update on login to ensure player stats are current
# This is especially important after server restarts
logger.info(f"Player {player_id} connected, triggering economy update to sync stats.")
await trigger_economy_update_and_save()
# Send updated player data after economy sync
updated_player = game_state.players.get(player_id, player)
await websocket.send_json({
"type": "init",
2025-10-05 03:31:45 +02:00
"player": updated_player.to_dict(),
"game_state": game_state.get_state()
})
while True:
data = await websocket.receive_json()
await handle_message(websocket, data)
2025-10-04 20:40:44 +02:00
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)
2025-10-04 20:40:44 +02:00
async def handle_message(websocket: WebSocket, data: dict):
"""Handle incoming WebSocket messages."""
2025-10-04 20:40:44 +02:00
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}")
2025-10-04 20:40:44 +02:00
if msg_type == "cursor_move":
await ws_manager.broadcast({"type": "cursor_move", "player_id": player_id, "x": data["x"], "y": data["y"]}, exclude=websocket)
2025-10-04 20:40:44 +02:00
elif msg_type == "place_building":
result = game_state.place_building(player_id, data["building_type"], data["x"], data["y"])
2025-10-04 20:40:44 +02:00
if result["success"]:
await ws_manager.broadcast({"type": "building_placed", "building": result["building"]})
2025-10-04 23:46:44 +02:00
# --- 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()
2025-10-04 20:40:44 +02:00
else:
await websocket.send_json({"type": "error", "message": result["error"]})
2025-10-04 20:40:44 +02:00
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"]})
2025-10-04 23:46:44 +02:00
# --- 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"]})
2025-10-04 20:40:44 +02:00
elif msg_type == "edit_building":
result = game_state.edit_building_name(player_id, data["x"], data["y"], data["name"])
2025-10-04 20:40:44 +02:00
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)
2025-10-05 06:13:39 +02:00
else:
await websocket.send_json({"type": "error", "message": result["error"]})
2025-10-04 20:40:44 +02:00
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"]})