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