Files
Progetto-ingegneria-del-sof…/src/main/java/it/polimi/ingsw/gc14/ServerLauncher.java
T
2026-05-27 20:09:48 +02:00

457 lines
21 KiB
Java

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.*;
import java.util.stream.Collectors;
/**
* 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<NetworkEvent> actionQueue;
/** Game controller. Used to apply events */
final 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<String,Boolean> 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<NetworkEvent> actionQueue, GameController gameController, RMIServer serverRMI, TCPServer serverTCP) {
this.actionQueue = actionQueue;
this.serverRMI = serverRMI;
this.gameController = gameController;
Game game= loadSave();
if(game!=null && game.disconnetedPlayers.entrySet().stream().filter(Map.Entry::getValue).count() >=game.getNPlayers()-1){
gameController.setModel(null);
this.deleteSave();
}
else
this.gameController.setModel(game);
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();
//Verify that the model exist and if it exists that's not ended
if(gameController.getModel()!=null && gameController.getModel().getCurrentState().getGameStage()!=GameStages.ENDED)
{
//if there is only one player ignore every event different by reconnection
if(disconnectionTimer!=null && event.getEventType() != EventType.RECONNECT_PLAYER)
{
event.setIsError(true);
serverRMI.notifyAll(event);
serverTCP.notifyAll(event);
return false;
}
//Cancel the timer if another player is reconnected , so there are more than one player
if (event.getEventType() == EventType.RECONNECT_PLAYER && disconnectionTimer != null && !disconnectionTimer.isDone()) {
disconnectionTimer.cancel(false);
disconnectionTimer = null;
}
synchronized(gameController){
int roundPrev=gameController.getModel().getCurrentState().getRound();
//applies the event and set if is an error
event.setIsError(!event.apply(gameController));
Game game=gameController.getModel();
//if the event wasn't an error , it will be sent to players
if(!event.getIsError())
{
if(!this.gameSave() ){
System.out.println("\n!!! Save failed !!!\n");
}
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.getPlayers());
if (event.getEventType().equals(EventType.TOTEM_CHOICE)) {
((TotemChoice) event).setAvailableTotems(gameController.getModel().getAvailableTotems());
} else if (event.getEventType().equals(EventType.DISCONNECTED_PLAYER) && game.getCurrentState().getGameStage().equals(GameStages.WAITING)) {
playerList.remove(event.getUsername());
}
}
//if there is only one player the game will be suspended and starts the forfeit timer
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);
}
//schedule endgame for forfeit
disconnectionTimer = timerExecutor.schedule(() -> {
synchronized (gameController)
{
gameController.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.");
for(Map.Entry<String,Boolean> entry:playerList.entrySet()){
if(!entry.getValue())
playerList.remove(entry.getKey());
}
if(!this.deleteSave()){
System.out.println("\n!!! Couldn't delete save !!!\n");
}
disconnectionTimer = null;
}
}, 1, TimeUnit.MINUTES);
}
//if the round is changed send a new next round event , the previous event is ignored and directly sent the next round(also upper and lower lists updated)
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);
}
// if the game ends after the event, delete the game. await that all players are disconnected and then create a new game
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(!this.deleteSave()){
System.out.println("\n!!! Couldn't delete save !!!\n");
}
for(Map.Entry<String,Boolean> entry:playerList.entrySet()){
if(!entry.getValue())
playerList.remove(entry.getKey());
}
}
//notify the event
else {
serverRMI.notifyAll(event);
serverTCP.notifyAll(event);
}
}
else {
serverRMI.notifyAll(event);
serverTCP.notifyAll(event);
}
return !event.getIsError();
}
}
else{
// removes disconneted players when the game is ended
if(event.getEventType().equals(EventType.DISCONNECTED_PLAYER))
{
synchronized (gameController) {
playerList.remove(event.getUsername());
if (playerList.isEmpty()) {
gameController.setModel(null);
System.out.println("\n!!! Player list is now empty, ready for a new game init !!!\n");
}
return true;
}
}
return false;
}
}
/**
* Entry point of the server application.
*
* <p>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.
*
* <p>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.
*
* <p>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<String,Boolean>(5, ()->{});
BlockingQueue<NetworkEvent> actionQueue = new LinkedBlockingQueue<>();
GameController gameController = new GameController();
String IP;
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.Entry<Player,Boolean>entry: gameController.getModel().disconnetedPlayers.entrySet())
{
if(entry.getValue())
{
playerList.put(entry.getKey().getUserName(),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(), game.getAvailableTotems(),game.getUpperListTribeCards(),game.getLowerListTribeCards(),game.getUpperListBuilding(),game.getLowerListBuilding(), game.disconnetedPlayers.entrySet().stream().filter(Map.Entry::getValue).map(x -> x.getKey().getUserName()).collect(Collectors.toCollection(ArrayList::new)));
}
serverRMI.notifyAll(miniModel);
serverTCP.notifyAll(miniModel);
}).start();
});
new Thread(()-> {
try {
launcher.run();
}
catch (Exception e)
{
System.out.println("Generic exception occurred");
e.printStackTrace();
}
}).start();
serverRMI.start();
new Thread(()->{serverTCP.start();}).start();
System.out.println("Server RMI: "+System.getProperty("java.rmi.server.hostname"));
}
/**
* Executes the main game loop.
*
* <p>The method repeatedly processes the first event in the action queue
* and, when a view is available, updates the rendered game state.
*
* <p>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();
}
catch (ConcurrentModificationException e)
{
System.err.println("Concurrent Exception");
}
catch(InterruptedException e){
Thread.currentThread().interrupt();
break;
}
}
}
/**
* Lets the user choose the network interface to be used by the server.
*
* <p>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<String> ips = new ArrayList<>();
// Lista tutte le interfacce attive con IP reale
Enumeration<NetworkInterface> 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<InetAddress> 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.
*
* <p>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.
*
* <p>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;
}
}
}