2025-10-04 20:40:44 +02:00
|
|
|
from fastapi import WebSocket
|
2025-10-04 23:26:27 +02:00
|
|
|
from typing import Dict, List
|
2025-10-04 20:40:44 +02:00
|
|
|
import uuid
|
2025-10-04 23:26:27 +02:00
|
|
|
import time
|
|
|
|
|
from server.game_state import GameState
|
|
|
|
|
from server.logger import logger
|
2025-10-04 20:40:44 +02:00
|
|
|
|
|
|
|
|
class WebSocketManager:
|
|
|
|
|
"""Manages WebSocket connections for multiplayer"""
|
|
|
|
|
|
2025-10-04 23:26:27 +02:00
|
|
|
def __init__(self, game_state: GameState):
|
|
|
|
|
self.active_connections: Dict[str, List[WebSocket]] = {}
|
2025-10-04 20:40:44 +02:00
|
|
|
self.player_nicknames: Dict[str, str] = {}
|
|
|
|
|
self.nickname_to_id: Dict[str, str] = {}
|
2025-10-04 23:26:27 +02:00
|
|
|
self.game_state = game_state
|
2025-10-05 06:13:39 +02:00
|
|
|
|
|
|
|
|
# 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())}")
|
2025-10-04 20:40:44 +02:00
|
|
|
|
|
|
|
|
async def connect(self, websocket: WebSocket, nickname: str):
|
2025-10-04 23:26:27 +02:00
|
|
|
"""Connect a new player, allowing multiple connections per nickname."""
|
|
|
|
|
logger.debug(f"Connection attempt from nickname '{nickname}'.")
|
|
|
|
|
|
2025-10-04 20:40:44 +02:00
|
|
|
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
|
2025-10-04 23:26:27 +02:00
|
|
|
logger.info(f"New player '{nickname}' assigned ID {player_id}.")
|
2025-10-04 20:40:44 +02:00
|
|
|
|
2025-10-04 23:26:27 +02:00
|
|
|
player = self.game_state.get_or_create_player(nickname, player_id)
|
2025-10-04 20:40:44 +02:00
|
|
|
|
2025-10-04 23:26:27 +02:00
|
|
|
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.")
|
2025-10-04 20:40:44 +02:00
|
|
|
await self.broadcast({
|
2025-10-04 23:26:27 +02:00
|
|
|
"type": "player_joined",
|
2025-10-04 20:40:44 +02:00
|
|
|
"player_id": player_id,
|
|
|
|
|
"nickname": nickname
|
|
|
|
|
})
|
2025-10-04 23:26:27 +02:00
|
|
|
|
|
|
|
|
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])}.")
|
2025-10-04 20:40:44 +02:00
|
|
|
|
2025-10-04 23:26:27 +02:00
|
|
|
async def disconnect(self, websocket: WebSocket):
|
|
|
|
|
"""Disconnect a player's specific websocket instance."""
|
|
|
|
|
player_id = await self.get_player_id(websocket)
|
2025-10-04 20:40:44 +02:00
|
|
|
|
2025-10-04 23:26:27 +02:00
|
|
|
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:
|
2025-10-04 20:40:44 +02:00
|
|
|
if websocket == exclude:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await websocket.send_json(message)
|
|
|
|
|
except Exception:
|
2025-10-04 23:26:27 +02:00
|
|
|
pass
|
2025-10-04 20:40:44 +02:00
|
|
|
|
|
|
|
|
async def get_player_id(self, websocket: WebSocket) -> str:
|
2025-10-04 23:26:27 +02:00
|
|
|
"""Get player ID from a specific websocket instance."""
|
|
|
|
|
for player_id, ws_list in self.active_connections.items():
|
|
|
|
|
if websocket in ws_list:
|
2025-10-04 20:40:44 +02:00
|
|
|
return player_id
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def get_nickname(self, websocket: WebSocket) -> str:
|
2025-10-04 23:26:27 +02:00
|
|
|
"""Get nickname from websocket."""
|
2025-10-04 20:40:44 +02:00
|
|
|
player_id = await self.get_player_id(websocket)
|
|
|
|
|
return self.player_nicknames.get(player_id, "Unknown")
|