Coverage Summary for Class: GameEventProcessor (it.polimi.ingsw.gc14)
| Class |
Method, %
|
Branch, %
|
Line, %
|
| GameEventProcessor |
0%
(0/19)
|
0%
(0/48)
|
0%
(0/110)
|
| GameEventProcessor$1 |
0%
(0/1)
|
0%
(0/1)
|
| Total |
0%
(0/20)
|
0%
(0/48)
|
0%
(0/111)
|
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.
*
* <p>The three top-level states that determine routing are:
* <ul>
* <li><b>Inactive</b> – no game model is present, or the game has ended.
* Only disconnection cleanup is performed.</li>
* <li><b>Suspended</b> – a forfeit timer is running because exactly one
* player remains online. Only reconnection events are accepted.</li>
* <li><b>Active</b> – normal gameplay; every event is applied, saved,
* and broadcast.</li>
* </ul>
*
* <p>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 {
/** Queue from which incoming client events are consumed one at a time. */
private final BlockingQueue<NetworkEvent> actionQueue;
/** Server-side game controller; all model mutations go through this. */
private final GameController gameController;
/** Shared map tracking each player's online status ({@code true} = online). */
private final LimitedMap<String, Boolean> playerList;
/** Broadcaster used to push events and model snapshots to all connected clients. */
private final CompositeClientBroadcaster broadcaster;
/** Persists and deletes game save files. */
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<NetworkEvent> actionQueue,
GameController gameController,
LimitedMap<String, Boolean> playerList,
CompositeClientBroadcaster broadcaster,
SaveManager saveManager) {
this.actionQueue = actionQueue;
this.gameController = gameController;
this.playerList = playerList;
this.broadcaster = broadcaster;
this.saveManager = saveManager;
}
/**
* 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);
}
}
/**
* 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;
}
/**
* 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");
}
}
}
/**
* Routes events while the game is suspended waiting for a reconnection.
* <ul>
* <li>A second disconnection while suspended means no player remains
* online: the game is aborted entirely.</li>
* <li>Any non-reconnection event is rejected with an error.</li>
* <li>A reconnection event is allowed through to {@link #applyAndBroadcast}.</li>
* </ul>
*
* @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);
}
/**
* Applies the event to the game controller, saves the updated state,
* and broadcasts the result to connected clients.
*
* <p>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.
*
* <p>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.
*
* <p>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:
* <ul>
* <li>If the round advanced, an {@link ApplyNextRound} event (with updated
* card lists) replaces the original event.</li>
* <li>If the game has ended, an {@link EndedGame} event is sent and the
* save file is deleted.</li>
* <li>Otherwise the original event is broadcast as-is.</li>
* </ul>
*
* @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();
}
}
/**
* 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<String> 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<String> toRemove = playerList.entrySet().stream()
.filter(e -> !e.getValue())
.map(Map.Entry::getKey)
.toList();
toRemove.forEach(playerList::remove);
}
}