from fastapi import WebSocket
from typing import Dict, List
import uuid
import time
from server.game_state import GameState
from server.logger import logger
class WebSocketManager:
"""Manages WebSocket connections for multiplayer"""
def __init__(self, game_state: GameState):
self.active_connections: Dict[str, List[WebSocket]] = {}
self.player_nicknames: Dict[str, str] = {}
self.nickname_to_id: Dict[str, str] = {}
self.game_state = game_state
# Rebuild nickname-to-ID mapping from game state
self._rebuild_nickname_mapping()
def _rebuild_nickname_mapping(self):
"""Rebuild the nickname-to-ID mapping from existing players in game state."""
for player_id, player in self.game_state.players.items():
self.nickname_to_id[player.nickname] = player_id
if self.nickname_to_id:
logger.info(f"Rebuilt nickname-to-ID mapping for {len(self.nickname_to_id)} players: {list(self.nickname_to_id.keys())}")
async def connect(self, websocket: WebSocket, nickname: str):
"""Connect a new player, allowing multiple connections per nickname."""
logger.debug(f"Connection attempt from nickname '{nickname}'.")
await websocket.accept()
if nickname in self.nickname_to_id:
player_id = self.nickname_to_id[nickname]
else:
player_id = str(uuid.uuid4())
self.nickname_to_id[nickname] = player_id
logger.info(f"New player '{nickname}' assigned ID {player_id}.")
player = self.game_state.get_or_create_player(nickname, player_id)
if not self.active_connections.get(player_id):
player.is_online = True
self.active_connections[player_id] = []
logger.info(f"Player '{nickname}' ({player_id}) is now online.")
await self.broadcast({
"type": "player_joined",
"player_id": player_id,
"nickname": nickname
})
self.active_connections[player_id].append(websocket)
self.player_nicknames[player_id] = nickname
logger.debug(f"Added new websocket for '{nickname}'. Total connections for user: {len(self.active_connections[player_id])}.")
async def disconnect(self, websocket: WebSocket):
"""Disconnect a player's specific websocket instance."""
player_id = await self.get_player_id(websocket)
if player_id and player_id in self.active_connections:
self.active_connections[player_id].remove(websocket)
logger.debug(f"Removed websocket for player {player_id}. Remaining connections: {len(self.active_connections[player_id])}.")
if not self.active_connections[player_id]:
del self.active_connections[player_id]
nickname = self.player_nicknames.pop(player_id, None)
# --- REVERTED CHANGE: The following lines that set a player
# --- to 'offline' have been removed to restore original economy.
# player = self.game_state.players.get(player_id)
# if player:
# player.is_online = False
# player.last_online = time.time()
logger.info(f"Player '{nickname}' ({player_id}) last connection closed.")
await self.broadcast({
"type": "player_left",
"player_id": player_id,
"nickname": nickname
})
async def send_to_player(self, player_id: str, message: dict):
"""Send a message to all active connections for a specific player."""
connections = self.active_connections.get(player_id, [])
if not connections: return
logger.debug(f"Sending '{message['type']}' to player {player_id} ({len(connections)} connections).")
disconnected_sockets = []
for websocket in connections:
try:
await websocket.send_json(message)
except Exception:
disconnected_sockets.append(websocket)
for ws in disconnected_sockets:
logger.warning(f"Found dead socket for player {player_id} during send. Cleaning up.")
if ws in self.active_connections.get(player_id, []):
self.active_connections[player_id].remove(ws)
async def broadcast(self, message: dict, exclude: WebSocket = None):
"""Broadcast message to all connected players and all their connections."""
msg_type = message.get("type", "unknown")
logger.debug(f"Broadcasting '{msg_type}' to all active connections.")
all_sockets = []
for ws_list in self.active_connections.values():
all_sockets.extend(ws_list)
for websocket in all_sockets:
if websocket == exclude:
continue
try:
await websocket.send_json(message)
except Exception:
pass
async def get_player_id(self, websocket: WebSocket) -> str:
"""Get player ID from a specific websocket instance."""
for player_id, ws_list in self.active_connections.items():
if websocket in ws_list:
return player_id
return None
async def get_nickname(self, websocket: WebSocket) -> str:
"""Get nickname from websocket."""
player_id = await self.get_player_id(websocket)
return self.player_nicknames.get(player_id, "Unknown")