Fix: full network stack refactor

This commit is contained in:
2026-06-14 13:31:32 +02:00
parent b11027a251
commit 3888529b96
27 changed files with 347 additions and 276 deletions
@@ -10,7 +10,7 @@ public enum ErrorType {
GENERIC_ERROR("Generic error"),
GAME_ALREADY_STARTED("Game already started"),
WRONG_ACTION("Wrong action");
private String description;
private final String description;
ErrorType(String description) {
this.description = description;
}
@@ -194,7 +194,7 @@ public class GameEventProcessor {
event.setIsError(!event.apply(gameController));
Game game = gameController.getModel();
if (event.getIsError()) {
if (event.isError()) {
broadcaster.notifyAll(event);
return;
}
@@ -191,10 +191,82 @@ public class MiniModel implements Serializable {
*
* @param standingPlayers the final ordered list of players.
*/
public void setStandingPlayers(ArrayList<Player> standingPlayers) {
public void setStandingPlayers(List<Player> standingPlayers) {
this.standingPlayers = standingPlayers;
}
/**
* Replaces the upper row of tribe cards on the board.
*
* @param cards the new upper tribe card list.
*/
public void setUpperListTribeCards(ArrayList<TribeCard> cards) {
this.upperListTribeCards = cards;
}
/**
* Replaces the lower row of tribe cards on the board.
*
* @param cards the new lower tribe card list.
*/
public void setLowerListTribeCards(ArrayList<TribeCard> cards) {
this.lowerListTribeCards = cards;
}
/**
* Replaces the upper row of building cards on the board.
*
* @param cards the new upper building card list.
*/
public void setUpperListBuildingCards(ArrayList<BuildingCard> cards) {
this.upperListBuildingCards = cards;
}
/**
* Replaces the lower row of building cards on the board.
*
* @param cards the new lower building card list.
*/
public void setLowerListBuildingCards(ArrayList<BuildingCard> cards) {
this.lowerListBuildingCards = cards;
}
/**
* Removes the card at the given position from the upper tribe card list.
*
* @param pos the zero-based index of the card to remove.
*/
public void removeUpperTribeCard(int pos) {
if (upperListTribeCards != null) upperListTribeCards.remove(pos);
}
/**
* Removes the card at the given position from the lower tribe card list.
*
* @param pos the zero-based index of the card to remove.
*/
public void removeLowerTribeCard(int pos) {
if (lowerListTribeCards != null) lowerListTribeCards.remove(pos);
}
/**
* Removes the card at the given position from the upper building card list.
*
* @param pos the zero-based index of the card to remove.
*/
public void removeUpperBuildingCard(int pos) {
if (upperListBuildingCards != null) upperListBuildingCards.remove(pos);
}
/**
* Removes the card at the given position from the lower building card list.
*
* @param pos the zero-based index of the card to remove.
*/
public void removeLowerBuildingCard(int pos) {
if (lowerListBuildingCards != null) lowerListBuildingCards.remove(pos);
}
/**
* Returns the position of the player with the specified username
* within the player collection.
@@ -66,7 +66,7 @@ public enum EventType {
*/
ENDED_GAME("Ended Game");
private String description;
private final String description;
private EventType(String description) {
this.description = description;
}
@@ -12,11 +12,11 @@ public interface IClient {
* and preferred connection type.
*
* @param username the username chosen by the player.
* @param preferredInt the preferred connection type selected by the client.
* @param proposedNPlayers the desired number of players proposed by this client.
* @return {@code true} if the connection is established successfully,
* {@code false} otherwise.
*/
ErrorType connect(String username, int preferredInt);
ErrorType connect(String username, int proposedNPlayers);
/**
* Requests to draw a tribe card from the upper tribe card list.
@@ -12,6 +12,10 @@ public final class NetworkConfig {
public static final long SILENCE_THRESHOLD_MS = 5_000;
/** Milliseconds between keep-alive pings sent by the client. */
public static final long KEEPALIVE_INTERVAL_MS = 3_000;
/** Minimum number of players required to start a game. */
public static final int MIN_PLAYERS = 2;
/** Maximum number of players allowed in a game. */
public static final int MAX_PLAYERS = 5;
private NetworkConfig() {}
}
@@ -80,15 +80,6 @@ public abstract class NetworkEvent implements Serializable {
*/
protected ArrayList<String> disconnectedPlayers;
/**
* Sets the list of currently disconnected player usernames.
*
* @param players the list of disconnected player usernames to attach to this event.
*/
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.
@@ -140,7 +131,7 @@ public abstract class NetworkEvent implements Serializable {
* @return {@code true} if the event could not be applied successfully,
* {@code false} otherwise.
*/
public boolean getIsError() {
public boolean isError() {
return isError;
}
@@ -154,6 +145,15 @@ public abstract class NetworkEvent implements Serializable {
this.isError = isError;
}
/**
* Overrides the error type for this event.
*
* @param errorType the error type to set.
*/
public void setErrorType(ErrorType errorType) {
this.errorType = errorType;
}
/**
* Constructs a network event.
*
@@ -162,7 +162,7 @@ public abstract class NetworkEvent implements Serializable {
* @param isError {@code true} if the event represents an error,
* {@code false} otherwise.
*/
protected NetworkEvent(String username, EventType eventType, boolean isError,ErrorType errorType) {
protected NetworkEvent(String username, EventType eventType, boolean isError, ErrorType errorType) {
this.username = username;
this.eventType = eventType;
this.isError = isError;
@@ -7,20 +7,15 @@ import it.polimi.ingsw.gc14.Network.EventType;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
/**
* NetworkEvent to add a player.
* Network event used to register a new player in the lobby.
*
* <p>The first player to join also proposes the desired number of players
* for the match. Subsequent joiners ignore {@code proposedNPlayer} if the
* game has already been created.
*/
public class AddPlayer extends NetworkEvent {
/** Number of proposed players to add to the match */
/** Number of players proposed by this player; used only when creating a new game. */
private int proposedNPlayer;
/**
* Overrides the default error type for this event.
*
* @param errorType the error type to set.
*/
public void setErrorType(ErrorType errorType)
{
this.errorType = errorType;
}
/**
* @return the number of proposed players to add to the match
@@ -17,45 +17,60 @@ import java.util.List;
import java.util.Map;
/**
* NetworkEvent to draw a tribe card from the lower card list.
* Network event that transitions all clients to the next round.
*
* <p>Carries refreshed card lists for all four board rows in addition
* to the standard game state (slot map, turn order, current state,
* player list). Only applied client-side; {@link #apply(GameController)}
* always returns {@code false}.
*/
public class ApplyNextRound extends NetworkEvent{
public class ApplyNextRound extends NetworkEvent {
List<Player> players;
ArrayList<TribeCard> upperListTribeCards;
ArrayList<TribeCard> lowerListTribeCards;
ArrayList<BuildingCard> upperListBuildingCards;
ArrayList<BuildingCard> lowerListBuildingCards;
private ArrayList<TribeCard> upperListTribeCards;
private ArrayList<TribeCard> lowerListTribeCards;
private ArrayList<BuildingCard> upperListBuildingCards;
private ArrayList<BuildingCard> lowerListBuildingCards;
/**
* Constructs an event that updates the game state for the next round.
*
* @param slotPlayerMap the map associating each slot with the player occupying it.
* @param orderLogicCard the order logic card that determines the player order.
* @param currentState the current state of the game.
* @param players the list of players in the game.
* @param slotPlayerMap the map associating each slot with the player occupying it.
* @param orderLogicCard the order logic card that determines the player order.
* @param currentState the current state of the game.
* @param players the list of players in the game.
* @param upperListTribeCards the upper row of tribe cards for the new round.
* @param lowerListTribeCards the lower row of tribe cards for the new round.
* @param upperListBuildingCards the upper row of building cards for the new round.
* @param lowerListBuildingCards the lower row of building cards for the new round.
*/
public ApplyNextRound(Map<Slot, Player> slotPlayerMap, OrderLogicCard orderLogicCard, CurrentState currentState, List<Player> players, ArrayList<TribeCard> upperListTribeCards, ArrayList<TribeCard>lowerListTribeCards, ArrayList<BuildingCard> upperListBuildingCards, ArrayList<BuildingCard>lowerListBuildingCards) {
super("SERVER",EventType.NEXT_ROUND,false, ErrorType.WRONG_ACTION);
public ApplyNextRound(Map<Slot, Player> slotPlayerMap,
OrderLogicCard orderLogicCard,
CurrentState currentState,
List<Player> players,
ArrayList<TribeCard> upperListTribeCards,
ArrayList<TribeCard> lowerListTribeCards,
ArrayList<BuildingCard> upperListBuildingCards,
ArrayList<BuildingCard> lowerListBuildingCards) {
super("SERVER", EventType.NEXT_ROUND, false, ErrorType.WRONG_ACTION);
setData(slotPlayerMap, orderLogicCard, currentState, players);
this.upperListBuildingCards = upperListBuildingCards;
this.lowerListBuildingCards = lowerListBuildingCards;
this.upperListTribeCards = upperListTribeCards;
this.lowerListTribeCards = lowerListTribeCards;
this.slotPlayerMap = slotPlayerMap;
this.orderLogicCard = orderLogicCard;
this.currentState = currentState;
this.players = players;
}
/**
* @param gameController the Game Controller on which to apply the event
* @return true if the player could draw the card, false otherwise
* Not applicable server-side.
*
* @param gameController the game controller (unused).
* @return {@code false} always.
*/
@Override
public boolean apply(GameController gameController){
public boolean apply(GameController gameController) {
return false;
}
/**
* Applies this event to the client-side mini model, updating all board lists,
* slot map, turn order, and game state for the new round.
@@ -63,20 +78,19 @@ public class ApplyNextRound extends NetworkEvent{
* @param miniModel the client-side model to update.
* @return {@code true} always (this event cannot produce an error).
*/
public boolean apply(MiniModel miniModel)
{
@Override
public boolean apply(MiniModel miniModel) {
synchronized (miniModel) {
miniModel.setSlotPlayerMap(slotPlayerMap);
miniModel.setOrderLogicCard(orderLogicCard);
miniModel.setCurrentState(currentState);
miniModel.setPlayers(players);
miniModel.upperListTribeCards = upperListTribeCards;
miniModel.lowerListTribeCards = lowerListTribeCards;
miniModel.upperListBuildingCards = upperListBuildingCards;
miniModel.lowerListBuildingCards = lowerListBuildingCards;
miniModel.setPlayers(playerList);
miniModel.setUpperListTribeCards(upperListTribeCards);
miniModel.setLowerListTribeCards(lowerListTribeCards);
miniModel.setUpperListBuildingCards(upperListBuildingCards);
miniModel.setLowerListBuildingCards(lowerListBuildingCards);
miniModel.setLastEvent(this);
return true;
}
}
}
@@ -9,14 +9,16 @@ import it.polimi.ingsw.gc14.Network.NetworkEvent;
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
* Network event used to notify all clients that a player has disconnected.
*
* <p>Also carries the updated list of available totems, since a disconnection
* during totem selection may free a totem for other players.
*/
public class DisconnectedPlayer extends NetworkEvent implements Serializable{
public class DisconnectedPlayer extends NetworkEvent {
private List<Totems> availableTotems;
@@ -29,18 +31,19 @@ public class DisconnectedPlayer extends NetworkEvent implements Serializable{
}
/**
* Class constructor.
* Initializes all the attributes.
* @param username the name of the player requesting the event
* Constructs a disconnection event for the specified player.
*
* @param username the username of the player who disconnected.
*/
public DisconnectedPlayer(String username){
public DisconnectedPlayer(String username) {
super(username, EventType.DISCONNECTED_PLAYER, false, ErrorType.GENERIC_ERROR);
}
/**
* @param gameController the Game Controller on which to apply the event
* @return true if the player could skipTheTurn, false otherwise
* Applies this disconnection event to the server-side game controller.
*
* @param gameController the game controller on which to apply the event.
* @return {@code true} if the disconnection is handled successfully.
*/
@Override
public boolean apply(GameController gameController){
@@ -16,7 +16,7 @@ public class DrawLowerBuildingCard extends NetworkEvent{
/**
* Class constructor.
* Initialized all the attributes.
* Initializes all the attributes.
* @param username the name of the player requesting the event
* @param pos the index of the card to draw
*/
@@ -48,7 +48,7 @@ public class DrawLowerBuildingCard extends NetworkEvent{
if (isError)
return false;
miniModel.lowerListBuildingCards.remove(pos);
miniModel.removeLowerBuildingCard(pos);
miniModel.setPlayers(playerList);
miniModel.setOrderLogicCard(orderLogicCard);
miniModel.setCurrentState(currentState);
@@ -17,7 +17,7 @@ public class DrawLowerTribeCard extends NetworkEvent{
/**
* Class constructor.
* Initialized all the attributes.
* Initializes all the attributes.
* @param username the name of the player requesting the event
* @param pos the index of the card to draw
*/
@@ -47,7 +47,7 @@ public class DrawLowerTribeCard extends NetworkEvent{
if (isError)
return false;
miniModel.lowerListTribeCards.remove(pos);
miniModel.removeLowerTribeCard(pos);
miniModel.setPlayers(playerList);
miniModel.setOrderLogicCard(orderLogicCard);
miniModel.setCurrentState(currentState);
@@ -17,7 +17,7 @@ public class DrawUpperBuildingCard extends NetworkEvent{
/**
* Class constructor.
* Initialized all the attributes.
* Initializes all the attributes.
* @param username the name of the player requesting the event
* @param pos the index of the card to draw
*/
@@ -48,7 +48,7 @@ public class DrawUpperBuildingCard extends NetworkEvent{
if (isError)
return false;
miniModel.upperListBuildingCards.remove(pos);
miniModel.removeUpperBuildingCard(pos);
miniModel.setPlayers(playerList);
miniModel.setOrderLogicCard(orderLogicCard);
miniModel.setCurrentState(currentState);
@@ -17,7 +17,7 @@ public class DrawUpperTribeCard extends NetworkEvent{
/**
* Class constructor.
* Initialized all the attributes.
* Initializes all the attributes.
* @param username the name of the player requesting the event
* @param pos the index of the card to draw
*/
@@ -46,7 +46,7 @@ public class DrawUpperTribeCard extends NetworkEvent{
synchronized (miniModel) {
if (isError)
return false;
miniModel.upperListTribeCards.remove(pos);
miniModel.removeUpperTribeCard(pos);
miniModel.setPlayers(playerList);
miniModel.setOrderLogicCard(orderLogicCard);
miniModel.setCurrentState(currentState);
@@ -11,58 +11,59 @@ import it.polimi.ingsw.gc14.Network.EventType;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* NetworkEvent to draw a tribe card from the lower card list.
* Network event that signals the end of the game and delivers the final standings.
*
* <p>Only applied client-side; {@link #apply(GameController)} always returns {@code false}.
*/
public class EndedGame extends NetworkEvent{
ArrayList<Player> players;
public class EndedGame extends NetworkEvent {
/**
* Constructs an event containing the final game state.
*
* @param slotPlayerMap the map associating each slot with the player occupying it.
* @param slotPlayerMap the map associating each slot with the player occupying it.
* @param orderLogicCard the order logic card that determines the player order.
* @param currentState the current state of the game.
* @param players the list of players at the end of the game.
* @param currentState the current state of the game.
* @param players the final list of players, used as the standings.
*/
public EndedGame(Map<Slot, Player> slotPlayerMap, OrderLogicCard orderLogicCard, CurrentState currentState, ArrayList<Player> players){
super("SERVER",EventType.ENDED_GAME,false, ErrorType.GENERIC_ERROR);
this.slotPlayerMap = slotPlayerMap;
this.orderLogicCard = orderLogicCard;
this.currentState = currentState;
this.players = players;
public EndedGame(Map<Slot, Player> slotPlayerMap,
OrderLogicCard orderLogicCard,
CurrentState currentState,
ArrayList<Player> players) {
super("SERVER", EventType.ENDED_GAME, false, ErrorType.GENERIC_ERROR);
setData(slotPlayerMap, orderLogicCard, currentState, players);
}
/**
* @param gameController the Game Controller on which to apply the event
* @return true if the player could draw the card, false otherwise
* Not applicable server-side.
*
* @param gameController the game controller (unused).
* @return {@code false} always.
*/
@Override
public boolean apply(GameController gameController){
public boolean apply(GameController gameController) {
return false;
}
/**
* Applies this event to the client-side mini model, updating the final
* game state and setting the standing players list for the leaderboard.
* game state and setting the standings list for the leaderboard.
*
* @param miniModel the client-side model to update.
* @return {@code true} always (this event cannot produce an error).
*/
public boolean apply(MiniModel miniModel)
{
@Override
public boolean apply(MiniModel miniModel) {
synchronized (miniModel) {
miniModel.setSlotPlayerMap(slotPlayerMap);
miniModel.setOrderLogicCard(orderLogicCard);
miniModel.setCurrentState(currentState);
miniModel.setPlayers(players);
miniModel.setStandingPlayers(players);
miniModel.setPlayers(playerList);
miniModel.setStandingPlayers(playerList);
miniModel.setLastEvent(this);
return true;
}
}
}
@@ -7,13 +7,12 @@ import it.polimi.ingsw.gc14.Model.MiniModel;
import it.polimi.ingsw.gc14.Network.EventType;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import java.io.Serializable;
import java.util.ArrayList;
/**
* Network event used to notify that a player has reconnected to the game.
*/
public class ReconnectPlayer extends NetworkEvent implements Serializable {
public class ReconnectPlayer extends NetworkEvent {
@Override
public void enrichWithGameState(Game game, ArrayList<String> disconnectedUsernames) {
@@ -6,12 +6,10 @@ import it.polimi.ingsw.gc14.Model.MiniModel;
import it.polimi.ingsw.gc14.Network.EventType;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import java.io.Serializable;
/**
* NetworkEvent to avoid drawing a card from the lower card list
* Network event used to skip the current player's optional card action.
*/
public class SkipTurn extends NetworkEvent implements Serializable{
public class SkipTurn extends NetworkEvent {
/**
* Class constructor.
@@ -12,14 +12,14 @@ import it.polimi.ingsw.gc14.Network.NetworkEvent;
*/
public class SlotChoice extends NetworkEvent {
/** Index of the card to draw */
/** Index of the selected slot. */
private int pos;
/**
* Class constructor.
* Initializes all the attributes.
* @param username the name of the player requesting the event
* @param pos the index of the card to draw
* @param pos the index of the selected slot
*/
public SlotChoice(String username, int pos) {
super(username, EventType.SLOT_CHOICE, false, ErrorType.WRONG_ACTION);
@@ -27,8 +27,8 @@ public class SlotChoice extends NetworkEvent {
}
/**
* @param gameController the Game Controller on which to apply the event
* @return true if the player could draw the card, false otherwise
* @param gameController the game controller on which to apply the event.
* @return {@code true} if the slot choice is handled successfully, {@code false} otherwise.
*/
@Override
public boolean apply(GameController gameController) {
@@ -6,7 +6,6 @@ import it.polimi.ingsw.gc14.Network.NetworkEvent;
import it.polimi.ingsw.gc14.Network.RMI.Common.IClientCallback;
import javafx.application.Platform;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
@@ -15,7 +14,7 @@ import java.rmi.server.UnicastRemoteObject;
* RMI client callback implementation of {@link IClientCallback}.
* Receives notifications from the server and updates the client game model.
*/
public class ClientCallbackImpl extends UnicastRemoteObject implements IClientCallback, Serializable {
public class ClientCallbackImpl extends UnicastRemoteObject implements IClientCallback {
/** The client controller used to apply events and update the model */
private final ClientController clientController;
@@ -53,7 +52,7 @@ public class ClientCallbackImpl extends UnicastRemoteObject implements IClientCa
*/
@Override
public void onAction(NetworkEvent event) throws RemoteException {
if(event.getIsError()) {
if(event.isError()) {
clientController.getView().showError(event.getErrorType(),event.toString());
} else {
event.apply(clientController.getMiniModel());
@@ -20,7 +20,7 @@ public class RMIClient implements IClient {
private final int port;
private IGameServer stub;
private ClientController controller;
private String myIP;
private String myIp;
private String username;
private volatile boolean running = false;
@@ -35,13 +35,13 @@ public class RMIClient implements IClient {
* @param controller the client controller associated with this RMI client.
* @param host the hostname or IP address of the RMI server.
* @param port the port used to connect to the RMI registry.
* @param myIP the IP address of the client.
* @param myIp the IP address of the client.
*/
public RMIClient(ClientController controller, String host, int port, String myIP) {
public RMIClient(ClientController controller, String host, int port, String myIp) {
this.controller = controller;
this.host = host;
this.port = port;
this.myIP = myIP;
this.myIp = myIp;
}
@@ -53,15 +53,15 @@ public class RMIClient implements IClient {
* opening a second socket).
*/
@Override
public ErrorType connect(String username, int preferredInt) {
public ErrorType connect(String username, int proposedNPlayers) {
try {
System.setProperty("java.rmi.server.hostname", this.myIP);
System.setProperty("java.rmi.server.hostname", this.myIp);
Registry registry = LocateRegistry.getRegistry(host, port);
this.stub = (IGameServer) registry.lookup("RMIGameServer");
this.username = username;
ClientCallbackImpl callback = new ClientCallbackImpl(controller);
ErrorType status = stub.joinGame(username, preferredInt, callback);
ErrorType status = stub.joinGame(username, proposedNPlayers, callback);
if(status==null)
{
running = true;
@@ -250,12 +250,12 @@ public class RMIClient implements IClient {
* the keep-alive scheduler.
*/
public void notifyDisconnection() {
running = false;
if (pingSender != null) pingSender.shutdownNow();
if (executor != null) executor.shutdownNow();
try{
try {
stub.disconnectPlayer(username);
}
catch (RemoteException e){
} catch (RemoteException e) {
System.out.println("Error during remote disconnection");
}
}
@@ -14,10 +14,10 @@ public interface IGameServer extends Remote {
* to notify the corresponding RMI client.
*
* @param username the username chosen by the player.
* @param preferredInt the preferred number of players for the game.
* @param preferredInt the desired number of players proposed by this client.
* @param callback the remote callback associated with the client.
* @return {@code true} if the player joins the game successfully,
* {@code false} otherwise.
* @return {@code null} if the player joins successfully;
* an {@link it.polimi.ingsw.gc14.ErrorType} value describing the rejection otherwise.
* @throws RemoteException if an RMI communication error occurs.
*/
ErrorType joinGame(String username, int preferredInt, IClientCallback callback) throws RemoteException;
@@ -96,5 +96,11 @@ public interface IGameServer extends Remote {
*/
void ping(String username) throws RemoteException;
/**
* Notifies the server of a voluntary disconnection for the specified player.
*
* @param username the username of the player disconnecting.
* @throws RemoteException if an RMI communication error occurs.
*/
void disconnectPlayer(String username) throws RemoteException;
}
@@ -4,6 +4,7 @@ import it.polimi.ingsw.gc14.LimitedMap;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import it.polimi.ingsw.gc14.Network.NetworkConfig;
import it.polimi.ingsw.gc14.Network.NetworkEvents.DisconnectedPlayer;
import it.polimi.ingsw.gc14.Network.RMI.Common.IClientCallback;
import java.util.Map;
import java.util.concurrent.*;
@@ -28,7 +29,7 @@ public class RMIHeartbeat {
private String username = "";
private final LimitedMap<String, Boolean> playerList;
private final Map<String, ?> clients; // ConcurrentHashMap<String, IClientCallback>
private final Map<String, IClientCallback> clients;
private final BlockingQueue<NetworkEvent> actionQueue;
/** Last time a ping was received from this client. */
@@ -53,7 +54,7 @@ public class RMIHeartbeat {
public RMIHeartbeat(
String username,
LimitedMap<String, Boolean> playerList,
Map<String, ?> clients,
Map<String, IClientCallback> clients,
BlockingQueue<NetworkEvent> actionQueue) {
this.username = username;
@@ -6,6 +6,7 @@ import it.polimi.ingsw.gc14.LimitedMap;
import it.polimi.ingsw.gc14.Model.Game;
import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;
import it.polimi.ingsw.gc14.Model.MiniModel;
import it.polimi.ingsw.gc14.Network.NetworkConfig;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import it.polimi.ingsw.gc14.Network.NetworkEvents.*;
import it.polimi.ingsw.gc14.Network.RMI.Common.IClientCallback;
@@ -33,10 +34,10 @@ import java.util.stream.Collectors;
* shared network event queue.
*/
public class RMIServer extends UnicastRemoteObject implements IGameServer {
private String host;
private final String host;
private final GameController controller;
private Registry registry;
private int nPort;
private final int nPort;
/** username → callback */
private final Map<String, IClientCallback> clients = new ConcurrentHashMap<>();
@@ -48,8 +49,8 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
*/
private final Map<String, RMIHeartbeat> watchdogs = new ConcurrentHashMap<>();
BlockingQueue<NetworkEvent> actionQueue;
private LimitedMap<String, Boolean> playerList;
private final BlockingQueue<NetworkEvent> actionQueue;
private final LimitedMap<String, Boolean> playerList;
/**
@@ -80,14 +81,17 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
/**
* {@inheritDoc}
*
* <p>After a successful join a {@link RMIHeartbeat} is created and
* <p>After a successful join an {@link RMIHeartbeat} watchdog is created and
* started for the new player — mirrors creating a {@code HeartbeatHandler} in
* {@code TCPServer.acceptHeartbeat()}.
*
* @return {@code null} on success; an {@link it.polimi.ingsw.gc14.ErrorType} value on rejection.
*/
@Override
public ErrorType joinGame(String username, int preferredInt, IClientCallback callback)
throws RemoteException {
if (preferredInt < 2 || preferredInt > 5) return ErrorType.WRONG_PLAYER_NUMBER;
if (preferredInt < NetworkConfig.MIN_PLAYERS || preferredInt > NetworkConfig.MAX_PLAYERS)
return ErrorType.WRONG_PLAYER_NUMBER;
synchronized (controller) {
@@ -97,7 +101,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
System.out.println("Game Created With :"+preferredInt+" Players");
}
if(controller.getModel().getCurrentState().getGameStage()!= GameStages.WAITING ) {
if (!controller.getModel().getPlayers().stream().anyMatch(p -> p.getUserName().equals(username))) {
if (controller.getModel().getPlayers().stream().noneMatch(p -> p.getUserName().equals(username))) {
return ErrorType.GAME_ALREADY_STARTED;
}
}
@@ -129,7 +133,12 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
System.out.println("Reconnected player: " + username);
startWatchdog(username);
Game game = controller.getModel();
callback.onGameInit(new MiniModel(game.getSlotMap(), game.getOrderLogicCard(), game.getCurrentState(), game.getPlayers(), game.getAvailableTotems(), game.getUpperListTribeCards(), game.getLowerListTribeCards(), game.getUpperListBuilding(), game.getLowerListBuilding(), game.getDisconnectedPlayers().entrySet().stream().filter(Map.Entry::getValue).map(x -> x.getKey().getUserName()).collect(Collectors.toCollection(ArrayList::new))));
callback.onGameInit(new MiniModel(
game.getSlotMap(), game.getOrderLogicCard(), game.getCurrentState(),
game.getPlayers(), game.getAvailableTotems(),
game.getUpperListTribeCards(), game.getLowerListTribeCards(),
game.getUpperListBuilding(), game.getLowerListBuilding(),
disconnectedUsernames(game)));
System.out.println("Model sent: " + username);
actionQueue.add(new ReconnectPlayer(username));
return null;
@@ -172,8 +181,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
*/
public void notifyAll(NetworkEvent action){
for (Map.Entry<String, IClientCallback> entry : clients.entrySet()) {
if (!action.getIsError() ||
(action.getIsError() && action.getUsername().equals(entry.getKey()))) {
if (!action.isError() || action.getUsername().equals(entry.getKey())) {
try{
entry.getValue().onAction(action);
}
@@ -300,7 +308,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
registry.rebind("RMIGameServer", this);
System.out.println("RMI Server started on port: " + nPort);
return true;
} catch (Exception e) {
} catch (RemoteException e) {
e.printStackTrace();
return false;
}
@@ -319,7 +327,8 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
try {
registry.unbind("RMIGameServer");
UnicastRemoteObject.unexportObject(this, true);
watchdogs.values().forEach(wd -> { /* watchdogs shut themselves down */ });
watchdogs.values().forEach(RMIHeartbeat::disconnect);
watchdogs.clear();
System.out.println("RMI Server stopped");
return true;
} catch (RemoteException | NotBoundException e) {
@@ -346,11 +355,20 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
*
* @param username the username of the player to disconnect.
*/
@Override
public void disconnectPlayer(String username) {
RMIHeartbeat wd = watchdogs.get(username);
if (wd != null) wd.disconnect();
}
/** Returns usernames of players currently marked as disconnected in the given game. */
private static ArrayList<String> disconnectedUsernames(Game game) {
return game.getDisconnectedPlayers().entrySet().stream()
.filter(Map.Entry::getValue)
.map(e -> e.getKey().getUserName())
.collect(Collectors.toCollection(ArrayList::new));
}
}
@@ -22,25 +22,25 @@ public class TCPClient implements IClient {
private static final int PONG = 2;
/** Socket TCP */
Socket communicationSocket;
private Socket communicationSocket;
/** Input stream used receive objects from the server */
ObjectInputStream socketReceive;
private ObjectInputStream socketReceive;
/** Output stream used to send objects to the server */
ObjectOutputStream socketSend;
private ObjectOutputStream socketSend;
/** Client game's controller */
ClientController controller;
private ClientController controller;
/** IP address of the server to connect to */
String hostname;
private String hostname;
private boolean running;
/** TCP port */
int mainPort;
private int mainPort;
int heartbeatPort;
private int heartbeatPort;
private Socket heartbeatSocket;
private OutputStream heartbeatOut;
private InputStream heartbeatIn;
@@ -80,26 +80,27 @@ public class TCPClient implements IClient {
try {
if( socketReceive.readObject() instanceof AddPlayer x)
{
if(x.getIsError())
if(x.isError())
return x.getErrorType();
}
} catch (ClassNotFoundException e) {
return ErrorType.GENERIC_ERROR;
}
running = true;
new Thread(this::receiveMessage, "tcp-reader").start();
// Socket heartbeat
// heartbeat socket
this.heartbeatSocket = new Socket(hostname, heartbeatPort);
this.heartbeatOut =heartbeatSocket.getOutputStream() ;
this.heartbeatOut = heartbeatSocket.getOutputStream();
this.heartbeatIn = heartbeatSocket.getInputStream();
// manda subito username per associare i due socket lato server
// send username immediately so server can associate the two sockets
new ObjectOutputStream(heartbeatSocket.getOutputStream()).writeObject(user);
heartbeatOut.flush();
new Thread(this::heartbeatLoop, "heartbeat").start();
running = true;
return null;
} catch (IOException e) {
@@ -135,23 +136,20 @@ public class TCPClient implements IClient {
int b = heartbeatIn.read();
if (b == -1 || b != PONG) {
disconnect();
controller.getView().showError(ErrorType.SERVER_CRASHED,ErrorType.SERVER_CRASHED.toString());
controller.getView().showError(ErrorType.SERVER_CRASHED, ErrorType.SERVER_CRASHED.toString());
break;
}
// pong ricevuto → server vivo
// pong received: server is alive
}
} catch (SocketTimeoutException e) {
System.out.println("Server heartbeat timeout");
disconnect();
controller.getView().showError(ErrorType.SERVER_CRASHED,ErrorType.SERVER_CRASHED.toString());
controller.getView().showError(ErrorType.SERVER_CRASHED, ErrorType.SERVER_CRASHED.toString());
} catch (IOException e) {
if(running)
{
if (running) {
disconnect();
controller.getView().showError(ErrorType.SERVER_CRASHED,ErrorType.SERVER_CRASHED.toString());
controller.getView().showError(ErrorType.SERVER_CRASHED, ErrorType.SERVER_CRASHED.toString());
}
} finally {
sender.shutdownNow();
}
@@ -170,7 +168,7 @@ public class TCPClient implements IClient {
* Listens continuously for incoming objects from the server.
* - If the received object is a {@link NetworkEvent} flagged as an error, it is printed.
* - If the received object is a valid {@link NetworkEvent}, it is applied to the game controller.
* - If the received object is a {@link Game} model, the controller's model is set.
* - If the received object is a {@link MiniModel}, the controller's model is set.
*/
private void receiveMessage() {
while (true) {
@@ -179,11 +177,12 @@ public class TCPClient implements IClient {
try {
read = socketReceive.readObject();
} catch (IOException e) {
if (running) disconnect();
break;
}
if (read instanceof NetworkEvent event) {
if (event.getIsError()) {
if (event.isError()) {
if(event.getErrorType() == ErrorType.WRONG_ACTION) {
controller.getView().showError(ErrorType.WRONG_ACTION,event.toString());
}
@@ -23,7 +23,7 @@ public class ClientHandler implements Runnable {
*/
private final String username;
private boolean running ;
private volatile boolean running;
/**
* Returns the username associated with this client.
@@ -37,22 +37,22 @@ public class ClientHandler implements Runnable {
private Socket clientSocket;
/** Input stream used to receive objects from the client */
public ObjectInputStream in;
private ObjectInputStream in;
/** Output stream used to send objects to the client */
public ObjectOutputStream out;
private ObjectOutputStream out;
/**
* Shared list of all client handlers.
* This handler removes itself from the list when disconnected.
*/
List<ClientHandler> clientHandlers;
private List<ClientHandler> clientHandlers;
/** Maps each connected player's username to their connection state (true = connected). */
LimitedMap<String,Boolean> limitedMap;
private LimitedMap<String, Boolean> limitedMap;
/** Queue containing the events to be applied to the game model */
BlockingQueue<NetworkEvent> actionQueue;
private BlockingQueue<NetworkEvent> actionQueue;
/**
@@ -87,7 +87,7 @@ public class ClientHandler implements Runnable {
try {
while (running) {
NetworkEvent event = (NetworkEvent) in.readObject();
if (!actionQueue.add(event)) {
if (!actionQueue.offer(event)) {
System.out.println("Error inserting action into queue");
}
}
@@ -84,14 +84,11 @@ public class HeartbeatHandler implements Runnable {
startWatchdog();
try {
while (running) {
int b = in.read(); // blocca finché non arriva un byte
int b = in.read(); // blocks until a byte arrives
if (b == -1) {
synchronized (this)
{
out.write(-1);
}
disconnect(); break;
} // stream chiusa
disconnect();
break; // stream closed
}
if (b == PING) {
lastReceivedTime = System.currentTimeMillis();
out.write(PONG);
@@ -7,6 +7,7 @@ import it.polimi.ingsw.gc14.Model.Game;
import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;
import it.polimi.ingsw.gc14.Model.MiniModel;
import it.polimi.ingsw.gc14.Network.EventType;
import it.polimi.ingsw.gc14.Network.NetworkConfig;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import it.polimi.ingsw.gc14.Network.NetworkEvents.AddPlayer;
import it.polimi.ingsw.gc14.Network.NetworkEvents.ReconnectPlayer;
@@ -34,47 +35,42 @@ public class TCPServer {
/**
* Main TCP port used for standard client-server communication.
*/
int port;
private int port;
/**
* TCP port dedicated to heartbeat communication.
*/
int heartbeatPort;
/**
* Number of players that have successfully connected.
*/
int connectedPlayers;
private int heartbeatPort;
/**
* Main server socket used to accept client connections.
*/
ServerSocket socketTCP;
private ServerSocket socketTCP;
/**
* Server socket used to accept heartbeat connections.
*/
ServerSocket heartbeatSocketTCP;
private ServerSocket heartbeatSocketTCP;
/**
* Game controller used to manage the server-side game logic.
*/
final GameController controller;
private final GameController controller;
/**
* Queue containing network events received from clients.
*/
BlockingQueue<NetworkEvent> actionQueue;
private BlockingQueue<NetworkEvent> actionQueue;
/**
* Map storing the online/offline status of connected players.
*/
LimitedMap<String, Boolean> playerList;
private LimitedMap<String, Boolean> playerList;
/**
* List of active TCP client handlers.
*/
CopyOnWriteArrayList<ClientHandler> clientHandlers;
private CopyOnWriteArrayList<ClientHandler> clientHandlers;
/**
@@ -99,7 +95,6 @@ public class TCPServer {
LimitedMap<String, Boolean> playerList) {
this.port = port;
this.heartbeatPort = heartbeatPort;
this.connectedPlayers = 0;
this.controller = controller;
this.actionQueue = actionQueue;
this.playerList = playerList;
@@ -152,8 +147,8 @@ public class TCPServer {
AddPlayer eventAddPlayer = (AddPlayer) event;
if (eventAddPlayer.getProposedNPlayer() < 2
|| eventAddPlayer.getProposedNPlayer() > 5) {
if (eventAddPlayer.getProposedNPlayer() < NetworkConfig.MIN_PLAYERS
|| eventAddPlayer.getProposedNPlayer() > NetworkConfig.MAX_PLAYERS) {
eventAddPlayer.setErrorType(ErrorType.WRONG_PLAYER_NUMBER);
eventAddPlayer.setIsError(true);
clientSend.writeObject(eventAddPlayer);
@@ -172,7 +167,7 @@ public class TCPServer {
System.out.println("Game Created With :"+eventAddPlayer.getProposedNPlayer()+" Players");
}
if(controller.getModel().getCurrentState().getGameStage()!= GameStages.WAITING ) {
if (!controller.getModel().getPlayers().stream().anyMatch(p -> p.getUserName().equals(username))) {
if (controller.getModel().getPlayers().stream().noneMatch(p -> p.getUserName().equals(username))) {
eventAddPlayer.setErrorType(ErrorType.GAME_ALREADY_STARTED);
eventAddPlayer.setIsError(true);
clientSend.writeObject(eventAddPlayer);
@@ -181,33 +176,16 @@ public class TCPServer {
continue;
}
}
else{
else {
if (controller.addPlayer(username)) {
playerList.put(username, true);
System.out.println("Accepted player: " + username);
ClientHandler handler = new ClientHandler(
username,
clientSocket,
clientSend,
clientReceive,
clientHandlers,
playerList,
actionQueue
);
clientSend.writeObject(eventAddPlayer);
pendingHeartbeat.put(username, handler);
Thread thread = new Thread(handler);
thread.start();
clientHandlers.add(handler);
connectedPlayers++;
ClientHandler handler = createAndRegisterHandler(
username, clientSocket, clientSend, clientReceive);
new Thread(handler).start();
continue;
}
else {
} else {
eventAddPlayer.setErrorType(ErrorType.USERNAME_ALREADY_USED);
eventAddPlayer.setIsError(true);
clientSend.writeObject(eventAddPlayer);
@@ -217,73 +195,36 @@ public class TCPServer {
}
}
if (!playerList.containsKey(username)) {
playerList.put(username, true);
System.out.println("(After crash)Reconnected player: " + username);
ClientHandler handler = new ClientHandler(
username,
clientSocket,
clientSend,
clientReceive,
clientHandlers,
playerList,
actionQueue
);
clientSend.writeObject(eventAddPlayer);
pendingHeartbeat.put(username, handler);
Thread thread = new Thread(handler);
thread.start();
clientHandlers.add(handler);
connectedPlayers++;
}
//reconnect a previously disconnected player
else{
if (!playerList.get(username)) {
playerList.put(username, true);
System.out.println("Reconnected player: " + username);
ClientHandler handler = new ClientHandler(
username,
clientSocket,
clientSend,
clientReceive,
clientHandlers,
playerList,
actionQueue
);
clientSend.writeObject(eventAddPlayer);
pendingHeartbeat.put(username, handler);
Game game = controller.getModel();
handler.notifyMiniModel(new MiniModel(
game.getSlotMap(),
game.getOrderLogicCard(),
game.getCurrentState(),
game.getPlayers(),
game.getAvailableTotems(),
game.getUpperListTribeCards(), game.getLowerListTribeCards(), game.getUpperListBuilding(), game.getLowerListBuilding(), game.getDisconnectedPlayers().entrySet().stream().filter(Map.Entry::getValue).map(x -> x.getKey().getUserName()).collect(Collectors.toCollection(ArrayList::new))
));
Thread thread = new Thread(handler);
thread.start();
clientHandlers.add(handler);
connectedPlayers++;
actionQueue.add(new ReconnectPlayer(username));
} else {
eventAddPlayer.setErrorType(ErrorType.USER_ALREADY_CONNECTED);
eventAddPlayer.setIsError(true);
clientSend.writeObject(eventAddPlayer);
clientSocket.close();
System.out.println("Invalid parameters. Connection terminated.");
}
ClientHandler handler = createAndRegisterHandler(
username, clientSocket, clientSend, clientReceive);
new Thread(handler).start();
} else if (!playerList.get(username)) {
// reconnect a previously disconnected player
playerList.put(username, true);
System.out.println("Reconnected player: " + username);
clientSend.writeObject(eventAddPlayer);
ClientHandler handler = createAndRegisterHandler(
username, clientSocket, clientSend, clientReceive);
Game game = controller.getModel();
handler.notifyMiniModel(new MiniModel(
game.getSlotMap(), game.getOrderLogicCard(),
game.getCurrentState(), game.getPlayers(),
game.getAvailableTotems(),
game.getUpperListTribeCards(), game.getLowerListTribeCards(),
game.getUpperListBuilding(), game.getLowerListBuilding(),
disconnectedUsernames(game)));
new Thread(handler).start();
actionQueue.add(new ReconnectPlayer(username));
} else {
eventAddPlayer.setErrorType(ErrorType.USER_ALREADY_CONNECTED);
eventAddPlayer.setIsError(true);
clientSend.writeObject(eventAddPlayer);
clientSocket.close();
System.out.println("Invalid parameters. Connection terminated.");
}
}
} catch (IOException | ClassNotFoundException e) {
@@ -340,7 +281,7 @@ public class TCPServer {
*/
public void notifyAll(NetworkEvent event) {
clientHandlers.forEach(h -> {
if (!event.getIsError() || event.getUsername().equals(h.getUsername())) {
if (!event.isError() || event.getUsername().equals(h.getUsername())) {
h.notifyEvent(event);
}
});
@@ -354,4 +295,28 @@ public class TCPServer {
public void notifyAll(MiniModel model) {
clientHandlers.forEach(h -> h.notifyMiniModel(model));
}
/**
* Creates a {@link ClientHandler}, registers it in {@code pendingHeartbeat}
* and {@code clientHandlers}, but does NOT start its thread — callers start
* the thread after any extra setup (e.g. sending a model snapshot).
*/
private ClientHandler createAndRegisterHandler(String username,
Socket socket,
ObjectOutputStream out,
ObjectInputStream in) {
ClientHandler handler = new ClientHandler(
username, socket, out, in, clientHandlers, playerList, actionQueue);
pendingHeartbeat.put(username, handler);
clientHandlers.add(handler);
return handler;
}
/** Returns usernames of players currently marked as disconnected in the given game. */
private static ArrayList<String> disconnectedUsernames(Game game) {
return game.getDisconnectedPlayers().entrySet().stream()
.filter(Map.Entry::getValue)
.map(e -> e.getKey().getUserName())
.collect(Collectors.toCollection(ArrayList::new));
}
}