playerList,
+ String host) throws RemoteException {
this.controller = controller;
this.nPort = nPort;
this.actionQueue = actionQueue;
@@ -67,37 +58,123 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
this.host = host;
}
- /**
- * 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
- */
- public boolean joinGame(String username, int preferredInt, IClientCallback callback) {
- 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.add(username);
- System.out.println("Accepted player: " + username);
- return true;
- }
- return false;
- }
+ // -------------------------------------------------------------------------
+ // Join
+ // -------------------------------------------------------------------------
+ /**
+ * {@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()}.
+ */
+ @Override
+ public boolean joinGame(String username, int preferredInt, IClientCallback callback)
+ throws RemoteException {
+ if (preferredInt < 2 || preferredInt > 5) return false;
+
+ synchronized (controller) {
+ 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(controller.getModel());
+ 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(controller.getModel());
+ 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 +282,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);
- }
- }
@@ -242,29 +290,25 @@ 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); // 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 +318,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();
+ }
+
+
+
}
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..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
@@ -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,82 @@ 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) {
+ 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;
- } else {
- Thread listener = new Thread(() -> receiveMessage());
- listener.start();
- return true;
}
+ new Thread(this::receiveMessage, "tcp-reader").start();
+
+ // 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();
+ new Thread(this::heartbeatLoop, "heartbeat").start();
+
+ running = true;
+
+
+ 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.
@@ -94,7 +161,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());
@@ -102,6 +168,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();
@@ -216,7 +283,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 83b482c..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
@@ -1,7 +1,9 @@
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.DisconnectedPlayer;
import java.io.*;
import java.net.*;
@@ -20,6 +22,9 @@ public class ClientHandler implements Runnable {
*/
private final String username;
+ private boolean running ;
+
+ private Game game;
/**
* Returns the username associated with this client.
*
@@ -43,6 +48,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,13 +65,14 @@ 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;
this.out = out;
this.clientHandlers = clientHandlers;
this.actionQueue = actionQueue;
+ this.limitedMap = playersMap;
}
@@ -74,11 +83,13 @@ public class ClientHandler implements Runnable {
@Override
public void run() {
try {
-
- while (true) {
- NetworkEvent event = (NetworkEvent) in.readObject();
- if (!actionQueue.add(event)) {
- System.out.println("Error inserting action into queue");
+ running = true;
+ while (running) {
+ synchronized (out){
+ NetworkEvent event = (NetworkEvent) in.readObject();
+ if (!actionQueue.add(event)) {
+ System.out.println("Error inserting action into queue");
+ }
}
}
} catch (IOException e) {
@@ -90,6 +101,7 @@ public class ClientHandler implements Runnable {
}
+
/**
* Sends a {@link NetworkEvent} to the client.
* @param event The network event to send to the client.
@@ -109,10 +121,20 @@ 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) {
e.printStackTrace();
}
}
+
+ public void disconnect() {
+ running = false;
+ clientHandlers.remove(this);
+ limitedMap.put(username, false);
+ actionQueue.add(new DisconnectedPlayer(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
new file mode 100644
index 0000000..a0dde68
--- /dev/null
+++ b/src/main/java/it/polimi/ingsw/gc14/Network/TCP/Server/HeartbeatHandler.java
@@ -0,0 +1,76 @@
+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.SynchronousQueue;
+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() {
+ 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 5380e36..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
@@ -1,161 +1,238 @@
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.Model.Player;
+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.*;
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 */
+ final GameController controller;
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;
+ boolean serverCrashed;
+ public void setServerCrashed(boolean serverCrashed) {
+ this.serverCrashed = serverCrashed;
+ }
- /** List containing all client's handlers */
- private List clientHandlers;
+ // Mappa temporanea: username → ClientHandler
+ // Serve per associare il socket heartbeat al giusto ClientHandler
+ private final Map pendingHeartbeat = new ConcurrentHashMap<>();
-
- /**
- * 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(boolean serverCrashed) {
+ this.serverCrashed = serverCrashed;
+ 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));
- 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++;
+ AddPlayer eventAddPlayer = (AddPlayer) event;
- 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");
- }
- }
+ if (eventAddPlayer.getProposedNPlayer() < 2 || eventAddPlayer.getProposedNPlayer() > 5) {
+ clientSocket.getOutputStream().write(-1);
+ clientSocket.close();
+ System.out.println("Invalid parameters. Connection terminated.");
+ continue;
}
+
+ synchronized (controller) {
+
+ 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());
+ }
+ 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.");
+ }
+ }
+
+ }
+
+
}
- 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..d478125 100644
--- a/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java
+++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java
@@ -3,16 +3,22 @@ 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.EventType;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
-import it.polimi.ingsw.gc14.Network.NetworkEvents.AddPlayer;
+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;
+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;
-import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.*;
import java.net.*;
@@ -21,10 +27,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,15 +58,19 @@ 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;
+ 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
@@ -72,6 +82,7 @@ public class ServerLauncher {
this.actionQueue = actionQueue;
this.serverRMI = serverRMI;
this.gameController = gameController;
+ this.gameController.setModel(loadSave());
this.serverTCP = serverTCP;
}
@@ -86,12 +97,57 @@ public class ServerLauncher {
*/
public boolean doFirstEvent() throws InterruptedException, RemoteException {
NetworkEvent event = actionQueue.take();
- event.setIsError(!event.apply(gameController));
+ 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");
+ }
- serverRMI.notifyAll(event);
- serverTCP.notifyAll(event);
+ }
+ 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.");
+ }, 30, TimeUnit.SECONDS);
+ }
+ return !event.getIsError();
+ }
+ }
+ else{
+ if(event.getEventType().equals(EventType.DISCONNECTED_PLAYER))
+ {
+ playerList.remove(event.getUsername());
+ }
+ return false;
+ }
- return !event.getIsError();
}
@@ -105,10 +161,11 @@ 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;
+ boolean serverCrashed;
try {
IP=chooseNetworkInterface(new Scanner(System.in));
} catch (Exception e) {
@@ -116,23 +173,46 @@ 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);
-
+ 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;
+ }
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();
- new Thread(()->{serverTCP.start();}).start();
+ serverRMI.start(serverCrashed);
+ new Thread(()->{serverTCP.start(serverCrashed);}).start();
}
@@ -144,26 +224,23 @@ 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) {
- 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<>();
@@ -196,4 +273,67 @@ public class ServerLauncher {
int choice = Integer.parseInt(scanner.nextLine().trim());
return ips.get(choice);
}
+
+ private boolean gameSave(){
+ 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;
+ } 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;
+ }
+ }
}