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 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:
+ * 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 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 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
+ *
+ *
+ * @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.
+ *
+ *
+ *
+ *
+ * @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