370 lines
15 KiB
Java
370 lines
15 KiB
Java
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 clean-up 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 ServerLauncherTest}) to drive it from a single
|
||
* thread via {@link #doFirstEvent()}.
|
||
*/
|
||
public class GameEventProcessor {
|
||
|
||
private final BlockingQueue<NetworkEvent> actionQueue;
|
||
private final GameController gameController;
|
||
private final LimitedMap<String, Boolean> playerList;
|
||
private final CompositeClientBroadcaster 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<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;
|
||
}
|
||
|
||
// ── 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.
|
||
* <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);
|
||
}
|
||
|
||
// ── Active game path ──────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 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.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.
|
||
*
|
||
* <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 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.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();
|
||
}
|
||
}
|
||
|
||
// ── 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<String> buildDisconnectedList(Game game) {
|
||
return game.disconnectedPlayers.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)
|
||
.collect(Collectors.toList());
|
||
toRemove.forEach(playerList::remove);
|
||
}
|
||
}
|