429 lines
19 KiB
Java
429 lines
19 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.*;
|
|
|
|
|
|
/**
|
|
* 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 */
|
|
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;
|
|
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<String,Boolean> 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.
|
|
*
|
|
* <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;
|
|
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.Entry<Player,Boolean>entry: 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.
|
|
*
|
|
* <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();
|
|
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.
|
|
*
|
|
* <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;
|
|
}
|
|
}
|
|
}
|