Files
Progetto-ingegneria-del-sof…/src/main/java/it/polimi/ingsw/gc14/GameEventProcessor.java
T
2026-06-19 19:47:21 +02:00

379 lines
15 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
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. 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;
}
// ── 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.
* <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.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();
}
}
// ── 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.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);
}
}