diff --git a/src/main/java/it/polimi/ingsw/gc14/GameEventProcessor.java b/src/main/java/it/polimi/ingsw/gc14/GameEventProcessor.java new file mode 100644 index 0000000..e8dd9c9 --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/GameEventProcessor.java @@ -0,0 +1,372 @@ +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.Network.ClientBroadcaster; +import it.polimi.ingsw.gc14.Network.EventType; +import it.polimi.ingsw.gc14.Network.NetworkEvent; +import it.polimi.ingsw.gc14.Network.NetworkEvents.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +/** + * Processes game events from the action queue and routes each one + * based on the current game state. + * + *

The three top-level states that determine routing are: + *

+ * + *

This class is not thread-safe by itself: it relies on the caller + * (the game loop in {@code ServerLauncherTest}) to drive it from a single + * thread via {@link #doFirstEvent()}. + */ +public class GameEventProcessor { + + private final BlockingQueue actionQueue; + private final GameController gameController; + private final LimitedMap playerList; + private final ClientBroadcaster broadcaster; + private final SaveManager saveManager; + + /** Single-thread executor used exclusively for the forfeit timer. */ + private final ScheduledExecutorService timerExecutor = + Executors.newSingleThreadScheduledExecutor(); + + /** + * Handle to the running forfeit timer, or {@code null} when no timer is active. + * A non-null value signals that the game is in the {@em suspended} state. + */ + private ScheduledFuture disconnectionTimer; + + /** + * Constructs a {@code GameEventProcessor} with all required dependencies. + * + * @param actionQueue the queue from which incoming events are consumed. + * @param gameController the server-side game controller. + * @param playerList the shared map tracking each player's online status. + * @param broadcaster the broadcaster used to notify all connected clients. + * @param saveManager the save manager used to persist the game state. + */ + public GameEventProcessor( + BlockingQueue actionQueue, + GameController gameController, + LimitedMap playerList, + ClientBroadcaster broadcaster, + SaveManager saveManager) { + this.actionQueue = actionQueue; + this.gameController = gameController; + this.playerList = playerList; + this.broadcaster = broadcaster; + this.saveManager = saveManager; + } + + // ── Public entry point ──────────────────────────────────────────────────── + + /** + * Blocks until one event is available in the queue, then routes it to the + * appropriate handler based on the current game state. + * + * @throws InterruptedException if the thread is interrupted while waiting + * for the next event. + */ + public void doFirstEvent() throws InterruptedException { + NetworkEvent event = actionQueue.take(); + + if (!isGameActive()) { + handleInactiveGame(event); + } else if (isSuspended()) { + handleSuspendedGame(event); + } else { + applyAndBroadcast(event); + } + } + + // ── State guards ────────────────────────────────────────────────────────── + + /** + * Returns {@code true} when there is an ongoing game that has not yet ended. + */ + private boolean isGameActive() { + Game model = gameController.getModel(); + return model != null + && model.getCurrentState().getGameStage() != GameStages.ENDED; + } + + /** + * Returns {@code true} when the forfeit timer is running, meaning only one + * player is currently online and the game is waiting for a reconnection. + */ + private boolean isSuspended() { + return disconnectionTimer != null; + } + + // ── Inactive game path ──────────────────────────────────────────────────── + + /** + * Handles events that arrive when no active game exists (not yet started, + * or already ended). Only disconnection clean-up is relevant here. + * + * @param event the incoming event. + */ + private void handleInactiveGame(NetworkEvent event) { + if (event.getEventType() != EventType.DISCONNECTED_PLAYER) return; + + 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"); + } + } + } + + // ── Suspended game path ─────────────────────────────────────────────────── + + /** + * Routes events while the game is suspended waiting for a reconnection. + *

+ * + * @param event the incoming event. + */ + private void handleSuspendedGame(NetworkEvent event) { + switch (event.getEventType()) { + case DISCONNECTED_PLAYER -> abortGame(); + case RECONNECT_PLAYER -> applyAndBroadcast(event); + default -> rejectEvent(event); + } + } + + /** + * Cancels the forfeit timer, clears the player list, and resets the model. + * Called when the last remaining player disconnects while the game is suspended. + */ + private void abortGame() { + disconnectionTimer.cancel(true); + disconnectionTimer = null; + playerList.clear(); + gameController.setModel(null); + System.out.println("\n!!! All players disconnected — game aborted, ready for a new game init !!!\n"); + } + + /** + * Marks the event as an error and broadcasts it back to the requesting + * player. Used to reject actions that are not permitted in the current state. + * + * @param event the event to reject. + */ + private void rejectEvent(NetworkEvent event) { + event.setIsError(true); + broadcaster.notifyAll(event); + } + + // ── Active game path ────────────────────────────────────────────────────── + + /** + * Applies the event to the game controller, saves the updated state, + * and broadcasts the result to connected clients. + * + *

The entire method body is synchronized on {@code gameController} to + * prevent concurrent modification of the game model by the network threads. + * + * @param event the event to apply. + */ + private void applyAndBroadcast(NetworkEvent event) { + synchronized (gameController) { + int roundBefore = gameController.getModel().getCurrentState().getRound(); + + event.setIsError(!event.apply(gameController)); + Game game = gameController.getModel(); + + if (event.getIsError()) { + broadcaster.notifyAll(event); + return; + } + + cancelForfeitTimerIfReconnect(event); + + if (!saveManager.save(game)) { + System.out.println("\n!!! Save failed !!!\n"); + } + + // During the lobby phase a disconnection only removes the player + // from the list; no broadcast is needed. + if (event.getEventType() == EventType.DISCONNECTED_PLAYER + && game.getCurrentState().getGameStage() == GameStages.WAITING) { + playerList.remove(event.getUsername()); + return; + } + + enrichEvent(event, game); + startForfeitTimerIfNeeded(event, game); + broadcastResult(event, game, roundBefore); + } + } + + /** + * Cancels the forfeit timer if the event is a successful reconnection. + * + * @param event the event that was just successfully applied. + */ + private void cancelForfeitTimerIfReconnect(NetworkEvent event) { + if (event.getEventType() == EventType.RECONNECT_PLAYER + && disconnectionTimer != null + && !disconnectionTimer.isDone()) { + disconnectionTimer.cancel(false); + disconnectionTimer = null; + } + } + + /** + * Populates the event with the current game state so that clients can + * update their mini-model after receiving it. + * + *

Each event subclass overrides {@link NetworkEvent#enrichWithGameState} + * to append any type-specific extra fields (available totems, etc.). + * + * @param event the event to enrich. + * @param game the current game model. + */ + private void enrichEvent(NetworkEvent event, Game game) { + event.enrichWithGameState(game, buildDisconnectedList(game)); + } + + /** + * Schedules the 60-second forfeit timer if a disconnection has left + * exactly one player online. + * + *

If a previous timer is still pending it is cancelled first to avoid + * duplicate timers. + * + * @param event the event that was just applied. + * @param game the current game model. + */ + private void startForfeitTimerIfNeeded(NetworkEvent event, Game game) { + if (event.getEventType() != EventType.DISCONNECTED_PLAYER) return; + if (game.getCurrentState().getGameStage() == GameStages.ENDED) return; + if (onlinePlayerCount() != 1) return; + + if (disconnectionTimer != null && !disconnectionTimer.isDone()) { + disconnectionTimer.cancel(false); + } + + disconnectionTimer = timerExecutor.schedule( + () -> endGameForFeit(game), + 1, TimeUnit.MINUTES + ); + } + + /** + * Ends the game by forfeit when the timer expires without a reconnection. + * Broadcasts an {@link EndedGame} event, deletes the save, and resets state. + * + * @param game the game model captured when the timer was scheduled. + */ + private void endGameForFeit(Game game) { + synchronized (gameController) { + gameController.EndGameForFeit(); + EndedGame forfeitEnd = new EndedGame( + game.getSlotMap(), game.orderLogicCard, + game.getCurrentState(), game.getPlayerStanding() + ); + forfeitEnd.setDisconnected(buildDisconnectedList(game)); + broadcaster.notifyAll(forfeitEnd); + System.out.println("Timer expired: no player reconnected in 60 s."); + removeOfflinePlayers(); + if (!saveManager.delete()) { + System.out.println("\n!!! Couldn't delete save !!!\n"); + } + disconnectionTimer = null; + } + } + + /** + * Determines what to broadcast after a successful event application: + *

+ * + * @param event the event that was applied. + * @param game the current game model (post-apply). + * @param roundBefore the round number before the event was applied. + */ + private void broadcastResult(NetworkEvent event, Game game, int roundBefore) { + broadcaster.notifyAll(event); + if (game.getCurrentState().getRound() != roundBefore) { + ApplyNextRound nextRound = new ApplyNextRound( + game.getSlotMap(), game.orderLogicCard, game.getCurrentState(), + game.getPlayers(), + game.getUpperListTribeCards(), game.getLowerListTribeCards(), + game.getUpperListBuilding(), game.getLowerListBuilding() + ); + nextRound.setDisconnected(buildDisconnectedList(game)); + broadcaster.notifyAll(nextRound); + } else if (game.getCurrentState().getGameStage() == GameStages.ENDED) { + EndedGame endedGame = new EndedGame( + game.getSlotMap(), game.orderLogicCard, + game.getCurrentState(), game.getPlayerStanding() + ); + endedGame.setDisconnected(buildDisconnectedList(game)); + broadcaster.notifyAll(endedGame); + if (!saveManager.delete()) { + System.out.println("\n!!! Couldn't delete save !!!\n"); + } + removeOfflinePlayers(); + } + } + + // ── Utilities ───────────────────────────────────────────────────────────── + + /** + * Returns the number of players currently marked as online in the player list. + */ + private long onlinePlayerCount() { + return playerList.values().stream().filter(v -> v).count(); + } + + /** + * Builds the list of usernames of players currently marked as disconnected + * in the game model. + * + * @param game the current game model. + * @return a new {@link ArrayList} of disconnected usernames. + */ + private ArrayList buildDisconnectedList(Game game) { + return game.disconnetedPlayers.entrySet().stream() + .filter(Map.Entry::getValue) + .map(e -> e.getKey().getUserName()) + .collect(Collectors.toCollection(ArrayList::new)); + } + + /** + * Removes all offline entries (value {@code false}) from the player list. + * Online players remain until they disconnect naturally. + */ + private void removeOfflinePlayers() { + List toRemove = playerList.entrySet().stream() + .filter(e -> !e.getValue()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + toRemove.forEach(playerList::remove); + } +} diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/ClientBroadcaster.java b/src/main/java/it/polimi/ingsw/gc14/Network/ClientBroadcaster.java new file mode 100644 index 0000000..b813478 --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/Network/ClientBroadcaster.java @@ -0,0 +1,34 @@ +package it.polimi.ingsw.gc14.Network; + +import it.polimi.ingsw.gc14.Model.MiniModel; + +/** + * Abstraction over the set of connected clients. + * + *

Implementations are expected to forward events and model snapshots + * to every transport layer (TCP, RMI, ...) in use, hiding the details + * of each protocol from the caller. + * + *

Error events (where {@link NetworkEvent#getIsError()} is {@code true}) + * must be delivered only to the requesting player; non-error events must + * be delivered to every connected client. Implementations are responsible + * for enforcing this rule. + */ +public interface ClientBroadcaster { + + /** + * Sends a network event to the appropriate connected clients. + * + * @param event the event to deliver; if it represents an error it is + * sent only to the player identified by + * {@link NetworkEvent#getUsername()}. + */ + void notifyAll(NetworkEvent event); + + /** + * Sends an updated game snapshot to every connected client. + * + * @param model the mini-model to deliver. + */ + void notifyAll(MiniModel model); +} diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/CompositeClientBroadcaster.java b/src/main/java/it/polimi/ingsw/gc14/Network/CompositeClientBroadcaster.java new file mode 100644 index 0000000..122d78d --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/Network/CompositeClientBroadcaster.java @@ -0,0 +1,53 @@ +package it.polimi.ingsw.gc14.Network; + +import it.polimi.ingsw.gc14.Model.MiniModel; +import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; +import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer; + +/** + * {@link ClientBroadcaster} implementation that forwards every notification + * to both the RMI and the TCP transport layers. + * + *

This follows the Composite pattern: the caller interacts with a single + * broadcaster without knowing which protocols are active underneath. + */ +public class CompositeClientBroadcaster implements ClientBroadcaster { + + private final RMIServer rmiServer; + private final TCPServer tcpServer; + + /** + * Constructs a composite broadcaster backed by the given RMI and TCP servers. + * + * @param rmiServer the RMI server used to reach RMI clients. + * @param tcpServer the TCP server used to reach TCP clients. + */ + public CompositeClientBroadcaster(RMIServer rmiServer, TCPServer tcpServer) { + this.rmiServer = rmiServer; + this.tcpServer = tcpServer; + } + + /** + * {@inheritDoc} + * + *

Forwards the event to both the RMI and the TCP server. + * Each server is responsible for filtering error events to the + * requesting player only. + */ + @Override + public void notifyAll(NetworkEvent event) { + rmiServer.notifyAll(event); + tcpServer.notifyAll(event); + } + + /** + * {@inheritDoc} + * + *

Forwards the model snapshot to every client on both transports. + */ + @Override + public void notifyAll(MiniModel model) { + rmiServer.notifyAll(model); + tcpServer.notifyAll(model); + } +} diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvent.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvent.java index 54effb0..25600cd 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvent.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvent.java @@ -2,6 +2,7 @@ package it.polimi.ingsw.gc14.Network; import it.polimi.ingsw.gc14.Controller.GameController; import it.polimi.ingsw.gc14.ErrorType; +import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Model.GamePackage.CurrentState; import it.polimi.ingsw.gc14.Model.MiniModel; import it.polimi.ingsw.gc14.Model.OrderLogicCard; @@ -9,6 +10,7 @@ import it.polimi.ingsw.gc14.Model.Player; import it.polimi.ingsw.gc14.Model.Slot; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -66,6 +68,29 @@ public abstract class NetworkEvent implements Serializable { */ protected List playerList; + /** + * Usernames of players currently disconnected from the game. + * Sent with every event so clients always have an up-to-date list. + */ + protected ArrayList disconnectedPlayers; + + public void setDisconnected(ArrayList players) { + this.disconnectedPlayers = players; + } + + /** + * Populates this event with the current game state so clients can + * update their local model after receiving it. + * Subclasses that carry extra state (available totems, etc.) override + * this method and call {@code super} first. + * + * @param game the current game model. + * @param disconnectedUsernames usernames of currently disconnected players. + */ + public void enrichWithGameState(Game game, ArrayList disconnectedUsernames) { + setData(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(), game.getPlayers()); + } + /** * Sets the game data associated with this network event. * 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 index cbf5118..c552b59 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/DisconnectedPlayer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/DisconnectedPlayer.java @@ -6,20 +6,28 @@ import it.polimi.ingsw.gc14.Model.MiniModel; import it.polimi.ingsw.gc14.Network.EventType; import it.polimi.ingsw.gc14.Network.NetworkEvent; -import javax.smartcardio.Card; +import it.polimi.ingsw.gc14.Model.Game; +import it.polimi.ingsw.gc14.Model.Totems; + import java.io.Serializable; import java.util.ArrayList; +import java.util.List; /** * NetworkEvent to avoid drawing a card from the lower card list */ public class DisconnectedPlayer extends NetworkEvent implements Serializable{ - private ArrayList disconnectedPlayers; - public void setDisconnected(ArrayList players) - { - this.disconnectedPlayers=players; + private List availableTotems; + + + @Override + public void enrichWithGameState(Game game, ArrayList disconnectedUsernames) { + super.enrichWithGameState(game, disconnectedUsernames); + this.availableTotems = game.getAvailableTotems(); + this.disconnectedPlayers = disconnectedUsernames; } + /** * Class constructor. * Initializes all the attributes. @@ -51,6 +59,7 @@ public class DisconnectedPlayer extends NetworkEvent implements Serializable{ miniModel.setCurrentState(currentState); miniModel.setSlotPlayerMap(slotPlayerMap); miniModel.setDisconnectedPlayers(disconnectedPlayers); + miniModel.setAvailableTotems(availableTotems); miniModel.setLastEvent(this); } return true; 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 index e51a282..eca8bad 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/ReconnectPlayer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/ReconnectPlayer.java @@ -2,6 +2,7 @@ package it.polimi.ingsw.gc14.Network.NetworkEvents; import it.polimi.ingsw.gc14.Controller.GameController; import it.polimi.ingsw.gc14.ErrorType; +import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Model.MiniModel; import it.polimi.ingsw.gc14.Network.EventType; import it.polimi.ingsw.gc14.Network.NetworkEvent; @@ -13,11 +14,13 @@ import java.util.ArrayList; * Network event used to notify that a player has reconnected to the game. */ public class ReconnectPlayer extends NetworkEvent implements Serializable { - private ArrayList disconnectedPlayers; - public void setDisconnected(ArrayList players) - { - this.disconnectedPlayers=players; + + @Override + public void enrichWithGameState(Game game, ArrayList disconnectedUsernames) { + super.enrichWithGameState(game, disconnectedUsernames); + this.disconnectedPlayers = disconnectedUsernames; } + /** * Constructs a reconnection event for the specified player. * diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/TotemChoice.java b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/TotemChoice.java index aa1022f..864eee9 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/TotemChoice.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/NetworkEvents/TotemChoice.java @@ -2,12 +2,14 @@ package it.polimi.ingsw.gc14.Network.NetworkEvents; import it.polimi.ingsw.gc14.Controller.GameController; import it.polimi.ingsw.gc14.ErrorType; +import it.polimi.ingsw.gc14.Model.Game; import it.polimi.ingsw.gc14.Model.MiniModel; import it.polimi.ingsw.gc14.Model.Totems; import it.polimi.ingsw.gc14.Network.EventType; import it.polimi.ingsw.gc14.Network.NetworkEvent; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; /** @@ -45,6 +47,12 @@ public class TotemChoice extends NetworkEvent implements Serializable { this.totem = totem; } + @Override + public void enrichWithGameState(Game game, ArrayList disconnectedUsernames) { + super.enrichWithGameState(game, disconnectedUsernames); + this.availableTotems = game.getAvailableTotems(); + } + /** * Applies the totem choice event to the server-side game controller. * diff --git a/src/main/java/it/polimi/ingsw/gc14/SaveManager.java b/src/main/java/it/polimi/ingsw/gc14/SaveManager.java new file mode 100644 index 0000000..346e926 --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/SaveManager.java @@ -0,0 +1,106 @@ +package it.polimi.ingsw.gc14; + +import it.polimi.ingsw.gc14.Model.Game; + +import java.io.*; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Handles persistence of the game model to and from disk. + * + *

The save file is stored at {@code GameSaves/save.dat} relative to the + * directory containing the running JAR. The anchor class passed to the + * constructor is used to resolve that directory at construction time. + * + *

All three operations ({@link #save}, {@link #load}, {@link #delete}) + * are independent and safe to call in any order; missing files are treated + * as a normal condition (no save present) rather than an error. + */ +public class SaveManager { + + /** Relative sub-path of the save file inside the JAR directory. */ + private static final String SAVE_RELATIVE_PATH = "GameSaves/save.dat"; + + /** Absolute path of the save file, resolved once at construction. */ + private final Path filePath; + + /** + * Constructs a {@code SaveManager} whose save file is located relative + * to the JAR directory of the given anchor class. + * + * @param anchorClass the class whose code-source location is used as + * the base directory for the save file. + * @throws RuntimeException if the JAR path cannot be resolved. + */ + public SaveManager(Class anchorClass) { + try { + Path jarDir = Paths.get( + anchorClass.getProtectionDomain().getCodeSource().getLocation().toURI() + ).getParent(); + this.filePath = jarDir.resolve(SAVE_RELATIVE_PATH); + } catch (URISyntaxException e) { + throw new RuntimeException("Could not resolve save file path", e); + } + } + + /** + * Serializes the game model to disk, creating parent directories if needed. + * + * @param game the game model to persist. + * @return {@code true} if the save succeeded, {@code false} on I/O error. + */ + public boolean save(Game game) { + try { + Files.createDirectories(filePath.getParent()); + try (ObjectOutputStream oos = + new ObjectOutputStream(new FileOutputStream(filePath.toFile()))) { + oos.writeObject(game); + System.out.println("Game saved to: " + filePath.toAbsolutePath()); + return true; + } + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + /** + * Deserializes the game model from disk. + * + * @return the saved {@link Game} instance, or {@code null} if no save file + * exists or an I/O error prevents reading. + * @throws RuntimeException if the serialized class cannot be found on + * the classpath (indicates a deployment mismatch). + */ + public Game load() { + 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("Save file references an unknown class", e); + } + } + + /** + * Deletes the save file. + * + * @return {@code true} if the file was deleted, {@code false} otherwise + * (including when the file did not exist). + */ + public boolean delete() { + try { + Files.delete(filePath); + return true; + } catch (IOException e) { + return false; + } + } +} diff --git a/src/main/java/it/polimi/ingsw/gc14/ServerLauncherTest.java b/src/main/java/it/polimi/ingsw/gc14/ServerLauncherTest.java new file mode 100644 index 0000000..19eef72 --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/ServerLauncherTest.java @@ -0,0 +1,229 @@ +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.MiniModel; +import it.polimi.ingsw.gc14.Model.Player; +import it.polimi.ingsw.gc14.Network.ClientBroadcaster; +import it.polimi.ingsw.gc14.Network.CompositeClientBroadcaster; +import it.polimi.ingsw.gc14.Network.NetworkEvent; +import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; +import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer; + +import java.net.*; +import java.rmi.RemoteException; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +/** + * Main server entry point. + * + *

Responsibilities of this class are intentionally limited to wiring: + * it creates all components, connects them together, restores a previously + * saved game if one exists, and starts the event-processing loop and the + * network servers. + * + *

All game-logic and event-routing decisions are delegated to + * {@link GameEventProcessor}; persistence is delegated to {@link SaveManager}. + * + *

The original {@code ServerLauncher} class is preserved and untouched. + * This class is a clean replacement that uses the new component structure. + */ +public class ServerLauncherTest { + + /** Drives the main game-event loop. */ + private final GameEventProcessor eventProcessor; + + /** + * Constructs a {@code ServerLauncherTest} and wires all components together. + * + * @param actionQueue the shared event queue. + * @param gameController the server-side game controller. + * @param playerList the shared player-status map. + * @param broadcaster the broadcaster used to reach all connected clients. + * @param saveManager the save manager used to persist game state. + */ + public ServerLauncherTest( + BlockingQueue actionQueue, + GameController gameController, + LimitedMap playerList, + ClientBroadcaster broadcaster, + SaveManager saveManager) { + this.eventProcessor = new GameEventProcessor( + actionQueue, gameController, playerList, broadcaster, saveManager); + } + + // ── Entry point ─────────────────────────────────────────────────────────── + + /** + * Initialises all server components, wires them together, restores a saved + * game if available, and starts the event-processing loop and both network + * servers. + * + * @param args command-line arguments (unused). + * @throws RemoteException if the RMI server cannot be created. + */ + public static void main(String[] args) throws RemoteException { + LimitedMap playerList = new LimitedMap<>(5, () -> {}); + BlockingQueue 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 rmiServer = new RMIServer(gameController, 1099, actionQueue, playerList, ip); + TCPServer tcpServer = new TCPServer(gameController, 8080, 8081, actionQueue, playerList); + ClientBroadcaster broadcaster = new CompositeClientBroadcaster(rmiServer, tcpServer); + SaveManager saveManager = new SaveManager(ServerLauncherTest.class); + + restoreGameIfSaved(gameController, playerList, saveManager); + + ServerLauncherTest launcher = new ServerLauncherTest( + actionQueue, gameController, playerList, broadcaster, saveManager); + + // When all required players have joined, broadcast the initial MiniModel. + playerList.setAction(() -> new Thread(() -> { + 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(e -> e.getKey().getUserName()) + .collect(Collectors.toCollection(ArrayList::new)) + ); + } + broadcaster.notifyAll(miniModel); + }).start()); + + new Thread(() -> { + try { launcher.run(); } + catch (Exception e) { + System.out.println("Unexpected exception in game loop"); + e.printStackTrace(); + } + }).start(); + + rmiServer.start(); + new Thread(() -> tcpServer.start()).start(); + System.out.println("Server RMI: " + System.getProperty("java.rmi.server.hostname")); + } + + // ── Game loop ───────────────────────────────────────────────────────────── + + /** + * Runs the event-processing loop until the thread is interrupted. + * + *

A {@link ConcurrentModificationException} is caught and logged rather + * than propagated; it is caused by an unsafe {@link ArrayList} in + * {@code TCPServer.clientHandlers} (tracked as a separate issue) and does + * not leave the game in an inconsistent state. + */ + public void run() { + while (true) { + try { + eventProcessor.doFirstEvent(); + } catch (ConcurrentModificationException e) { + System.err.println("Concurrent modification in notifyAll — skipping tick"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + // ── Crash recovery ──────────────────────────────────────────────────────── + + /** + * Attempts to restore a previously saved game. + * + *

The save is discarded if all but at most one player was offline at the + * time of the crash, since there would be nobody to resume the game with. + * Otherwise the model is restored, the player-list limit is set, and each + * player who was offline at crash time is pre-populated as offline so the + * reconnection flow can handle them correctly. + * + * @param gameController the controller that will receive the restored model. + * @param playerList the player-status map to populate. + * @param saveManager the save manager to load from. + */ + private static void restoreGameIfSaved( + GameController gameController, + LimitedMap playerList, + SaveManager saveManager) { + + Game game = saveManager.load(); + if (game == null) return; + + long disconnectedCount = game.disconnetedPlayers.entrySet().stream() + .filter(Map.Entry::getValue).count(); + + if (disconnectedCount >= game.getNPlayers() - 1) { + saveManager.delete(); + System.out.println("Save discarded: too many players were offline at crash time."); + return; + } + + gameController.setModel(game); + playerList.setLimit(game.getNPlayers()); + + for (Map.Entry entry : game.disconnetedPlayers.entrySet()) { + if (entry.getValue()) { + playerList.put(entry.getKey().getUserName(), false); + } + } + System.out.println("Game restored from save (" + game.getNPlayers() + " players)."); + } + + // ── Network interface selection ─────────────────────────────────────────── + + /** + * Lists active non-loopback IPv4 network interfaces and prompts the operator + * to choose one. If only one interface is available it is selected automatically. + * + * @param scanner the scanner used to read the operator's choice. + * @return the IPv4 address of the selected interface. + * @throws Exception if no valid network interface is available. + */ + public static String chooseNetworkInterface(Scanner scanner) throws Exception { + List ips = new ArrayList<>(); + + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface ni = interfaces.nextElement(); + if (!ni.isUp() || ni.isLoopback() || ni.isVirtual()) continue; + + Enumeration addresses = ni.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr instanceof Inet4Address) { + System.out.println("[" + ips.size() + "] " + ni.getDisplayName() + + " -> " + addr.getHostAddress()); + ips.add(addr.getHostAddress()); + } + } + } + + if (ips.isEmpty()) throw new Exception("No active network interface available"); + + if (ips.size() == 1) { + System.out.println("One interface found, using: " + ips.get(0)); + return ips.get(0); + } + + System.out.print("Choose interface: "); + int choice = Integer.parseInt(scanner.nextLine().trim()); + return ips.get(choice); + } +}