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.CompositeClientBroadcaster; 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 ServerLauncher}) 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 CompositeClientBroadcaster broadcaster; private final SaveManager saveManager; /** Single-thread executor used exclusively for the forfeit timer. Daemon so it does not block JVM shutdown. */ private final ScheduledExecutorService timerExecutor = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "forfeit-timer"); t.setDaemon(true); return t; }); /** * 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 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, CompositeClientBroadcaster 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 cleanup 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.isError()) { broadcaster.notifyAll(event); return; } cancelForfeitTimerIfReconnect(event); if (!saveManager.save(game)) { System.out.println("\n!!! Save failed !!!\n"); } else { System.out.println(event.getUsername()+": "+ event.getEventType() + " save successful."); } // 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 canceled 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); System.out.println("Disconnection TIMER reset"); } disconnectionTimer = timerExecutor.schedule( () -> endGameForfeit(game), 1, TimeUnit.MINUTES ); System.out.println("Disconnection TIMER started, 60 seconds from now..." ); } /** * 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.getOrderLogicCard(), game.getCurrentState(), game.getPlayerStanding() ); 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.getOrderLogicCard(), game.getCurrentState(), game.getPlayers(), game.getUpperListTribeCards(), game.getLowerListTribeCards(), game.getUpperListBuilding(), game.getLowerListBuilding() ); broadcaster.notifyAll(nextRound); } else if (game.getCurrentState().getGameStage() == GameStages.ENDED) { EndedGame endedGame = new EndedGame( game.getSlotMap(), game.getOrderLogicCard(), game.getCurrentState(), game.getPlayerStanding() ); 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.getDisconnectedPlayers().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) .toList(); toRemove.forEach(playerList::remove); } }