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..3074ec1 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,13 @@ public class Game implements Serializable {
{
return false;
}
- return disconnetedPlayers.put(player,false);
+ disconnetedPlayers.put(player,false);
+ if(currentState.getGameStage().equals(GameStages.SLOT_CHOICE))
+ {
+ disconnetedPlayers.remove(player);
+ orderLogicCard.pushNoEffect(player);
+ }
+ 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 b7baf63..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,42 +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()){
- if(controller.getModel() == null){
- model = new Game(preferredInt);
- controller.setModel(model);
- playerList.setLimit(preferredInt);
- }
- else{
- model = controller.getModel();
- }
+ @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.
@@ -210,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);
- }
- }
@@ -249,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) {
@@ -279,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();
+ }
+
+
+
}