372 lines
16 KiB
Java
372 lines
16 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.DisconnectedPlayer;
|
|
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
|
|
* @throws RemoteException if an RMI error occurs
|
|
*/
|
|
public boolean doFirstEvent() throws InterruptedException, RemoteException {
|
|
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 roundPrec=gameController.getModel().getCurrentState().getRound();
|
|
synchronized(gameController){
|
|
event.setIsError(!event.apply(gameController));
|
|
Game game=gameController.getModel();
|
|
if(!event.getIsError())
|
|
{
|
|
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());
|
|
}
|
|
serverRMI.notifyAll(event);
|
|
serverTCP.notifyAll(event);
|
|
if(game.getCurrentState().getRound()!=roundPrec)
|
|
{
|
|
ApplyNextRound nextRound=new ApplyNextRound(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayers());
|
|
serverRMI.notifyAll(nextRound);
|
|
serverTCP.notifyAll(nextRound);
|
|
}
|
|
if(!event.getIsError()){
|
|
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");
|
|
}
|
|
|
|
}
|
|
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());
|
|
if(playerList.isEmpty())
|
|
{
|
|
gameController.setModel(null);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* The first method executed when the server program is launched.
|
|
* It creates all the objects needed: playerList, actionQueue, gameController, serverRMI, serverTCP, launcher.
|
|
* Then sets the playerList's action to execute launcher.run() and starts the TCP/RMI servers.
|
|
* Note: the model is initialized and set in the controller in TCP/RMI servers when the first user decides the number of players.
|
|
*
|
|
* @throws InterruptedException if this exception is issued by run method
|
|
* @throws RemoteException if this exception is issued by run method
|
|
*/
|
|
public static void main(String[] args) throws InterruptedException, 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));
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
|
|
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(()->{
|
|
try {
|
|
System.out.println("\n\nNotifying model");
|
|
MiniModel miniModel;
|
|
synchronized (gameController) {
|
|
Game game = gameController.getModel();
|
|
miniModel= new MiniModel(game.getBoard(),game.getSlotMap(), game.orderLogicCard,game.getCurrentState(),game.getPlayers(), new ArrayList<>(List.of(Totems.values())));
|
|
}
|
|
serverRMI.notifyAll(miniModel);
|
|
serverTCP.notifyAll(miniModel);
|
|
view = new TUI(miniModel);
|
|
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(serverCrashed);
|
|
new Thread(()->{serverTCP.start(serverCrashed);}).start();
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates and executes the game.
|
|
* Game creation: TCP/RMI servers send the game model to all players.
|
|
* Game execution: repeatedly calls doFirstEvent() to process the events in the actionQueue.
|
|
* @throws InterruptedException if the TCP server thread is interrupted
|
|
* @throws RemoteException if an RMI error occurs
|
|
*/
|
|
public void run() throws InterruptedException, RemoteException {
|
|
|
|
// Game execution
|
|
while (true) {
|
|
try{
|
|
this.doFirstEvent();
|
|
if(view!=null)
|
|
{
|
|
view.fullRender();
|
|
}
|
|
}
|
|
catch(InterruptedException e){
|
|
Thread.currentThread().interrupt();
|
|
break;
|
|
}
|
|
catch(RemoteException e){
|
|
System.out.println("Exception:"+e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|