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.MiniModel; import it.polimi.ingsw.gc14.Model.Player; import it.polimi.ingsw.gc14.Model.Totems; import it.polimi.ingsw.gc14.Network.EventType; import it.polimi.ingsw.gc14.Network.NetworkEvent; import it.polimi.ingsw.gc14.Network.NetworkEvents.ApplyNextRound; import it.polimi.ingsw.gc14.Network.NetworkEvents.EndedGame; import it.polimi.ingsw.gc14.Network.NetworkEvents.TotemChoice; 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.*; import java.net.*; /** * Main server launcher that handles both TCP and RMI connections. * 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 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 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: * - The TCP/RMI server receives a {@link NetworkEvent} from a client and adds it to the {@link #actionQueue} * - The {@link #run()} method repeatedly calls {@link #doFirstEvent()}, which takes the first event in the {@link #actionQueue} and tries to apply it * - If the event cannot be successfully applied to the model, its {@code isError} flag is set to {@code true} * - All players are notified of the event */ public class ServerLauncher { /** * Queue containinetworkTypeng the events to be applied to the game model. * Thread safe by design. */ BlockingQueue actionQueue; /** Game controller. Used to apply events */ GameController gameController; /** Server RMI. Handles RMI clients */ RMIServer serverRMI; /** Server TCP. Handles TCP clients */ TCPServer serverTCP; /** * List containing the usernames of joined players. * {@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 LimitedMap playerList; 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 * @param gameController The game controller * @param serverRMI The server RMI * @param serverTCP The server TCP */ public ServerLauncher(BlockingQueue actionQueue, GameController gameController, RMIServer serverRMI, TCPServer serverTCP) { this.actionQueue = actionQueue; this.serverRMI = serverRMI; this.gameController = gameController; this.gameController.setModel(loadSave()); this.serverTCP = serverTCP; } /** * Takes the first event in the actionQueue and attempts to apply it to the game controller. * If the event cannot be applied, its isError flag is set to true; otherwise, it is set to false. * All clients (both TCP and RMI) are notified of the event * @return the outcome of applying the event to the controller * @throws InterruptedException if an error occurs while accessing the actionQueue */ public boolean doFirstEvent() throws InterruptedException { NetworkEvent event = actionQueue.take(); 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; } int roundPrev=gameController.getModel().getCurrentState().getRound(); synchronized(gameController){ event.setIsError(!event.apply(gameController)); Game game=gameController.getModel(); if(!event.getIsError()) { if(event.getEventType().equals(EventType.DISCONNECTED_PLAYER)&& game.getCurrentState().getGameStage().equals(GameStages.WAITING)) { playerList.remove(event.getUsername()); }else { event.setData(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(), game.getPlayerByUsername(event.getUsername())); if (event.getEventType().equals(EventType.TOTEM_CHOICE)) { ((TotemChoice) event).setAvailableTotems(gameController.getModel().getAvailableTotems()); } else if (event.getEventType().equals(EventType.DISCONNECTED_PLAYER) && game.getCurrentState().equals(GameStages.WAITING)) { playerList.remove(event.getUsername()); } } if(game.getCurrentState().getGameStage() == GameStages.ENDED){ if(!this.deleteSave()){ System.out.println("\n!!! Couldn't delete save !!!\n"); } 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(game.getCurrentState().getRound()!=roundPrev) { ApplyNextRound nextRound=new ApplyNextRound(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayers(),game.getUpperListTribeCards(),game.getLowerListTribeCards(),game.getUpperListBuilding(),game.getLowerListBuilding()); serverRMI.notifyAll(nextRound); serverTCP.notifyAll(nextRound); } else if(game.getCurrentState().getGameStage().equals(GameStages.ENDED)) { serverRMI.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding())); serverTCP.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding())); } if (event.getEventType().equals(EventType.DISCONNECTED_PLAYER) && playerList.values().stream().filter(x -> x).count() == 1&& game.getCurrentState().getGameStage() != GameStages.ENDED) { if (disconnectionTimer != null && !disconnectionTimer.isDone()) { disconnectionTimer.cancel(false); } disconnectionTimer = timerExecutor.schedule(() -> { game.EndGameForFeit(); serverRMI.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding())); serverTCP.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding())); System.out.println("Timer expired: no player reconnected in 60s."); if(!this.deleteSave()){ System.out.println("\n!!! Couldn't delete save !!!\n"); } }, 1, TimeUnit.MINUTES); } return !event.getIsError(); } } else{ if(event.getEventType().equals(EventType.DISCONNECTED_PLAYER)) { playerList.remove(event.getUsername()); if(playerList.isEmpty()) { gameController.setModel(null); } } return false; } } /** * Entry point of the server application. * *

The method initializes the shared player list, the network event queue, * the game controller, the RMI server, the TCP server, and the server launcher. * It also selects the network interface to expose, configures the server crash * recovery state, and defines the action to execute once the required number * of players has joined the game. * *

When the player list reaches its limit, the current game model is converted * into a {@link MiniModel}, sent to all connected clients through both RMI and TCP, * and rendered on the server-side TUI. Finally, the event-processing launcher * and both network servers are started. * *

The game model itself is initialized by the TCP or RMI server when the first * player joins and selects the total number of players. * * @param args the command-line arguments passed to the server application. * @throws RemoteException if the RMI server cannot be created. */ public static void main(String[] args) throws RemoteException { playerList = new LimitedMap(5, ()->{}); BlockingQueue actionQueue = new LinkedBlockingQueue<>(); GameController gameController = new GameController(); String IP; boolean serverCrashed; try { IP=chooseNetworkInterface(new Scanner(System.in)); System.out.println(IP); } catch (Exception e) { throw new RuntimeException(e); } System.setProperty("java.rmi.server.hostname", IP); RMIServer serverRMI = new RMIServer(gameController, 1099, actionQueue, playerList,IP); TCPServer serverTCP = new TCPServer(gameController, 8080, 8081,actionQueue, playerList); ServerLauncher launcher = new ServerLauncher(actionQueue, gameController, serverRMI, serverTCP); if(gameController.getModel() != null){ playerList.setLimit(gameController.getModel().getNPlayers()); 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(()->{ System.out.println("\n\nNotifying model"); MiniModel miniModel; synchronized (gameController) { Game game = gameController.getModel(); miniModel= new MiniModel(game.getSlotMap(), game.orderLogicCard,game.getCurrentState(),game.getPlayers(), new ArrayList<>(List.of(Totems.values())),game.getUpperListTribeCards(),game.getLowerListTribeCards(),game.getUpperListBuilding(),game.getLowerListBuilding()); } serverRMI.notifyAll(miniModel); serverTCP.notifyAll(miniModel); view = new TUI(miniModel); view.fullRender(); }).start(); }); new Thread(()-> { try { launcher.run(); } catch (Exception e) { System.out.println("Generic exception occurred"); e.printStackTrace(); } }).start(); serverRMI.start(serverCrashed); new Thread(()->{serverTCP.start(serverCrashed);}).start(); System.out.println("Server RMI: "+System.getProperty("java.rmi.server.hostname")); } /** * Executes the main game loop. * *

The method repeatedly processes the first event in the action queue * and, when a view is available, updates the rendered game state. * *

If the thread is interrupted while waiting for an event, * the interruption status is restored and the loop terminates. */ public void run() { while (true) { try{ this.doFirstEvent(); if(view!=null) { view.fullRender(); } } catch(InterruptedException e){ Thread.currentThread().interrupt(); break; } } } /** * Lets the user choose the network interface to be used by the server. * *

The method scans all active, non-loopback, and non-virtual network * interfaces, collecting their IPv4 addresses. If only one valid address is * found, it is selected automatically. Otherwise, the available addresses are * printed and the user is asked to choose one by index. * * @param scanner the scanner used to read the user's selection. * @return the IPv4 address of the selected network interface. * @throws Exception if no valid network interface is available. */ public static String chooseNetworkInterface(Scanner scanner) throws Exception { List ips = new ArrayList<>(); // Lista tutte le interfacce attive con IP reale Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); while (interfaces.hasMoreElements()) { NetworkInterface ni = interfaces.nextElement(); // Salta loopback, interfacce spente o virtuali if (!ni.isUp() || ni.isLoopback() || ni.isVirtual()) continue; Enumeration addresses = ni.getInetAddresses(); while (addresses.hasMoreElements()) { InetAddress addr = addresses.nextElement(); // Solo IPv4 if (addr instanceof Inet4Address) { System.out.println("[" + ips.size() + "] " + ni.getDisplayName() + " -> " + addr.getHostAddress()); ips.add(addr.getHostAddress()); } } } if (ips.isEmpty()) throw new Exception("Nessuna interfaccia disponibile"); if (ips.size() == 1) { System.out.println("Una sola interfaccia trovata, uso: " + ips.get(0)); return ips.get(0); } System.out.print("Scegli interfaccia: "); int choice = Integer.parseInt(scanner.nextLine().trim()); return ips.get(choice); } /** * Saves the current game model to a local file. * *

The save file is stored in the {@code GameSaves/save.dat} path relative * to the server executable location. If the directory does not exist, it is * created before writing the serialized game model. * * @return {@code true} if the game is saved successfully, * {@code false} otherwise. */ 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); } } /** * Loads a previously saved game model from the local save file. * *

The method attempts to deserialize the game stored in * {@code GameSaves/save.dat}. If no save file exists or an I/O error occurs, * {@code null} is returned. * * @return the loaded {@link Game} instance, or {@code null} if no valid save * can be loaded. */ 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); } } /** * Deletes the current local game save file. * * @return {@code true} if the save file is deleted successfully, * {@code false} otherwise. */ 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 | IOException e){ return false; } } }