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/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/Controller/GameController.java b/src/main/java/it/polimi/ingsw/gc14/Controller/GameController.java index 3b1de8a..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; /** @@ -30,7 +29,22 @@ public class GameController { */ public GameController() { } - + //TODO + public boolean DisconnectedPlayer(String username) + { + Player player= model.getPlayerByUsername(username); + if(player==null) + return false; + return model.DisconnectedPlayer(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/LimitedList.java b/src/main/java/it/polimi/ingsw/gc14/LimitedList.java deleted file mode 100644 index 5ac39ff..0000000 --- a/src/main/java/it/polimi/ingsw/gc14/LimitedList.java +++ /dev/null @@ -1,75 +0,0 @@ -package it.polimi.ingsw.gc14; -import java.util.ArrayList; - -/** - * An {@link ArrayList} 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. - * - * @param the type of elements held in this list. - */ -public class LimitedList extends ArrayList { - - /** - * The maximum number of elements allowed in the list before the action is triggered. - */ - private int limit; - - /** - * The action to execute when the list size reaches or exceeds the limit. - */ - private Runnable action; - - /** - * Creates a new {@code LimitedList} 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) { - 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. - * - * @param element the element to add. - * @return {@code true} if the element was successfully added. - */ - @Override - public boolean add(T element) { - boolean result = super.add(element); - if (size() >= limit) { - action.run(); - } - return result; - } - - /** - * Sets a new size limit for this list. - * - * @param num the new limit. - */ - public void setLimit(int num) { - this.limit = num; - } - - /** - * Returns the current size limit of this list. - * - * @return the current limit. - */ - public int getLimit() { - return limit; - } - - /** - * Sets a new action to execute when the list size reaches or exceeds the limit. - * - * @param action the new action to set. - */ - public void setAction(Runnable action) { - this.action = action; - } -} \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java b/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java new file mode 100644 index 0000000..d4833ae --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/LimitedMap.java @@ -0,0 +1,117 @@ +package it.polimi.ingsw.gc14; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * 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 keys maintained by this map. + * @param the type of mapped values. + */ +public class LimitedMap implements Map { + + private final LinkedHashMap map = new LinkedHashMap<>(); + + /** + * The maximum number of elements allowed in the map before the action is triggered. + */ + private volatile int limit; + + /** + * The action to execute when the map size reaches or exceeds the limit. + */ + private volatile Runnable 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 LimitedMap(int limit, Runnable action) { + this.limit = limit; + this.action = action; + } + + /** + * 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 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 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 && added) { + 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 map. + * + * @param num the new limit. + */ + public void setLimit(int num) { this.limit = num; } + + /** + * Returns the current size limit of this map. + * + * @return the current limit. + */ + public int getLimit() { return 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; } +} \ 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 d939baf..b549192 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java @@ -41,6 +41,44 @@ public class Game implements Serializable { return playersList; } + //TODO + public Map disconnetedPlayers = new HashMap<>(); + //TODO + public boolean DisconnectedPlayer(Player player) + { + if(disconnetedPlayers.containsKey(player) && disconnetedPlayers.get(player)) + { + return false; + } + disconnetedPlayers.put(player,true); + if(currentState.getCurrentPlayer().equals(player)) + nextPlayerSetup(); + return true; + } + + //TODO + public boolean ReconnectPlayer(Player player) + { + if(!disconnetedPlayers.containsKey(player)) + { + return false; + } + disconnetedPlayers.put(player,false); + if(currentState.getGameStage().equals(GameStages.SLOT_CHOICE) ) + { + disconnetedPlayers.remove(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. @@ -607,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; @@ -652,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()); @@ -679,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; } @@ -718,6 +765,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/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/EventType.java b/src/main/java/it/polimi/ingsw/gc14/Network/EventType.java index 46a6d15..8d01f34 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,8 @@ public enum EventType { DRAW_LOWER_BUILD, PICK_OPTIONAL_TRIBE, PICK_OPTIONAL_BUILD, - SKIP_UPPER, - SKIP_LOWER, + SKIP_NO_DRAWABLE, + 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 new file mode 100644 index 0000000..f38151a --- /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.DISCONNECTED_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.DisconnectedPlayer(username); + } + +} + 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/NetworkEvents/SkipNoDrawable.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/SkipNoDrawable.java index 7651ba1..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_LOWER, false); + super(username, EventType.SKIP_NO_DRAWABLE, false); } /** 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..e9ee1f0 --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java @@ -0,0 +1,105 @@ +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.DisconnectedPlayer; + +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 DisconnectedPlayer} 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); + 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 66d03d2..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 @@ -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.*; @@ -24,42 +24,33 @@ 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; + private LimitedMap playerList; - /** - * 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; + private boolean serverCrashed; + public void setServerCrashed(boolean serverCrashed) { + this.serverCrashed = serverCrashed; + } - - /** - * 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,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; @@ -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; + } + } }