Coverage Summary for Class: RMIServer (it.polimi.ingsw.gc14.Network.RMI.Server)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
| RMIServer |
0%
(0/1)
|
0%
(0/16)
|
0%
(0/34)
|
0%
(0/81)
|
package it.polimi.ingsw.gc14.Network.RMI.Server;
import it.polimi.ingsw.gc14.Controller.GameController;
import it.polimi.ingsw.gc14.ErrorType;
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;
import it.polimi.ingsw.gc14.Network.RMI.Common.IGameServer;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* RMI server responsible for handling remote client connections,
* receiving player actions, and propagating game updates.
*
* <p>The server manages player registration, reconnection handling,
* heartbeat monitoring, and the forwarding of client requests to the
* shared network event queue.
*/
public class RMIServer extends UnicastRemoteObject implements IGameServer {
/** Hostname or IP address exposed by the RMI runtime ({@code java.rmi.server.hostname}). */
private final String host;
/** Server-side game controller; all model access is synchronized on this object. */
private final GameController controller;
/** Port on which the RMI registry is bound. */
private final int nPort;
/** username → callback */
private final Map<String, IClientCallback> clients = new ConcurrentHashMap<>();
/**
* username → watchdog.
* One watchdog per connected player, mirrors {@code pendingHeartbeat} / per-socket
* HeartbeatHandler in the TCP stack.
*/
private final Map<String, RMIHeartbeat> watchdogs = new ConcurrentHashMap<>();
/** Shared queue to which player actions are added for sequential processing. */
private final BlockingQueue<NetworkEvent> actionQueue;
/** Shared map tracking each player's connection status ({@code true} = online). */
private final LimitedMap<String, Boolean> playerList;
/**
* Constructs an RMI server with the required game and network components.
*
* @param controller the game controller used to manage the game logic.
* @param nPort the port used by the RMI registry.
* @param actionQueue the queue containing incoming network events.
* @param playerList the map storing the connection status of the players.
* @param host the hostname or IP address exposed by the RMI server.
* @throws RemoteException if the remote object cannot be exported.
*/
public RMIServer(GameController controller, int nPort,
BlockingQueue<NetworkEvent> actionQueue,
LimitedMap<String, Boolean> playerList,
String host) throws RemoteException {
this.controller = controller;
this.nPort = nPort;
this.actionQueue = actionQueue;
this.playerList = playerList;
this.host = host;
}
// -------------------------------------------------------------------------
// Join
// -------------------------------------------------------------------------
/**
* {@inheritDoc}
*
* <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 < NetworkConfig.MIN_PLAYERS || preferredInt > NetworkConfig.MAX_PLAYERS)
return ErrorType.WRONG_PLAYER_NUMBER;
synchronized (controller) {
if (playerList.isEmpty() &&(controller.getModel()==null || controller.getModel().getCurrentState().getGameStage().equals(GameStages.WAITING))) {
controller.setModel(new Game(preferredInt));
playerList.setLimit(preferredInt);
System.out.println("Game Created With: "+preferredInt+" Players");
}
if(controller.getModel().getCurrentState().getGameStage()!= GameStages.WAITING ) {
if (controller.getModel().getPlayers().stream().noneMatch(p -> p.getUserName().equals(username))|| controller.getModel().getCurrentState().getGameStage()==GameStages.ENDED) {
return ErrorType.GAME_ALREADY_STARTED;
}
}
else {
if (controller.addPlayer(username)) {
clients.put(username, callback);
playerList.put(username, true);
System.out.println("Accepted player: " + username);
startWatchdog(username);
System.out.println("Heartbeat connected for: " + username);
return null;
} else {
return ErrorType.INVALID_USERNAME;
}
}
if(!playerList.containsKey(username)){
clients.put(username, callback);
playerList.put(username, true);
System.out.println("(After crash)Reconnected player: " + username);
startWatchdog(username);
System.out.println("Heartbeat connected for: " + username);
return null;
}
else {
if(!playerList.get(username))
{
playerList.put(username, true);
clients.put(username, callback);
System.out.println("Reconnected player: " + username);
startWatchdog(username);
System.out.println("Heartbeat connected for: " + 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(),
disconnectedUsernames(game)));
System.out.println("Model sent: " + username);
actionQueue.add(new ReconnectPlayer(username));
return null;
}
else
return ErrorType.USER_ALREADY_CONNECTED;
}
}
}
// -------------------------------------------------------------------------
// Heartbeat — called by RMIClient every ~3 s
// -------------------------------------------------------------------------
/**
* Receives a heartbeat ping from the client.
* Mirrors the server reading {@code PING} and replying {@code PONG} in
* {@code HeartbeatHandler.run()}.
*
* @param username the username of the pinging client.
*/
@Override
public void ping(String username) throws RemoteException {
RMIHeartbeat wd = watchdogs.get(username);
if (wd != null) wd.receivePing();
}
// -------------------------------------------------------------------------
// Game model propagation — keep watchdogs in sync
// -------------------------------------------------------------------------
/**
* Notifies connected RMI clients of a new network event.
*
* <p>If the event does not represent an error, it is sent to all registered clients.
* If it represents an error, it is sent only to the client that requested the action.
* Communication errors are logged without interrupting the notification process.
*
* @param action the network event to send to the clients.
*/
public void notifyAll(NetworkEvent action){
for (Map.Entry<String, IClientCallback> entry : clients.entrySet()) {
if (!action.isError() || action.getUsername().equals(entry.getKey())) {
try{
entry.getValue().onAction(action);
}
catch (RemoteException e)
{
System.out.println("Remote exception: Exception during action sending attempt " + e.getMessage());
}
}
}
}
/**
* Notifies all connected RMI clients of a new game model.
*
* <p>The updated mini model is sent to each registered client callback.
* If a communication error occurs for a client, the exception is logged
* without interrupting the notification of the remaining clients.
*
* @param model the updated mini model to send to the connected clients.
*/
public void notifyAll(MiniModel model) {
for (IClientCallback cb : clients.values()) {
try{
cb.onGameInit(model);
}
catch (RemoteException e)
{
System.out.println("Remote exception: Exception during model sending attempt " + e.getMessage());
}
}
}
/**
* Requests to draw a tribe card from the upper list.
* Creates a NetworkEvent and sends it through the network client.
* @param playerUsername the name of the player performing the action
* @param pos the index of the card to draw
*/
public void drawUpperTribeCard(String playerUsername, int pos) {
actionQueue.offer(new DrawUpperTribeCard(playerUsername,pos));
}
/**
* Requests to draw a tribe card from the lower list.
* Creates a NetworkEvent and sends it through the network client.
* @param playerUsername the name of the player performing the action
* @param pos the index of the card to draw
*/
public void drawLowerTribeCard(String playerUsername,int pos) {
actionQueue.offer(new DrawLowerTribeCard(playerUsername,pos));
}
/**
* Requests to draw a building card from the upper list.
* Creates a NetworkEvent and sends it through the network client.
* @param playerUsername the name of the player performing the action
* @param pos the index of the card to draw
*/
public void drawUpperBuildingCard(String playerUsername,int pos) {
actionQueue.offer(new DrawUpperBuildingCard(playerUsername,pos));
}
/**
* Requests to draw a building card from the lower list.
* Creates a NetworkEvent and sends it through the network client.
* @param playerUsername the name of the player performing the action
* @param pos the index of the card to draw
*/
public void drawLowerBuildingCard(String playerUsername,int pos) {
actionQueue.offer(new DrawLowerBuildingCard(playerUsername,pos));
}
/**
* Requests to skip the turn.
* This action is available only when the player cannot draw any tribe card, but still can buy some buildings.
* @param playerUsername the name of the player performing the action
*/
public void skipTurn(String playerUsername) {
actionQueue.offer(new SkipTurn(playerUsername));
}
/**
* Used to perform the slot choice action for the specified player at the specified position.
* @param playerUsername the name of the player performing the action
* @param pos the index of the selected slot
*/
public void slotChoice(String playerUsername,int pos) {
actionQueue.offer(new SlotChoice(playerUsername,pos));
}
/**
* Enqueues a totem choice event for the specified player.
*
* @param playerUsername the username of the player choosing the totem.
* @param totems the name of the chosen totem.
* @throws RemoteException if the RMI call fails.
*/
public void totemChoice(String playerUsername, String totems) throws RemoteException {
actionQueue.offer(new TotemChoice(playerUsername,totems));
}
/**
* Starts the RMI server and binds it to the configured registry.
*
* <p>The method configures the server hostname, creates the RMI registry
* on the specified port, and registers this server instance under the
* {@code RMIGameServer} name.
*/
public void start() {
try {
System.setProperty("java.rmi.server.hostname", host);
Registry registry = LocateRegistry.createRegistry(nPort);
registry.rebind("RMIGameServer", this);
System.out.println("RMI Server started on port: " + nPort);
} catch (RemoteException e) {
e.printStackTrace();
}
}
/**
* Creates and starts an {@link RMIHeartbeat} for the specified player.
*
* @param username the username of the player monitored by the heartbeat watchdog.
*/
private void startWatchdog(String username) {
RMIHeartbeat wd = new RMIHeartbeat(
username, playerList, clients, actionQueue);
watchdogs.put(username, wd);
wd.start();
}
/**
* Triggers a voluntary disconnection for the specified player
* by stopping their heartbeat watchdog.
*
* @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));
}
}