Coverage Summary for Class: TCPServer (it.polimi.ingsw.gc14.Network.TCP.Server)

Class Class, % Method, % Branch, % Line, %
TCPServer 0% (0/1) 0% (0/8) 0% (0/30) 0% (0/105)


 package it.polimi.ingsw.gc14.Network.TCP.Server;
 
 import it.polimi.ingsw.gc14.Controller.GameController;
 import it.polimi.ingsw.gc14.ErrorType;
 import it.polimi.ingsw.gc14.LimitedMap;
 import it.polimi.ingsw.gc14.Model.Game;
 import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;
 import it.polimi.ingsw.gc14.Model.MiniModel;
 import it.polimi.ingsw.gc14.Network.EventType;
 import it.polimi.ingsw.gc14.Network.NetworkConfig;
 import it.polimi.ingsw.gc14.Network.NetworkEvent;
 import it.polimi.ingsw.gc14.Network.NetworkEvents.AddPlayer;
 import it.polimi.ingsw.gc14.Network.NetworkEvents.ReconnectPlayer;
 
 import java.io.*;
 import java.net.*;
 import java.util.ArrayList;
 import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 
 /**
  * TCP server responsible for accepting client connections,
  * handling player registration and reconnection, and sending
  * game updates to connected clients.
  *
  * <p>The server also manages a dedicated heartbeat channel used
  * to detect disconnected clients and associate each heartbeat
  * connection with the corresponding {@link ClientHandler}.
  */
 public class TCPServer {
 
     /**
      * Main TCP port used for standard client-server communication.
      */
     private final int port;
 
     /**
      * TCP port dedicated to heartbeat communication.
      */
     private final int heartbeatPort;
 
     /**
      * Main server socket used to accept client connections.
      */
     private ServerSocket socketTCP;
 
     /**
      * Server socket used to accept heartbeat connections.
      */
     private ServerSocket heartbeatSocketTCP;
 
     /**
      * Game controller used to manage the server-side game logic.
      */
     private final GameController controller;
 
     /**
      * Queue containing network events received from clients.
      */
     private final BlockingQueue<NetworkEvent> actionQueue;
 
     /**
      * Map storing the online/offline status of connected players.
      */
     private final LimitedMap<String, Boolean> playerList;
 
     /**
      * List of active TCP client handlers.
      */
     private final CopyOnWriteArrayList<ClientHandler> clientHandlers;
 
 
     /**
      * Temporary map associating each username with the corresponding
      * {@link ClientHandler} waiting for its heartbeat connection.
      */
     private final Map<String, ClientHandler> pendingHeartbeat = new ConcurrentHashMap<>();
 
     /**
      * Constructs a TCP server with the required game and network components.
      *
      * @param controller    the game controller used to manage the game logic.
      * @param port          the main TCP port used for client communication.
      * @param heartbeatPort the TCP port dedicated to heartbeat connections.
      * @param actionQueue   the queue containing incoming network events.
      * @param playerList    the map storing the connection status of the players.
      */
     public TCPServer(GameController controller,
                      int port,
                      int heartbeatPort,
                      BlockingQueue<NetworkEvent> actionQueue,
                      LimitedMap<String, Boolean> playerList) {
         this.port = port;
         this.heartbeatPort = heartbeatPort;
         this.controller = controller;
         this.actionQueue = actionQueue;
         this.playerList = playerList;
         this.clientHandlers = new CopyOnWriteArrayList<>();
     }
 
     /**
      * Starts the TCP server and begins accepting client connections.
      *
      * <p>The method opens both the main TCP server socket and the dedicated
      * heartbeat server socket. It then starts a separate thread for heartbeat
      * connections and continuously waits for new players or reconnecting clients.
      *
      * <p>When a new connection is received, the first event must be an
      * {@link AddPlayer} request. Depending on the current server state,
      * the connection is handled either as a new player joining the game
      * or as a reconnection attempt.
      *
      */
     public void start() {
         try {
             socketTCP = new ServerSocket(port);
             heartbeatSocketTCP = new ServerSocket(heartbeatPort);
         } catch (IOException e) {
             System.out.println("Could not start TCP server");
             e.printStackTrace();
             return;
         }
 
         System.out.println("TCP server started on port: " + port);
         System.out.println("TCP heartbeat server started on port: " + heartbeatPort);
 
         new Thread(this::acceptHeartbeat, "heartbeat-acceptor").start();
 
         while (true) {
             try {
                 Socket clientSocket = socketTCP.accept();
 
                 ObjectOutputStream clientSend = new ObjectOutputStream(clientSocket.getOutputStream());
                 clientSend.flush();
                 ObjectInputStream clientReceive = new ObjectInputStream(clientSocket.getInputStream());
 
                 NetworkEvent event = (NetworkEvent) clientReceive.readObject();
 
                 if (!(event.getEventType() == EventType.ADD_PLAYER)) {
                     clientSocket.close();
                     System.out.println("Invalid parameters. Connection terminated.");
                     continue;
                 }
 
                 AddPlayer eventAddPlayer = (AddPlayer) event;
 
                 if (eventAddPlayer.getProposedNPlayer() < NetworkConfig.MIN_PLAYERS
                         || eventAddPlayer.getProposedNPlayer() > NetworkConfig.MAX_PLAYERS) {
                     eventAddPlayer.setErrorType(ErrorType.WRONG_PLAYER_NUMBER);
                     eventAddPlayer.setIsError(true);
                     clientSend.writeObject(eventAddPlayer);
                     clientSocket.close();
                     System.out.println("Invalid parameters. Connection terminated.");
                     continue;
                 }
 
                 synchronized (controller) {
                     String username = eventAddPlayer.getUsername();
                     //reconnect players after a server crash
                     if (playerList.isEmpty() &&(controller.getModel()==null || controller.getModel().getCurrentState().getGameStage().equals(GameStages.WAITING))) {
                         Game model = new Game(eventAddPlayer.getProposedNPlayer());
                         controller.setModel(model);
                         playerList.setLimit(eventAddPlayer.getProposedNPlayer());
                         System.out.println("Game Created With: "+eventAddPlayer.getProposedNPlayer()+" Players");
                     }
                     if(controller.getModel().getCurrentState().getGameStage()!= GameStages.WAITING ) {
                         if (controller.getModel().getPlayers().stream().noneMatch(p -> p.getUserName().equals(username))|| controller.getModel().getCurrentState().getGameStage()==GameStages.ENDED) {
                             eventAddPlayer.setErrorType(ErrorType.GAME_ALREADY_STARTED);
                             eventAddPlayer.setIsError(true);
                             clientSend.writeObject(eventAddPlayer);
                             clientSocket.close();
                             System.out.println("Invalid parameters. Connection terminated.");
                             continue;
                         }
                     }
                     else {
                         if (controller.addPlayer(username)) {
                             playerList.put(username, true);
                             System.out.println("Accepted player: " + username);
                             clientSend.writeObject(eventAddPlayer);
                             ClientHandler handler = createAndRegisterHandler(
                                     username, clientSocket, clientSend, clientReceive);
                             new Thread(handler).start();
                             continue;
                         } else {
                             eventAddPlayer.setErrorType(ErrorType.INVALID_USERNAME);
                             eventAddPlayer.setIsError(true);
                             clientSend.writeObject(eventAddPlayer);
                             clientSocket.close();
                             System.out.println("Invalid parameters. Connection terminated.");
                             continue;
                         }
                     }
 
                     if (!playerList.containsKey(username)) {
                         playerList.put(username, true);
                         System.out.println("(After crash)Reconnected player: " + username);
                         clientSend.writeObject(eventAddPlayer);
                         ClientHandler handler = createAndRegisterHandler(
                                 username, clientSocket, clientSend, clientReceive);
                         new Thread(handler).start();
                     } else if (!playerList.get(username)) {
                         // reconnect a previously disconnected player
                         playerList.put(username, true);
                         System.out.println("Reconnected player: " + username);
                         clientSend.writeObject(eventAddPlayer);
                         ClientHandler handler = createAndRegisterHandler(
                                 username, clientSocket, clientSend, clientReceive);
                         Game game = controller.getModel();
                         handler.notifyMiniModel(new MiniModel(
                                 game.getSlotMap(), game.getOrderLogicCard(),
                                 game.getCurrentState(), game.getPlayers(),
                                 game.getAvailableTotems(),
                                 game.getUpperListTribeCards(), game.getLowerListTribeCards(),
                                 game.getUpperListBuilding(), game.getLowerListBuilding(),
                                 disconnectedUsernames(game)));
                         new Thread(handler).start();
                         actionQueue.add(new ReconnectPlayer(username));
                     } else {
                         eventAddPlayer.setErrorType(ErrorType.USER_ALREADY_CONNECTED);
                         eventAddPlayer.setIsError(true);
                         clientSend.writeObject(eventAddPlayer);
                         clientSocket.close();
                         System.out.println("Invalid parameters. Connection terminated.");
                     }
                 }
             } catch (IOException | ClassNotFoundException e) {
                 e.printStackTrace();
             }
         }
     }
 
     /**
      * Accepts heartbeat connections and associates them with the correct client handler.
      *
      * <p>Each client immediately sends its username on the heartbeat channel.
      * The method uses that username to retrieve the pending {@link ClientHandler}
      * and starts a dedicated {@link HeartbeatHandler}. If no pending handler is found,
      * the heartbeat socket is closed.
      */
     private void acceptHeartbeat() {
         while (true) {
             try {
                 Socket hbSocket = heartbeatSocketTCP.accept();
                 ObjectInputStream hbIn =
                         new ObjectInputStream(hbSocket.getInputStream());
 
                 String username = (String) hbIn.readObject();
 
                 ClientHandler handler = pendingHeartbeat.remove(username);
 
                 if (handler != null) {
                     HeartbeatHandler hb =
                             new HeartbeatHandler(username, hbSocket, handler);
 
                     new Thread(hb, "heartbeat-" + username).start();
                     System.out.println("Heartbeat connected for: " + username);
                 } else {
                     System.out.println(
                             "No pending handler for: " + username + ", closing heartbeat."
                     );
                     hbSocket.close();
                 }
 
             } catch (IOException | ClassNotFoundException e) {
                 e.printStackTrace();
             }
         }
     }
 
     /**
      * Notifies connected TCP clients of a new network event.
      *
      * <p>If the event does not represent an error, it is sent to all connected clients.
      * If it represents an error, it is sent only to the client that requested the action.
      *
      * @param event the network event to send to the clients.
      */
     public void notifyAll(NetworkEvent event) {
         clientHandlers.forEach(h -> {
             if (!event.isError() || event.getUsername().equals(h.getUsername())) {
                 h.notifyEvent(event);
             }
         });
     }
 
     /**
      * Notifies all connected TCP clients of a new game model.
      *
      * @param model the updated mini model to send to the clients.
      */
     public void notifyAll(MiniModel model) {
         clientHandlers.forEach(h -> h.notifyMiniModel(model));
     }
 
     /**
      * Creates a {@link ClientHandler}, registers it in {@code pendingHeartbeat}
      * and {@code clientHandlers}, but does NOT start its thread — callers start
      * the thread after any extra setup (e.g. sending a model snapshot).
      */
     private ClientHandler createAndRegisterHandler(String username,
                                                    Socket socket,
                                                    ObjectOutputStream out,
                                                    ObjectInputStream in) {
         ClientHandler handler = new ClientHandler(
                 username, socket, out, in, clientHandlers, playerList, actionQueue);
         pendingHeartbeat.put(username, handler);
         clientHandlers.add(handler);
         return handler;
     }
 
     /** Returns usernames of players currently marked as disconnected in the given game. */
     private static ArrayList<String> disconnectedUsernames(Game game) {
         return game.getDisconnectedPlayers().entrySet().stream()
                 .filter(Map.Entry::getValue)
                 .map(e -> e.getKey().getUserName())
                 .collect(Collectors.toCollection(ArrayList::new));
     }
 }