From a2dd6f3d9b4e419994a328cadba293c28364acb8 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Thu, 7 May 2026 15:52:37 +0200 Subject: [PATCH 01/17] Added: SkipNotConnected model method --- .../polimi/ingsw/gc14/Controller/GameController.java | 6 +++++- src/main/java/it/polimi/ingsw/gc14/Model/Game.java | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java index 3b1de8a..8b2e1be 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java +++ b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java @@ -30,7 +30,11 @@ public class GameController { */ public GameController() { } - + //TODO + public boolean SkipNotConnectedPlayer(Player player) + { + return model.SkipNotConnectedPlayer(player); + } /** * Returns the game model managed by this controller. * diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java index d939baf..ae75315 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java @@ -40,7 +40,17 @@ public class Game implements Serializable { public List getPlayers() { return playersList; } + //TODO + public boolean SkipNotConnectedPlayer(Player player) + { + if(currentState.getCurrentPlayer()!=player) + { + return false; + } + nextPlayerSetup(); + return true; + } /** * Returns the current number of players participating in the game. * @return the current number of players. From 68be878ef1331809921d2685e3927feb1ca4c027 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Thu, 7 May 2026 18:09:34 +0200 Subject: [PATCH 02/17] Add: PING PONG TCP --- .../it/polimi/ingsw/gc14/{LimitedList.java => LimitedMap.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/java/it/polimi/ingsw/gc14/{LimitedList.java => LimitedMap.java} (100%) diff --git a/src/main/java/it/polimi/ingsw/gc14/LimitedList.java b/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java similarity index 100% rename from src/main/java/it/polimi/ingsw/gc14/LimitedList.java rename to src/main/java/it/polimi/ingsw/gc14/LimitedMap.java From 7757b38155898cd6d0875d040309a410d9af9f05 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Thu, 7 May 2026 18:09:39 +0200 Subject: [PATCH 03/17] Add: PING PONG TCP --- .../polimi/ingsw/gc14/ClientLauncherGUI.java | 2 +- .../polimi/ingsw/gc14/ClientLauncherTUI.java | 2 +- .../java/it/polimi/ingsw/gc14/LimitedMap.java | 94 +++++--- .../ingsw/gc14/Network/ClientPlayer.java | 15 ++ .../gc14/Network/RMI/Server/RMIServer.java | 10 +- .../gc14/Network/TCP/Client/TCPClient.java | 87 ++++++- .../Network/TCP/Server/ClientHandler.java | 20 +- .../Network/TCP/Server/HeartbeatHandler.java | 75 ++++++ .../gc14/Network/TCP/Server/TCPServer.java | 227 ++++++++++-------- .../it/polimi/ingsw/gc14/ServerLauncher.java | 16 +- 10 files changed, 388 insertions(+), 160 deletions(-) create mode 100644 src/main/java/it/polimi/ingsw/gc14/Network/ClientPlayer.java create mode 100644 src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/HeartbeatHandler.java diff --git a/src/main/java/it/polimi/ingsw/gc14/ClientLauncherGUI.java b/src/main/java/it/polimi/ingsw/gc14/ClientLauncherGUI.java index dfec71d..b42f6ec 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ClientLauncherGUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/ClientLauncherGUI.java @@ -42,7 +42,7 @@ public class ClientLauncherGUI extends Application { Platform.runLater(() -> loginView.setErrore("Connessione RMI fallita.")); } } else { - TCPClient client = new TCPClient(controller, ip, 8080); + TCPClient client = new TCPClient(controller, ip, 8080,8081); if (client.connect(nome, numPlayer)) { controller.setClient(client); } else { diff --git a/src/main/java/it/polimi/ingsw/gc14/ClientLauncherTUI.java b/src/main/java/it/polimi/ingsw/gc14/ClientLauncherTUI.java index aa7bb88..45a1da6 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ClientLauncherTUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/ClientLauncherTUI.java @@ -88,7 +88,7 @@ public class ClientLauncherTUI { // TCP } else if (networkType == 1) { // Connect - TCPClient client = new TCPClient(controller, IP, 8080); + TCPClient client = new TCPClient(controller, IP, 8080,8081); if (client.connect(username, proposedNumPlayers)) { System.out.println("Succesfully connected to TCP server\n\n"); } else { diff --git a/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java b/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java index 5ac39ff..e209803 100644 --- a/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java +++ b/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java @@ -1,75 +1,111 @@ package it.polimi.ingsw.gc14; -import java.util.ArrayList; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; /** - * An {@link ArrayList} with a configurable size limit and an associated action. + * A {@link LinkedHashMap} with a configurable size limit and an associated action. * When the number of elements reaches or exceeds the limit, the specified action is automatically triggered. + * This implementation is thread-safe. * - * @param the type of elements held in this list. + * @param the type of keys maintained by this map. + * @param the type of mapped values. */ -public class LimitedList extends ArrayList { +public class LimitedMap implements Map { + + private final LinkedHashMap map = new LinkedHashMap<>(); /** - * The maximum number of elements allowed in the list before the action is triggered. + * The maximum number of elements allowed in the map before the action is triggered. */ - private int limit; + private volatile int limit; /** - * The action to execute when the list size reaches or exceeds the limit. + * The action to execute when the map size reaches or exceeds the limit. */ - private Runnable action; + private volatile Runnable action; /** - * Creates a new {@code LimitedList} with the specified limit and action. + * Creates a new {@code LimitedMap} with the specified limit and action. * * @param limit the maximum number of elements before the action is triggered. * @param action the action to execute when the limit is reached. */ - public LimitedList(int limit, Runnable action) { + public LimitedMap(int limit, Runnable action) { this.limit = limit; this.action = action; } /** - * Adds the specified element to the list. - * If the list size reaches or exceeds the limit after the insertion, the configured action is triggered. + * Associates the specified value with the specified key in this map. + * If the map size reaches or exceeds the limit after the insertion, the configured action is triggered. * - * @param element the element to add. - * @return {@code true} if the element was successfully added. + * @param key the key with which the specified value is to be associated. + * @param value the value to be associated with the specified key. + * @return the previous value associated with the key, or {@code null} if there was no mapping. */ @Override - public boolean add(T element) { - boolean result = super.add(element); - if (size() >= limit) { + public synchronized V put(K key, V value) { + V result = map.put(key, value); + if (map.size() >= limit) { action.run(); } return result; } + @Override + public synchronized V remove(Object key) { return map.remove(key); } + + @Override + public synchronized V get(Object key) { return map.get(key); } + + @Override + public synchronized boolean containsKey(Object key) { return map.containsKey(key); } + + @Override + public synchronized boolean containsValue(Object value) { return map.containsValue(value); } + + @Override + public synchronized int size() { return map.size(); } + + @Override + public synchronized boolean isEmpty() { return map.isEmpty(); } + + @Override + public synchronized void putAll(Map m) { m.forEach(this::put); } + + @Override + public synchronized void clear() { map.clear(); } + + @Override + public synchronized Set keySet() { return map.keySet(); } + + @Override + public synchronized Collection values() { return map.values(); } + + @Override + public synchronized Set> entrySet() { return map.entrySet(); } + /** - * Sets a new size limit for this list. + * Sets a new size limit for this map. * * @param num the new limit. */ - public void setLimit(int num) { - this.limit = num; - } + public void setLimit(int num) { this.limit = num; } /** - * Returns the current size limit of this list. + * Returns the current size limit of this map. * * @return the current limit. */ - public int getLimit() { - return limit; - } + public int getLimit() { return limit; } /** - * Sets a new action to execute when the list size reaches or exceeds the limit. + * Sets a new action to execute when the map size reaches or exceeds the limit. * * @param action the new action to set. */ - public void setAction(Runnable action) { - this.action = action; - } + public void setAction(Runnable action) { this.action = action; } } \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/ClientPlayer.java b/src/main/java/it/polimi/ingsw/gc14/Network/ClientPlayer.java new file mode 100644 index 0000000..29d816d --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/Network/ClientPlayer.java @@ -0,0 +1,15 @@ +package it.polimi.ingsw.gc14.Network; +//TODO +public class ClientPlayer { + private String username; + + public String getUsername() { + return username; + } + public boolean connected; + + public ClientPlayer(String username,boolean connected) { + this.username = username; + this.connected = connected; + } +} diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java index 66d03d2..69bdb6c 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java @@ -1,7 +1,7 @@ package it.polimi.ingsw.gc14.Network.RMI.Server; import it.polimi.ingsw.gc14.Controller.GameController; -import it.polimi.ingsw.gc14.LimitedList; +import it.polimi.ingsw.gc14.LimitedMap; import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Network.NetworkEvent; import it.polimi.ingsw.gc14.Network.NetworkEvents.*; @@ -44,9 +44,9 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { /** * List containing the usernames of joined players. - * {@link LimitedList}'s limit defines at which size the list calls its action. The limit can be set using {@link LimitedList#setLimit(int)}. + * {@link LimitedMap}'s limit defines at which size the list calls its action. The limit can be set using {@link LimitedMap#setLimit(int)}. */ - private LimitedList playerList; + private LimitedMap playerList; /** @@ -59,7 +59,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { * @param host the host address of the RMI server. * @throws RemoteException if an RMI error occurs. */ - public RMIServer(GameController controller, int nPort, BlockingQueue actionQueue,LimitedList playerList,String host) throws RemoteException { + public RMIServer(GameController controller, int nPort, BlockingQueue actionQueue, LimitedMap playerList, String host) throws RemoteException { this.controller = controller; this.nPort = nPort; this.actionQueue = actionQueue; @@ -89,7 +89,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { } if (controller.addPlayer(username)) { clients.put(username, callback); - playerList.add(username); + playerList.put(username,true); System.out.println("Accepted player: " + username); return true; } diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java index f0453cc..543c7ac 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java @@ -8,11 +8,16 @@ 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 { + private static final int PING = 1; + private static final int PONG = 2; /** Socket TCP */ Socket communicationSocket; @@ -29,8 +34,14 @@ public class TCPClient implements IClient { /** IP address of the server to connect to */ String hostname; + private boolean running; /** TCP port */ - int port; + int mainPort; + + int heartbeatPort; + private Socket heartbeatSocket; + private OutputStream heartbeatOut; + private InputStream heartbeatIn; /** @@ -39,10 +50,11 @@ public class TCPClient implements IClient { * @param hostname The IP address of the server * @param port The TCP port of the server */ - public TCPClient(ClientController controller, String hostname, int port) { + public TCPClient(ClientController controller, String hostname, int mainPort,int heartbeatPort ) { this.controller = controller; this.hostname = hostname; - this.port = port; + this.mainPort = mainPort; + this.heartbeatPort = heartbeatPort; } @@ -56,27 +68,78 @@ public class TCPClient implements IClient { */ public boolean connect(String user, int proposedNPlayers) { try { - - communicationSocket = new Socket(hostname, port); - socketSend = new ObjectOutputStream(communicationSocket.getOutputStream()); + // Socket principale + communicationSocket = new Socket(hostname, mainPort); + socketSend = new ObjectOutputStream(communicationSocket.getOutputStream()); socketReceive = new ObjectInputStream(communicationSocket.getInputStream()); - - doEvent(new AddPlayer(user, proposedNPlayers)); + if (communicationSocket.getInputStream().read() == -1) { System.out.println("Could not connect to server"); return false; - } else { - Thread listener = new Thread(() -> receiveMessage()); - listener.start(); - return true; } + + // Socket heartbeat + this.heartbeatSocket = new Socket(hostname, heartbeatPort); + this.heartbeatOut =heartbeatSocket.getOutputStream() ; + this.heartbeatIn = heartbeatSocket.getInputStream(); + + // manda subito username per associare i due socket lato server + new ObjectOutputStream(heartbeatSocket.getOutputStream()).writeObject(user); + heartbeatOut.flush(); + + running = true; + new Thread(this::receiveMessage, "tcp-reader").start(); + new Thread(this::heartbeatLoop, "heartbeat").start(); + return true; + } catch (IOException e) { e.printStackTrace(); return false; } } + private void heartbeatLoop() { + // thread che manda ping ogni 3s + ScheduledExecutorService sender = Executors.newSingleThreadScheduledExecutor(); + sender.scheduleAtFixedRate(() -> { + try { + heartbeatOut.write(PING); + heartbeatOut.flush(); + } catch (IOException e) { + sender.shutdownNow(); + disconnect(); + } + }, 0, 3, TimeUnit.SECONDS); + + // thread corrente: aspetta pong con timeout + try { + heartbeatSocket.setSoTimeout(5_000); + while (running) { + int b = heartbeatIn.read(); + if (b == -1 || b != PONG) { + disconnect(); + break; + } + // pong ricevuto → server vivo + } + } catch (SocketTimeoutException e) { + System.out.println("Server heartbeat timeout"); + disconnect(); + } catch (IOException e) { + disconnect(); + } finally { + sender.shutdownNow(); + } + } + private void disconnect() { + if (!running) return; + running = false; + try { communicationSocket.close(); } catch (IOException ignored) {} + try { heartbeatSocket.close(); } catch (IOException ignored) {} + controller.view.showError("Connessione al server persa"); + } + /** * Listens continuously for incoming objects from the server. diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java index 83b482c..2e9e8d2 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java @@ -1,5 +1,6 @@ package it.polimi.ingsw.gc14.Network.TCP.Server; +import it.polimi.ingsw.gc14.LimitedMap; import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Network.NetworkEvent; @@ -20,6 +21,8 @@ public class ClientHandler implements Runnable { */ private final String username; + private boolean running ; + /** * Returns the username associated with this client. * @@ -43,6 +46,9 @@ public class ClientHandler implements Runnable { */ List clientHandlers; + //TODO + LimitedMap limitedMap; + /** Queue containing the events to be applied to the game model */ BlockingQueue actionQueue; @@ -57,7 +63,7 @@ public class ClientHandler implements Runnable { * @param clientHandlers the shared list of all active client handlers. * @param actionQueue the queue containing incoming events. */ - public ClientHandler(String username, Socket clientSocket, ObjectOutputStream out, ObjectInputStream in, List clientHandlers, BlockingQueue actionQueue) { + public ClientHandler(String username, Socket clientSocket, ObjectOutputStream out, ObjectInputStream in, List clientHandlers, LimitedMap playersMap, BlockingQueue actionQueue) { this.username=username; this.clientSocket = clientSocket; this.in = in; @@ -74,8 +80,8 @@ public class ClientHandler implements Runnable { @Override public void run() { try { - - while (true) { + running = true; + while (running) { NetworkEvent event = (NetworkEvent) in.readObject(); if (!actionQueue.add(event)) { System.out.println("Error inserting action into queue"); @@ -90,6 +96,7 @@ public class ClientHandler implements Runnable { } + /** * Sends a {@link NetworkEvent} to the client. * @param event The network event to send to the client. @@ -115,4 +122,11 @@ public class ClientHandler implements Runnable { e.printStackTrace(); } } + + public void disconnect() { + if (!running) return; + running = false; + clientHandlers.remove(this); + try { clientSocket.close(); } catch (IOException ignored) {} + } } \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/HeartbeatHandler.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/HeartbeatHandler.java new file mode 100644 index 0000000..8353369 --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/HeartbeatHandler.java @@ -0,0 +1,75 @@ +package it.polimi.ingsw.gc14.Network.TCP.Server; + +import java.io.*; +import java.net.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class HeartbeatHandler implements Runnable { + + private static final long SILENCE_THRESHOLD_MS = 5_000; + private static final long KEEPALIVE_INTERVAL_MS = 3_000; + + private final String username; + private final Socket socket; + private final InputStream in; + private final OutputStream out; + + private static final int PING = 1; + private static final int PONG = 2; + + // riferimento al ClientHandler principale per disconnetterlo insieme + private final ClientHandler mainHandler; + + private volatile long lastReceivedTime = System.currentTimeMillis(); + private volatile boolean running = true; + + private final ScheduledExecutorService watchdog = + Executors.newSingleThreadScheduledExecutor(); + + public HeartbeatHandler(String username, Socket socket, ClientHandler mainHandler) + throws IOException { + this.username = username; + this.socket = socket; + this.mainHandler = mainHandler; + this.in = socket.getInputStream(); + this.out = socket.getOutputStream(); + } + + @Override + public void run() { + startWatchdog(); + try { + while (running) { + int b = in.read(); // blocca finché non arriva un byte + if (b == -1) { disconnect(); break; } // stream chiusa + if (b == PING) { + lastReceivedTime = System.currentTimeMillis(); + out.write(PONG); + out.flush(); + } + } + } catch (IOException e) { + disconnect(); + } + } + + private void startWatchdog() { + watchdog.scheduleAtFixedRate(() -> { + if (System.currentTimeMillis() - lastReceivedTime > SILENCE_THRESHOLD_MS) { + System.out.println("Heartbeat timeout: " + username); + disconnect(); + } + }, 1, 1, TimeUnit.SECONDS); + } + + + private void disconnect() { + if (!running) return; + running = false; + watchdog.shutdownNow(); + mainHandler.disconnect(); // disconnette anche il socket principale + try { socket.close(); } catch (IOException ignored) {} + } +} diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java index 5380e36..dffc508 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java @@ -1,8 +1,9 @@ package it.polimi.ingsw.gc14.Network.TCP.Server; import it.polimi.ingsw.gc14.Controller.GameController; -import it.polimi.ingsw.gc14.LimitedList; +import it.polimi.ingsw.gc14.LimitedMap; import it.polimi.ingsw.gc14.Model.Game; +import it.polimi.ingsw.gc14.Network.ClientPlayer; import it.polimi.ingsw.gc14.Network.EventType; import it.polimi.ingsw.gc14.Network.NetworkEvent; import it.polimi.ingsw.gc14.Network.NetworkEvents.AddPlayer; @@ -11,151 +12,175 @@ import java.io.*; import java.net.*; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; /** * Server TCP. Accepts connections and manages all client handlers. */ public class TCPServer { - /** TCP port */ int port; + int heartbeatPort; // ← nuova porta - /** Number of currently connected clients */ - int ConnectedPlayers; - - /** Socket TCP */ + int connectedPlayers; ServerSocket socketTCP; + ServerSocket heartbeatSocketTCP; // ← nuovo ServerSocket - /** Server game's controller */ GameController controller; - - /** Queue containing the events to be applied to the game model */ BlockingQueue actionQueue; + LimitedMap playerList; + List clientHandlers; - /** - * List containing the usernames of joined players. - * {@link LimitedList}'s limit defines at which size the list calls its action. The limit can be set using {@link LimitedList#setLimit(int)}. - */ - private LimitedList playerList; + // Mappa temporanea: username → ClientHandler + // Serve per associare il socket heartbeat al giusto ClientHandler + private final Map pendingHeartbeat = new ConcurrentHashMap<>(); - /** List containing all client's handlers */ - private List clientHandlers; - - - /** - * Class constructor that initializes the attributes. - * @param controller The game controller - * @param port The TCP port - * @param actionQueue The action queue - * @param playerList The player's usernames list - */ - public TCPServer(GameController controller, int port, BlockingQueue actionQueue, LimitedList playerList){ + public TCPServer(GameController controller, int port, int heartbeatPort, + BlockingQueue actionQueue, LimitedMap playerList) { this.port = port; - this.ConnectedPlayers = 0; - this.socketTCP = null; + this.heartbeatPort = heartbeatPort; + this.connectedPlayers = 0; this.controller = controller; this.actionQueue = actionQueue; this.playerList = playerList; this.clientHandlers = new ArrayList<>(); } - - /** - * Starts the TCP server. - * If the first event is not AddPlayer, the request is rejected. - * If the desired number of player is invalid, the request is rejected. - * If this is the first player to connect, a new game model is created and passed to the controller. Additionally, the playerList's limit is set. - * If the controller successfully adds the player, the username is added to {@link #playerList} and the handler is added to {@link #clientHandlers}. - * If any error occurs, the server sends -1 back to the client. Otherwise, it sends 1. - */ - public void start(){ - - try{ + public void start() { + try { socketTCP = new ServerSocket(port); - } - catch (IOException e){ - System.out.println("Could not start the server TCP on port: " + port); + heartbeatSocketTCP = new ServerSocket(heartbeatPort); + } catch (IOException e) { + System.out.println("Could not start TCP server"); e.printStackTrace(); return; } - System.out.println("Server TCP started on port: " + port); - Socket clientSocket; - while(true){ + System.out.println("TCP server started on port: " + port); + System.out.println("Heartbeat server started on port: " + heartbeatPort); - try{ - clientSocket = socketTCP.accept(); + // Thread separato per accettare le connessioni heartbeat + new Thread(this::acceptHeartbeat, "heartbeat-acceptor").start(); + + // Loop principale — invariato nella logica, cambia solo la creazione del ClientHandler + while (true) { + try { + Socket clientSocket = socketTCP.accept(); ObjectOutputStream clientSend = new ObjectOutputStream(clientSocket.getOutputStream()); ObjectInputStream clientReceive = new ObjectInputStream(clientSocket.getInputStream()); NetworkEvent event = (NetworkEvent) clientReceive.readObject(); - if(!(event.getEventType() == EventType.ADD_PLAYER)){ - clientSocket.getOutputStream().write((int)(-1)); + if (!(event.getEventType() == EventType.ADD_PLAYER)) { + clientSocket.getOutputStream().write(-1); clientSocket.close(); - System.out.println("Invalid parameters. Connection terminated.\n"); + System.out.println("Invalid parameters. Connection terminated."); + continue; } - else{ - AddPlayer eventAddPlayer = (AddPlayer) event; - if (eventAddPlayer.getProposedNPlayer() < 2 || eventAddPlayer.getProposedNPlayer() > 5) { - clientSocket.getOutputStream().write((int) (-1)); + + AddPlayer eventAddPlayer = (AddPlayer) event; + + if (eventAddPlayer.getProposedNPlayer() < 2 || eventAddPlayer.getProposedNPlayer() > 5) { + clientSocket.getOutputStream().write(-1); + clientSocket.close(); + System.out.println("Invalid parameters. Connection terminated."); + continue; + } + + synchronized (controller) { + if (playerList.isEmpty()) { + Game model = new Game(eventAddPlayer.getProposedNPlayer()); + controller.setModel(model); + playerList.setLimit(eventAddPlayer.getProposedNPlayer()); + } + + String username = eventAddPlayer.getUsername(); + + if (controller.addPlayer(username)) { + // nuovo giocatore + playerList.put(username, true); + clientSocket.getOutputStream().write(1); + System.out.println("Accepted player: " + username); + + ClientHandler handler = new ClientHandler( + username, clientSocket, clientSend, clientReceive, + clientHandlers,playerList, actionQueue + ); + clientHandlers.add(handler); + connectedPlayers++; + + // metti in attesa del socket heartbeat + pendingHeartbeat.put(username, handler); + + + } else if (playerList.containsKey(username) && !playerList.get(username)) { + // riconnessione + playerList.put(username, true); + clientSocket.getOutputStream().write(1); + System.out.println("Reconnected player: " + username); + + ClientHandler handler = new ClientHandler( + username, clientSocket, clientSend, clientReceive, + clientHandlers, playerList, actionQueue + ); + handler.notifyModel(controller.getModel()); + clientHandlers.add(handler); + connectedPlayers++; + + pendingHeartbeat.put(username, handler); + + + } else { + clientSocket.getOutputStream().write(-1); clientSocket.close(); - System.out.println("Invalid parameters. Connection terminated.\n"); - } - synchronized (controller) { - if (playerList.isEmpty()){ - Game model = new Game(eventAddPlayer.getProposedNPlayer()); - controller.setModel(model); - playerList.setLimit(eventAddPlayer.getProposedNPlayer()); - } - if (controller.addPlayer(eventAddPlayer.getUsername())) { - playerList.add(eventAddPlayer.getUsername()); - clientSocket.getOutputStream().write((int) (1)); - - System.out.println("Accepted player: " + eventAddPlayer.getUsername()); - ClientHandler clientHandler = new ClientHandler(eventAddPlayer.getUsername(),clientSocket, clientSend, clientReceive, clientHandlers, actionQueue); - clientHandlers.add(clientHandler); - ConnectedPlayers++; - - Thread t = new Thread(clientHandler); - t.start(); - } else { - clientSocket.getOutputStream().write((int) (-1)); - clientSocket.close(); - System.out.println("Player could not be added. Connection terminated.\n"); - } + System.out.println("Player could not be added. Connection terminated."); } } - } - catch(IOException e){ + + } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } - catch(ClassNotFoundException e){ - throw new RuntimeException(e); - } } } - /** - * Sends an action to all TCP clients. - * - * @param event the network event to send to all connected TCP clients. + * Accetta connessioni sul socket heartbeat e le associa al ClientHandler giusto. + * Il client manda subito il proprio username per identificarsi. */ - public void notifyAll(NetworkEvent event){ - clientHandlers.forEach((x) -> { - if(!event.getIsError()||(event.getIsError()&& event.getUsername().equals(x.getUsername()))) - x.notifyEvent(event); + private void acceptHeartbeat() { + while (true) { + try { + Socket hbSocket = heartbeatSocketTCP.accept(); + ObjectInputStream hbIn = new ObjectInputStream(hbSocket.getInputStream()); + + // il client manda subito il suo username + 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(); + } + } + } + + public void notifyAll(NetworkEvent event) { + clientHandlers.forEach(h -> { + if (!event.getIsError() || event.getUsername().equals(h.getUsername())) + h.notifyEvent(event); }); } - - /** - * Sends a game model to all TCP clients. - * - * @param model the game model to send to all connected TCP clients. - */ - public void notifyAll(Game model){ - clientHandlers.forEach((x) -> x.notifyModel(model)); + public void notifyAll(Game model) { + clientHandlers.forEach(h -> h.notifyModel(model)); } -} +} \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java index 3d5bdde..4db758c 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java @@ -3,8 +3,8 @@ package it.polimi.ingsw.gc14; import it.polimi.ingsw.gc14.Controller.GameController; import it.polimi.ingsw.gc14.Model.Game; +import it.polimi.ingsw.gc14.Network.ClientPlayer; import it.polimi.ingsw.gc14.Network.NetworkEvent; -import it.polimi.ingsw.gc14.Network.NetworkEvents.AddPlayer; import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer; import it.polimi.ingsw.gc14.View.TUI.TUI; @@ -21,10 +21,10 @@ import java.net.*; * The workflow is divided into two parts: game creation and game execution. * The process flow for game creation is as follows: * - The first client (TCP/RMI) requests to join the game by providing a username and the desired number of players - * - The TCP/RMI server checks {@link #playerList} and, if it is empty, sets the number of players according to the first user's request using {@link LimitedList#setLimit(int)} + * - The TCP/RMI server checks {@link #playerList} and, if it is empty, sets the number of players according to the first user's request using {@link LimitedMap#setLimit(int)} * - The TCP/RMI server creates the {@link Game} with the requested number of players and adds the player to {@link #playerList} * - Other players request to join the game (their requested number of players is ignored) - * - When the number of players in {@link #playerList} reaches the {@link LimitedList}'s limit, the list calls {@link #run()} + * - When the number of players in {@link #playerList} reaches the {@link LimitedMap}'s limit, the list calls {@link #run()} * - All players are notified of the {@link Game} * * The process flow for game execution is as follows: @@ -52,11 +52,11 @@ public class ServerLauncher { /** * List containing the usernames of joined players. - * {@link LimitedList}'s limit defines at which size the list calls its action - * Both the limit and the action can be set using {@link LimitedList#setLimit(int)} and {@link LimitedList#setAction(Runnable)} + * {@link LimitedMap}'s limit defines at which size the list calls its action + * Both the limit and the action can be set using {@link LimitedMap#setLimit(int)} and {@link LimitedMap#setAction(Runnable)} * The limit is set by the first player joining the game. The action consists in calling {@link #run()} */ - static LimitedList playerList; + static LimitedMap playerList; TUI view; @@ -105,7 +105,7 @@ public class ServerLauncher { * @throws RemoteException if this exception is issued by run method */ public static void main(String[] args) throws InterruptedException, RemoteException { - playerList = new LimitedList<>(5, ()->{}); + playerList = new LimitedMap(5, ()->{}); BlockingQueue actionQueue = new LinkedBlockingQueue<>(); GameController gameController = new GameController(); String IP; @@ -116,7 +116,7 @@ public class ServerLauncher { } RMIServer serverRMI = new RMIServer(gameController, 1099, actionQueue, playerList,IP); - TCPServer serverTCP = new TCPServer(gameController, 8080, actionQueue, playerList); + TCPServer serverTCP = new TCPServer(gameController, 8080, 8081,actionQueue, playerList); ServerLauncher launcher = new ServerLauncher(actionQueue, gameController, serverRMI, serverTCP); playerList.setAction(()->{ From e78096d2e0308931945b148611d67f38fa1a4404 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Thu, 7 May 2026 21:17:00 +0200 Subject: [PATCH 04/17] Partial implementation client resilience --- .../ingsw/gc14/Controller/GameController.java | 5 ++- .../java/it/polimi/ingsw/gc14/LimitedMap.java | 8 ++++- .../polimi/ingsw/gc14/Network/EventType.java | 4 +-- .../Network/NetworkEvents/SkipNoDrawable.java | 2 +- .../NetworkEvents/SkipPlayerDisconnected.java | 33 +++++++++++++++++++ .../gc14/Network/TCP/Client/TCPClient.java | 19 +++++++---- .../Network/TCP/Server/ClientHandler.java | 18 +++++++--- .../Network/TCP/Server/HeartbeatHandler.java | 3 +- .../gc14/Network/TCP/Server/TCPServer.java | 24 ++++++++------ .../it/polimi/ingsw/gc14/ServerLauncher.java | 19 +++++++---- 10 files changed, 101 insertions(+), 34 deletions(-) create mode 100644 src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipPlayerDisconnected.java diff --git a/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java index 8b2e1be..16c9ec4 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java +++ b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java @@ -31,8 +31,11 @@ public class GameController { public GameController() { } //TODO - public boolean SkipNotConnectedPlayer(Player player) + public boolean SkipNotConnectedPlayer(String username) { + Player player= model.getPlayerByUsername(username); + if(player==null) + return false; return model.SkipNotConnectedPlayer(player); } /** diff --git a/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java b/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java index e209803..d4833ae 100644 --- a/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java +++ b/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java @@ -48,8 +48,14 @@ public class LimitedMap implements Map { */ @Override public synchronized V put(K key, V value) { + boolean added = true; + if(map.size()==limit) { + if(!map.containsKey(key)) + return null; + added = false; + } V result = map.put(key, value); - if (map.size() >= limit) { + if (map.size() >= limit && added) { action.run(); } return result; diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java b/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java index 46a6d15..dd887b5 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java @@ -13,7 +13,7 @@ public enum EventType { DRAW_LOWER_BUILD, PICK_OPTIONAL_TRIBE, PICK_OPTIONAL_BUILD, - SKIP_UPPER, - SKIP_LOWER, + SKIP_NO_DRAWABLE, + SKIP_PLAYER_DISCONNECTED, NO_OPTIONAL_CARD } diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipNoDrawable.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipNoDrawable.java index 7651ba1..3f84ec8 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipNoDrawable.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipNoDrawable.java @@ -17,7 +17,7 @@ public class SkipNoDrawable extends NetworkEvent implements Serializable{ * @param username the name of the player requesting the event */ public SkipNoDrawable(String username){ - super(username, EventType.SKIP_LOWER, false); + super(username, EventType.SKIP_PLAYER_DISCONNECTED, false); } /** diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipPlayerDisconnected.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipPlayerDisconnected.java new file mode 100644 index 0000000..63ea7fd --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipPlayerDisconnected.java @@ -0,0 +1,33 @@ +package it.polimi.ingsw.gc14.Network.NetworkEvents; + +import it.polimi.ingsw.gc14.Controller.GameController; +import it.polimi.ingsw.gc14.Network.EventType; +import it.polimi.ingsw.gc14.Network.NetworkEvent; + +import java.io.Serializable; + +/** + * NetworkEvent to avoid drawing a card from the lower card list + */ +public class SkipPlayerDisconnected extends NetworkEvent implements Serializable{ + + /** + * Class constructor. + * Initializes all the attributes. + * @param username the name of the player requesting the event + */ + public SkipPlayerDisconnected(String username){ + super(username, EventType.SKIP_PLAYER_DISCONNECTED, false); + } + + /** + * @param gameController the Game Controller on which to apply the event + * @return true if the player could skipTheTurn, false otherwise + */ + @Override + public boolean apply(GameController gameController){ + return gameController.SkipNotConnectedPlayer(username); + } + +} + diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java index 543c7ac..2986e75 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java @@ -72,13 +72,15 @@ public class TCPClient implements IClient { communicationSocket = new Socket(hostname, mainPort); socketSend = new ObjectOutputStream(communicationSocket.getOutputStream()); socketReceive = new ObjectInputStream(communicationSocket.getInputStream()); - doEvent(new AddPlayer(user, proposedNPlayers)); - - if (communicationSocket.getInputStream().read() == -1) { + NetworkEvent event= new AddPlayer(user, proposedNPlayers); + System.out.println("Sending event: " + event); + socketSend.writeObject(event); + int read= communicationSocket.getInputStream().read(); + if ( read== -1) { System.out.println("Could not connect to server"); return false; } - + new Thread(this::receiveMessage, "tcp-reader").start(); // Socket heartbeat this.heartbeatSocket = new Socket(hostname, heartbeatPort); this.heartbeatOut =heartbeatSocket.getOutputStream() ; @@ -89,7 +91,7 @@ public class TCPClient implements IClient { heartbeatOut.flush(); running = true; - new Thread(this::receiveMessage, "tcp-reader").start(); + new Thread(this::heartbeatLoop, "heartbeat").start(); return true; @@ -157,7 +159,6 @@ public class TCPClient implements IClient { e.printStackTrace(); break; } - if (read instanceof NetworkEvent event) { //TODO: avoid instanceof if (event.getIsError()) { controller.view.showError(event.toString()); @@ -165,6 +166,7 @@ public class TCPClient implements IClient { event.apply(controller.localController); controller.view.render(); } + } else if (read instanceof Game model) { controller.setModel(model); controller.view.render(); @@ -279,7 +281,10 @@ public class TCPClient implements IClient { */ private void doEvent(NetworkEvent event) { try { - socketSend.writeObject(event); + synchronized (socketSend) { + System.out.println("Sending event: " + event); + socketSend.writeObject(event); + } } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java index 2e9e8d2..1071fee 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java @@ -3,6 +3,7 @@ package it.polimi.ingsw.gc14.Network.TCP.Server; import it.polimi.ingsw.gc14.LimitedMap; import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Network.NetworkEvent; +import it.polimi.ingsw.gc14.Network.NetworkEvents.SkipPlayerDisconnected; import java.io.*; import java.net.*; @@ -23,6 +24,7 @@ public class ClientHandler implements Runnable { private boolean running ; + private Game game; /** * Returns the username associated with this client. * @@ -70,6 +72,7 @@ public class ClientHandler implements Runnable { this.out = out; this.clientHandlers = clientHandlers; this.actionQueue = actionQueue; + this.limitedMap = playersMap; } @@ -82,9 +85,11 @@ public class ClientHandler implements Runnable { try { running = true; while (running) { - NetworkEvent event = (NetworkEvent) in.readObject(); - if (!actionQueue.add(event)) { - System.out.println("Error inserting action into queue"); + synchronized (out){ + NetworkEvent event = (NetworkEvent) in.readObject(); + if (!actionQueue.add(event)) { + System.out.println("Error inserting action into queue"); + } } } } catch (IOException e) { @@ -116,6 +121,7 @@ public class ClientHandler implements Runnable { * @param game The current state of the game to send to the client. */ public synchronized void notifyModel(Game game) { + this.game = game; try { out.writeObject(game); } catch (IOException e) { @@ -124,9 +130,13 @@ public class ClientHandler implements Runnable { } public void disconnect() { - if (!running) return; running = false; clientHandlers.remove(this); + limitedMap.put(username, false); + if(this.game!=null && this.game.getCurrentState().getCurrentPlayer().getUserName().equals(username)) { + actionQueue.add(new SkipPlayerDisconnected(username)); + } + System.out.println("Disconnected player: " + username); try { clientSocket.close(); } catch (IOException ignored) {} } } \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/HeartbeatHandler.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/HeartbeatHandler.java index 8353369..a0dde68 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/HeartbeatHandler.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/HeartbeatHandler.java @@ -4,6 +4,7 @@ import java.io.*; import java.net.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; public class HeartbeatHandler implements Runnable { @@ -66,10 +67,10 @@ public class HeartbeatHandler implements Runnable { private void disconnect() { - if (!running) return; running = false; watchdog.shutdownNow(); mainHandler.disconnect(); // disconnette anche il socket principale + System.out.println("Disconnected: " + username); try { socket.close(); } catch (IOException ignored) {} } } diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java index dffc508..e30c8a2 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java @@ -71,7 +71,6 @@ public class TCPServer { ObjectOutputStream clientSend = new ObjectOutputStream(clientSocket.getOutputStream()); ObjectInputStream clientReceive = new ObjectInputStream(clientSocket.getInputStream()); NetworkEvent event = (NetworkEvent) clientReceive.readObject(); - if (!(event.getEventType() == EventType.ADD_PLAYER)) { clientSocket.getOutputStream().write(-1); clientSocket.close(); @@ -88,7 +87,7 @@ public class TCPServer { continue; } - synchronized (controller) { + if (playerList.isEmpty()) { Game model = new Game(eventAddPlayer.getProposedNPlayer()); controller.setModel(model); @@ -100,35 +99,40 @@ public class TCPServer { if (controller.addPlayer(username)) { // nuovo giocatore playerList.put(username, true); - clientSocket.getOutputStream().write(1); System.out.println("Accepted player: " + username); - ClientHandler handler = new ClientHandler( username, clientSocket, clientSend, clientReceive, clientHandlers,playerList, actionQueue ); + clientSocket.getOutputStream().write(1); + pendingHeartbeat.put(username, handler); + Thread thread = new Thread(handler); + thread.start(); clientHandlers.add(handler); connectedPlayers++; - // metti in attesa del socket heartbeat - pendingHeartbeat.put(username, handler); } else if (playerList.containsKey(username) && !playerList.get(username)) { // riconnessione playerList.put(username, true); - clientSocket.getOutputStream().write(1); System.out.println("Reconnected player: " + username); ClientHandler handler = new ClientHandler( username, clientSocket, clientSend, clientReceive, clientHandlers, playerList, actionQueue ); - handler.notifyModel(controller.getModel()); + clientSocket.getOutputStream().write(1); + pendingHeartbeat.put(username, handler); + synchronized (controller) { + handler.notifyModel(controller.getModel()); + } + Thread thread = new Thread(handler); + thread.start(); clientHandlers.add(handler); connectedPlayers++; - pendingHeartbeat.put(username, handler); + } else { @@ -136,7 +140,7 @@ public class TCPServer { clientSocket.close(); System.out.println("Player could not be added. Connection terminated."); } - } + } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java index 4db758c..f63254f 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java @@ -3,8 +3,10 @@ package it.polimi.ingsw.gc14; import it.polimi.ingsw.gc14.Controller.GameController; import it.polimi.ingsw.gc14.Model.Game; +import it.polimi.ingsw.gc14.Model.Player; import it.polimi.ingsw.gc14.Network.ClientPlayer; import it.polimi.ingsw.gc14.Network.NetworkEvent; +import it.polimi.ingsw.gc14.Network.NetworkEvents.SkipPlayerDisconnected; import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer; import it.polimi.ingsw.gc14.View.TUI.TUI; @@ -85,13 +87,16 @@ public class ServerLauncher { * @throws RemoteException if an RMI error occurs */ public boolean doFirstEvent() throws InterruptedException, RemoteException { - NetworkEvent event = actionQueue.take(); - event.setIsError(!event.apply(gameController)); - - serverRMI.notifyAll(event); - serverTCP.notifyAll(event); - - return !event.getIsError(); + synchronized (gameController) { + NetworkEvent event = actionQueue.take(); + event.setIsError(!event.apply(gameController)); + serverRMI.notifyAll(event); + serverTCP.notifyAll(event); + if (!playerList.get(gameController.getModel().getCurrentState().getCurrentPlayer().getUserName())) { + actionQueue.offer(new SkipPlayerDisconnected(gameController.getModel().getCurrentState().getCurrentPlayer().getUserName())); + } + return !event.getIsError(); + } } From 9fe23c6f9e4e0b20d0920e5071d15b12daa70a4e Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Fri, 8 May 2026 17:16:09 +0200 Subject: [PATCH 05/17] Fixed:Client Resilience (TCP) --- .../ingsw/gc14/Controller/GameController.java | 8 +++++ .../java/it/polimi/ingsw/gc14/Model/Game.java | 34 +++++++++++++++++-- .../ingsw/gc14/Model/OrderLogicCard.java | 14 ++++++++ .../polimi/ingsw/gc14/Network/EventType.java | 1 + .../NetworkEvents/ReconnectPlayer.java | 30 ++++++++++++++++ .../gc14/Network/TCP/Client/TCPClient.java | 4 ++- .../gc14/Network/TCP/Server/TCPServer.java | 10 ++---- 7 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/ReconnectPlayer.java diff --git a/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java index 16c9ec4..54399af 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java +++ b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java @@ -38,6 +38,14 @@ public class GameController { return false; return model.SkipNotConnectedPlayer(player); } + //TODO + public boolean ReconnectPlayer(String username) + { + Player player= model.getPlayerByUsername(username); + if(player==null) + return false; + return model.ReconnectPlayer(player); + } /** * Returns the game model managed by this controller. * diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java index ae75315..7fe3929 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java @@ -40,6 +40,9 @@ public class Game implements Serializable { public List getPlayers() { return playersList; } + + //TODO + Map disconnetedPlayers = new HashMap<>(); //TODO public boolean SkipNotConnectedPlayer(Player player) { @@ -47,9 +50,28 @@ public class Game implements Serializable { { return false; } - nextPlayerSetup(); - return true; + + if(currentState.getGameStage().equals(GameStages.SLOT_CHOICE)) + { + nextPlayerSetup(); + disconnetedPlayers.put(player,false); + } + else + { + nextPlayerSetup(); + } + return true; + } + + //TODO + public boolean ReconnectPlayer(Player player) + { + if(!disconnetedPlayers.containsKey(player)) + { + return false; + } + return disconnetedPlayers.put(player,false); } /** * Returns the current number of players participating in the game. @@ -728,6 +750,14 @@ public class Game implements Serializable { if (currentState.getRound() < 10) { nextRound(); + for(Map.Entry entry: disconnetedPlayers.entrySet()) + { + if(!entry.getValue()) + { + orderLogicCard.pushNoEffect(entry.getKey()); + disconnetedPlayers.remove(entry.getKey()); + } + } currentState.PlayerUpdate(orderLogicCard.pull(), null); currentState.GameStageUpdate(GameStages.SLOT_CHOICE); } else { diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/OrderLogicCard.java b/src/main/java/it/polimi/ingsw/gc14/Model/OrderLogicCard.java index b0e0c4f..fc2b9c8 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/OrderLogicCard.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/OrderLogicCard.java @@ -59,6 +59,20 @@ public abstract class OrderLogicCard implements Serializable { } + /** + * Adds the player to the end of the queue, without effects. + * + * @param player the player to be pushed into the queue. + */ + public void pushNoEffect(Player player){ + if(players.size()==0) + { + playerList=new ArrayList<>(); + + } + playerList.add(new OrderPlayer(player,false)); + players.add(player); + } /** * Removes and returns the first player in the queue. diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java b/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java index dd887b5..dd697c3 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java @@ -15,5 +15,6 @@ public enum EventType { PICK_OPTIONAL_BUILD, SKIP_NO_DRAWABLE, SKIP_PLAYER_DISCONNECTED, + RECONNECT_PLAYER, NO_OPTIONAL_CARD } diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/ReconnectPlayer.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/ReconnectPlayer.java new file mode 100644 index 0000000..d6f61a9 --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/ReconnectPlayer.java @@ -0,0 +1,30 @@ +package it.polimi.ingsw.gc14.Network.NetworkEvents; + +import it.polimi.ingsw.gc14.Controller.GameController; +import it.polimi.ingsw.gc14.Network.EventType; +import it.polimi.ingsw.gc14.Network.NetworkEvent; + +import java.io.Serializable; + +//TODO +public class ReconnectPlayer extends NetworkEvent implements Serializable{ + + /** + * Class constructor. + * Initializes all the attributes. + * @param username the name of the player requesting the event + */ + public ReconnectPlayer(String username){ + super(username, EventType.RECONNECT_PLAYER, false); + } + + /** + * @param gameController the Game Controller on which to apply the event + * @return true if the player could skipTheTurn, false otherwise + */ + @Override + public boolean apply(GameController gameController){ + return gameController.ReconnectPlayer(username); + } + +} \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java index 2986e75..498ced8 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Client/TCPClient.java @@ -81,6 +81,7 @@ public class TCPClient implements IClient { return false; } new Thread(this::receiveMessage, "tcp-reader").start(); + // Socket heartbeat this.heartbeatSocket = new Socket(hostname, heartbeatPort); this.heartbeatOut =heartbeatSocket.getOutputStream() ; @@ -89,10 +90,11 @@ public class TCPClient implements IClient { // manda subito username per associare i due socket lato server new ObjectOutputStream(heartbeatSocket.getOutputStream()).writeObject(user); heartbeatOut.flush(); + new Thread(this::heartbeatLoop, "heartbeat").start(); running = true; - new Thread(this::heartbeatLoop, "heartbeat").start(); + return true; } catch (IOException e) { diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java index e30c8a2..8844ffc 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java @@ -7,6 +7,7 @@ import it.polimi.ingsw.gc14.Network.ClientPlayer; import it.polimi.ingsw.gc14.Network.EventType; 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.*; @@ -124,17 +125,12 @@ public class TCPServer { ); clientSocket.getOutputStream().write(1); pendingHeartbeat.put(username, handler); - synchronized (controller) { - handler.notifyModel(controller.getModel()); - } + handler.notifyModel(controller.getModel()); Thread thread = new Thread(handler); thread.start(); clientHandlers.add(handler); connectedPlayers++; - - - - + actionQueue.add(new ReconnectPlayer(username)); } else { clientSocket.getOutputStream().write(-1); clientSocket.close(); From 506e6677af358121644b7fbac9095fbc7405f9c8 Mon Sep 17 00:00:00 2001 From: GabrieleRadice <265572328+GabrieleRadice@users.noreply.github.com> Date: Fri, 8 May 2026 19:32:52 +0200 Subject: [PATCH 06/17] Add: Added "gameSave" And "loadSave" Methods In ServerLauncher.java. --- .../it/polimi/ingsw/gc14/ServerLauncher.java | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java index f63254f..6f07405 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java @@ -3,6 +3,7 @@ package it.polimi.ingsw.gc14; import it.polimi.ingsw.gc14.Controller.GameController; import it.polimi.ingsw.gc14.Model.Game; +import it.polimi.ingsw.gc14.Model.GamePackage.GameStages; import it.polimi.ingsw.gc14.Model.Player; import it.polimi.ingsw.gc14.Network.ClientPlayer; import it.polimi.ingsw.gc14.Network.NetworkEvent; @@ -11,6 +12,10 @@ import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer; import it.polimi.ingsw.gc14.View.TUI.TUI; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.rmi.RemoteException; import java.util.*; import java.util.concurrent.BlockingQueue; @@ -74,6 +79,7 @@ public class ServerLauncher { this.actionQueue = actionQueue; this.serverRMI = serverRMI; this.gameController = gameController; + this.gameController.setModel(loadSave()); this.serverTCP = serverTCP; } @@ -87,13 +93,19 @@ public class ServerLauncher { * @throws RemoteException if an RMI error occurs */ public boolean doFirstEvent() throws InterruptedException, RemoteException { - synchronized (gameController) { + synchronized(gameController){ NetworkEvent event = actionQueue.take(); event.setIsError(!event.apply(gameController)); serverRMI.notifyAll(event); serverTCP.notifyAll(event); if (!playerList.get(gameController.getModel().getCurrentState().getCurrentPlayer().getUserName())) { actionQueue.offer(new SkipPlayerDisconnected(gameController.getModel().getCurrentState().getCurrentPlayer().getUserName())); + + if(this.gameController.getModel().getCurrentState().getGameStage() != GameStages.ENDED && !event.getIsError()){ + if(!this.gameSave() ){ + System.out.println("\n!!! Save failed !!!\n"); + } + } } return !event.getIsError(); } @@ -114,6 +126,7 @@ public class ServerLauncher { BlockingQueue actionQueue = new LinkedBlockingQueue<>(); GameController gameController = new GameController(); String IP; + try { IP=chooseNetworkInterface(new Scanner(System.in)); } catch (Exception e) { @@ -158,17 +171,20 @@ public class ServerLauncher { // Game execution while (true) { - try { + try{ this.doFirstEvent(); this.view.fullRender(); - } catch (InterruptedException e) { + } + catch(InterruptedException e){ Thread.currentThread().interrupt(); break; - } catch (RemoteException e) { + } + catch(RemoteException e){ throw new RuntimeException(e); } } } + public static String chooseNetworkInterface(Scanner scanner) throws Exception { List ips = new ArrayList<>(); @@ -201,4 +217,34 @@ public class ServerLauncher { int choice = Integer.parseInt(scanner.nextLine().trim()); return ips.get(choice); } + + private boolean gameSave(){ + Path filePath = Paths.get("src/main/resources/GameSaves/save.dat"); + try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath.toFile()))){ + oos.writeObject(this.gameController.getModel()); + System.out.println("Game saved to: " + filePath.toAbsolutePath()); + return true; + } + catch(IOException e){ + e.printStackTrace(); + return false; + } + } + + private static Game loadSave(){ + Path filePath = Paths.get("src/main/resources/GameSaves/save.dat"); + try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath.toFile()))){ + return (Game)(ois.readObject()); + } + catch(FileNotFoundException e){ + return null; + } + catch(IOException e){ + e.printStackTrace(); + return null; + } + catch(ClassNotFoundException e){ + throw new RuntimeException(e); + } + } } From 875a986602b61b729287c1e0ebaa414282932807 Mon Sep 17 00:00:00 2001 From: GabrieleRadice <265572328+GabrieleRadice@users.noreply.github.com> Date: Fri, 8 May 2026 19:34:20 +0200 Subject: [PATCH 07/17] Fix: Fixed Model Setting Logic In RMIServer.java And TCPServer.java When A Game Is Restored From Disk. --- .../gc14/Network/RMI/Server/RMIServer.java | 11 ++- .../gc14/Network/TCP/Server/TCPServer.java | 84 ++++++++++--------- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java index 69bdb6c..b7baf63 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java @@ -83,9 +83,14 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { } synchronized (controller) { if(playerList.isEmpty()){ - model = new Game(preferredInt); - controller.setModel(model); - playerList.setLimit(preferredInt); + if(controller.getModel() == null){ + model = new Game(preferredInt); + controller.setModel(model); + playerList.setLimit(preferredInt); + } + else{ + model = controller.getModel(); + } } if (controller.addPlayer(username)) { clients.put(username, callback); diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java index 8844ffc..d364ede 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java @@ -88,57 +88,61 @@ public class TCPServer { continue; } - - if (playerList.isEmpty()) { + if (playerList.isEmpty()) { + if(controller.getModel() == null){ Game model = new Game(eventAddPlayer.getProposedNPlayer()); controller.setModel(model); playerList.setLimit(eventAddPlayer.getProposedNPlayer()); } + } - String username = eventAddPlayer.getUsername(); + String username = eventAddPlayer.getUsername(); - if (controller.addPlayer(username)) { - // nuovo giocatore - playerList.put(username, true); - System.out.println("Accepted player: " + username); - ClientHandler handler = new ClientHandler( - username, clientSocket, clientSend, clientReceive, - clientHandlers,playerList, actionQueue - ); - clientSocket.getOutputStream().write(1); - pendingHeartbeat.put(username, handler); - Thread thread = new Thread(handler); - thread.start(); - clientHandlers.add(handler); - connectedPlayers++; - // metti in attesa del socket heartbeat + if (controller.addPlayer(username)) { + // nuovo giocatore + playerList.put(username, true); + System.out.println("Accepted player: " + username); + ClientHandler handler = new ClientHandler( + username, clientSocket, clientSend, clientReceive, + clientHandlers,playerList, actionQueue + ); + clientSocket.getOutputStream().write(1); + pendingHeartbeat.put(username, handler); + Thread thread = new Thread(handler); + thread.start(); + clientHandlers.add(handler); + connectedPlayers++; + // metti in attesa del socket heartbeat - } else if (playerList.containsKey(username) && !playerList.get(username)) { - // riconnessione - playerList.put(username, true); - System.out.println("Reconnected player: " + username); + } + else if(playerList.containsKey(username) && !playerList.get(username)){ + // riconnessione + playerList.put(username, true); + System.out.println("Reconnected player: " + username); - ClientHandler handler = new ClientHandler( - username, clientSocket, clientSend, clientReceive, - clientHandlers, playerList, actionQueue - ); - clientSocket.getOutputStream().write(1); - pendingHeartbeat.put(username, handler); - handler.notifyModel(controller.getModel()); - Thread thread = new Thread(handler); - thread.start(); - clientHandlers.add(handler); - connectedPlayers++; - actionQueue.add(new ReconnectPlayer(username)); - } else { - clientSocket.getOutputStream().write(-1); - clientSocket.close(); - System.out.println("Player could not be added. Connection terminated."); - } + ClientHandler handler = new ClientHandler( + username, clientSocket, clientSend, clientReceive, + clientHandlers, playerList, actionQueue + ); + clientSocket.getOutputStream().write(1); + pendingHeartbeat.put(username, handler); + handler.notifyModel(controller.getModel()); + Thread thread = new Thread(handler); + thread.start(); + clientHandlers.add(handler); + connectedPlayers++; + actionQueue.add(new ReconnectPlayer(username)); + } + else{ + clientSocket.getOutputStream().write(-1); + clientSocket.close(); + System.out.println("Player could not be added. Connection terminated."); + } - } catch (IOException | ClassNotFoundException e) { + } + catch(IOException | ClassNotFoundException e){ e.printStackTrace(); } } From 9335db7db0d882217b3c765d2fcffc3033aa4ca7 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Fri, 8 May 2026 20:26:31 +0200 Subject: [PATCH 08/17] Fix: RMI resilience --- .idea/inspectionProfiles/Project_Default.xml | 3 + .../java/it/polimi/ingsw/gc14/Model/Game.java | 3 +- .../gc14/Network/RMI/Client/RMIClient.java | 118 ++++++++--- .../gc14/Network/RMI/Common/IGameServer.java | 55 ++--- .../gc14/Network/RMI/Server/RMIHeartbeat.java | 112 ++++++++++ .../gc14/Network/RMI/Server/RMIServer.java | 195 ++++++++++-------- 6 files changed, 333 insertions(+), 153 deletions(-) create mode 100644 src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index a257030..1f0d743 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,9 @@ \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java index 7fe3929..0fe8691 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java @@ -71,7 +71,8 @@ public class Game implements Serializable { { return false; } - return disconnetedPlayers.put(player,false); + disconnetedPlayers.put(player,false); + return true; } /** * Returns the current number of players participating in the game. diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java index febd764..f35065b 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java @@ -4,6 +4,7 @@ import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.Objects; +import java.util.concurrent.*; import it.polimi.ingsw.gc14.Controller.ClientController; import it.polimi.ingsw.gc14.Network.IClient; @@ -18,33 +19,24 @@ import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; */ public class RMIClient implements IClient { - /** The host address of the RMI server */ + private static final long PING_INTERVAL_S = 3; // mirrors TCPClient 3 s + private static final long PING_TIMEOUT_MS = 5_000; // mirrors SILENCE_THRESHOLD_MS + private final String host; - - /** The port of the RMI server */ private final int port; - - /** The remote stub used to call methods on the server */ private IGameServer stub; - - /** Client game's controller */ - ClientController controller; - - /** - * Local IP address of the RMI client. - */ + private ClientController controller; private String myIP; + private String username; + + private volatile boolean running = false; + + /** Scheduler that fires ping() every PING_INTERVAL_S seconds. */ + private ScheduledExecutorService pingSender; + - /** - * Class constructor. - * - * @param controller the client controller used to create the callback. - * @param host the host address of the RMI server. - * @param port the port of the RMI server. - * @param myIP the local IP address used by the RMI client. - */ public RMIClient(ClientController controller, String host, int port, String myIP) { - this.controller=controller; + this.controller = controller; this.host = host; this.port = port; this.myIP = myIP; @@ -52,29 +44,89 @@ public class RMIClient implements IClient { /** - * Connects to the RMI server and attempts to join the game. - * Looks up the RMI registry to retrieve the {@link IGameServer} stub. - * Then, creates a {@link ClientCallbackImpl} and calls {@link RMIServer#joinGame(String, int, IClientCallback)}. - * @param username the player's username - * @param preferredInt the desired number of players - * @return true if the player successfully joined the game, false otherwise + * Connects to the RMI server and starts the heartbeat loop. + * + *

Mirrors {@code TCPClient.connect()}: after a successful join the + * heartbeat channel is opened (here: a scheduler is started instead of + * opening a second socket). */ - public boolean connect(String username,int preferredInt) { + @Override + public boolean connect(String username, int preferredInt) { try { System.setProperty("java.rmi.server.hostname", this.myIP); Registry registry = LocateRegistry.getRegistry(host, port); this.stub = (IGameServer) registry.lookup("RMIGameServer"); - ClientCallbackImpl callback = new ClientCallbackImpl(controller); + this.username = username; - return stub.joinGame(username, preferredInt, callback); - } - catch (Exception e) { + ClientCallbackImpl callback = new ClientCallbackImpl(controller); + boolean joined = stub.joinGame(username, preferredInt, callback); + if (!joined) return false; + + running = true; + startHeartbeat(); + return true; + + } catch (Exception e) { e.printStackTrace(); return false; } - } + // ------------------------------------------------------------------------- + // Heartbeat — mirrors TCPClient.heartbeatLoop() + // ------------------------------------------------------------------------- + + /** + * Starts sending periodic pings to the server. + * + *

Mirrors the {@code ScheduledExecutorService} in + * {@code TCPClient.heartbeatLoop()} that writes {@code PING} every 3 s. + * On {@link RemoteException} the server is considered gone and + * {@link #disconnect()} is called — mirrors the behaviour on + * {@code SocketTimeoutException} / {@code IOException} in the TCP version. + */ + private void startHeartbeat() { + pingSender = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "rmi-heartbeat"); + t.setDaemon(true); + return t; + }); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + pingSender.scheduleAtFixedRate(() -> { + Future future = executor.submit(() -> { + try { + stub.ping(username); + } catch (RemoteException e) { + disconnect(); + } + }); + try { + future.get(PING_TIMEOUT_MS, TimeUnit.MILLISECONDS); // mirrors setSoTimeout(5000) + } catch (TimeoutException e) { + future.cancel(true); + System.out.println("RMI ping timeout: " + username); + disconnect(); + } catch (Exception e) { + disconnect(); + } + }, 0, PING_INTERVAL_S, TimeUnit.SECONDS); + } + + /** + * Tears down the connection. + * Mirrors {@code TCPClient.disconnect()}: stops the heartbeat and notifies + * the view. + */ + private void disconnect() { + if (!running) return; + running = false; + if (pingSender != null) pingSender.shutdownNow(); + controller.view.showError("Connessione al server persa"); + } + + + /** * Requests to draw a tribe card from the upper list. diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Common/IGameServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Common/IGameServer.java index bfee30b..edb8fc2 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Common/IGameServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Common/IGameServer.java @@ -9,47 +9,26 @@ import java.rmi.*; */ public interface IGameServer extends Remote { - /** - * Adds a player to the game through the remote server. - * - * @param username the username of the player joining the game. - * @param preferredInt the preferred player number or slot selected by the client. - * @param callback the client callback used by the server to send updates. - * @return {@code true} if the player successfully joins the game; - * {@code false} otherwise. - * @throws RemoteException if an RMI communication error occurs. - */ - boolean joinGame(String username,int preferredInt, IClientCallback callback) throws RemoteException; + boolean joinGame(String username, int preferredInt, IClientCallback callback) throws RemoteException; - /** - * Sends a network event to the game server. - * - * @param event the event to be processed by the server. - * @return {@code true} if the event is accepted and processed; - * {@code false} otherwise. - * @throws RemoteException if an RMI communication error occurs. - */ boolean doEvent(NetworkEvent event) throws RemoteException; + void drawUpperTribeCard(String playerUsername, int pos) throws RemoteException; - - void drawLowerTribeCard(String playerUsername,int pos) throws RemoteException; - - void drawUpperBuildingCard(String playerUsername,int pos) throws RemoteException; - - - void drawLowerBuildingCard(String playerUsername,int pos) throws RemoteException; - + void drawLowerTribeCard(String playerUsername, int pos) throws RemoteException; + void drawUpperBuildingCard(String playerUsername, int pos) throws RemoteException; + void drawLowerBuildingCard(String playerUsername, int pos) throws RemoteException; void skipTurn(String playerUsername) throws RemoteException; - - - - void pickOptionalTribeCard(String playerUsername,int pos) throws RemoteException; - - void pickOptionalBuildingCard(String playerUsername,int pos) throws RemoteException; - - + void pickOptionalTribeCard(String playerUsername, int pos) throws RemoteException; + void pickOptionalBuildingCard(String playerUsername, int pos) throws RemoteException; void noOptionalCard(String playerUsername) throws RemoteException; + void slotChoice(String playerUsername, int pos) throws RemoteException; - void slotChoice(String playerUsername,int pos) throws RemoteException; - -} + /** + * Heartbeat: called periodically by the client to signal it is still alive. + * Mirrors the PING/PONG mechanism used in the TCP heartbeat channel. + * + * @param username the username of the client sending the ping. + * @throws RemoteException if an RMI communication error occurs. + */ + void ping(String username) throws RemoteException; +} \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java new file mode 100644 index 0000000..315c99e --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java @@ -0,0 +1,112 @@ +package it.polimi.ingsw.gc14.Network.RMI.Server; + +import it.polimi.ingsw.gc14.LimitedMap; +import it.polimi.ingsw.gc14.Model.Game; +import it.polimi.ingsw.gc14.Network.NetworkEvent; +import it.polimi.ingsw.gc14.Network.NetworkEvents.SkipPlayerDisconnected; + +import java.util.Map; +import java.util.concurrent.*; + +/** + * Server-side heartbeat watchdog for a single RMI client. + * + *

Mirrors {@code HeartbeatHandler} used in the TCP stack, but adapted for RMI: + * instead of reading raw bytes from a dedicated socket, it relies on {@link #receivePing()} + * being called by {@link RMIServer#ping(String)} every time the client sends a ping. + * + *

If no ping is received within {@value SILENCE_THRESHOLD_MS} ms the player is + * considered disconnected and {@link #disconnect()} is invoked, which: + *

    + *
  • stops the watchdog;
  • + *
  • marks the player as offline in {@code playerList};
  • + *
  • removes the callback from {@code clients};
  • + *
  • optionally pushes a {@link SkipPlayerDisconnected} event if it was that + * player's turn.
  • + *
+ */ +public class RMIHeartbeat { + + private static final long SILENCE_THRESHOLD_MS = 5_000; + + private String username = ""; + private final LimitedMap playerList; + private final Map clients; // ConcurrentHashMap + private final BlockingQueue actionQueue; + + /** Last time a ping was received from this client. */ + private volatile long lastPingTime = System.currentTimeMillis(); + private volatile boolean running = true; + + /** Reference to the current game model — needed to check whose turn it is. */ + private volatile Game game; + + private final ScheduledExecutorService watchdog = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "rmi-watchdog-" + username); + t.setDaemon(true); + return t; + }); + + public RMIHeartbeat( + String username, + LimitedMap playerList, + Map clients, + BlockingQueue actionQueue) { + + this.username = username; + this.playerList = playerList; + this.clients = clients; + this.actionQueue = actionQueue; + } + + /** Called by {@link RMIServer} whenever it starts tracking this player. */ + public void start() { + watchdog.scheduleAtFixedRate(() -> { + if (System.currentTimeMillis() - lastPingTime > SILENCE_THRESHOLD_MS) { + System.out.println("RMI heartbeat timeout: " + username); + disconnect(); + } + }, 1, 1, TimeUnit.SECONDS); + } + + /** + * Called by {@link RMIServer#ping(String)} each time the client pings. + * Resets the silence timer — mirrors writing {@code lastReceivedTime} in + * {@code HeartbeatHandler}. + */ + public void receivePing() { + lastPingTime = System.currentTimeMillis(); + } + + /** + * Allows the server to keep the watchdog up-to-date with the current game + * model (needed to check whose turn it is on disconnect). + */ + public void setGame(Game game) { + this.game = game; + } + + // ------------------------------------------------------------------------- + + private void disconnect() { + if (!running) return; + running = false; + watchdog.shutdownNow(); + + // Mark player as offline + playerList.put(username, false); + + // Remove RMI callback so notifyAll skips this client + clients.remove(username); + + // If it was this player's turn, skip it + Game snapshot = this.game; + if (snapshot != null && + snapshot.getCurrentState().getCurrentPlayer().getUserName().equals(username)) { + actionQueue.add(new SkipPlayerDisconnected(username)); + } + + System.out.println("RMI disconnected: " + username); + } +} \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java index 69bdb6c..f86e0b6 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java @@ -24,42 +24,29 @@ import java.rmi.*; */ public class RMIServer extends UnicastRemoteObject implements IGameServer { private String host; - /** Server game's controller */ private GameController controller; - - /** Server game's model */ private Game model; - - /** RMI registry */ private Registry registry; - - /** RMI port */ private int nPort; - /** Map containing the associations between a player's username and its callback */ + /** username → callback */ private final Map clients = new ConcurrentHashMap<>(); - /** Queue containing the events to be applied to the game model */ + /** + * username → watchdog. + * One watchdog per connected player, mirrors {@code pendingHeartbeat} / per-socket + * HeartbeatHandler in the TCP stack. + */ + private final Map watchdogs = new ConcurrentHashMap<>(); + BlockingQueue actionQueue; - - /** - * List containing the usernames of joined players. - * {@link LimitedMap}'s limit defines at which size the list calls its action. The limit can be set using {@link LimitedMap#setLimit(int)}. - */ - private LimitedMap playerList; + private LimitedMap playerList; - /** - * Class constructor that initializes the attributes. - * - * @param controller the game controller. - * @param nPort the RMI port. - * @param actionQueue the action queue. - * @param playerList the players' usernames list. - * @param host the host address of the RMI server. - * @throws RemoteException if an RMI error occurs. - */ - public RMIServer(GameController controller, int nPort, BlockingQueue actionQueue, LimitedMap playerList, String host) throws RemoteException { + public RMIServer(GameController controller, int nPort, + BlockingQueue actionQueue, + LimitedMap playerList, + String host) throws RemoteException { this.controller = controller; this.nPort = nPort; this.actionQueue = actionQueue; @@ -67,37 +54,102 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { this.host = host; } + // ------------------------------------------------------------------------- + // Join + // ------------------------------------------------------------------------- + /** - * Allows a player to join the game. - * If the desired number of player is invalid, the request is rejected. - * If this is the first player, a new game model is created and passed to the controller. Additionally, the playerList's limit is set. - * Then, if the controller successfully adds the player, the username is added to {@link #playerList} and {@link #clients}. - * @param username The player's name - * @param preferredInt The desired number of players - * @param callback The client's callback interface - * @return true if the player successfully joined the game, false otherwise + * {@inheritDoc} + * + *

After a successful join a {@link RMIHeartbeat} is created and + * started for the new player — mirrors creating a {@code HeartbeatHandler} in + * {@code TCPServer.acceptHeartbeat()}. */ - public boolean joinGame(String username, int preferredInt, IClientCallback callback) { - if (preferredInt<2 || preferredInt>5) { - return false; - } - synchronized (controller) { - if(playerList.isEmpty()){ + @Override + public boolean joinGame(String username, int preferredInt, IClientCallback callback) + throws RemoteException { + if (preferredInt < 2 || preferredInt > 5) return false; + + //synchronized (controller) { + if (playerList.isEmpty()) { model = new Game(preferredInt); controller.setModel(model); playerList.setLimit(preferredInt); } if (controller.addPlayer(username)) { clients.put(username, callback); - playerList.put(username,true); + playerList.put(username, true); + startWatchdog(username); System.out.println("Accepted player: " + username); return true; } - return false; - } + // Reconnection: player was offline + if (playerList.containsKey(username) && !playerList.get(username)) { + playerList.put(username, true); + clients.put(username, callback); + System.out.println("Reconnected player: " + username); + startWatchdog(username); + callback.onGameInit(model); + System.out.println("Model sent: " + username); + actionQueue.add(new ReconnectPlayer(username)); + + return true; + } + return false; + //} } + // ------------------------------------------------------------------------- + // Heartbeat — called by RMIClient every ~3 s + // ------------------------------------------------------------------------- + + /** + * Receives a heartbeat ping from the client. + * Mirrors the server reading {@code PING} and replying {@code PONG} in + * {@code HeartbeatHandler.run()}. + * + * @param username the username of the pinging client. + */ + @Override + public void ping(String username) throws RemoteException { + RMIHeartbeat wd = watchdogs.get(username); + if (wd != null) wd.receivePing(); + } + + // ------------------------------------------------------------------------- + // Game model propagation — keep watchdogs in sync + // ------------------------------------------------------------------------- + + /** + * Notifies all clients of a new event. + * Also updates every watchdog with the latest model so disconnect logic + * knows whose turn it is. + */ + public void notifyAll(NetworkEvent action) throws RemoteException { + for (Map.Entry entry : clients.entrySet()) { + if (!action.getIsError() || + (action.getIsError() && action.getUsername().equals(entry.getKey()))) { + entry.getValue().onAction(action); + } + } + } + + /** + * Notifies all clients of a new game model and keeps watchdogs up-to-date. + * Mirrors {@code TCPServer.notifyAll(Game)} + the {@code ClientHandler.notifyModel} + * call that stores the model for disconnect-turn checking. + */ + public void notifyAll(Game model) throws RemoteException { + this.model = model; + // Keep every watchdog's game reference up to date + watchdogs.values().forEach(wd -> wd.setGame(model)); + for (IClientCallback cb : clients.values()) { + cb.onGameInit(model); + } + } + + /** * Push an action in actionQueue. @@ -205,35 +257,6 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { } - // RMI's internal methods - /** - * Sends an action to the RMI clients. - * - *

If the action is an error, it is sent only to the client associated with - * the action username. Otherwise, it is sent to all connected RMI clients. - * - * @param action the network action to send. - * @throws RemoteException if an RMI communication error occurs. - */ - public void notifyAll(NetworkEvent action) throws RemoteException { - for (Map.Entry entry : clients.entrySet()) { - if(!action.getIsError() ||(action.getIsError()&& action.getUsername().equals(entry.getKey()))) - entry.getValue().onAction(action); - } - } - - - /** - * Sends a game model to all RMI clients. - * - * @param model the game model to send to all connected RMI clients. - * @throws RemoteException if an RMI communication error occurs. - */ - public void notifyAll(Game model) throws RemoteException { - for (IClientCallback cb : clients.values()) { - cb.onGameInit(model); - } - } @@ -244,27 +267,22 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { */ public boolean start() { try { - System.setProperty("java.rmi.server.hostname", host); // o il tuo IP/hostname + System.setProperty("java.rmi.server.hostname", host); registry = LocateRegistry.createRegistry(nPort); registry.rebind("RMIGameServer", this); - System.out.println("RMI Server started on port: "+nPort); + System.out.println("RMI Server started on port: " + nPort); return true; - } - catch (Exception e) { + } catch (Exception e) { e.printStackTrace(); return false; } } - - /** - * Stops the RMI server. - * @return true if the server stops successfully, false otherwise - */ public boolean stop() { try { registry.unbind("RMIGameServer"); UnicastRemoteObject.unexportObject(this, true); + watchdogs.values().forEach(wd -> { /* watchdogs shut themselves down */ }); System.out.println("RMI Server fermato"); return true; } catch (RemoteException | NotBoundException e) { @@ -274,4 +292,19 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { } + /** + * Creates and starts a {@link RMIHeartbeat} for {@code username}. + * Also seeds the watchdog with the current model if one already exists + * (reconnection case). + */ + private void startWatchdog(String username) { + RMIHeartbeat wd = new RMIHeartbeat( + username, playerList, clients, actionQueue); + if (model != null) wd.setGame(model); + watchdogs.put(username, wd); + wd.start(); + } + + + } From 0e477d48d157cb707805f6c4c0164fc08229f911 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Sat, 9 May 2026 12:46:39 +0200 Subject: [PATCH 09/17] Fix: Reconnection during Slot Choice --- src/main/java/it/polimi/ingsw/gc14/Model/Game.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java index 0fe8691..3074ec1 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java @@ -72,6 +72,11 @@ public class Game implements Serializable { return false; } disconnetedPlayers.put(player,false); + if(currentState.getGameStage().equals(GameStages.SLOT_CHOICE)) + { + disconnetedPlayers.remove(player); + orderLogicCard.pushNoEffect(player); + } return true; } /** From 680c748d4c810dce56ce4678b6d0c8807263efa5 Mon Sep 17 00:00:00 2001 From: GabrieleRadice <265572328+GabrieleRadice@users.noreply.github.com> Date: Sun, 10 May 2026 17:12:34 +0200 Subject: [PATCH 10/17] Add: Added "deleteSave" Method In ServerLauncher.java. Fix: Fixed Loading/Deletion Logic For "gameSave" Method In ServerLauncher.java. --- .../it/polimi/ingsw/gc14/ServerLauncher.java | 90 +++++++++++++------ 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java index 6f07405..baaf454 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java @@ -98,14 +98,17 @@ public class ServerLauncher { event.setIsError(!event.apply(gameController)); serverRMI.notifyAll(event); serverTCP.notifyAll(event); + + if(!event.getIsError()){ + if(this.gameController.getModel().getCurrentState().getGameStage() == GameStages.ENDED){ + this.deleteSave(); + } + else if(!this.gameSave() ){ + System.out.println("\n!!! Save failed !!!\n"); + } + } if (!playerList.get(gameController.getModel().getCurrentState().getCurrentPlayer().getUserName())) { actionQueue.offer(new SkipPlayerDisconnected(gameController.getModel().getCurrentState().getCurrentPlayer().getUserName())); - - if(this.gameController.getModel().getCurrentState().getGameStage() != GameStages.ENDED && !event.getIsError()){ - if(!this.gameSave() ){ - System.out.println("\n!!! Save failed !!!\n"); - } - } } return !event.getIsError(); } @@ -219,32 +222,65 @@ public class ServerLauncher { } private boolean gameSave(){ - Path filePath = Paths.get("src/main/resources/GameSaves/save.dat"); - try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath.toFile()))){ - oos.writeObject(this.gameController.getModel()); - System.out.println("Game saved to: " + filePath.toAbsolutePath()); - return true; + try{ + Path jarPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent(); + Path filePath = jarPath.resolve("GameSaves/save.dat"); + Files.createDirectories(filePath.getParent()); + + try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath.toFile()))){ + oos.writeObject(this.gameController.getModel()); + System.out.println("Game saved to: " + filePath.toAbsolutePath()); + return true; + } + catch(IOException e){ + e.printStackTrace(); + return false; + } } catch(IOException e){ + System.out.println("Couldn't create directory."); e.printStackTrace(); return false; - } - } - - private static Game loadSave(){ - Path filePath = Paths.get("src/main/resources/GameSaves/save.dat"); - try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath.toFile()))){ - return (Game)(ois.readObject()); - } - catch(FileNotFoundException e){ - return null; - } - catch(IOException e){ - e.printStackTrace(); - return null; - } - catch(ClassNotFoundException e){ + } catch (URISyntaxException e) { throw new RuntimeException(e); } } + + private Game loadSave(){ + try { + Path jarPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent(); + Path filePath = jarPath.resolve("GameSaves/save.dat"); + try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath.toFile()))){ + return (Game)(ois.readObject()); + } + catch(FileNotFoundException e){ + return null; + } + catch(IOException e){ + e.printStackTrace(); + return null; + } + catch(ClassNotFoundException e){ + throw new RuntimeException(e); + } + } + catch(URISyntaxException e){ + throw new RuntimeException(e); + } + } + + private boolean deleteSave(){ + try{ + Path jarPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent(); + Path filePath = jarPath.resolve("GameSaves/save.dat"); + Files.delete(filePath); + return true; + } + catch(URISyntaxException e){ + return false; + } + catch (IOException e){ + return false; + } + } } From f9fa12ac644309307ef65dad32373fff625c333b Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Sun, 10 May 2026 18:59:33 +0200 Subject: [PATCH 11/17] Fix: TCP Reconnection system -> model too --- .../NetworkEvents/DisconnectedPlayer.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/DisconnectedPlayer.java diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/DisconnectedPlayer.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/DisconnectedPlayer.java new file mode 100644 index 0000000..e1400cd --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/DisconnectedPlayer.java @@ -0,0 +1,33 @@ +package it.polimi.ingsw.gc14.Network.NetworkEvents; + +import it.polimi.ingsw.gc14.Controller.GameController; +import it.polimi.ingsw.gc14.Network.EventType; +import it.polimi.ingsw.gc14.Network.NetworkEvent; + +import java.io.Serializable; + +/** + * NetworkEvent to avoid drawing a card from the lower card list + */ +public class DisconnectedPlayer extends NetworkEvent implements Serializable{ + + /** + * Class constructor. + * Initializes all the attributes. + * @param username the name of the player requesting the event + */ + public DisconnectedPlayer(String username){ + super(username, EventType.SKIP_PLAYER_DISCONNECTED, false); + } + + /** + * @param gameController the Game Controller on which to apply the event + * @return true if the player could skipTheTurn, false otherwise + */ + @Override + public boolean apply(GameController gameController){ + return gameController.DisconnectedPlayer(username); + } + +} + From 670afb40a8223553092c780ab524604e95a6b137 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Sun, 10 May 2026 18:59:49 +0200 Subject: [PATCH 12/17] Fix: TCP Reconnection system -> model too --- .../ingsw/gc14/Controller/GameController.java | 5 +- .../java/it/polimi/ingsw/gc14/Model/Game.java | 43 ++++--- .../NetworkEvents/SkipPlayerDisconnected.java | 33 ------ .../gc14/Network/RMI/Server/RMIHeartbeat.java | 6 +- .../Network/TCP/Server/ClientHandler.java | 6 +- .../gc14/Network/TCP/Server/TCPServer.java | 108 ++++++++++-------- .../it/polimi/ingsw/gc14/ServerLauncher.java | 33 +++--- 7 files changed, 107 insertions(+), 127 deletions(-) delete mode 100644 src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipPlayerDisconnected.java diff --git a/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java index 54399af..3fbc4c8 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java +++ b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java @@ -1,7 +1,6 @@ package it.polimi.ingsw.gc14.Controller; import it.polimi.ingsw.gc14.Model.Game; -import it.polimi.ingsw.gc14.Model.GamePackage.GameStages; import it.polimi.ingsw.gc14.Model.Player; /** @@ -31,12 +30,12 @@ public class GameController { public GameController() { } //TODO - public boolean SkipNotConnectedPlayer(String username) + public boolean DisconnectedPlayer(String username) { Player player= model.getPlayerByUsername(username); if(player==null) return false; - return model.SkipNotConnectedPlayer(player); + return model.DisconnectedPlayer(player); } //TODO public boolean ReconnectPlayer(String username) diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java index 3074ec1..f8c5e22 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java @@ -44,23 +44,15 @@ public class Game implements Serializable { //TODO Map disconnetedPlayers = new HashMap<>(); //TODO - public boolean SkipNotConnectedPlayer(Player player) + public boolean DisconnectedPlayer(Player player) { - if(currentState.getCurrentPlayer()!=player) + if(disconnetedPlayers.containsKey(player) && disconnetedPlayers.get(player)) { return false; } - - - if(currentState.getGameStage().equals(GameStages.SLOT_CHOICE)) - { + disconnetedPlayers.put(player,true); + if(currentState.getCurrentPlayer().equals(player)) nextPlayerSetup(); - disconnetedPlayers.put(player,false); - } - else - { - nextPlayerSetup(); - } return true; } @@ -72,13 +64,21 @@ public class Game implements Serializable { return false; } disconnetedPlayers.put(player,false); - if(currentState.getGameStage().equals(GameStages.SLOT_CHOICE)) + if(currentState.getGameStage().equals(GameStages.SLOT_CHOICE) ) { disconnetedPlayers.remove(player); - orderLogicCard.pushNoEffect(player); + if(!orderLogicCard.players.contains(player)) + { + orderLogicCard.pushNoEffect(player); + } } return true; } + + public void ClearDisconnected() + { + disconnetedPlayers.clear(); + } /** * Returns the current number of players participating in the game. * @return the current number of players. @@ -645,7 +645,10 @@ public class Game implements Serializable { if (GameStages.SLOT_CHOICE == currentState.getGameStage()) { Player tempPlayer = orderLogicCard.pull(); - + if(disconnetedPlayers.containsKey(tempPlayer) && disconnetedPlayers.get(tempPlayer)) { + nextPlayerSetup(); + return; + } if (tempPlayer != null) { currentState.PlayerUpdate(tempPlayer, null); return; @@ -690,12 +693,15 @@ public class Game implements Serializable { for (Slot s : slotMap.keySet()) { if (slotMap.get(s) != null) { currentState.PlayerUpdate(slotMap.get(s), s); - + if(disconnetedPlayers.containsKey(currentState.getCurrentPlayer())&& disconnetedPlayers.get(currentState.getCurrentPlayer())) + { + slotMap.put(currentState.getSlot(), null); + continue; + } boolean hasDrawableLower = currentState.getNLower() > 0 && (hasDrawableDown() || !getLowerListBuilding().isEmpty()); boolean hasDrawableUpper = currentState.getNUpper() > 0 && (hasDrawableUp() || !getUpperListBuilding().isEmpty()); - if (!hasDrawableLower && !hasDrawableUpper) { // This player also has nothing, skip and continue the loop orderLogicCard.push(currentState.getCurrentPlayer()); @@ -717,6 +723,9 @@ public class Game implements Serializable { if (optionalPlayer != null) { currentState.PlayerUpdate(optionalPlayer, null); + if(disconnetedPlayers.containsKey(currentState.getCurrentPlayer())&& disconnetedPlayers.get(currentState.getCurrentPlayer())) { + nextPlayerSetup(); + } return; } diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipPlayerDisconnected.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipPlayerDisconnected.java deleted file mode 100644 index 63ea7fd..0000000 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipPlayerDisconnected.java +++ /dev/null @@ -1,33 +0,0 @@ -package it.polimi.ingsw.gc14.Network.NetworkEvents; - -import it.polimi.ingsw.gc14.Controller.GameController; -import it.polimi.ingsw.gc14.Network.EventType; -import it.polimi.ingsw.gc14.Network.NetworkEvent; - -import java.io.Serializable; - -/** - * NetworkEvent to avoid drawing a card from the lower card list - */ -public class SkipPlayerDisconnected extends NetworkEvent implements Serializable{ - - /** - * Class constructor. - * Initializes all the attributes. - * @param username the name of the player requesting the event - */ - public SkipPlayerDisconnected(String username){ - super(username, EventType.SKIP_PLAYER_DISCONNECTED, false); - } - - /** - * @param gameController the Game Controller on which to apply the event - * @return true if the player could skipTheTurn, false otherwise - */ - @Override - public boolean apply(GameController gameController){ - return gameController.SkipNotConnectedPlayer(username); - } - -} - diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java index 315c99e..e38b489 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java @@ -3,7 +3,7 @@ package it.polimi.ingsw.gc14.Network.RMI.Server; import it.polimi.ingsw.gc14.LimitedMap; import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Network.NetworkEvent; -import it.polimi.ingsw.gc14.Network.NetworkEvents.SkipPlayerDisconnected; +import it.polimi.ingsw.gc14.Network.NetworkEvents.DisconnectedPlayer; import java.util.Map; import java.util.concurrent.*; @@ -21,7 +21,7 @@ import java.util.concurrent.*; *

  • stops the watchdog;
  • *
  • marks the player as offline in {@code playerList};
  • *
  • removes the callback from {@code clients};
  • - *
  • optionally pushes a {@link SkipPlayerDisconnected} event if it was that + *
  • optionally pushes a {@link DisconnectedPlayer} event if it was that * player's turn.
  • * */ @@ -104,7 +104,7 @@ public class RMIHeartbeat { Game snapshot = this.game; if (snapshot != null && snapshot.getCurrentState().getCurrentPlayer().getUserName().equals(username)) { - actionQueue.add(new SkipPlayerDisconnected(username)); + actionQueue.add(new DisconnectedPlayer(username)); } System.out.println("RMI disconnected: " + username); diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java index 1071fee..d321664 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/ClientHandler.java @@ -3,7 +3,7 @@ package it.polimi.ingsw.gc14.Network.TCP.Server; import it.polimi.ingsw.gc14.LimitedMap; import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Network.NetworkEvent; -import it.polimi.ingsw.gc14.Network.NetworkEvents.SkipPlayerDisconnected; +import it.polimi.ingsw.gc14.Network.NetworkEvents.DisconnectedPlayer; import java.io.*; import java.net.*; @@ -133,9 +133,7 @@ public class ClientHandler implements Runnable { running = false; clientHandlers.remove(this); limitedMap.put(username, false); - if(this.game!=null && this.game.getCurrentState().getCurrentPlayer().getUserName().equals(username)) { - actionQueue.add(new SkipPlayerDisconnected(username)); - } + actionQueue.add(new DisconnectedPlayer(username)); System.out.println("Disconnected player: " + username); try { clientSocket.close(); } catch (IOException ignored) {} } diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java index d364ede..bce4110 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java @@ -3,6 +3,7 @@ package it.polimi.ingsw.gc14.Network.TCP.Server; import it.polimi.ingsw.gc14.Controller.GameController; import it.polimi.ingsw.gc14.LimitedMap; import it.polimi.ingsw.gc14.Model.Game; +import it.polimi.ingsw.gc14.Model.Player; import it.polimi.ingsw.gc14.Network.ClientPlayer; import it.polimi.ingsw.gc14.Network.EventType; import it.polimi.ingsw.gc14.Network.NetworkEvent; @@ -29,7 +30,7 @@ public class TCPServer { ServerSocket socketTCP; ServerSocket heartbeatSocketTCP; // ← nuovo ServerSocket - GameController controller; + final GameController controller; BlockingQueue actionQueue; LimitedMap playerList; List clientHandlers; @@ -88,57 +89,66 @@ public class TCPServer { continue; } - if (playerList.isEmpty()) { - if(controller.getModel() == null){ - Game model = new Game(eventAddPlayer.getProposedNPlayer()); - controller.setModel(model); - playerList.setLimit(eventAddPlayer.getProposedNPlayer()); - } - } - - String username = eventAddPlayer.getUsername(); - - if (controller.addPlayer(username)) { - // nuovo giocatore - playerList.put(username, true); - System.out.println("Accepted player: " + username); - ClientHandler handler = new ClientHandler( - username, clientSocket, clientSend, clientReceive, - clientHandlers,playerList, actionQueue - ); - clientSocket.getOutputStream().write(1); - pendingHeartbeat.put(username, handler); - Thread thread = new Thread(handler); - thread.start(); - clientHandlers.add(handler); - connectedPlayers++; - // metti in attesa del socket heartbeat + synchronized (controller) { + if (playerList.isEmpty()) { + if(controller.getModel() == null){ + Game model = new Game(eventAddPlayer.getProposedNPlayer()); + controller.setModel(model); + playerList.setLimit(eventAddPlayer.getProposedNPlayer()); + } + else + { + for(Player p:controller.getModel().getPlayers()) { + playerList.put(p.getUserName(),false); + } + } + } - } - else if(playerList.containsKey(username) && !playerList.get(username)){ - // riconnessione - playerList.put(username, true); - System.out.println("Reconnected player: " + username); + String username = eventAddPlayer.getUsername(); - ClientHandler handler = new ClientHandler( - username, clientSocket, clientSend, clientReceive, - clientHandlers, playerList, actionQueue - ); - clientSocket.getOutputStream().write(1); - pendingHeartbeat.put(username, handler); - handler.notifyModel(controller.getModel()); - Thread thread = new Thread(handler); - thread.start(); - clientHandlers.add(handler); - connectedPlayers++; - actionQueue.add(new ReconnectPlayer(username)); - } - else{ - clientSocket.getOutputStream().write(-1); - clientSocket.close(); - System.out.println("Player could not be added. Connection terminated."); - } + if (controller.addPlayer(username)) { + // nuovo giocatore + playerList.put(username, true); + System.out.println("Accepted player: " + username); + ClientHandler handler = new ClientHandler( + username, clientSocket, clientSend, clientReceive, + clientHandlers,playerList, actionQueue + ); + clientSocket.getOutputStream().write(1); + pendingHeartbeat.put(username, handler); + Thread thread = new Thread(handler); + thread.start(); + clientHandlers.add(handler); + connectedPlayers++; + // metti in attesa del socket heartbeat + + + } + else if(playerList.containsKey(username) && !playerList.get(username)){ + // riconnessione + playerList.put(username, true); + System.out.println("Reconnected player: " + username); + + ClientHandler handler = new ClientHandler( + username, clientSocket, clientSend, clientReceive, + clientHandlers, playerList, actionQueue + ); + clientSocket.getOutputStream().write(1); + pendingHeartbeat.put(username, handler); + handler.notifyModel(controller.getModel()); + Thread thread = new Thread(handler); + thread.start(); + clientHandlers.add(handler); + connectedPlayers++; + actionQueue.add(new ReconnectPlayer(username)); + } + else{ + clientSocket.getOutputStream().write(-1); + clientSocket.close(); + System.out.println("Player could not be added. Connection terminated."); + } + } } diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java index baaf454..2337f91 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java @@ -4,10 +4,8 @@ package it.polimi.ingsw.gc14; import it.polimi.ingsw.gc14.Controller.GameController; import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Model.GamePackage.GameStages; -import it.polimi.ingsw.gc14.Model.Player; -import it.polimi.ingsw.gc14.Network.ClientPlayer; import it.polimi.ingsw.gc14.Network.NetworkEvent; -import it.polimi.ingsw.gc14.Network.NetworkEvents.SkipPlayerDisconnected; +import it.polimi.ingsw.gc14.Network.NetworkEvents.DisconnectedPlayer; import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer; import it.polimi.ingsw.gc14.View.TUI.TUI; @@ -93,25 +91,24 @@ public class ServerLauncher { * @throws RemoteException if an RMI error occurs */ public boolean doFirstEvent() throws InterruptedException, RemoteException { - synchronized(gameController){ - NetworkEvent event = actionQueue.take(); - event.setIsError(!event.apply(gameController)); - serverRMI.notifyAll(event); - serverTCP.notifyAll(event); + NetworkEvent event = actionQueue.take(); + synchronized(gameController){ - if(!event.getIsError()){ - if(this.gameController.getModel().getCurrentState().getGameStage() == GameStages.ENDED){ - this.deleteSave(); - } - else if(!this.gameSave() ){ - System.out.println("\n!!! Save failed !!!\n"); - } + event.setIsError(!event.apply(gameController)); + serverRMI.notifyAll(event); + serverTCP.notifyAll(event); + + if(!event.getIsError()){ + if(this.gameController.getModel().getCurrentState().getGameStage() == GameStages.ENDED){ + this.deleteSave(); } - if (!playerList.get(gameController.getModel().getCurrentState().getCurrentPlayer().getUserName())) { - actionQueue.offer(new SkipPlayerDisconnected(gameController.getModel().getCurrentState().getCurrentPlayer().getUserName())); + else if(!this.gameSave() ){ + System.out.println("\n!!! Save failed !!!\n"); } - return !event.getIsError(); } + //actionQueue.offer(new DisconnectedPlayer(gameController.getModel().getCurrentState().getCurrentPlayer().getUserName())); + return !event.getIsError(); + } } From 6491eb3a85ecbd2f161261ec05731045dd4b1a53 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Sun, 10 May 2026 19:13:18 +0200 Subject: [PATCH 13/17] Fix: RMI Reconnection system --- .../ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java | 9 +-------- .../polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java | 6 ++---- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java index e38b489..e9ee1f0 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java @@ -99,14 +99,7 @@ public class RMIHeartbeat { // Remove RMI callback so notifyAll skips this client clients.remove(username); - - // If it was this player's turn, skip it - Game snapshot = this.game; - if (snapshot != null && - snapshot.getCurrentState().getCurrentPlayer().getUserName().equals(username)) { - actionQueue.add(new DisconnectedPlayer(username)); - } - + actionQueue.add(new DisconnectedPlayer(username)); System.out.println("RMI disconnected: " + username); } } \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java index f86e0b6..e5cb7ba 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java @@ -70,7 +70,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { throws RemoteException { if (preferredInt < 2 || preferredInt > 5) return false; - //synchronized (controller) { + synchronized (controller) { if (playerList.isEmpty()) { model = new Game(preferredInt); controller.setModel(model); @@ -92,12 +92,10 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { callback.onGameInit(model); System.out.println("Model sent: " + username); actionQueue.add(new ReconnectPlayer(username)); - - return true; } return false; - //} + } } // ------------------------------------------------------------------------- From 8106adb0d0fd13162dfd8c10cc8b1073c111842a Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Sun, 10 May 2026 20:09:19 +0200 Subject: [PATCH 14/17] Fix: Server Persistence --- .../gc14/Network/RMI/Server/RMIServer.java | 69 ++++++--- .../gc14/Network/TCP/Server/TCPServer.java | 138 +++++++++++------- .../it/polimi/ingsw/gc14/ServerLauncher.java | 14 +- 3 files changed, 143 insertions(+), 78 deletions(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java index e5cb7ba..675944d 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java @@ -42,6 +42,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { BlockingQueue actionQueue; private LimitedMap playerList; + private boolean serverCrashed; public RMIServer(GameController controller, int nPort, BlockingQueue actionQueue, @@ -71,28 +72,51 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { if (preferredInt < 2 || preferredInt > 5) return false; synchronized (controller) { - if (playerList.isEmpty()) { - model = new Game(preferredInt); - controller.setModel(model); - playerList.setLimit(preferredInt); + if(serverCrashed) + { + if(controller.getModel().getPlayers().stream().anyMatch(p -> p.getUserName().equals(username))&& !playerList.containsKey(username)) { + clients.put(username, callback); + playerList.put(username, true); + startWatchdog(username); + System.out.println("(After crash)Reconnected player: " + username); + return true; + } + if (playerList.containsKey(username) && !playerList.get(username)) { + playerList.put(username, true); + clients.put(username, callback); + System.out.println("Reconnected player: " + username); + startWatchdog(username); + callback.onGameInit(model); + System.out.println("Model sent: " + username); + actionQueue.add(new ReconnectPlayer(username)); + return true; + } } - if (controller.addPlayer(username)) { - clients.put(username, callback); - playerList.put(username, true); - startWatchdog(username); - System.out.println("Accepted player: " + username); - return true; - } - // Reconnection: player was offline - if (playerList.containsKey(username) && !playerList.get(username)) { - playerList.put(username, true); - clients.put(username, callback); - System.out.println("Reconnected player: " + username); - startWatchdog(username); - callback.onGameInit(model); - System.out.println("Model sent: " + username); - actionQueue.add(new ReconnectPlayer(username)); - return true; + else + { + if (playerList.isEmpty()) { + model = new Game(preferredInt); + controller.setModel(model); + playerList.setLimit(preferredInt); + } + if (controller.addPlayer(username)) { + clients.put(username, callback); + playerList.put(username, true); + startWatchdog(username); + System.out.println("Accepted player: " + username); + return true; + } + // Reconnection: player was offline + if (playerList.containsKey(username) && !playerList.get(username)) { + playerList.put(username, true); + clients.put(username, callback); + System.out.println("Reconnected player: " + username); + startWatchdog(username); + callback.onGameInit(model); + System.out.println("Model sent: " + username); + actionQueue.add(new ReconnectPlayer(username)); + return true; + } } return false; } @@ -263,7 +287,8 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { * Starts the RMI server. * @return true if the server starts successfully, false otherwise */ - public boolean start() { + public boolean start(boolean serverCrashed) { + this.serverCrashed = serverCrashed; try { System.setProperty("java.rmi.server.hostname", host); registry = LocateRegistry.createRegistry(nPort); diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java index bce4110..eaea963 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java @@ -35,6 +35,7 @@ public class TCPServer { LimitedMap playerList; List clientHandlers; + boolean serverCrashed; // Mappa temporanea: username → ClientHandler // Serve per associare il socket heartbeat al giusto ClientHandler private final Map pendingHeartbeat = new ConcurrentHashMap<>(); @@ -50,7 +51,8 @@ public class TCPServer { this.clientHandlers = new ArrayList<>(); } - public void start() { + public void start(boolean serverCrashed) { + this.serverCrashed = serverCrashed; try { socketTCP = new ServerSocket(port); heartbeatSocketTCP = new ServerSocket(heartbeatPort); @@ -90,64 +92,96 @@ public class TCPServer { } synchronized (controller) { - if (playerList.isEmpty()) { - if(controller.getModel() == null){ + + String username = eventAddPlayer.getUsername(); + if(serverCrashed){ + if(controller.getModel().getPlayers().stream().anyMatch(p -> p.getUserName().equals(username))&& !playerList.containsKey(username)){ + playerList.put(username, true); + System.out.println("(After crash)Reconnected player: " + username); + ClientHandler handler = new ClientHandler( + username, clientSocket, clientSend, clientReceive, + clientHandlers,playerList, actionQueue + ); + clientSocket.getOutputStream().write(1); + pendingHeartbeat.put(username, handler); + Thread thread = new Thread(handler); + thread.start(); + clientHandlers.add(handler); + connectedPlayers++; + } + else if(playerList.containsKey(username) && !playerList.get(username)){ + // riconnessione + playerList.put(username, true); + System.out.println("Reconnected player: " + username); + + ClientHandler handler = new ClientHandler( + username, clientSocket, clientSend, clientReceive, + clientHandlers, playerList, actionQueue + ); + clientSocket.getOutputStream().write(1); + pendingHeartbeat.put(username, handler); + handler.notifyModel(controller.getModel()); + Thread thread = new Thread(handler); + thread.start(); + clientHandlers.add(handler); + connectedPlayers++; + actionQueue.add(new ReconnectPlayer(username)); + } + else + { + clientSocket.getOutputStream().write(-1); + clientSocket.close(); + System.out.println("Player could not be added. Connection terminated."); + } + } + else + { + if (playerList.isEmpty()) { Game model = new Game(eventAddPlayer.getProposedNPlayer()); controller.setModel(model); playerList.setLimit(eventAddPlayer.getProposedNPlayer()); } - else - { - for(Player p:controller.getModel().getPlayers()) { - playerList.put(p.getUserName(),false); - } + if (controller.addPlayer(username)) { + // nuovo giocatore + playerList.put(username, true); + System.out.println("Accepted player: " + username); + ClientHandler handler = new ClientHandler( + username, clientSocket, clientSend, clientReceive, + clientHandlers,playerList, actionQueue + ); + clientSocket.getOutputStream().write(1); + pendingHeartbeat.put(username, handler); + Thread thread = new Thread(handler); + thread.start(); + clientHandlers.add(handler); + connectedPlayers++; + // metti in attesa del socket heartbeat + } + else if(playerList.containsKey(username) && !playerList.get(username)){ + // riconnessione + playerList.put(username, true); + System.out.println("Reconnected player: " + username); + + ClientHandler handler = new ClientHandler( + username, clientSocket, clientSend, clientReceive, + clientHandlers, playerList, actionQueue + ); + clientSocket.getOutputStream().write(1); + pendingHeartbeat.put(username, handler); + handler.notifyModel(controller.getModel()); + Thread thread = new Thread(handler); + thread.start(); + clientHandlers.add(handler); + connectedPlayers++; + actionQueue.add(new ReconnectPlayer(username)); + } + else{ + clientSocket.getOutputStream().write(-1); + clientSocket.close(); + System.out.println("Player could not be added. Connection terminated."); } } - - String username = eventAddPlayer.getUsername(); - - if (controller.addPlayer(username)) { - // nuovo giocatore - playerList.put(username, true); - System.out.println("Accepted player: " + username); - ClientHandler handler = new ClientHandler( - username, clientSocket, clientSend, clientReceive, - clientHandlers,playerList, actionQueue - ); - clientSocket.getOutputStream().write(1); - pendingHeartbeat.put(username, handler); - Thread thread = new Thread(handler); - thread.start(); - clientHandlers.add(handler); - connectedPlayers++; - // metti in attesa del socket heartbeat - - - } - else if(playerList.containsKey(username) && !playerList.get(username)){ - // riconnessione - playerList.put(username, true); - System.out.println("Reconnected player: " + username); - - ClientHandler handler = new ClientHandler( - username, clientSocket, clientSend, clientReceive, - clientHandlers, playerList, actionQueue - ); - clientSocket.getOutputStream().write(1); - pendingHeartbeat.put(username, handler); - handler.notifyModel(controller.getModel()); - Thread thread = new Thread(handler); - thread.start(); - clientHandlers.add(handler); - connectedPlayers++; - actionQueue.add(new ReconnectPlayer(username)); - } - else{ - clientSocket.getOutputStream().write(-1); - clientSocket.close(); - System.out.println("Player could not be added. Connection terminated."); - } } diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java index 2337f91..a08d7fb 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java @@ -4,6 +4,7 @@ package it.polimi.ingsw.gc14; import it.polimi.ingsw.gc14.Controller.GameController; import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Model.GamePackage.GameStages; +import it.polimi.ingsw.gc14.Model.Player; import it.polimi.ingsw.gc14.Network.NetworkEvent; import it.polimi.ingsw.gc14.Network.NetworkEvents.DisconnectedPlayer; import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; @@ -126,7 +127,7 @@ public class ServerLauncher { BlockingQueue actionQueue = new LinkedBlockingQueue<>(); GameController gameController = new GameController(); String IP; - + boolean serverCrashed; try { IP=chooseNetworkInterface(new Scanner(System.in)); } catch (Exception e) { @@ -136,7 +137,12 @@ public class ServerLauncher { RMIServer serverRMI = new RMIServer(gameController, 1099, actionQueue, playerList,IP); TCPServer serverTCP = new TCPServer(gameController, 8080, 8081,actionQueue, playerList); ServerLauncher launcher = new ServerLauncher(actionQueue, gameController, serverRMI, serverTCP); - + if(gameController.getModel() != null){ + playerList.setLimit(gameController.getModel().getNPlayers()); + serverCrashed = true; + } else { + serverCrashed = false; + } playerList.setAction(()->{ new Thread(()->{ try { @@ -149,8 +155,8 @@ public class ServerLauncher { }).start(); }); - serverRMI.start(); - new Thread(()->{serverTCP.start();}).start(); + serverRMI.start(serverCrashed); + new Thread(()->{serverTCP.start(serverCrashed);}).start(); } From ef34f780a3347f2a9b13f67ff47344d14caf95c8 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Sun, 10 May 2026 22:29:42 +0200 Subject: [PATCH 15/17] Updated: now the disconnected player before the crash are ignored for the game restarting --- src/main/java/it/polimi/ingsw/gc14/Model/Game.java | 2 +- .../it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java | 4 ++-- src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java index f8c5e22..b549192 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java @@ -42,7 +42,7 @@ public class Game implements Serializable { } //TODO - Map disconnetedPlayers = new HashMap<>(); + public Map disconnetedPlayers = new HashMap<>(); //TODO public boolean DisconnectedPlayer(Player player) { diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java index 675944d..d148ac5 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java @@ -86,7 +86,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { clients.put(username, callback); System.out.println("Reconnected player: " + username); startWatchdog(username); - callback.onGameInit(model); + callback.onGameInit(controller.getModel()); System.out.println("Model sent: " + username); actionQueue.add(new ReconnectPlayer(username)); return true; @@ -112,7 +112,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { clients.put(username, callback); System.out.println("Reconnected player: " + username); startWatchdog(username); - callback.onGameInit(model); + callback.onGameInit(controller.getModel()); System.out.println("Model sent: " + username); actionQueue.add(new ReconnectPlayer(username)); return true; diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java index a08d7fb..294400a 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java @@ -139,6 +139,13 @@ public class ServerLauncher { ServerLauncher launcher = new ServerLauncher(actionQueue, gameController, serverRMI, serverTCP); if(gameController.getModel() != null){ playerList.setLimit(gameController.getModel().getNPlayers()); + for(Map.Entryentry: gameController.getModel().disconnetedPlayers.entrySet()) + { + if(entry.getValue()) + { + playerList.put(entry.getKey().getUserName(),false); + } + } serverCrashed = true; } else { serverCrashed = false; From 3a39d889e4fcbc890850f2542b9df88a6fd5fb9b Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Mon, 11 May 2026 18:05:53 +0200 Subject: [PATCH 16/17] Fix: Only one player logic (todo go to end-game) --- .../polimi/ingsw/gc14/Network/EventType.java | 2 +- .../NetworkEvents/DisconnectedPlayer.java | 2 +- .../Network/NetworkEvents/SkipNoDrawable.java | 2 +- .../gc14/Network/RMI/Server/RMIServer.java | 3 + .../gc14/Network/TCP/Server/TCPServer.java | 4 + .../it/polimi/ingsw/gc14/ServerLauncher.java | 97 +++++++++++++------ 6 files changed, 80 insertions(+), 30 deletions(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java b/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java index dd697c3..8d01f34 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java @@ -14,7 +14,7 @@ public enum EventType { PICK_OPTIONAL_TRIBE, PICK_OPTIONAL_BUILD, SKIP_NO_DRAWABLE, - SKIP_PLAYER_DISCONNECTED, + DISCONNECTED_PLAYER, RECONNECT_PLAYER, NO_OPTIONAL_CARD } diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/DisconnectedPlayer.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/DisconnectedPlayer.java index e1400cd..f38151a 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/DisconnectedPlayer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/DisconnectedPlayer.java @@ -17,7 +17,7 @@ public class DisconnectedPlayer extends NetworkEvent implements Serializable{ * @param username the name of the player requesting the event */ public DisconnectedPlayer(String username){ - super(username, EventType.SKIP_PLAYER_DISCONNECTED, false); + super(username, EventType.DISCONNECTED_PLAYER, false); } /** diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipNoDrawable.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipNoDrawable.java index 3f84ec8..249c218 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipNoDrawable.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipNoDrawable.java @@ -17,7 +17,7 @@ public class SkipNoDrawable extends NetworkEvent implements Serializable{ * @param username the name of the player requesting the event */ public SkipNoDrawable(String username){ - super(username, EventType.SKIP_PLAYER_DISCONNECTED, false); + super(username, EventType.SKIP_NO_DRAWABLE, false); } /** diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java index d148ac5..e9862ad 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java @@ -43,6 +43,9 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { private LimitedMap playerList; private boolean serverCrashed; + public void setServerCrashed(boolean serverCrashed) { + this.serverCrashed = serverCrashed; + } public RMIServer(GameController controller, int nPort, BlockingQueue actionQueue, diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java index eaea963..e2d5215 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/TCPServer.java @@ -36,6 +36,10 @@ public class TCPServer { List clientHandlers; boolean serverCrashed; + public void setServerCrashed(boolean serverCrashed) { + this.serverCrashed = serverCrashed; + } + // Mappa temporanea: username → ClientHandler // Serve per associare il socket heartbeat al giusto ClientHandler private final Map pendingHeartbeat = new ConcurrentHashMap<>(); diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java index 294400a..309200c 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java @@ -5,6 +5,7 @@ import it.polimi.ingsw.gc14.Controller.GameController; import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Model.GamePackage.GameStages; import it.polimi.ingsw.gc14.Model.Player; +import it.polimi.ingsw.gc14.Network.EventType; import it.polimi.ingsw.gc14.Network.NetworkEvent; import it.polimi.ingsw.gc14.Network.NetworkEvents.DisconnectedPlayer; import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; @@ -17,8 +18,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.rmi.RemoteException; import java.util.*; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.*; import java.net.*; @@ -64,9 +64,13 @@ public class ServerLauncher { */ static LimitedMap playerList; - TUI view; + static TUI view; + // Campo da aggiungere in ServerLauncher + private final ScheduledExecutorService timerExecutor = Executors.newSingleThreadScheduledExecutor(); + private ScheduledFuture disconnectionTimer; + /** * Class constructor that initializes the attributes. * @param actionQueue The queue containing the events @@ -93,23 +97,57 @@ public class ServerLauncher { */ public boolean doFirstEvent() throws InterruptedException, RemoteException { NetworkEvent event = actionQueue.take(); - synchronized(gameController){ - - event.setIsError(!event.apply(gameController)); - serverRMI.notifyAll(event); - serverTCP.notifyAll(event); - - if(!event.getIsError()){ - if(this.gameController.getModel().getCurrentState().getGameStage() == GameStages.ENDED){ - this.deleteSave(); - } - else if(!this.gameSave() ){ - System.out.println("\n!!! Save failed !!!\n"); - } + if(gameController.getModel()!=null && !gameController.getModel().getCurrentState().equals(GameStages.ENDED)) + { + if(disconnectionTimer!=null && event.getEventType() != EventType.RECONNECT_PLAYER) + { + event.setIsError(true); + serverRMI.notifyAll(event); + serverTCP.notifyAll(event); + return false; + } + if (event.getEventType() == EventType.RECONNECT_PLAYER && disconnectionTimer != null && !disconnectionTimer.isDone()) { + disconnectionTimer.cancel(false); + disconnectionTimer = null; + } + synchronized(gameController){ + event.setIsError(!event.apply(gameController)); + serverRMI.notifyAll(event); + serverTCP.notifyAll(event); + if(!event.getIsError()){ + if(this.gameController.getModel().getCurrentState().getGameStage() == GameStages.ENDED){ + this.deleteSave(); + for(Map.Entry entry:playerList.entrySet()){ + if(entry.getValue()) + playerList.remove(entry.getKey()); + } + serverRMI.setServerCrashed(false); + serverTCP.setServerCrashed(false); + } + else if(!this.gameSave() ){ + System.out.println("\n!!! Save failed !!!\n"); + } + + } + if (event.getEventType().equals(EventType.DISCONNECTED_PLAYER) && playerList.values().stream().filter(x -> x).count() == 1) { + if (disconnectionTimer != null && !disconnectionTimer.isDone()) { + disconnectionTimer.cancel(false); + } + disconnectionTimer = timerExecutor.schedule(() -> { + System.out.println("Timer scaduto: nessun giocatore riconnesso in 30s."); + }, 500, TimeUnit.MILLISECONDS); + } + return !event.getIsError(); } - //actionQueue.offer(new DisconnectedPlayer(gameController.getModel().getCurrentState().getCurrentPlayer().getUserName())); - return !event.getIsError(); } + else{ + if(event.getEventType().equals(EventType.DISCONNECTED_PLAYER)) + { + playerList.remove(event.getUsername()); + } + return false; + } + } @@ -153,14 +191,25 @@ public class ServerLauncher { playerList.setAction(()->{ new Thread(()->{ try { - launcher.run(); - } catch (InterruptedException e) { - throw new RuntimeException(e); + System.out.println("\n\nNotifying model"); + serverRMI.notifyAll(gameController.getModel()); + serverTCP.notifyAll(gameController.getModel()); + view = new TUI(gameController.getModel()); + view.fullRender(); } catch (RemoteException e) { throw new RuntimeException(e); } }).start(); }); + new Thread(()-> { + try { + launcher.run(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + }).start(); serverRMI.start(serverCrashed); new Thread(()->{serverTCP.start(serverCrashed);}).start(); @@ -175,12 +224,6 @@ public class ServerLauncher { * @throws RemoteException if an RMI error occurs */ public void run() throws InterruptedException, RemoteException { - // Game creation - System.out.println("\n\nNotifying model"); - serverRMI.notifyAll(gameController.getModel()); - serverTCP.notifyAll(gameController.getModel()); - this.view = new TUI(gameController.getModel()); - this.view.fullRender(); // Game execution while (true) { From c7ca9ff22ee4823b870fe32eea3e9a90432c3737 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Tue, 12 May 2026 14:09:46 +0200 Subject: [PATCH 17/17] Changed: disconnection timeout --- src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java index 309200c..d478125 100644 --- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java @@ -135,7 +135,7 @@ public class ServerLauncher { } disconnectionTimer = timerExecutor.schedule(() -> { System.out.println("Timer scaduto: nessun giocatore riconnesso in 30s."); - }, 500, TimeUnit.MILLISECONDS); + }, 30, TimeUnit.SECONDS); } return !event.getIsError(); }