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 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 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.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