Coverage Summary for Class: TCPClient (it.polimi.ingsw.gc14.Network.TCP.Client)

Class Class, % Method, % Branch, % Line, %
TCPClient 0% (0/1) 0% (0/15) 0% (0/24) 0% (0/83)


 package it.polimi.ingsw.gc14.Network.TCP.Client;
 
 import it.polimi.ingsw.gc14.Controller.ClientController;
 import it.polimi.ingsw.gc14.ErrorType;
 import it.polimi.ingsw.gc14.Model.MiniModel;
 import it.polimi.ingsw.gc14.Network.IClient;
 import it.polimi.ingsw.gc14.Network.NetworkEvent;
 import it.polimi.ingsw.gc14.Network.NetworkConfig;
 import it.polimi.ingsw.gc14.Network.NetworkEvents.*;
 
 import java.io.*;
 import java.net.*;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
 /**
  * Client TCP. Sends and receives messages with the TCP server.
  */
 public class TCPClient implements IClient {
     /** Byte value sent by the client to signal it is still alive. */
     private static final int PING = 1;
     /** Byte value expected from the server in response to a {@link #PING}. */
     private static final int PONG = 2;
 
     /** Socket TCP */
     private Socket communicationSocket;
 
     /** Input stream used receive objects from the server */
     private ObjectInputStream socketReceive;
 
     /** Output stream used to send objects to the server */
     private ObjectOutputStream socketSend;
 
     /** Client game's controller */
     private final ClientController controller;
 
     /** IP address of the server to connect to */
     private final String hostname;
     /** {@code true} while the connection is active; set to {@code false} on disconnect. */
     private boolean running;
     /** TCP port */
     private final int mainPort;
     /** TCP port dedicated to heartbeat communication. */
     private final int heartbeatPort;
     /** Socket used exclusively for heartbeat ping/pong exchange. */
     private Socket heartbeatSocket;
     /** Output stream of {@link #heartbeatSocket}, used to write {@link #PING} bytes. */
     private OutputStream heartbeatOut;
     /** Input stream of {@link #heartbeatSocket}, used to read {@link #PONG} bytes. */
     private InputStream heartbeatIn;
 
 
     /**
      * Constructs a TCP client and initializes its connection parameters.
      *
      * @param controller    the client controller.
      * @param hostname      the IP address or hostname of the server.
      * @param mainPort      the main TCP port of the server.
      * @param heartbeatPort the TCP port used for heartbeat communication.
      */
     public TCPClient(ClientController controller, String hostname, int mainPort,int heartbeatPort ) {
         this.controller = controller;
         this.hostname = hostname;
         this.mainPort = mainPort;
         this.heartbeatPort = heartbeatPort;
     }
 
 
     /**
      * Starts the TCP connection with the server.
      * Sends an {@link AddPlayer} event, if the server responds with {@code -1}, the connection is refused and the method returns {@code false}.
      * Otherwise, a listener thread is started.
      * @param user             The username of the player
      * @param proposedNPlayers The desired number of players for the game
      * @return null if the connection is successful, GENERIC_ERROR otherwise.
      */
     public ErrorType connect(String user, int proposedNPlayers) {
         try {
             communicationSocket = new Socket(hostname, mainPort);
             socketSend    = new ObjectOutputStream(communicationSocket.getOutputStream());
             socketReceive = new ObjectInputStream(communicationSocket.getInputStream());
             NetworkEvent event= new AddPlayer(user, proposedNPlayers);
             socketSend.writeObject(event);
             try {
                 if( socketReceive.readObject() instanceof AddPlayer x)
                 {
                     if(x.isError())
                         return x.getErrorType();
                 }
             } catch (ClassNotFoundException e) {
                 return ErrorType.GENERIC_ERROR;
             }
 
             running = true;
 
             new Thread(this::receiveMessage, "tcp-reader").start();
 
             // heartbeat socket
             this.heartbeatSocket = new Socket(hostname, heartbeatPort);
             this.heartbeatOut = heartbeatSocket.getOutputStream();
             this.heartbeatIn  = heartbeatSocket.getInputStream();
 
             // send username immediately so server can associate the two sockets
             new ObjectOutputStream(heartbeatSocket.getOutputStream()).writeObject(user);
             heartbeatOut.flush();
             new Thread(this::heartbeatLoop, "heartbeat").start();
 
             return null;
 
         } catch (IOException e) {
             e.printStackTrace();
             return ErrorType.SERVER_UNREACHABLE;
         }
     }
     /**
      * Sends a {@link #PING} byte to the server every 3 seconds and waits for a {@link #PONG} reply.
      * If no reply arrives within {@value NetworkConfig#SILENCE_THRESHOLD_MS} ms, the server is
      * considered unreachable and {@link #disconnect()} is called.
      */
     private void heartbeatLoop() {
         ScheduledExecutorService sender = Executors.newSingleThreadScheduledExecutor();
         sender.scheduleAtFixedRate(() -> {
             try {
                 synchronized (heartbeatOut)
                 {
                     heartbeatOut.write(PING);
                     heartbeatOut.flush();
                 }
 
             } catch (IOException e) {
                 sender.shutdownNow();
                 if(running)
                 {
                     disconnect();
                     controller.getView().showError(ErrorType.SERVER_CRASHED,ErrorType.SERVER_CRASHED.toString());
                 }
 
             }
         }, 0, 3, TimeUnit.SECONDS);
 
         try {
             heartbeatSocket.setSoTimeout((int) NetworkConfig.SILENCE_THRESHOLD_MS);
             while (running) {
                 int b = heartbeatIn.read();
                 if (b != PONG) {
                     disconnect();
                     controller.getView().showError(ErrorType.SERVER_CRASHED, ErrorType.SERVER_CRASHED.toString());
                     break;
                 }
                 // pong received: server is alive
             }
         } catch (SocketTimeoutException e) {
             System.out.println("Server heartbeat timeout");
             disconnect();
             controller.getView().showError(ErrorType.SERVER_CRASHED, ErrorType.SERVER_CRASHED.toString());
         } catch (IOException e) {
             if (running) {
                 disconnect();
                 controller.getView().showError(ErrorType.SERVER_CRASHED, ErrorType.SERVER_CRASHED.toString());
             }
         } finally {
             sender.shutdownNow();
         }
     }
 
     /**
      * Disconnects the player and cleans model and controller
      */
     private synchronized void disconnect() {
         if (!running) return;
         running = false;
         try { communicationSocket.close(); } catch (IOException ignored) {}
         try { heartbeatSocket.close();     } catch (IOException ignored) {}
         controller.setClient(null);
         controller.setModel(null);
     }
 
 
     /**
      * Listens continuously for incoming objects from the server.
      *   - If the received object is a {@link NetworkEvent} flagged as an error, it is printed.
      *   - If the received object is a valid {@link NetworkEvent}, it is applied to the game controller.
      *   - If the received object is a {@link MiniModel}, the controller's model is set.
      */
     private void receiveMessage() {
         while (true) {
             try {
                 Object read;
                 try {
                     read = socketReceive.readObject();
                 } catch (IOException e) {
                     if (running) disconnect();
                     break;
                 }
 
                 if (read instanceof NetworkEvent event) {
                     if (event.isError()) {
                         if(event.getErrorType() == ErrorType.WRONG_ACTION) {
                             controller.getView().showError(ErrorType.WRONG_ACTION,event.toString());
                         }
                         else
                             controller.getView().showError(event.getErrorType(),event.getErrorType().toString());
                     } else {
                         event.apply(controller.getMiniModel());
                         controller.getView().render();
                     }
 
                 } else if (read instanceof MiniModel model) {
                     controller.setModel(model);
                     controller.getView().render();
                 }
 
             } catch (ClassNotFoundException e) {
                 e.printStackTrace();
                 break;
             }
         }
     }
 
     /**
      * Requests to draw a tribe card from the upper list.
      * Creates a NetworkEvent and sends it through the network client.
      * @param playerUsername the name of the player performing the action
      * @param pos the index of the card to draw
      */
     public void drawUpperTribeCard(String playerUsername, int pos) {
         doEvent(new DrawUpperTribeCard(playerUsername,pos));
 
     }
 
 
     /**
      * Requests to draw a tribe card from the lower list.
      * Creates a NetworkEvent and sends it through the network client.
      * @param playerUsername the name of the player performing the action
      * @param pos the index of the card to draw
      */
     public void drawLowerTribeCard(String playerUsername,int pos) {
         doEvent(new DrawLowerTribeCard(playerUsername,pos));
     }
 
 
     /**
      * Requests to draw a building card from the upper list.
      * Creates a NetworkEvent and sends it through the network client.
      * @param playerUsername the name of the player performing the action
      * @param pos the index of the card to draw
      */
     public void drawUpperBuildingCard(String playerUsername,int pos) {
         doEvent(new DrawUpperBuildingCard(playerUsername,pos));
 
     }
 
 
     /**
      * Requests to draw a building card from the lower list.
      * Creates a NetworkEvent and sends it through the network client.
      * @param playerUsername the name of the player performing the action
      * @param pos the index of the card to draw
      */
     public void drawLowerBuildingCard(String playerUsername,int pos) {
         doEvent(new DrawLowerBuildingCard(playerUsername,pos));
     }
 
 
 
     /**
      * Requests to skip drawing turn.
      * This action is available only when  the player cannot draw any tribe card, but still can buy some buildings.
      * @param playerUsername the name of the player performing the action
      */
     public void skipTurn(String playerUsername) {
         doEvent(new SkipTurn(playerUsername));
     }
 
 
     /**
      * Used to perform the slot choice action for the specified player at the specified position.
      * @param playerUsername the name of the player performing the action
      * @param pos the index of the selected slot
      */
     public void slotChoice(String playerUsername,int pos) {
         doEvent(new SlotChoice(playerUsername,pos));
     }
     /**
      * Sends a {@link NetworkEvent} to the server.
      * @param event The NetworkEvent to send.
      */
     private void doEvent(NetworkEvent event) {
         try {
             synchronized (socketSend) {
                 socketSend.writeObject(event);
             }
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
 
     /**
      * Sends a totem choice event to the server for the specified player.
      *
      * @param playerUsername the username of the player choosing the totem.
      * @param totems the name of the chosen totem.
      */
     public void totemChoice(String playerUsername,String totems) {
         doEvent(new TotemChoice(playerUsername,totems));
     }
 
     /**
      * Notifies the server of a voluntary disconnection by sending a sentinel value
      * on the heartbeat channel, then closes the connection.
      */
     public void notifyDisconnection()
     {
         synchronized (heartbeatOut)
         {
             try {
                 heartbeatOut.write(-1);
             } catch (IOException e) {
                 disconnect();
             }
         }
         disconnect();
     }
 }