Temporary refactor of code
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
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.ClientBroadcaster;
|
||||
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 ClientBroadcaster 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,
|
||||
ClientBroadcaster 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.orderLogicCard,
|
||||
game.getCurrentState(), game.getPlayerStanding()
|
||||
);
|
||||
forfeitEnd.setDisconnected(buildDisconnectedList(game));
|
||||
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.orderLogicCard, game.getCurrentState(),
|
||||
game.getPlayers(),
|
||||
game.getUpperListTribeCards(), game.getLowerListTribeCards(),
|
||||
game.getUpperListBuilding(), game.getLowerListBuilding()
|
||||
);
|
||||
nextRound.setDisconnected(buildDisconnectedList(game));
|
||||
broadcaster.notifyAll(nextRound);
|
||||
} else if (game.getCurrentState().getGameStage() == GameStages.ENDED) {
|
||||
EndedGame endedGame = new EndedGame(
|
||||
game.getSlotMap(), game.orderLogicCard,
|
||||
game.getCurrentState(), game.getPlayerStanding()
|
||||
);
|
||||
endedGame.setDisconnected(buildDisconnectedList(game));
|
||||
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.disconnetedPlayers.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package it.polimi.ingsw.gc14.Network;
|
||||
|
||||
import it.polimi.ingsw.gc14.Model.MiniModel;
|
||||
|
||||
/**
|
||||
* Abstraction over the set of connected clients.
|
||||
*
|
||||
* <p>Implementations are expected to forward events and model snapshots
|
||||
* to every transport layer (TCP, RMI, ...) in use, hiding the details
|
||||
* of each protocol from the caller.
|
||||
*
|
||||
* <p>Error events (where {@link NetworkEvent#getIsError()} is {@code true})
|
||||
* must be delivered only to the requesting player; non-error events must
|
||||
* be delivered to every connected client. Implementations are responsible
|
||||
* for enforcing this rule.
|
||||
*/
|
||||
public interface ClientBroadcaster {
|
||||
|
||||
/**
|
||||
* Sends a network event to the appropriate connected clients.
|
||||
*
|
||||
* @param event the event to deliver; if it represents an error it is
|
||||
* sent only to the player identified by
|
||||
* {@link NetworkEvent#getUsername()}.
|
||||
*/
|
||||
void notifyAll(NetworkEvent event);
|
||||
|
||||
/**
|
||||
* Sends an updated game snapshot to every connected client.
|
||||
*
|
||||
* @param model the mini-model to deliver.
|
||||
*/
|
||||
void notifyAll(MiniModel model);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package it.polimi.ingsw.gc14.Network;
|
||||
|
||||
import it.polimi.ingsw.gc14.Model.MiniModel;
|
||||
import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer;
|
||||
import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer;
|
||||
|
||||
/**
|
||||
* {@link ClientBroadcaster} implementation that forwards every notification
|
||||
* to both the RMI and the TCP transport layers.
|
||||
*
|
||||
* <p>This follows the Composite pattern: the caller interacts with a single
|
||||
* broadcaster without knowing which protocols are active underneath.
|
||||
*/
|
||||
public class CompositeClientBroadcaster implements ClientBroadcaster {
|
||||
|
||||
private final RMIServer rmiServer;
|
||||
private final TCPServer tcpServer;
|
||||
|
||||
/**
|
||||
* Constructs a composite broadcaster backed by the given RMI and TCP servers.
|
||||
*
|
||||
* @param rmiServer the RMI server used to reach RMI clients.
|
||||
* @param tcpServer the TCP server used to reach TCP clients.
|
||||
*/
|
||||
public CompositeClientBroadcaster(RMIServer rmiServer, TCPServer tcpServer) {
|
||||
this.rmiServer = rmiServer;
|
||||
this.tcpServer = tcpServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>Forwards the event to both the RMI and the TCP server.
|
||||
* Each server is responsible for filtering error events to the
|
||||
* requesting player only.
|
||||
*/
|
||||
@Override
|
||||
public void notifyAll(NetworkEvent event) {
|
||||
rmiServer.notifyAll(event);
|
||||
tcpServer.notifyAll(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>Forwards the model snapshot to every client on both transports.
|
||||
*/
|
||||
@Override
|
||||
public void notifyAll(MiniModel model) {
|
||||
rmiServer.notifyAll(model);
|
||||
tcpServer.notifyAll(model);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package it.polimi.ingsw.gc14.Network;
|
||||
|
||||
import it.polimi.ingsw.gc14.Controller.GameController;
|
||||
import it.polimi.ingsw.gc14.ErrorType;
|
||||
import it.polimi.ingsw.gc14.Model.Game;
|
||||
import it.polimi.ingsw.gc14.Model.GamePackage.CurrentState;
|
||||
import it.polimi.ingsw.gc14.Model.MiniModel;
|
||||
import it.polimi.ingsw.gc14.Model.OrderLogicCard;
|
||||
@@ -9,6 +10,7 @@ import it.polimi.ingsw.gc14.Model.Player;
|
||||
import it.polimi.ingsw.gc14.Model.Slot;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -66,6 +68,29 @@ public abstract class NetworkEvent implements Serializable {
|
||||
*/
|
||||
protected List<Player> playerList;
|
||||
|
||||
/**
|
||||
* Usernames of players currently disconnected from the game.
|
||||
* Sent with every event so clients always have an up-to-date list.
|
||||
*/
|
||||
protected ArrayList<String> disconnectedPlayers;
|
||||
|
||||
public void setDisconnected(ArrayList<String> players) {
|
||||
this.disconnectedPlayers = players;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates this event with the current game state so clients can
|
||||
* update their local model after receiving it.
|
||||
* Subclasses that carry extra state (available totems, etc.) override
|
||||
* this method and call {@code super} first.
|
||||
*
|
||||
* @param game the current game model.
|
||||
* @param disconnectedUsernames usernames of currently disconnected players.
|
||||
*/
|
||||
public void enrichWithGameState(Game game, ArrayList<String> disconnectedUsernames) {
|
||||
setData(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(), game.getPlayers());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the game data associated with this network event.
|
||||
*
|
||||
|
||||
@@ -6,20 +6,28 @@ import it.polimi.ingsw.gc14.Model.MiniModel;
|
||||
import it.polimi.ingsw.gc14.Network.EventType;
|
||||
import it.polimi.ingsw.gc14.Network.NetworkEvent;
|
||||
|
||||
import javax.smartcardio.Card;
|
||||
import it.polimi.ingsw.gc14.Model.Game;
|
||||
import it.polimi.ingsw.gc14.Model.Totems;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* NetworkEvent to avoid drawing a card from the lower card list
|
||||
*/
|
||||
public class DisconnectedPlayer extends NetworkEvent implements Serializable{
|
||||
private ArrayList<String> disconnectedPlayers;
|
||||
|
||||
public void setDisconnected(ArrayList<String> players)
|
||||
{
|
||||
this.disconnectedPlayers=players;
|
||||
private List<Totems> availableTotems;
|
||||
|
||||
|
||||
@Override
|
||||
public void enrichWithGameState(Game game, ArrayList<String> disconnectedUsernames) {
|
||||
super.enrichWithGameState(game, disconnectedUsernames);
|
||||
this.availableTotems = game.getAvailableTotems();
|
||||
this.disconnectedPlayers = disconnectedUsernames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
* Initializes all the attributes.
|
||||
@@ -51,6 +59,7 @@ public class DisconnectedPlayer extends NetworkEvent implements Serializable{
|
||||
miniModel.setCurrentState(currentState);
|
||||
miniModel.setSlotPlayerMap(slotPlayerMap);
|
||||
miniModel.setDisconnectedPlayers(disconnectedPlayers);
|
||||
miniModel.setAvailableTotems(availableTotems);
|
||||
miniModel.setLastEvent(this);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -2,6 +2,7 @@ package it.polimi.ingsw.gc14.Network.NetworkEvents;
|
||||
|
||||
import it.polimi.ingsw.gc14.Controller.GameController;
|
||||
import it.polimi.ingsw.gc14.ErrorType;
|
||||
import it.polimi.ingsw.gc14.Model.Game;
|
||||
import it.polimi.ingsw.gc14.Model.MiniModel;
|
||||
import it.polimi.ingsw.gc14.Network.EventType;
|
||||
import it.polimi.ingsw.gc14.Network.NetworkEvent;
|
||||
@@ -13,11 +14,13 @@ import java.util.ArrayList;
|
||||
* Network event used to notify that a player has reconnected to the game.
|
||||
*/
|
||||
public class ReconnectPlayer extends NetworkEvent implements Serializable {
|
||||
private ArrayList<String> disconnectedPlayers;
|
||||
public void setDisconnected(ArrayList<String> players)
|
||||
{
|
||||
this.disconnectedPlayers=players;
|
||||
|
||||
@Override
|
||||
public void enrichWithGameState(Game game, ArrayList<String> disconnectedUsernames) {
|
||||
super.enrichWithGameState(game, disconnectedUsernames);
|
||||
this.disconnectedPlayers = disconnectedUsernames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a reconnection event for the specified player.
|
||||
*
|
||||
|
||||
@@ -2,12 +2,14 @@ package it.polimi.ingsw.gc14.Network.NetworkEvents;
|
||||
|
||||
import it.polimi.ingsw.gc14.Controller.GameController;
|
||||
import it.polimi.ingsw.gc14.ErrorType;
|
||||
import it.polimi.ingsw.gc14.Model.Game;
|
||||
import it.polimi.ingsw.gc14.Model.MiniModel;
|
||||
import it.polimi.ingsw.gc14.Model.Totems;
|
||||
import it.polimi.ingsw.gc14.Network.EventType;
|
||||
import it.polimi.ingsw.gc14.Network.NetworkEvent;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -45,6 +47,12 @@ public class TotemChoice extends NetworkEvent implements Serializable {
|
||||
this.totem = totem;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrichWithGameState(Game game, ArrayList<String> disconnectedUsernames) {
|
||||
super.enrichWithGameState(game, disconnectedUsernames);
|
||||
this.availableTotems = game.getAvailableTotems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the totem choice event to the server-side game controller.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package it.polimi.ingsw.gc14;
|
||||
|
||||
import it.polimi.ingsw.gc14.Model.Game;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Handles persistence of the game model to and from disk.
|
||||
*
|
||||
* <p>The save file is stored at {@code GameSaves/save.dat} relative to the
|
||||
* directory containing the running JAR. The anchor class passed to the
|
||||
* constructor is used to resolve that directory at construction time.
|
||||
*
|
||||
* <p>All three operations ({@link #save}, {@link #load}, {@link #delete})
|
||||
* are independent and safe to call in any order; missing files are treated
|
||||
* as a normal condition (no save present) rather than an error.
|
||||
*/
|
||||
public class SaveManager {
|
||||
|
||||
/** Relative sub-path of the save file inside the JAR directory. */
|
||||
private static final String SAVE_RELATIVE_PATH = "GameSaves/save.dat";
|
||||
|
||||
/** Absolute path of the save file, resolved once at construction. */
|
||||
private final Path filePath;
|
||||
|
||||
/**
|
||||
* Constructs a {@code SaveManager} whose save file is located relative
|
||||
* to the JAR directory of the given anchor class.
|
||||
*
|
||||
* @param anchorClass the class whose code-source location is used as
|
||||
* the base directory for the save file.
|
||||
* @throws RuntimeException if the JAR path cannot be resolved.
|
||||
*/
|
||||
public SaveManager(Class<?> anchorClass) {
|
||||
try {
|
||||
Path jarDir = Paths.get(
|
||||
anchorClass.getProtectionDomain().getCodeSource().getLocation().toURI()
|
||||
).getParent();
|
||||
this.filePath = jarDir.resolve(SAVE_RELATIVE_PATH);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException("Could not resolve save file path", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the game model to disk, creating parent directories if needed.
|
||||
*
|
||||
* @param game the game model to persist.
|
||||
* @return {@code true} if the save succeeded, {@code false} on I/O error.
|
||||
*/
|
||||
public boolean save(Game game) {
|
||||
try {
|
||||
Files.createDirectories(filePath.getParent());
|
||||
try (ObjectOutputStream oos =
|
||||
new ObjectOutputStream(new FileOutputStream(filePath.toFile()))) {
|
||||
oos.writeObject(game);
|
||||
System.out.println("Game saved to: " + filePath.toAbsolutePath());
|
||||
return true;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the game model from disk.
|
||||
*
|
||||
* @return the saved {@link Game} instance, or {@code null} if no save file
|
||||
* exists or an I/O error prevents reading.
|
||||
* @throws RuntimeException if the serialized class cannot be found on
|
||||
* the classpath (indicates a deployment mismatch).
|
||||
*/
|
||||
public Game load() {
|
||||
try (ObjectInputStream ois =
|
||||
new ObjectInputStream(new FileInputStream(filePath.toFile()))) {
|
||||
return (Game) ois.readObject();
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException("Save file references an unknown class", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the save file.
|
||||
*
|
||||
* @return {@code true} if the file was deleted, {@code false} otherwise
|
||||
* (including when the file did not exist).
|
||||
*/
|
||||
public boolean delete() {
|
||||
try {
|
||||
Files.delete(filePath);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
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.MiniModel;
|
||||
import it.polimi.ingsw.gc14.Model.Player;
|
||||
import it.polimi.ingsw.gc14.Network.ClientBroadcaster;
|
||||
import it.polimi.ingsw.gc14.Network.CompositeClientBroadcaster;
|
||||
import it.polimi.ingsw.gc14.Network.NetworkEvent;
|
||||
import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer;
|
||||
import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer;
|
||||
|
||||
import java.net.*;
|
||||
import java.rmi.RemoteException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Main server entry point.
|
||||
*
|
||||
* <p>Responsibilities of this class are intentionally limited to wiring:
|
||||
* it creates all components, connects them together, restores a previously
|
||||
* saved game if one exists, and starts the event-processing loop and the
|
||||
* network servers.
|
||||
*
|
||||
* <p>All game-logic and event-routing decisions are delegated to
|
||||
* {@link GameEventProcessor}; persistence is delegated to {@link SaveManager}.
|
||||
*
|
||||
* <p>The original {@code ServerLauncher} class is preserved and untouched.
|
||||
* This class is a clean replacement that uses the new component structure.
|
||||
*/
|
||||
public class ServerLauncherTest {
|
||||
|
||||
/** Drives the main game-event loop. */
|
||||
private final GameEventProcessor eventProcessor;
|
||||
|
||||
/**
|
||||
* Constructs a {@code ServerLauncherTest} and wires all components together.
|
||||
*
|
||||
* @param actionQueue the shared event queue.
|
||||
* @param gameController the server-side game controller.
|
||||
* @param playerList the shared player-status map.
|
||||
* @param broadcaster the broadcaster used to reach all connected clients.
|
||||
* @param saveManager the save manager used to persist game state.
|
||||
*/
|
||||
public ServerLauncherTest(
|
||||
BlockingQueue<NetworkEvent> actionQueue,
|
||||
GameController gameController,
|
||||
LimitedMap<String, Boolean> playerList,
|
||||
ClientBroadcaster broadcaster,
|
||||
SaveManager saveManager) {
|
||||
this.eventProcessor = new GameEventProcessor(
|
||||
actionQueue, gameController, playerList, broadcaster, saveManager);
|
||||
}
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Initialises all server components, wires them together, restores a saved
|
||||
* game if available, and starts the event-processing loop and both network
|
||||
* servers.
|
||||
*
|
||||
* @param args command-line arguments (unused).
|
||||
* @throws RemoteException if the RMI server cannot be created.
|
||||
*/
|
||||
public static void main(String[] args) throws RemoteException {
|
||||
LimitedMap<String, Boolean> playerList = new LimitedMap<>(5, () -> {});
|
||||
BlockingQueue<NetworkEvent> actionQueue = new LinkedBlockingQueue<>();
|
||||
GameController gameController = new GameController();
|
||||
|
||||
String ip;
|
||||
try {
|
||||
ip = chooseNetworkInterface(new Scanner(System.in));
|
||||
System.out.println(ip);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
System.setProperty("java.rmi.server.hostname", ip);
|
||||
|
||||
RMIServer rmiServer = new RMIServer(gameController, 1099, actionQueue, playerList, ip);
|
||||
TCPServer tcpServer = new TCPServer(gameController, 8080, 8081, actionQueue, playerList);
|
||||
ClientBroadcaster broadcaster = new CompositeClientBroadcaster(rmiServer, tcpServer);
|
||||
SaveManager saveManager = new SaveManager(ServerLauncherTest.class);
|
||||
|
||||
restoreGameIfSaved(gameController, playerList, saveManager);
|
||||
|
||||
ServerLauncherTest launcher = new ServerLauncherTest(
|
||||
actionQueue, gameController, playerList, broadcaster, saveManager);
|
||||
|
||||
// When all required players have joined, broadcast the initial MiniModel.
|
||||
playerList.setAction(() -> new Thread(() -> {
|
||||
MiniModel miniModel;
|
||||
synchronized (gameController) {
|
||||
Game game = gameController.getModel();
|
||||
miniModel = new MiniModel(
|
||||
game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),
|
||||
game.getPlayers(), game.getAvailableTotems(),
|
||||
game.getUpperListTribeCards(), game.getLowerListTribeCards(),
|
||||
game.getUpperListBuilding(), game.getLowerListBuilding(),
|
||||
game.disconnetedPlayers.entrySet().stream()
|
||||
.filter(Map.Entry::getValue)
|
||||
.map(e -> e.getKey().getUserName())
|
||||
.collect(Collectors.toCollection(ArrayList::new))
|
||||
);
|
||||
}
|
||||
broadcaster.notifyAll(miniModel);
|
||||
}).start());
|
||||
|
||||
new Thread(() -> {
|
||||
try { launcher.run(); }
|
||||
catch (Exception e) {
|
||||
System.out.println("Unexpected exception in game loop");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}).start();
|
||||
|
||||
rmiServer.start();
|
||||
new Thread(() -> tcpServer.start()).start();
|
||||
System.out.println("Server RMI: " + System.getProperty("java.rmi.server.hostname"));
|
||||
}
|
||||
|
||||
// ── Game loop ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Runs the event-processing loop until the thread is interrupted.
|
||||
*
|
||||
* <p>A {@link ConcurrentModificationException} is caught and logged rather
|
||||
* than propagated; it is caused by an unsafe {@link ArrayList} in
|
||||
* {@code TCPServer.clientHandlers} (tracked as a separate issue) and does
|
||||
* not leave the game in an inconsistent state.
|
||||
*/
|
||||
public void run() {
|
||||
while (true) {
|
||||
try {
|
||||
eventProcessor.doFirstEvent();
|
||||
} catch (ConcurrentModificationException e) {
|
||||
System.err.println("Concurrent modification in notifyAll — skipping tick");
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Crash recovery ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Attempts to restore a previously saved game.
|
||||
*
|
||||
* <p>The save is discarded if all but at most one player was offline at the
|
||||
* time of the crash, since there would be nobody to resume the game with.
|
||||
* Otherwise the model is restored, the player-list limit is set, and each
|
||||
* player who was offline at crash time is pre-populated as offline so the
|
||||
* reconnection flow can handle them correctly.
|
||||
*
|
||||
* @param gameController the controller that will receive the restored model.
|
||||
* @param playerList the player-status map to populate.
|
||||
* @param saveManager the save manager to load from.
|
||||
*/
|
||||
private static void restoreGameIfSaved(
|
||||
GameController gameController,
|
||||
LimitedMap<String, Boolean> playerList,
|
||||
SaveManager saveManager) {
|
||||
|
||||
Game game = saveManager.load();
|
||||
if (game == null) return;
|
||||
|
||||
long disconnectedCount = game.disconnetedPlayers.entrySet().stream()
|
||||
.filter(Map.Entry::getValue).count();
|
||||
|
||||
if (disconnectedCount >= game.getNPlayers() - 1) {
|
||||
saveManager.delete();
|
||||
System.out.println("Save discarded: too many players were offline at crash time.");
|
||||
return;
|
||||
}
|
||||
|
||||
gameController.setModel(game);
|
||||
playerList.setLimit(game.getNPlayers());
|
||||
|
||||
for (Map.Entry<Player, Boolean> entry : game.disconnetedPlayers.entrySet()) {
|
||||
if (entry.getValue()) {
|
||||
playerList.put(entry.getKey().getUserName(), false);
|
||||
}
|
||||
}
|
||||
System.out.println("Game restored from save (" + game.getNPlayers() + " players).");
|
||||
}
|
||||
|
||||
// ── Network interface selection ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Lists active non-loopback IPv4 network interfaces and prompts the operator
|
||||
* to choose one. If only one interface is available it is selected automatically.
|
||||
*
|
||||
* @param scanner the scanner used to read the operator's choice.
|
||||
* @return the IPv4 address of the selected interface.
|
||||
* @throws Exception if no valid network interface is available.
|
||||
*/
|
||||
public static String chooseNetworkInterface(Scanner scanner) throws Exception {
|
||||
List<String> ips = new ArrayList<>();
|
||||
|
||||
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||
while (interfaces.hasMoreElements()) {
|
||||
NetworkInterface ni = interfaces.nextElement();
|
||||
if (!ni.isUp() || ni.isLoopback() || ni.isVirtual()) continue;
|
||||
|
||||
Enumeration<InetAddress> addresses = ni.getInetAddresses();
|
||||
while (addresses.hasMoreElements()) {
|
||||
InetAddress addr = addresses.nextElement();
|
||||
if (addr instanceof Inet4Address) {
|
||||
System.out.println("[" + ips.size() + "] " + ni.getDisplayName()
|
||||
+ " -> " + addr.getHostAddress());
|
||||
ips.add(addr.getHostAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ips.isEmpty()) throw new Exception("No active network interface available");
|
||||
|
||||
if (ips.size() == 1) {
|
||||
System.out.println("One interface found, using: " + ips.get(0));
|
||||
return ips.get(0);
|
||||
}
|
||||
|
||||
System.out.print("Choose interface: ");
|
||||
int choice = Integer.parseInt(scanner.nextLine().trim());
|
||||
return ips.get(choice);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user