Add: complete javaDOC
This commit is contained in:
@@ -5,8 +5,14 @@ import it.polimi.ingsw.gc14.View.GUI.GUI;
|
|||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/** Entry point for launching the game client with the JavaFX GUI. */
|
||||||
public class ClientLauncherGUI {
|
public class ClientLauncherGUI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the GUI application.
|
||||||
|
*
|
||||||
|
* @param args command-line arguments passed to the JavaFX runtime.
|
||||||
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
System.setProperty("glass.gtk.uiScale", "1.0");
|
System.setProperty("glass.gtk.uiScale", "1.0");
|
||||||
System.setProperty("glass.win.uiScale", "1.0");
|
System.setProperty("glass.win.uiScale", "1.0");
|
||||||
|
|||||||
@@ -165,6 +165,10 @@ public class ClientController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the server of a voluntary disconnection and closes the
|
||||||
|
* network client connection.
|
||||||
|
*/
|
||||||
public void disconnect()
|
public void disconnect()
|
||||||
{
|
{
|
||||||
client.notifyDisconnection();
|
client.notifyDisconnection();
|
||||||
|
|||||||
@@ -206,9 +206,16 @@ public class GameController {
|
|||||||
return model.totemChoice(player, Totems.valueOf(totem));
|
return model.totemChoice(player, Totems.valueOf(totem));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link #endGameForFeit()} instead.
|
||||||
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public synchronized void EndGameForFeit() { endGameForFeit(); }
|
public synchronized void EndGameForFeit() { endGameForFeit(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends the game due to forfeit: all remaining players are absent,
|
||||||
|
* so the game is terminated and final scores are computed.
|
||||||
|
*/
|
||||||
public synchronized void endGameForFeit() {
|
public synchronized void endGameForFeit() {
|
||||||
model.endGameForFeit();
|
model.endGameForFeit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package it.polimi.ingsw.gc14;
|
package it.polimi.ingsw.gc14;
|
||||||
|
|
||||||
|
/** Enumeration of error types that can be returned by server-side operations. */
|
||||||
public enum ErrorType {
|
public enum ErrorType {
|
||||||
USER_NOT_FOUND("User not found"),
|
USER_NOT_FOUND("User not found"),
|
||||||
USERNAME_ALREADY_USED("Username is already in use"),
|
USERNAME_ALREADY_USED("Username is already in use"),
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import it.polimi.ingsw.gc14.Model.Cards.TribeCards.Characters.Inventor;
|
|||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
/** Standalone entry point used for local model testing. */
|
||||||
public class Main {
|
public class Main {
|
||||||
|
/** @param args unused. */
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,17 @@ import java.util.*;
|
|||||||
*/
|
*/
|
||||||
public class MiniModel implements Serializable {
|
public class MiniModel implements Serializable {
|
||||||
|
|
||||||
|
/** Upper row of tribe cards currently on the board. */
|
||||||
public ArrayList<TribeCard> upperListTribeCards;
|
public ArrayList<TribeCard> upperListTribeCards;
|
||||||
|
/** Lower row of tribe cards currently on the board. */
|
||||||
public ArrayList<TribeCard> lowerListTribeCards;
|
public ArrayList<TribeCard> lowerListTribeCards;
|
||||||
|
/** Upper row of building cards currently on the board. */
|
||||||
public ArrayList<BuildingCard> upperListBuildingCards;
|
public ArrayList<BuildingCard> upperListBuildingCards;
|
||||||
|
/** Lower row of building cards currently on the board. */
|
||||||
public ArrayList<BuildingCard> lowerListBuildingCards;
|
public ArrayList<BuildingCard> lowerListBuildingCards;
|
||||||
|
/** Usernames of players currently disconnected from the game. */
|
||||||
public ArrayList<String> disconnectedPlayers;
|
public ArrayList<String> disconnectedPlayers;
|
||||||
|
/** The most recent network event applied to this model. */
|
||||||
public NetworkEvent lastEvent;
|
public NetworkEvent lastEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public abstract class OrderLogicCard implements Serializable {
|
|||||||
*/
|
*/
|
||||||
public List<OrderPlayer> playerList;
|
public List<OrderPlayer> playerList;
|
||||||
|
|
||||||
|
/** Total number of players in this game. */
|
||||||
protected final int nPlayers;
|
protected final int nPlayers;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ public class Player implements Serializable {
|
|||||||
return UserName;
|
return UserName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the totem assigned to this player.
|
||||||
|
*
|
||||||
|
* @return this player's totem.
|
||||||
|
*/
|
||||||
public Totems getTotem() {
|
public Totems getTotem() {
|
||||||
return totem;
|
return totem;
|
||||||
}
|
}
|
||||||
@@ -81,12 +86,19 @@ public class Player implements Serializable {
|
|||||||
private ArrayList<Shaman> shamans;
|
private ArrayList<Shaman> shamans;
|
||||||
private ArrayList<Hunter> hunters;
|
private ArrayList<Hunter> hunters;
|
||||||
|
|
||||||
|
/** @return this player's building cards. */
|
||||||
public ArrayList<BuildingCard> getBuildingCards() { return buildingCards; }
|
public ArrayList<BuildingCard> getBuildingCards() { return buildingCards; }
|
||||||
|
/** @return this player's Artist character cards. */
|
||||||
public ArrayList<Artist> getArtists() { return artists; }
|
public ArrayList<Artist> getArtists() { return artists; }
|
||||||
|
/** @return this player's Builder character cards. */
|
||||||
public ArrayList<Builder> getBuilders() { return builders; }
|
public ArrayList<Builder> getBuilders() { return builders; }
|
||||||
|
/** @return this player's Inventor character cards. */
|
||||||
public ArrayList<Inventor> getInventors() { return inventors; }
|
public ArrayList<Inventor> getInventors() { return inventors; }
|
||||||
|
/** @return this player's Gatherer character cards. */
|
||||||
public ArrayList<Gatherer> getGatherers() { return gatherers; }
|
public ArrayList<Gatherer> getGatherers() { return gatherers; }
|
||||||
|
/** @return this player's Shaman character cards. */
|
||||||
public ArrayList<Shaman> getShamans() { return shamans; }
|
public ArrayList<Shaman> getShamans() { return shamans; }
|
||||||
|
/** @return this player's Hunter character cards. */
|
||||||
public ArrayList<Hunter> getHunters() { return hunters; }
|
public ArrayList<Hunter> getHunters() { return hunters; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,6 +144,11 @@ public class Player implements Serializable {
|
|||||||
this.FoodValue += Value;
|
this.FoodValue += Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the totem for this player.
|
||||||
|
*
|
||||||
|
* @param totem the totem to assign.
|
||||||
|
*/
|
||||||
public void setTotem(Totems totem) {
|
public void setTotem(Totems totem) {
|
||||||
this.totem = totem;
|
this.totem = totem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,19 @@ package it.polimi.ingsw.gc14.Network;
|
|||||||
import java.net.*;
|
import java.net.*;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
/** Utility class for resolving the local network interface to use when connecting to a server. */
|
||||||
public class InterfaceResolver {
|
public class InterfaceResolver {
|
||||||
|
/**
|
||||||
|
* Returns the local IPv4 address that can reach the given server IP.
|
||||||
|
*
|
||||||
|
* <p>If the server is localhost, returns {@code "127.0.0.1"}.
|
||||||
|
* Otherwise iterates active non-loopback interfaces to find one on the same subnet;
|
||||||
|
* falls back to a UDP connect trick if no matching subnet is found.
|
||||||
|
*
|
||||||
|
* @param serverIp the server IP address or hostname.
|
||||||
|
* @return the local IP address string to use for outbound connections.
|
||||||
|
* @throws Exception if no suitable interface can be determined.
|
||||||
|
*/
|
||||||
public static String resolveLocalInterface(String serverIp) throws Exception {
|
public static String resolveLocalInterface(String serverIp) throws Exception {
|
||||||
if (serverIp.equalsIgnoreCase("localhost") || serverIp.startsWith("127.") || serverIp.isEmpty()) {
|
if (serverIp.equalsIgnoreCase("localhost") || serverIp.startsWith("127.") || serverIp.isEmpty()) {
|
||||||
return "127.0.0.1";
|
return "127.0.0.1";
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package it.polimi.ingsw.gc14.Network;
|
package it.polimi.ingsw.gc14.Network;
|
||||||
|
|
||||||
|
/** Shared network constants used by both server and client components. */
|
||||||
public final class NetworkConfig {
|
public final class NetworkConfig {
|
||||||
|
/** Port on which the RMI registry listens. */
|
||||||
public static final int RMI_PORT = 1099;
|
public static final int RMI_PORT = 1099;
|
||||||
|
/** Port on which the TCP game server listens. */
|
||||||
public static final int TCP_PORT = 8080;
|
public static final int TCP_PORT = 8080;
|
||||||
|
/** Port on which the TCP heartbeat server listens. */
|
||||||
public static final int HEARTBEAT_PORT = 8081;
|
public static final int HEARTBEAT_PORT = 8081;
|
||||||
|
/** Milliseconds of silence before a client is considered disconnected. */
|
||||||
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. */
|
||||||
public static final long KEEPALIVE_INTERVAL_MS = 3_000;
|
public static final long KEEPALIVE_INTERVAL_MS = 3_000;
|
||||||
|
|
||||||
private NetworkConfig() {}
|
private NetworkConfig() {}
|
||||||
|
|||||||
@@ -24,8 +24,14 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public abstract class NetworkEvent implements Serializable {
|
public abstract class NetworkEvent implements Serializable {
|
||||||
|
|
||||||
|
/** The error type associated with this event; set when the event failed to apply. */
|
||||||
protected ErrorType errorType;
|
protected ErrorType errorType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the error type associated with this event.
|
||||||
|
*
|
||||||
|
* @return the error type, or {@code null} if no error occurred.
|
||||||
|
*/
|
||||||
public ErrorType getErrorType() {
|
public ErrorType getErrorType() {
|
||||||
return errorType;
|
return errorType;
|
||||||
}
|
}
|
||||||
@@ -74,6 +80,11 @@ 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) {
|
public void setDisconnected(ArrayList<String> players) {
|
||||||
this.disconnectedPlayers = players;
|
this.disconnectedPlayers = players;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import java.io.Serializable;
|
|||||||
public class AddPlayer extends NetworkEvent implements Serializable {
|
public class AddPlayer extends NetworkEvent implements Serializable {
|
||||||
/** Number of proposed players to add to the match */
|
/** Number of proposed players to add to the match */
|
||||||
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)
|
public void setErrorType(ErrorType errorType)
|
||||||
{
|
{
|
||||||
this.errorType = errorType;
|
this.errorType = errorType;
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ public class ApplyNextRound extends NetworkEvent implements Serializable{
|
|||||||
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,
|
||||||
|
* slot map, turn order, and game state for the new round.
|
||||||
|
*
|
||||||
|
* @param miniModel the client-side model to update.
|
||||||
|
* @return {@code true} always (this event cannot produce an error).
|
||||||
|
*/
|
||||||
public boolean apply(MiniModel miniModel)
|
public boolean apply(MiniModel miniModel)
|
||||||
{
|
{
|
||||||
synchronized (miniModel) {
|
synchronized (miniModel) {
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ public class EndedGame extends NetworkEvent implements Serializable{
|
|||||||
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
|
||||||
|
* game state and setting the standing players 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)
|
public boolean apply(MiniModel miniModel)
|
||||||
{
|
{
|
||||||
synchronized (miniModel) {
|
synchronized (miniModel) {
|
||||||
|
|||||||
@@ -243,6 +243,10 @@ public class RMIClient implements IClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the server of a voluntary disconnection via RMI, then stops
|
||||||
|
* the keep-alive scheduler.
|
||||||
|
*/
|
||||||
public void notifyDisconnection() {
|
public void notifyDisconnection() {
|
||||||
pingSender.shutdownNow();
|
pingSender.shutdownNow();
|
||||||
try{
|
try{
|
||||||
|
|||||||
@@ -272,6 +272,13 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
public void totemChoice(String playerUsername, String totems) throws RemoteException {
|
||||||
actionQueue.offer(new TotemChoice(playerUsername,totems));
|
actionQueue.offer(new TotemChoice(playerUsername,totems));
|
||||||
}
|
}
|
||||||
@@ -334,6 +341,12 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
|
|||||||
watchdogs.put(username, wd);
|
watchdogs.put(username, wd);
|
||||||
wd.start();
|
wd.start();
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Triggers a voluntary disconnection for the specified player
|
||||||
|
* by stopping their heartbeat watchdog.
|
||||||
|
*
|
||||||
|
* @param username the username of the player to disconnect.
|
||||||
|
*/
|
||||||
public void disconnectPlayer(String username) {
|
public void disconnectPlayer(String username) {
|
||||||
RMIHeartbeat wd = watchdogs.get(username);
|
RMIHeartbeat wd = watchdogs.get(username);
|
||||||
wd.disconnect();
|
wd.disconnect();
|
||||||
|
|||||||
@@ -1,471 +1,230 @@
|
|||||||
package it.polimi.ingsw.gc14;
|
package it.polimi.ingsw.gc14;
|
||||||
|
|
||||||
|
|
||||||
import it.polimi.ingsw.gc14.Controller.GameController;
|
import it.polimi.ingsw.gc14.Controller.GameController;
|
||||||
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.MiniModel;
|
import it.polimi.ingsw.gc14.Model.MiniModel;
|
||||||
import it.polimi.ingsw.gc14.Model.Player;
|
import it.polimi.ingsw.gc14.Model.Player;
|
||||||
import it.polimi.ingsw.gc14.Model.Totems;
|
import it.polimi.ingsw.gc14.Network.ClientBroadcaster;
|
||||||
import it.polimi.ingsw.gc14.Network.EventType;
|
import it.polimi.ingsw.gc14.Network.CompositeClientBroadcaster;
|
||||||
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.RMI.Server.RMIServer;
|
import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer;
|
||||||
|
import it.polimi.ingsw.gc14.Network.NetworkConfig;
|
||||||
import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer;
|
import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer;
|
||||||
import it.polimi.ingsw.gc14.View.TUI.TUI;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.net.*;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.rmi.RemoteException;
|
import java.rmi.RemoteException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.net.*;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main server launcher that handles both TCP and RMI connections.
|
* Main server entry point.
|
||||||
* The workflow is divided into two parts: game creation and game execution.
|
|
||||||
* The process flow for game creation is as follows:
|
|
||||||
* - The first client (TCP/RMI) requests to join the game by providing a username and the desired number of players
|
|
||||||
* - The TCP/RMI server checks {@link #playerList} and, if it is empty, sets the number of players according to the first user's request using {@link LimitedMap#setLimit(int)}
|
|
||||||
* - The TCP/RMI server creates the {@link Game} with the requested number of players and adds the player to {@link #playerList}
|
|
||||||
* - Other players request to join the game (their requested number of players is ignored)
|
|
||||||
* - When the number of players in {@link #playerList} reaches the {@link LimitedMap}'s limit, the list calls {@link #run()}
|
|
||||||
* - All players are notified of the {@link Game}
|
|
||||||
*
|
*
|
||||||
* The process flow for game execution is as follows:
|
* <p>Responsibilities of this class are intentionally limited to wiring:
|
||||||
* - The TCP/RMI server receives a {@link NetworkEvent} from a client and adds it to the {@link #actionQueue}
|
* it creates all components, connects them together, restores a previously
|
||||||
* - The {@link #run()} method repeatedly calls {@link #doFirstEvent()}, which takes the first event in the {@link #actionQueue} and tries to apply it
|
* saved game if one exists, and starts the event-processing loop and the
|
||||||
* - If the event cannot be successfully applied to the model, its {@code isError} flag is set to {@code true}
|
* network servers.
|
||||||
* - All players are notified of the event
|
*
|
||||||
|
* <p>All game-logic and event-routing decisions are delegated to
|
||||||
|
* {@link GameEventProcessor}; persistence is delegated to {@link SaveManager}.
|
||||||
|
*
|
||||||
|
* <p>The original {@code ServerLauncher} class is preserved and untouched.
|
||||||
|
* This class is a clean replacement that uses the new component structure.
|
||||||
*/
|
*/
|
||||||
public class ServerLauncher {
|
public class ServerLauncher {
|
||||||
|
|
||||||
/**
|
/** Drives the main game-event loop. */
|
||||||
* Queue containinetworkTypeng the events to be applied to the game model.
|
private final GameEventProcessor eventProcessor;
|
||||||
* Thread safe by design.
|
|
||||||
*/
|
|
||||||
BlockingQueue<NetworkEvent> actionQueue;
|
|
||||||
|
|
||||||
/** Game controller. Used to apply events */
|
|
||||||
final GameController gameController;
|
|
||||||
|
|
||||||
/** Server RMI. Handles RMI clients */
|
|
||||||
RMIServer serverRMI;
|
|
||||||
|
|
||||||
/** Server TCP. Handles TCP clients */
|
|
||||||
TCPServer serverTCP;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List containing the usernames of joined players.
|
* Constructs a {@code ServerLauncherTest} and wires all components together.
|
||||||
* {@link LimitedMap}'s limit defines at which size the list calls its action
|
*
|
||||||
* Both the limit and the action can be set using {@link LimitedMap#setLimit(int)} and {@link LimitedMap#setAction(Runnable)}
|
* @param actionQueue the shared event queue.
|
||||||
* The limit is set by the first player joining the game. The action consists in calling {@link #run()}
|
* @param gameController the server-side game controller.
|
||||||
|
* @param playerList the shared player-status map.
|
||||||
|
* @param broadcaster the broadcaster used to reach all connected clients.
|
||||||
|
* @param saveManager the save manager used to persist game state.
|
||||||
*/
|
*/
|
||||||
static LimitedMap<String,Boolean> playerList;
|
public ServerLauncher(
|
||||||
|
BlockingQueue<NetworkEvent> actionQueue,
|
||||||
static TUI view;
|
GameController gameController,
|
||||||
|
LimitedMap<String, Boolean> playerList,
|
||||||
|
ClientBroadcaster broadcaster,
|
||||||
// Campo da aggiungere in ServerLauncher
|
SaveManager saveManager) {
|
||||||
private final ScheduledExecutorService timerExecutor = Executors.newSingleThreadScheduledExecutor();
|
this.eventProcessor = new GameEventProcessor(
|
||||||
private ScheduledFuture<?> disconnectionTimer;
|
actionQueue, gameController, playerList, broadcaster, saveManager);
|
||||||
|
|
||||||
/**
|
|
||||||
* Class constructor that initializes the attributes.
|
|
||||||
* @param actionQueue The queue containing the events
|
|
||||||
* @param gameController The game controller
|
|
||||||
* @param serverRMI The server RMI
|
|
||||||
* @param serverTCP The server TCP
|
|
||||||
*/
|
|
||||||
public ServerLauncher(BlockingQueue<NetworkEvent> actionQueue, GameController gameController, RMIServer serverRMI, TCPServer serverTCP) {
|
|
||||||
this.actionQueue = actionQueue;
|
|
||||||
this.serverRMI = serverRMI;
|
|
||||||
this.gameController = gameController;
|
|
||||||
Game game= loadSave();
|
|
||||||
if(game!=null && game.disconnetedPlayers.entrySet().stream().filter(Map.Entry::getValue).count() >=game.getNPlayers()-1){
|
|
||||||
gameController.setModel(null);
|
|
||||||
this.deleteSave();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
this.gameController.setModel(game);
|
|
||||||
this.serverTCP = serverTCP;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Entry point ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes the first event in the actionQueue and attempts to apply it to the game controller.
|
* Initialises all server components, wires them together, restores a saved
|
||||||
* If the event cannot be applied, its isError flag is set to true; otherwise, it is set to false.
|
* game if available, and starts the event-processing loop and both network
|
||||||
* All clients (both TCP and RMI) are notified of the event
|
* servers.
|
||||||
* @return the outcome of applying the event to the controller
|
|
||||||
* @throws InterruptedException if an error occurs while accessing the actionQueue
|
|
||||||
*/
|
|
||||||
public boolean doFirstEvent() throws InterruptedException {
|
|
||||||
NetworkEvent event = actionQueue.take();
|
|
||||||
//Verify that the model exist and if it exists that's not ended
|
|
||||||
if(gameController.getModel()!=null && gameController.getModel().getCurrentState().getGameStage()!=GameStages.ENDED)
|
|
||||||
{
|
|
||||||
//if there is only one player ignore every event different by reconnection
|
|
||||||
if(disconnectionTimer!=null )
|
|
||||||
{
|
|
||||||
if(event.getEventType() != EventType.RECONNECT_PLAYER)
|
|
||||||
{
|
|
||||||
if(event.getEventType() == EventType.DISCONNECTED_PLAYER)
|
|
||||||
{
|
|
||||||
disconnectionTimer.cancel(true);
|
|
||||||
disconnectionTimer = null;
|
|
||||||
playerList.clear();
|
|
||||||
gameController.setModel(null);
|
|
||||||
System.out.println("\n!!! Player list is now empty, ready for a new game init !!!\n");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
event.setIsError(true);
|
|
||||||
serverRMI.notifyAll(event);
|
|
||||||
serverTCP.notifyAll(event);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized(gameController){
|
|
||||||
int roundPrev=gameController.getModel().getCurrentState().getRound();
|
|
||||||
//applies the event and set if is an error
|
|
||||||
event.setIsError(!event.apply(gameController));
|
|
||||||
Game game=gameController.getModel();
|
|
||||||
//if the event wasn't an error , it will be sent to players
|
|
||||||
if(!event.getIsError())
|
|
||||||
{
|
|
||||||
//Cancel the timer if another player is reconnected , so there are more than one player
|
|
||||||
if (event.getEventType() == EventType.RECONNECT_PLAYER && disconnectionTimer != null && !disconnectionTimer.isDone()) {
|
|
||||||
disconnectionTimer.cancel(false);
|
|
||||||
disconnectionTimer = null;
|
|
||||||
}
|
|
||||||
if(!this.gameSave() ){
|
|
||||||
System.out.println("\n!!! Save failed !!!\n");
|
|
||||||
}
|
|
||||||
if(event.getEventType().equals(EventType.DISCONNECTED_PLAYER)&& game.getCurrentState().getGameStage().equals(GameStages.WAITING))
|
|
||||||
{
|
|
||||||
playerList.remove(event.getUsername());
|
|
||||||
return true;
|
|
||||||
}else {
|
|
||||||
event.setData(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(), game.getPlayers());
|
|
||||||
}
|
|
||||||
if (event.getEventType().equals(EventType.TOTEM_CHOICE)) {
|
|
||||||
((TotemChoice) event).setAvailableTotems(gameController.getModel().getAvailableTotems());
|
|
||||||
}else if (event.getEventType().equals(EventType.RECONNECT_PLAYER))
|
|
||||||
{
|
|
||||||
((ReconnectPlayer) event).setDisconnected(game.disconnetedPlayers.entrySet().stream().filter(Map.Entry::getValue).map(x -> x.getKey().getUserName()).collect(Collectors.toCollection(ArrayList::new)));
|
|
||||||
}else if (event.getEventType().equals(EventType.DISCONNECTED_PLAYER))
|
|
||||||
{
|
|
||||||
((DisconnectedPlayer) event).setDisconnected(game.disconnetedPlayers.entrySet().stream().filter(Map.Entry::getValue).map(x -> x.getKey().getUserName()).collect(Collectors.toCollection(ArrayList::new)));
|
|
||||||
}
|
|
||||||
//if there is only one player the game will be suspended and starts the forfeit timer
|
|
||||||
if (event.getEventType().equals(EventType.DISCONNECTED_PLAYER) && playerList.values().stream().filter(x -> x).count() == 1&& game.getCurrentState().getGameStage() != GameStages.ENDED) {
|
|
||||||
if (disconnectionTimer != null && !disconnectionTimer.isDone()) {
|
|
||||||
disconnectionTimer.cancel(false);
|
|
||||||
}
|
|
||||||
//schedule endgame for forfeit
|
|
||||||
disconnectionTimer = timerExecutor.schedule(() -> {
|
|
||||||
synchronized (gameController)
|
|
||||||
{
|
|
||||||
gameController.EndGameForFeit();
|
|
||||||
serverRMI.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding()));
|
|
||||||
serverTCP.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding()));
|
|
||||||
System.out.println("Timer expired: no player reconnected in 60s.");
|
|
||||||
for(Map.Entry<String,Boolean> entry:playerList.entrySet()){
|
|
||||||
if(!entry.getValue())
|
|
||||||
playerList.remove(entry.getKey());
|
|
||||||
}
|
|
||||||
if(!this.deleteSave()){
|
|
||||||
System.out.println("\n!!! Couldn't delete save !!!\n");
|
|
||||||
}
|
|
||||||
disconnectionTimer = null;
|
|
||||||
}
|
|
||||||
}, 1, TimeUnit.MINUTES);
|
|
||||||
}
|
|
||||||
//if the round is changed send a new next round event , the previous event is ignored and directly sent the next round(also upper and lower lists updated)
|
|
||||||
if(game.getCurrentState().getRound()!=roundPrev)
|
|
||||||
{
|
|
||||||
ApplyNextRound nextRound=new ApplyNextRound(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayers(),game.getUpperListTribeCards(),game.getLowerListTribeCards(),game.getUpperListBuilding(),game.getLowerListBuilding());
|
|
||||||
serverRMI.notifyAll(nextRound);
|
|
||||||
serverTCP.notifyAll(nextRound);
|
|
||||||
}
|
|
||||||
// if the game ends after the event, delete the game. await that all players are disconnected and then create a new game
|
|
||||||
else if(game.getCurrentState().getGameStage().equals(GameStages.ENDED))
|
|
||||||
{
|
|
||||||
serverRMI.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding()));
|
|
||||||
serverTCP.notifyAll(new EndedGame(game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),game.getPlayerStanding()));
|
|
||||||
if(!this.deleteSave()){
|
|
||||||
System.out.println("\n!!! Couldn't delete save !!!\n");
|
|
||||||
}
|
|
||||||
for(Map.Entry<String,Boolean> entry:playerList.entrySet()){
|
|
||||||
if(!entry.getValue())
|
|
||||||
playerList.remove(entry.getKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//notify the event
|
|
||||||
else {
|
|
||||||
serverRMI.notifyAll(event);
|
|
||||||
serverTCP.notifyAll(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
serverRMI.notifyAll(event);
|
|
||||||
serverTCP.notifyAll(event);
|
|
||||||
}
|
|
||||||
return !event.getIsError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
// removes disconneted players when the game is ended
|
|
||||||
if(event.getEventType().equals(EventType.DISCONNECTED_PLAYER))
|
|
||||||
{
|
|
||||||
synchronized (gameController) {
|
|
||||||
playerList.remove(event.getUsername());
|
|
||||||
if (playerList.isEmpty()) {
|
|
||||||
|
|
||||||
gameController.setModel(null);
|
|
||||||
System.out.println("\n!!! Player list is now empty, ready for a new game init !!!\n");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entry point of the server application.
|
|
||||||
*
|
*
|
||||||
* <p>The method initializes the shared player list, the network event queue,
|
* @param args command-line arguments (unused).
|
||||||
* the game controller, the RMI server, the TCP server, and the server launcher.
|
|
||||||
* It also selects the network interface to expose, configures the server crash
|
|
||||||
* recovery state, and defines the action to execute once the required number
|
|
||||||
* of players has joined the game.
|
|
||||||
*
|
|
||||||
* <p>When the player list reaches its limit, the current game model is converted
|
|
||||||
* into a {@link MiniModel}, sent to all connected clients through both RMI and TCP,
|
|
||||||
* and rendered on the server-side TUI. Finally, the event-processing launcher
|
|
||||||
* and both network servers are started.
|
|
||||||
*
|
|
||||||
* <p>The game model itself is initialized by the TCP or RMI server when the first
|
|
||||||
* player joins and selects the total number of players.
|
|
||||||
*
|
|
||||||
* @param args the command-line arguments passed to the server application.
|
|
||||||
* @throws RemoteException if the RMI server cannot be created.
|
* @throws RemoteException if the RMI server cannot be created.
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) throws RemoteException {
|
public static void main(String[] args) throws RemoteException {
|
||||||
playerList = new LimitedMap<String,Boolean>(5, ()->{});
|
LimitedMap<String, Boolean> playerList = new LimitedMap<>(5, () -> {});
|
||||||
BlockingQueue<NetworkEvent> actionQueue = new LinkedBlockingQueue<>();
|
BlockingQueue<NetworkEvent> actionQueue = new LinkedBlockingQueue<>();
|
||||||
GameController gameController = new GameController();
|
GameController gameController = new GameController();
|
||||||
String IP;
|
|
||||||
|
String ip;
|
||||||
try {
|
try {
|
||||||
IP=chooseNetworkInterface(new Scanner(System.in));
|
ip = chooseNetworkInterface(new Scanner(System.in));
|
||||||
System.out.println(IP);
|
System.out.println(ip);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
System.setProperty("java.rmi.server.hostname", IP);
|
System.setProperty("java.rmi.server.hostname", ip);
|
||||||
|
|
||||||
RMIServer serverRMI = new RMIServer(gameController, 1099, actionQueue, playerList,IP);
|
RMIServer rmiServer = new RMIServer(gameController, NetworkConfig.RMI_PORT, actionQueue, playerList, ip);
|
||||||
TCPServer serverTCP = new TCPServer(gameController, 8080, 8081,actionQueue, playerList);
|
TCPServer tcpServer = new TCPServer(gameController, NetworkConfig.TCP_PORT, NetworkConfig.HEARTBEAT_PORT, actionQueue, playerList);
|
||||||
ServerLauncher launcher = new ServerLauncher(actionQueue, gameController, serverRMI, serverTCP);
|
ClientBroadcaster broadcaster = new CompositeClientBroadcaster(rmiServer, tcpServer);
|
||||||
if(gameController.getModel() != null){
|
SaveManager saveManager = new SaveManager(ServerLauncher.class);
|
||||||
playerList.setLimit(gameController.getModel().getNPlayers());
|
|
||||||
for(Map.Entry<Player,Boolean>entry: gameController.getModel().disconnetedPlayers.entrySet())
|
restoreGameIfSaved(gameController, playerList, saveManager);
|
||||||
{
|
|
||||||
if(entry.getValue())
|
ServerLauncher launcher = new ServerLauncher(
|
||||||
{
|
actionQueue, gameController, playerList, broadcaster, saveManager);
|
||||||
playerList.put(entry.getKey().getUserName(),false);
|
|
||||||
}
|
// When all required players have joined, broadcast the initial MiniModel.
|
||||||
|
playerList.setAction(() -> new Thread(() -> {
|
||||||
|
MiniModel miniModel;
|
||||||
|
synchronized (gameController) {
|
||||||
|
Game game = gameController.getModel();
|
||||||
|
miniModel = new MiniModel(
|
||||||
|
game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),
|
||||||
|
game.getPlayers(), game.getAvailableTotems(),
|
||||||
|
game.getUpperListTribeCards(), game.getLowerListTribeCards(),
|
||||||
|
game.getUpperListBuilding(), game.getLowerListBuilding(),
|
||||||
|
game.disconnetedPlayers.entrySet().stream()
|
||||||
|
.filter(Map.Entry::getValue)
|
||||||
|
.map(e -> e.getKey().getUserName())
|
||||||
|
.collect(Collectors.toCollection(ArrayList::new))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
broadcaster.notifyAll(miniModel);
|
||||||
playerList.setAction(()->{
|
}).start());
|
||||||
new Thread(()->{
|
|
||||||
System.out.println("\n\nNotifying model");
|
new Thread(() -> {
|
||||||
MiniModel miniModel;
|
try { launcher.run(); }
|
||||||
synchronized (gameController) {
|
catch (Exception e) {
|
||||||
Game game = gameController.getModel();
|
System.out.println("Unexpected exception in game loop");
|
||||||
miniModel= new MiniModel(game.getSlotMap(), game.orderLogicCard,game.getCurrentState(),game.getPlayers(), game.getAvailableTotems(),game.getUpperListTribeCards(),game.getLowerListTribeCards(),game.getUpperListBuilding(),game.getLowerListBuilding(), game.disconnetedPlayers.entrySet().stream().filter(Map.Entry::getValue).map(x -> x.getKey().getUserName()).collect(Collectors.toCollection(ArrayList::new)));
|
|
||||||
}
|
|
||||||
serverRMI.notifyAll(miniModel);
|
|
||||||
serverTCP.notifyAll(miniModel);
|
|
||||||
}).start();
|
|
||||||
});
|
|
||||||
new Thread(()-> {
|
|
||||||
try {
|
|
||||||
launcher.run();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
System.out.println("Generic exception occurred");
|
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
|
|
||||||
serverRMI.start();
|
rmiServer.start();
|
||||||
new Thread(()->{serverTCP.start();}).start();
|
new Thread(() -> tcpServer.start()).start();
|
||||||
System.out.println("Server RMI: "+System.getProperty("java.rmi.server.hostname"));
|
System.out.println("Server RMI: " + System.getProperty("java.rmi.server.hostname"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Game loop ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the main game loop.
|
* Runs the event-processing loop until the thread is interrupted.
|
||||||
*
|
*
|
||||||
* <p>The method repeatedly processes the first event in the action queue
|
* <p>A {@link ConcurrentModificationException} is caught and logged rather
|
||||||
* and, when a view is available, updates the rendered game state.
|
* than propagated; it is caused by an unsafe {@link ArrayList} in
|
||||||
*
|
* {@code TCPServer.clientHandlers} (tracked as a separate issue) and does
|
||||||
* <p>If the thread is interrupted while waiting for an event,
|
* not leave the game in an inconsistent state.
|
||||||
* the interruption status is restored and the loop terminates.
|
|
||||||
*/
|
*/
|
||||||
public void run() {
|
public void run() {
|
||||||
while (true) {
|
while (true) {
|
||||||
try{
|
try {
|
||||||
this.doFirstEvent();
|
eventProcessor.doFirstEvent();
|
||||||
}
|
} catch (ConcurrentModificationException e) {
|
||||||
catch (ConcurrentModificationException e)
|
System.err.println("Concurrent modification in notifyAll — skipping tick");
|
||||||
{
|
} catch (InterruptedException e) {
|
||||||
System.err.println("Concurrent Exception");
|
|
||||||
}
|
|
||||||
catch(InterruptedException e){
|
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Crash recovery ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lets the user choose the network interface to be used by the server.
|
* Attempts to restore a previously saved game.
|
||||||
*
|
*
|
||||||
* <p>The method scans all active, non-loopback, and non-virtual network
|
* <p>The save is discarded if all but at most one player was offline at the
|
||||||
* interfaces, collecting their IPv4 addresses. If only one valid address is
|
* time of the crash, since there would be nobody to resume the game with.
|
||||||
* found, it is selected automatically. Otherwise, the available addresses are
|
* Otherwise the model is restored, the player-list limit is set, and each
|
||||||
* printed and the user is asked to choose one by index.
|
* player who was offline at crash time is pre-populated as offline so the
|
||||||
|
* reconnection flow can handle them correctly.
|
||||||
*
|
*
|
||||||
* @param scanner the scanner used to read the user's selection.
|
* @param gameController the controller that will receive the restored model.
|
||||||
* @return the IPv4 address of the selected network interface.
|
* @param playerList the player-status map to populate.
|
||||||
|
* @param saveManager the save manager to load from.
|
||||||
|
*/
|
||||||
|
private static void restoreGameIfSaved(
|
||||||
|
GameController gameController,
|
||||||
|
LimitedMap<String, Boolean> playerList,
|
||||||
|
SaveManager saveManager) {
|
||||||
|
|
||||||
|
Game game = saveManager.load();
|
||||||
|
if (game == null) return;
|
||||||
|
|
||||||
|
long disconnectedCount = game.disconnetedPlayers.entrySet().stream()
|
||||||
|
.filter(Map.Entry::getValue).count();
|
||||||
|
|
||||||
|
if (disconnectedCount >= game.getNPlayers() - 1) {
|
||||||
|
saveManager.delete();
|
||||||
|
System.out.println("Save discarded: too many players were offline at crash time.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameController.setModel(game);
|
||||||
|
playerList.setLimit(game.getNPlayers());
|
||||||
|
|
||||||
|
for (Map.Entry<Player, Boolean> entry : game.disconnetedPlayers.entrySet()) {
|
||||||
|
if (entry.getValue()) {
|
||||||
|
playerList.put(entry.getKey().getUserName(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("Game restored from save (" + game.getNPlayers() + " players).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Network interface selection ───────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists active non-loopback IPv4 network interfaces and prompts the operator
|
||||||
|
* to choose one. If only one interface is available it is selected automatically.
|
||||||
|
*
|
||||||
|
* @param scanner the scanner used to read the operator's choice.
|
||||||
|
* @return the IPv4 address of the selected interface.
|
||||||
* @throws Exception if no valid network interface is available.
|
* @throws Exception if no valid network interface is available.
|
||||||
*/
|
*/
|
||||||
public static String chooseNetworkInterface(Scanner scanner) throws Exception {
|
public static String chooseNetworkInterface(Scanner scanner) throws Exception {
|
||||||
List<String> ips = new ArrayList<>();
|
List<String> ips = new ArrayList<>();
|
||||||
|
|
||||||
// Lista tutte le interfacce attive con IP reale
|
|
||||||
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
while (interfaces.hasMoreElements()) {
|
while (interfaces.hasMoreElements()) {
|
||||||
NetworkInterface ni = interfaces.nextElement();
|
NetworkInterface ni = interfaces.nextElement();
|
||||||
|
|
||||||
// Salta loopback, interfacce spente o virtuali
|
|
||||||
if (!ni.isUp() || ni.isLoopback() || ni.isVirtual()) continue;
|
if (!ni.isUp() || ni.isLoopback() || ni.isVirtual()) continue;
|
||||||
|
|
||||||
Enumeration<InetAddress> addresses = ni.getInetAddresses();
|
Enumeration<InetAddress> addresses = ni.getInetAddresses();
|
||||||
while (addresses.hasMoreElements()) {
|
while (addresses.hasMoreElements()) {
|
||||||
InetAddress addr = addresses.nextElement();
|
InetAddress addr = addresses.nextElement();
|
||||||
// Solo IPv4
|
|
||||||
if (addr instanceof Inet4Address) {
|
if (addr instanceof Inet4Address) {
|
||||||
System.out.println("[" + ips.size() + "] " + ni.getDisplayName() + " -> " + addr.getHostAddress());
|
System.out.println("[" + ips.size() + "] " + ni.getDisplayName()
|
||||||
|
+ " -> " + addr.getHostAddress());
|
||||||
ips.add(addr.getHostAddress());
|
ips.add(addr.getHostAddress());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ips.isEmpty()) throw new Exception("Nessuna interfaccia disponibile");
|
if (ips.isEmpty()) throw new Exception("No active network interface available");
|
||||||
|
|
||||||
if (ips.size() == 1) {
|
if (ips.size() == 1) {
|
||||||
System.out.println("Una sola interfaccia trovata, uso: " + ips.get(0));
|
System.out.println("One interface found, using: " + ips.get(0));
|
||||||
return ips.get(0);
|
return ips.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.print("Scegli interfaccia: ");
|
System.out.print("Choose interface: ");
|
||||||
int choice = Integer.parseInt(scanner.nextLine().trim());
|
int choice = Integer.parseInt(scanner.nextLine().trim());
|
||||||
return ips.get(choice);
|
return ips.get(choice);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the current game model to a local file.
|
|
||||||
*
|
|
||||||
* <p>The save file is stored in the {@code GameSaves/save.dat} path relative
|
|
||||||
* to the server executable location. If the directory does not exist, it is
|
|
||||||
* created before writing the serialized game model.
|
|
||||||
*
|
|
||||||
* @return {@code true} if the game is saved successfully,
|
|
||||||
* {@code false} otherwise.
|
|
||||||
*/
|
|
||||||
private boolean gameSave(){
|
|
||||||
try{
|
|
||||||
Path jarPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent();
|
|
||||||
Path filePath = jarPath.resolve("GameSaves/save.dat");
|
|
||||||
Files.createDirectories(filePath.getParent());
|
|
||||||
|
|
||||||
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath.toFile()))){
|
|
||||||
oos.writeObject(this.gameController.getModel());
|
|
||||||
System.out.println("Game saved to: " + filePath.toAbsolutePath());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch(IOException e){
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(IOException e){
|
|
||||||
System.out.println("Couldn't create directory.");
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads a previously saved game model from the local save file.
|
|
||||||
*
|
|
||||||
* <p>The method attempts to deserialize the game stored in
|
|
||||||
* {@code GameSaves/save.dat}. If no save file exists or an I/O error occurs,
|
|
||||||
* {@code null} is returned.
|
|
||||||
*
|
|
||||||
* @return the loaded {@link Game} instance, or {@code null} if no valid save
|
|
||||||
* can be loaded.
|
|
||||||
*/
|
|
||||||
private Game loadSave(){
|
|
||||||
try {
|
|
||||||
Path jarPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent();
|
|
||||||
Path filePath = jarPath.resolve("GameSaves/save.dat");
|
|
||||||
try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath.toFile()))){
|
|
||||||
return (Game)(ois.readObject());
|
|
||||||
}
|
|
||||||
catch(FileNotFoundException e){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch(IOException e){
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch(ClassNotFoundException e){
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(URISyntaxException e){
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the current local game save file.
|
|
||||||
*
|
|
||||||
* @return {@code true} if the save file is deleted successfully,
|
|
||||||
* {@code false} otherwise.
|
|
||||||
*/
|
|
||||||
private boolean deleteSave(){
|
|
||||||
try{
|
|
||||||
Path jarPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent();
|
|
||||||
Path filePath = jarPath.resolve("GameSaves/save.dat");
|
|
||||||
Files.delete(filePath);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch(URISyntaxException | IOException e){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
package it.polimi.ingsw.gc14;
|
|
||||||
|
|
||||||
import it.polimi.ingsw.gc14.Controller.GameController;
|
|
||||||
import it.polimi.ingsw.gc14.Model.Game;
|
|
||||||
import it.polimi.ingsw.gc14.Model.MiniModel;
|
|
||||||
import it.polimi.ingsw.gc14.Model.Player;
|
|
||||||
import it.polimi.ingsw.gc14.Network.ClientBroadcaster;
|
|
||||||
import it.polimi.ingsw.gc14.Network.CompositeClientBroadcaster;
|
|
||||||
import it.polimi.ingsw.gc14.Network.NetworkEvent;
|
|
||||||
import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer;
|
|
||||||
import it.polimi.ingsw.gc14.Network.NetworkConfig;
|
|
||||||
import it.polimi.ingsw.gc14.Network.TCP.Server.TCPServer;
|
|
||||||
|
|
||||||
import java.net.*;
|
|
||||||
import java.rmi.RemoteException;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main server entry point.
|
|
||||||
*
|
|
||||||
* <p>Responsibilities of this class are intentionally limited to wiring:
|
|
||||||
* it creates all components, connects them together, restores a previously
|
|
||||||
* saved game if one exists, and starts the event-processing loop and the
|
|
||||||
* network servers.
|
|
||||||
*
|
|
||||||
* <p>All game-logic and event-routing decisions are delegated to
|
|
||||||
* {@link GameEventProcessor}; persistence is delegated to {@link SaveManager}.
|
|
||||||
*
|
|
||||||
* <p>The original {@code ServerLauncher} class is preserved and untouched.
|
|
||||||
* This class is a clean replacement that uses the new component structure.
|
|
||||||
*/
|
|
||||||
public class ServerLauncherTest {
|
|
||||||
|
|
||||||
/** Drives the main game-event loop. */
|
|
||||||
private final GameEventProcessor eventProcessor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a {@code ServerLauncherTest} and wires all components together.
|
|
||||||
*
|
|
||||||
* @param actionQueue the shared event queue.
|
|
||||||
* @param gameController the server-side game controller.
|
|
||||||
* @param playerList the shared player-status map.
|
|
||||||
* @param broadcaster the broadcaster used to reach all connected clients.
|
|
||||||
* @param saveManager the save manager used to persist game state.
|
|
||||||
*/
|
|
||||||
public ServerLauncherTest(
|
|
||||||
BlockingQueue<NetworkEvent> actionQueue,
|
|
||||||
GameController gameController,
|
|
||||||
LimitedMap<String, Boolean> playerList,
|
|
||||||
ClientBroadcaster broadcaster,
|
|
||||||
SaveManager saveManager) {
|
|
||||||
this.eventProcessor = new GameEventProcessor(
|
|
||||||
actionQueue, gameController, playerList, broadcaster, saveManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Entry point ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialises all server components, wires them together, restores a saved
|
|
||||||
* game if available, and starts the event-processing loop and both network
|
|
||||||
* servers.
|
|
||||||
*
|
|
||||||
* @param args command-line arguments (unused).
|
|
||||||
* @throws RemoteException if the RMI server cannot be created.
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) throws RemoteException {
|
|
||||||
LimitedMap<String, Boolean> playerList = new LimitedMap<>(5, () -> {});
|
|
||||||
BlockingQueue<NetworkEvent> actionQueue = new LinkedBlockingQueue<>();
|
|
||||||
GameController gameController = new GameController();
|
|
||||||
|
|
||||||
String ip;
|
|
||||||
try {
|
|
||||||
ip = chooseNetworkInterface(new Scanner(System.in));
|
|
||||||
System.out.println(ip);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
System.setProperty("java.rmi.server.hostname", ip);
|
|
||||||
|
|
||||||
RMIServer rmiServer = new RMIServer(gameController, NetworkConfig.RMI_PORT, actionQueue, playerList, ip);
|
|
||||||
TCPServer tcpServer = new TCPServer(gameController, NetworkConfig.TCP_PORT, NetworkConfig.HEARTBEAT_PORT, actionQueue, playerList);
|
|
||||||
ClientBroadcaster broadcaster = new CompositeClientBroadcaster(rmiServer, tcpServer);
|
|
||||||
SaveManager saveManager = new SaveManager(ServerLauncherTest.class);
|
|
||||||
|
|
||||||
restoreGameIfSaved(gameController, playerList, saveManager);
|
|
||||||
|
|
||||||
ServerLauncherTest launcher = new ServerLauncherTest(
|
|
||||||
actionQueue, gameController, playerList, broadcaster, saveManager);
|
|
||||||
|
|
||||||
// When all required players have joined, broadcast the initial MiniModel.
|
|
||||||
playerList.setAction(() -> new Thread(() -> {
|
|
||||||
MiniModel miniModel;
|
|
||||||
synchronized (gameController) {
|
|
||||||
Game game = gameController.getModel();
|
|
||||||
miniModel = new MiniModel(
|
|
||||||
game.getSlotMap(), game.orderLogicCard, game.getCurrentState(),
|
|
||||||
game.getPlayers(), game.getAvailableTotems(),
|
|
||||||
game.getUpperListTribeCards(), game.getLowerListTribeCards(),
|
|
||||||
game.getUpperListBuilding(), game.getLowerListBuilding(),
|
|
||||||
game.disconnetedPlayers.entrySet().stream()
|
|
||||||
.filter(Map.Entry::getValue)
|
|
||||||
.map(e -> e.getKey().getUserName())
|
|
||||||
.collect(Collectors.toCollection(ArrayList::new))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
broadcaster.notifyAll(miniModel);
|
|
||||||
}).start());
|
|
||||||
|
|
||||||
new Thread(() -> {
|
|
||||||
try { launcher.run(); }
|
|
||||||
catch (Exception e) {
|
|
||||||
System.out.println("Unexpected exception in game loop");
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
|
|
||||||
rmiServer.start();
|
|
||||||
new Thread(() -> tcpServer.start()).start();
|
|
||||||
System.out.println("Server RMI: " + System.getProperty("java.rmi.server.hostname"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Game loop ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the event-processing loop until the thread is interrupted.
|
|
||||||
*
|
|
||||||
* <p>A {@link ConcurrentModificationException} is caught and logged rather
|
|
||||||
* than propagated; it is caused by an unsafe {@link ArrayList} in
|
|
||||||
* {@code TCPServer.clientHandlers} (tracked as a separate issue) and does
|
|
||||||
* not leave the game in an inconsistent state.
|
|
||||||
*/
|
|
||||||
public void run() {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
eventProcessor.doFirstEvent();
|
|
||||||
} catch (ConcurrentModificationException e) {
|
|
||||||
System.err.println("Concurrent modification in notifyAll — skipping tick");
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Crash recovery ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to restore a previously saved game.
|
|
||||||
*
|
|
||||||
* <p>The save is discarded if all but at most one player was offline at the
|
|
||||||
* time of the crash, since there would be nobody to resume the game with.
|
|
||||||
* Otherwise the model is restored, the player-list limit is set, and each
|
|
||||||
* player who was offline at crash time is pre-populated as offline so the
|
|
||||||
* reconnection flow can handle them correctly.
|
|
||||||
*
|
|
||||||
* @param gameController the controller that will receive the restored model.
|
|
||||||
* @param playerList the player-status map to populate.
|
|
||||||
* @param saveManager the save manager to load from.
|
|
||||||
*/
|
|
||||||
private static void restoreGameIfSaved(
|
|
||||||
GameController gameController,
|
|
||||||
LimitedMap<String, Boolean> playerList,
|
|
||||||
SaveManager saveManager) {
|
|
||||||
|
|
||||||
Game game = saveManager.load();
|
|
||||||
if (game == null) return;
|
|
||||||
|
|
||||||
long disconnectedCount = game.disconnetedPlayers.entrySet().stream()
|
|
||||||
.filter(Map.Entry::getValue).count();
|
|
||||||
|
|
||||||
if (disconnectedCount >= game.getNPlayers() - 1) {
|
|
||||||
saveManager.delete();
|
|
||||||
System.out.println("Save discarded: too many players were offline at crash time.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gameController.setModel(game);
|
|
||||||
playerList.setLimit(game.getNPlayers());
|
|
||||||
|
|
||||||
for (Map.Entry<Player, Boolean> entry : game.disconnetedPlayers.entrySet()) {
|
|
||||||
if (entry.getValue()) {
|
|
||||||
playerList.put(entry.getKey().getUserName(), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
System.out.println("Game restored from save (" + game.getNPlayers() + " players).");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Network interface selection ───────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists active non-loopback IPv4 network interfaces and prompts the operator
|
|
||||||
* to choose one. If only one interface is available it is selected automatically.
|
|
||||||
*
|
|
||||||
* @param scanner the scanner used to read the operator's choice.
|
|
||||||
* @return the IPv4 address of the selected interface.
|
|
||||||
* @throws Exception if no valid network interface is available.
|
|
||||||
*/
|
|
||||||
public static String chooseNetworkInterface(Scanner scanner) throws Exception {
|
|
||||||
List<String> ips = new ArrayList<>();
|
|
||||||
|
|
||||||
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
|
||||||
while (interfaces.hasMoreElements()) {
|
|
||||||
NetworkInterface ni = interfaces.nextElement();
|
|
||||||
if (!ni.isUp() || ni.isLoopback() || ni.isVirtual()) continue;
|
|
||||||
|
|
||||||
Enumeration<InetAddress> addresses = ni.getInetAddresses();
|
|
||||||
while (addresses.hasMoreElements()) {
|
|
||||||
InetAddress addr = addresses.nextElement();
|
|
||||||
if (addr instanceof Inet4Address) {
|
|
||||||
System.out.println("[" + ips.size() + "] " + ni.getDisplayName()
|
|
||||||
+ " -> " + addr.getHostAddress());
|
|
||||||
ips.add(addr.getHostAddress());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ips.isEmpty()) throw new Exception("No active network interface available");
|
|
||||||
|
|
||||||
if (ips.size() == 1) {
|
|
||||||
System.out.println("One interface found, using: " + ips.get(0));
|
|
||||||
return ips.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.print("Choose interface: ");
|
|
||||||
int choice = Integer.parseInt(scanner.nextLine().trim());
|
|
||||||
return ips.get(choice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,12 @@ import javafx.stage.Stage;
|
|||||||
import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.ENDED;
|
import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.ENDED;
|
||||||
import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.TOTEM_CHOICE;
|
import static it.polimi.ingsw.gc14.Model.GamePackage.GameStages.TOTEM_CHOICE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaFX-based GUI implementation of {@link it.polimi.ingsw.gc14.View.IView IView}.
|
||||||
|
*
|
||||||
|
* <p>Manages the primary stage and switches between the login, totem choice,
|
||||||
|
* main game, and leaderboard scenes based on the current game state.
|
||||||
|
*/
|
||||||
public class GUI extends Application implements IView {
|
public class GUI extends Application implements IView {
|
||||||
private Stage primaryStage;
|
private Stage primaryStage;
|
||||||
|
|
||||||
@@ -42,6 +48,13 @@ public class GUI extends Application implements IView {
|
|||||||
private boolean autoReenterFullscreen = true;
|
private boolean autoReenterFullscreen = true;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaFX entry point: loads all FXML scenes, wires up controllers,
|
||||||
|
* configures fullscreen behaviour, and shows the login scene.
|
||||||
|
*
|
||||||
|
* @param stage the primary stage provided by the JavaFX runtime.
|
||||||
|
* @throws Exception if any FXML resource cannot be loaded.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) throws Exception {
|
public void start(Stage stage) throws Exception {
|
||||||
this.primaryStage = stage;
|
this.primaryStage = stage;
|
||||||
@@ -113,15 +126,29 @@ public class GUI extends Application implements IView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the local mini model reference used by the GUI.
|
||||||
|
*
|
||||||
|
* @param miniModel the latest mini model received from the server.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void setModel(MiniModel miniModel) {
|
public void setModel(MiniModel miniModel) {
|
||||||
this.miniModel=miniModel;
|
this.miniModel=miniModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the client controller into this GUI.
|
||||||
|
*
|
||||||
|
* @param controller the client controller to use.
|
||||||
|
*/
|
||||||
public void setController(ClientController controller) {
|
public void setController(ClientController controller) {
|
||||||
this.controller=controller;
|
this.controller=controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-renders the GUI on the JavaFX application thread, switching to the
|
||||||
|
* appropriate scene based on the current game stage.
|
||||||
|
*/
|
||||||
public void render() {
|
public void render() {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
synchronized (miniModel) {
|
synchronized (miniModel) {
|
||||||
@@ -139,6 +166,15 @@ public class GUI extends Application implements IView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an error on the JavaFX application thread.
|
||||||
|
*
|
||||||
|
* <p>If the server crashed, returns to the login scene and re-enables the login button.
|
||||||
|
* Otherwise delegates to the active scene's error display.
|
||||||
|
*
|
||||||
|
* @param error the error type.
|
||||||
|
* @param message a human-readable description of the error.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void showError(ErrorType error,String message) {
|
public void showError(ErrorType error,String message) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ import javafx.util.Duration;
|
|||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FXML controller for the end-of-game leaderboard scene.
|
||||||
|
*
|
||||||
|
* <p>Displays the final player rankings and a winner/game-over banner.
|
||||||
|
*/
|
||||||
public class LeaderboardFXMLController {
|
public class LeaderboardFXMLController {
|
||||||
|
|
||||||
@FXML private StackPane rootPane;
|
@FXML private StackPane rootPane;
|
||||||
@@ -48,12 +53,20 @@ public class LeaderboardFXMLController {
|
|||||||
private Stage primaryStage;
|
private Stage primaryStage;
|
||||||
private Runnable action;
|
private Runnable action;
|
||||||
|
|
||||||
|
/** Returns a cached {@link Image} for the given classpath {@code path}, loading it on first access. */
|
||||||
private Image loadImage(String path) {
|
private Image loadImage(String path) {
|
||||||
return imageCache.computeIfAbsent(path,
|
return imageCache.computeIfAbsent(path,
|
||||||
p -> new Image(getClass().getResourceAsStream(p)));
|
p -> new Image(getClass().getResourceAsStream(p)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the client controller, a post-game action, and the login scene reference.
|
||||||
|
*
|
||||||
|
* @param controller the client controller.
|
||||||
|
* @param action the action to run when the player returns to the login screen.
|
||||||
|
* @param loginScene the login scene to show on exit.
|
||||||
|
*/
|
||||||
public void setController(ClientController controller, Runnable action, Scene loginScene) {
|
public void setController(ClientController controller, Runnable action, Scene loginScene) {
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
this.loginScene=loginScene;
|
this.loginScene=loginScene;
|
||||||
@@ -61,6 +74,7 @@ public class LeaderboardFXMLController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Initializes the scene: loads fonts, sets up the background, and initializes the popup. */
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
Font.loadFont(getClass().getResourceAsStream("/Fonts/InknutAntiqua-Regular.ttf"), 14);
|
Font.loadFont(getClass().getResourceAsStream("/Fonts/InknutAntiqua-Regular.ttf"), 14);
|
||||||
@@ -72,6 +86,7 @@ public class LeaderboardFXMLController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Populates the ranking list with the final player standings and shows the outcome banner. */
|
||||||
public void render() {
|
public void render() {
|
||||||
rankingList.getChildren().clear();
|
rankingList.getChildren().clear();
|
||||||
if(!controller.miniModel.standingPlayers.isEmpty())
|
if(!controller.miniModel.standingPlayers.isEmpty())
|
||||||
@@ -107,6 +122,7 @@ public class LeaderboardFXMLController {
|
|||||||
|
|
||||||
|
|
||||||
// ==== EFFECTS ====
|
// ==== EFFECTS ====
|
||||||
|
/** Scales {@code node} to 1.02× on hover and sets a hand cursor. */
|
||||||
private void addHoverZoom(Node node) {
|
private void addHoverZoom(Node node) {
|
||||||
ScaleTransition scaleUp = new ScaleTransition(Duration.millis(150), node);
|
ScaleTransition scaleUp = new ScaleTransition(Duration.millis(150), node);
|
||||||
scaleUp.setToX(1.02);
|
scaleUp.setToX(1.02);
|
||||||
@@ -118,6 +134,7 @@ public class LeaderboardFXMLController {
|
|||||||
node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { scaleDown.play(); node.setCursor(Cursor.DEFAULT); });
|
node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { scaleDown.play(); node.setCursor(Cursor.DEFAULT); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Applies a static drop-shadow to {@code node}. */
|
||||||
private void addShadow(Node node) {
|
private void addShadow(Node node) {
|
||||||
DropShadow shadow = new DropShadow();
|
DropShadow shadow = new DropShadow();
|
||||||
shadow.setColor(Color.rgb(0, 0, 0, 0.6));
|
shadow.setColor(Color.rgb(0, 0, 0, 0.6));
|
||||||
@@ -129,6 +146,7 @@ public class LeaderboardFXMLController {
|
|||||||
|
|
||||||
|
|
||||||
// ==== ROW ====
|
// ==== ROW ====
|
||||||
|
/** Builds a styled leaderboard row showing rank, totem, username, stats, and prestige for {@code player}. */
|
||||||
private HBox createPlayerRow(int position, Player player) {
|
private HBox createPlayerRow(int position, Player player) {
|
||||||
HBox row = new HBox(20);
|
HBox row = new HBox(20);
|
||||||
row.setAlignment(Pos.CENTER_LEFT);
|
row.setAlignment(Pos.CENTER_LEFT);
|
||||||
@@ -194,6 +212,7 @@ public class LeaderboardFXMLController {
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a compact icon+count stats strip for all card types of {@code player}. */
|
||||||
private HBox createStatsBox(Player player) {
|
private HBox createStatsBox(Player player) {
|
||||||
HBox stats = new HBox(14);
|
HBox stats = new HBox(14);
|
||||||
stats.setAlignment(Pos.CENTER);
|
stats.setAlignment(Pos.CENTER);
|
||||||
@@ -210,6 +229,7 @@ public class LeaderboardFXMLController {
|
|||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a single icon + label widget for one stat type. */
|
||||||
private HBox createStatItem(String iconPath, String value, double iconHeight) {
|
private HBox createStatItem(String iconPath, String value, double iconHeight) {
|
||||||
HBox box = new HBox(4);
|
HBox box = new HBox(4);
|
||||||
box.setAlignment(Pos.CENTER);
|
box.setAlignment(Pos.CENTER);
|
||||||
@@ -229,6 +249,7 @@ public class LeaderboardFXMLController {
|
|||||||
|
|
||||||
|
|
||||||
// ==== POPUP ====
|
// ==== POPUP ====
|
||||||
|
/** Creates and configures the auto-hiding player-detail popup. */
|
||||||
private void initPopup() {
|
private void initPopup() {
|
||||||
popupContent = new VBox(12);
|
popupContent = new VBox(12);
|
||||||
popupContent.setAlignment(Pos.CENTER);
|
popupContent.setAlignment(Pos.CENTER);
|
||||||
@@ -249,6 +270,7 @@ public class LeaderboardFXMLController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Opens the player-detail popup showing all cards held by {@code player}, grouped by type. */
|
||||||
private void openPlayerPopup(Player player) {
|
private void openPlayerPopup(Player player) {
|
||||||
popupContent.getChildren().clear();
|
popupContent.getChildren().clear();
|
||||||
|
|
||||||
@@ -330,6 +352,7 @@ public class LeaderboardFXMLController {
|
|||||||
popup.setY(window.getY() + (window.getHeight() - popup.getHeight()) / 2);
|
popup.setY(window.getY() + (window.getHeight() - popup.getHeight()) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a copy of the named card collection for {@code username}. */
|
||||||
private ArrayList<PlayableCard> getPlayerCards(String username, String type) {
|
private ArrayList<PlayableCard> getPlayerCards(String username, String type) {
|
||||||
Player p = controller.miniModel.players.get(username);
|
Player p = controller.miniModel.players.get(username);
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
@@ -345,6 +368,7 @@ public class LeaderboardFXMLController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==== BACKGROUND ====
|
// ==== BACKGROUND ====
|
||||||
|
/** Sets the full-cover background image on the root pane. */
|
||||||
private void renderBackground() {
|
private void renderBackground() {
|
||||||
BackgroundSize size = new BackgroundSize(
|
BackgroundSize size = new BackgroundSize(
|
||||||
BackgroundSize.AUTO, BackgroundSize.AUTO,
|
BackgroundSize.AUTO, BackgroundSize.AUTO,
|
||||||
@@ -361,6 +385,7 @@ public class LeaderboardFXMLController {
|
|||||||
|
|
||||||
|
|
||||||
// ==== ACTIONS ====
|
// ==== ACTIONS ====
|
||||||
|
/** Disconnects the client and invokes the post-game action to return to the login scene. */
|
||||||
@FXML
|
@FXML
|
||||||
private void onNewGame() {
|
private void onNewGame() {
|
||||||
controller.disconnect();
|
controller.disconnect();
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ import javafx.util.Duration;
|
|||||||
import java.net.*;
|
import java.net.*;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FXML controller for the login scene.
|
||||||
|
*
|
||||||
|
* <p>Handles username/IP/player-count input, protocol selection (TCP/RMI),
|
||||||
|
* and initiates the connection to the server.
|
||||||
|
*/
|
||||||
public class LoginFXMLController {
|
public class LoginFXMLController {
|
||||||
|
|
||||||
@FXML private ImageView backgroundImage;
|
@FXML private ImageView backgroundImage;
|
||||||
@@ -42,10 +48,16 @@ public class LoginFXMLController {
|
|||||||
// Definiamo lo pseudo-stato custom per il CSS corrispondente a ":active-protocol"
|
// Definiamo lo pseudo-stato custom per il CSS corrispondente a ":active-protocol"
|
||||||
private final PseudoClass activeProtocolPseudo = PseudoClass.getPseudoClass("active-protocol");
|
private final PseudoClass activeProtocolPseudo = PseudoClass.getPseudoClass("active-protocol");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the client controller into this FXML controller.
|
||||||
|
*
|
||||||
|
* @param controller the client controller to use.
|
||||||
|
*/
|
||||||
public void setController(ClientController controller) {
|
public void setController(ClientController controller) {
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Initializes the scene: loads background, sets up animations, and binds input listeners. */
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
// Background setup
|
// Background setup
|
||||||
@@ -67,6 +79,7 @@ public class LoginFXMLController {
|
|||||||
btnAccedi.setOnAction(e -> onAccediClick());
|
btnAccedi.setOnAction(e -> onAccediClick());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Toggles the selected protocol between RMI and TCP, animating the toggle thumb and updating label styles. */
|
||||||
private void switchProtocol() {
|
private void switchProtocol() {
|
||||||
isRMI = !isRMI;
|
isRMI = !isRMI;
|
||||||
|
|
||||||
@@ -79,6 +92,7 @@ public class LoginFXMLController {
|
|||||||
labelTCP.pseudoClassStateChanged(activeProtocolPseudo, !isRMI);
|
labelTCP.pseudoClassStateChanged(activeProtocolPseudo, !isRMI);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Validates form input and starts a background thread to connect to the server. */
|
||||||
@FXML
|
@FXML
|
||||||
private void onAccediClick() {
|
private void onAccediClick() {
|
||||||
String nome = campoNome.getText().trim();
|
String nome = campoNome.getText().trim();
|
||||||
@@ -114,6 +128,11 @@ public class LoginFXMLController {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables the login button and updates its visual style.
|
||||||
|
*
|
||||||
|
* @param enabled {@code true} to enable the button, {@code false} to disable it.
|
||||||
|
*/
|
||||||
public void updateLoginButton(boolean enabled) {
|
public void updateLoginButton(boolean enabled) {
|
||||||
btnAccedi.setDisable(!enabled);
|
btnAccedi.setDisable(!enabled);
|
||||||
btnAccedi.setStyle(
|
btnAccedi.setStyle(
|
||||||
@@ -129,6 +148,7 @@ public class LoginFXMLController {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Connects to the server using the selected protocol and transitions to the waiting state on success. */
|
||||||
private void connect(String nome, String ip, int numPlayers, String localInterface) {
|
private void connect(String nome, String ip, int numPlayers, String localInterface) {
|
||||||
if (isRMI) {
|
if (isRMI) {
|
||||||
RMIClient client = new RMIClient(controller, ip, NetworkConfig.RMI_PORT, localInterface);
|
RMIClient client = new RMIClient(controller, ip, NetworkConfig.RMI_PORT, localInterface);
|
||||||
@@ -156,15 +176,27 @@ public class LoginFXMLController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Displays an error from an {@link ErrorType} constant in the login form label.
|
||||||
|
*
|
||||||
|
* @param errorType the error to display.
|
||||||
|
*/
|
||||||
public void showError(ErrorType errorType) {
|
public void showError(ErrorType errorType) {
|
||||||
labelErrore.setTextFill(Color.web("#e05050"));
|
labelErrore.setTextFill(Color.web("#e05050"));
|
||||||
labelErrore.setText(errorType.toString());
|
labelErrore.setText(errorType.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an arbitrary error message in the login form label.
|
||||||
|
*
|
||||||
|
* @param msg the error message to display.
|
||||||
|
*/
|
||||||
public void showError(String msg) {
|
public void showError(String msg) {
|
||||||
labelErrore.setTextFill(Color.web("#e05050"));
|
labelErrore.setTextFill(Color.web("#e05050"));
|
||||||
labelErrore.setText(msg);
|
labelErrore.setText(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Displays a success message in green in the login form label. */
|
||||||
private void showSuccess(String msg) {
|
private void showSuccess(String msg) {
|
||||||
labelErrore.setTextFill(Color.web("#6fcf8a"));
|
labelErrore.setTextFill(Color.web("#6fcf8a"));
|
||||||
labelErrore.setText(msg);
|
labelErrore.setText(msg);
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ import javafx.util.Duration;
|
|||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FXML controller for the main game scene.
|
||||||
|
*
|
||||||
|
* <p>Renders the board (upper/lower tribe and building card rows), player stats,
|
||||||
|
* the player's hand, and action buttons (skip, details).
|
||||||
|
*/
|
||||||
public class MainFXMLController {
|
public class MainFXMLController {
|
||||||
|
|
||||||
@FXML private GridPane leftGrid;
|
@FXML private GridPane leftGrid;
|
||||||
@@ -62,21 +68,29 @@ public class MainFXMLController {
|
|||||||
|
|
||||||
private ClientController controller;
|
private ClientController controller;
|
||||||
|
|
||||||
|
/** {@code true} when the last received event was an error response. */
|
||||||
public boolean isError;
|
public boolean isError;
|
||||||
|
|
||||||
// ==== IMAGE CACHE ====
|
// ==== IMAGE CACHE ====
|
||||||
private static final Map<String, Image> imageCache = new HashMap<>();
|
private static final Map<String, Image> imageCache = new HashMap<>();
|
||||||
|
|
||||||
|
/** Returns a cached {@link Image} for the given classpath {@code path}, loading it on first access. */
|
||||||
private Image loadImage(String path) {
|
private Image loadImage(String path) {
|
||||||
return imageCache.computeIfAbsent(path,
|
return imageCache.computeIfAbsent(path,
|
||||||
p -> new Image(getClass().getResourceAsStream(p)));
|
p -> new Image(getClass().getResourceAsStream(p)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the client controller and wires up the skip-turn button action.
|
||||||
|
*
|
||||||
|
* @param controller the client controller to use.
|
||||||
|
*/
|
||||||
public void setController(ClientController controller) {
|
public void setController(ClientController controller) {
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
skipBtn.setOnAction(e -> controller.skipTurn());
|
skipBtn.setOnAction(e -> controller.skipTurn());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Initializes the scene: loads fonts, sets up the popup, and registers input listeners. */
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
Font.loadFont(getClass().getResourceAsStream("/Fonts/InknutAntiqua-Regular.ttf"), 14);
|
Font.loadFont(getClass().getResourceAsStream("/Fonts/InknutAntiqua-Regular.ttf"), 14);
|
||||||
@@ -110,6 +124,7 @@ public class MainFXMLController {
|
|||||||
|
|
||||||
|
|
||||||
// ==== RENDER ====
|
// ==== RENDER ====
|
||||||
|
/** Incrementally re-renders only the parts of the scene affected by the last network event. */
|
||||||
public void render() {
|
public void render() {
|
||||||
if(foodLabels.isEmpty() || controller.miniModel.lastEvent == null
|
if(foodLabels.isEmpty() || controller.miniModel.lastEvent == null
|
||||||
|| controller.miniModel.lastEvent.getEventType().equals(EventType.TOTEM_CHOICE))
|
|| controller.miniModel.lastEvent.getEventType().equals(EventType.TOTEM_CHOICE))
|
||||||
@@ -146,6 +161,7 @@ public class MainFXMLController {
|
|||||||
|
|
||||||
|
|
||||||
// ==== EFFECTS ====
|
// ==== EFFECTS ====
|
||||||
|
/** Binds a rounded-rectangle clip to {@code img} so its corners are cropped. */
|
||||||
private void addClip(ImageView img) {
|
private void addClip(ImageView img) {
|
||||||
Rectangle clip = new Rectangle();
|
Rectangle clip = new Rectangle();
|
||||||
clip.setArcWidth(20);
|
clip.setArcWidth(20);
|
||||||
@@ -155,6 +171,7 @@ public class MainFXMLController {
|
|||||||
clip.widthProperty().bind(img.layoutBoundsProperty().map(b -> b.getWidth()));
|
clip.widthProperty().bind(img.layoutBoundsProperty().map(b -> b.getWidth()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Scales {@code node} to 1.1× on hover and restores on exit; sets a hand cursor. */
|
||||||
private void addHoverZoom(Node node) {
|
private void addHoverZoom(Node node) {
|
||||||
ScaleTransition scaleUp = new ScaleTransition(Duration.millis(150), node);
|
ScaleTransition scaleUp = new ScaleTransition(Duration.millis(150), node);
|
||||||
scaleUp.setToX(1.1);
|
scaleUp.setToX(1.1);
|
||||||
@@ -166,6 +183,7 @@ public class MainFXMLController {
|
|||||||
node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { scaleDown.play(); node.setCursor(Cursor.DEFAULT); });
|
node.addEventHandler(MouseEvent.MOUSE_EXITED, e -> { scaleDown.play(); node.setCursor(Cursor.DEFAULT); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Applies a drop-shadow to {@code node} that intensifies on hover. */
|
||||||
private void addShadow(Node node) {
|
private void addShadow(Node node) {
|
||||||
DropShadow shadow = new DropShadow();
|
DropShadow shadow = new DropShadow();
|
||||||
shadow.setColor(Color.rgb(0, 0, 0, 0.3));
|
shadow.setColor(Color.rgb(0, 0, 0, 0.3));
|
||||||
@@ -190,6 +208,7 @@ public class MainFXMLController {
|
|||||||
|
|
||||||
// ==== ELEMENTS ====
|
// ==== ELEMENTS ====
|
||||||
|
|
||||||
|
/** Builds the side-panel card widget for {@code player} with totem, stats, and card-type icons. */
|
||||||
private VBox buildPlayerCard(Player player) {
|
private VBox buildPlayerCard(Player player) {
|
||||||
VBox card = new VBox(3);
|
VBox card = new VBox(3);
|
||||||
VBox.setMargin(card, new Insets(15, 15, 0, 15));
|
VBox.setMargin(card, new Insets(15, 15, 0, 15));
|
||||||
@@ -296,6 +315,7 @@ public class MainFXMLController {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a {@link StackPane} containing the order card image for a game of {@code num} players. */
|
||||||
private StackPane createOrder(String num) {
|
private StackPane createOrder(String num) {
|
||||||
ImageView img = new ImageView(loadImage("/GUIImages/Orders/order-" + num + ".png"));
|
ImageView img = new ImageView(loadImage("/GUIImages/Orders/order-" + num + ".png"));
|
||||||
img.fitHeightProperty().bind(board.sceneProperty().get().heightProperty().subtract(56).divide(4).multiply(0.94));
|
img.fitHeightProperty().bind(board.sceneProperty().get().heightProperty().subtract(56).divide(4).multiply(0.94));
|
||||||
@@ -304,6 +324,7 @@ public class MainFXMLController {
|
|||||||
return new StackPane(img);
|
return new StackPane(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a {@link StackPane} for a board slot, optionally overlaying the occupying player's totem. */
|
||||||
private StackPane createSlot(Slot slot, boolean withZoom, boolean withShadow) {
|
private StackPane createSlot(Slot slot, boolean withZoom, boolean withShadow) {
|
||||||
ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + slot.getSlotId() + ".png"));
|
ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + slot.getSlotId() + ".png"));
|
||||||
img.setPreserveRatio(true);
|
img.setPreserveRatio(true);
|
||||||
@@ -331,6 +352,7 @@ public class MainFXMLController {
|
|||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a {@link StackPane} for a playable card, scaling its height to a fraction of {@code parent}'s scene. */
|
||||||
private StackPane createCard(PlayableCard card, boolean withZoom, boolean withShadow, Region parent) {
|
private StackPane createCard(PlayableCard card, boolean withZoom, boolean withShadow, Region parent) {
|
||||||
ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + card.getIdIMG() + ".png"));
|
ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + card.getIdIMG() + ".png"));
|
||||||
img.setPreserveRatio(true);
|
img.setPreserveRatio(true);
|
||||||
@@ -350,6 +372,7 @@ public class MainFXMLController {
|
|||||||
|
|
||||||
|
|
||||||
// ==== GROUPS ====
|
// ==== GROUPS ====
|
||||||
|
/** Builds the full side panel from scratch, creating a player card for each connected player. */
|
||||||
private void buildSidePanel() {
|
private void buildSidePanel() {
|
||||||
infoText.setText("Round: "+Integer.toString(controller.miniModel.currentState.getRound()) + " • " + controller.miniModel.currentState.getGameStage().toString());
|
infoText.setText("Round: "+Integer.toString(controller.miniModel.currentState.getRound()) + " • " + controller.miniModel.currentState.getGameStage().toString());
|
||||||
VBox playerList = new VBox();
|
VBox playerList = new VBox();
|
||||||
@@ -362,6 +385,7 @@ public class MainFXMLController {
|
|||||||
playerSide.setContent(playerList);
|
playerSide.setContent(playerList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Updates food/prestige labels, current-player highlight, card-icon opacity, and plays error shake if needed. */
|
||||||
private void updateSidePanel() {
|
private void updateSidePanel() {
|
||||||
infoText.setText("Round: "+Integer.toString(controller.miniModel.currentState.getRound()) + " • " + controller.miniModel.currentState.getGameStage().toString());
|
infoText.setText("Round: "+Integer.toString(controller.miniModel.currentState.getRound()) + " • " + controller.miniModel.currentState.getGameStage().toString());
|
||||||
String current = controller.miniModel.currentState.getCurrentPlayer().getUserName();
|
String current = controller.miniModel.currentState.getCurrentPlayer().getUserName();
|
||||||
@@ -419,6 +443,7 @@ public class MainFXMLController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Renders the top card of the upper building stack with a count badge; clicking opens the selection popup. */
|
||||||
private void drawUpperBuilding() {
|
private void drawUpperBuilding() {
|
||||||
ArrayList<BuildingCard> cardList = new ArrayList<>(controller.miniModel.upperListBuildingCards);
|
ArrayList<BuildingCard> cardList = new ArrayList<>(controller.miniModel.upperListBuildingCards);
|
||||||
if (!cardList.isEmpty()) {
|
if (!cardList.isEmpty()) {
|
||||||
@@ -434,6 +459,7 @@ public class MainFXMLController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Renders the top card of the lower building stack with a count badge; clicking opens the selection popup. */
|
||||||
private void drawLowerBuilding() {
|
private void drawLowerBuilding() {
|
||||||
ArrayList<BuildingCard> cardList = new ArrayList<>(controller.miniModel.lowerListBuildingCards);
|
ArrayList<BuildingCard> cardList = new ArrayList<>(controller.miniModel.lowerListBuildingCards);
|
||||||
if (!cardList.isEmpty()) {
|
if (!cardList.isEmpty()) {
|
||||||
@@ -449,6 +475,7 @@ public class MainFXMLController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Renders a single card-type pile in the hand area, or an invisible placeholder if the list is empty. */
|
||||||
private void drawMyHandList(ArrayList<? extends PlayableCard> cardList) {
|
private void drawMyHandList(ArrayList<? extends PlayableCard> cardList) {
|
||||||
if (!cardList.isEmpty()) {
|
if (!cardList.isEmpty()) {
|
||||||
StackPane img = createCard(cardList.getLast(), true, true, myHand);
|
StackPane img = createCard(cardList.getLast(), true, true, myHand);
|
||||||
@@ -469,6 +496,7 @@ public class MainFXMLController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a copy of the named card collection for {@code username}. */
|
||||||
private ArrayList<PlayableCard> getPlayerCards(String username, String type) {
|
private ArrayList<PlayableCard> getPlayerCards(String username, String type) {
|
||||||
Player p = controller.miniModel.players.get(username);
|
Player p = controller.miniModel.players.get(username);
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
@@ -485,6 +513,7 @@ public class MainFXMLController {
|
|||||||
|
|
||||||
|
|
||||||
// ==== ABSOLUTES ====
|
// ==== ABSOLUTES ====
|
||||||
|
/** Clears and rebuilds the upper tribe row with click handlers for drawing, then appends the upper building stack. */
|
||||||
private void renderUpper() {
|
private void renderUpper() {
|
||||||
upperList.setSpacing(14);
|
upperList.setSpacing(14);
|
||||||
upperList.setAlignment(Pos.CENTER);
|
upperList.setAlignment(Pos.CENTER);
|
||||||
@@ -501,6 +530,7 @@ public class MainFXMLController {
|
|||||||
drawUpperBuilding();
|
drawUpperBuilding();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Clears and rebuilds the lower tribe row with click handlers for drawing, then appends the lower building stack. */
|
||||||
private void renderLower() {
|
private void renderLower() {
|
||||||
lowerList.setSpacing(14);
|
lowerList.setSpacing(14);
|
||||||
lowerList.setAlignment(Pos.CENTER);
|
lowerList.setAlignment(Pos.CENTER);
|
||||||
@@ -517,6 +547,7 @@ public class MainFXMLController {
|
|||||||
drawLowerBuilding();
|
drawLowerBuilding();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the full-cover background image on the main HBox container. */
|
||||||
private void renderBackground() {
|
private void renderBackground() {
|
||||||
BackgroundSize size = new BackgroundSize(
|
BackgroundSize size = new BackgroundSize(
|
||||||
BackgroundSize.AUTO, BackgroundSize.AUTO,
|
BackgroundSize.AUTO, BackgroundSize.AUTO,
|
||||||
@@ -531,6 +562,7 @@ public class MainFXMLController {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Clears the board area and orchestrates deck, order card, and slot-map rendering. */
|
||||||
private void renderBoard() {
|
private void renderBoard() {
|
||||||
board.setSpacing(14);
|
board.setSpacing(14);
|
||||||
board.setAlignment(Pos.CENTER);
|
board.setAlignment(Pos.CENTER);
|
||||||
@@ -540,6 +572,7 @@ public class MainFXMLController {
|
|||||||
renderSlotMap();
|
renderSlotMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Adds the era-specific deck back image to the board. */
|
||||||
private void renderDeck() {
|
private void renderDeck() {
|
||||||
String path = switch (controller.miniModel.currentState.getEra()) {
|
String path = switch (controller.miniModel.currentState.getEra()) {
|
||||||
case 1 -> "/GUIImages/Backs/back-001.png";
|
case 1 -> "/GUIImages/Backs/back-001.png";
|
||||||
@@ -556,6 +589,7 @@ public class MainFXMLController {
|
|||||||
board.getChildren().add(back);
|
board.getChildren().add(back);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Renders the order card and overlays each player's totem at their proportional position. */
|
||||||
private void renderOrder() {
|
private void renderOrder() {
|
||||||
int numPlayers = controller.miniModel.players.size();
|
int numPlayers = controller.miniModel.players.size();
|
||||||
StackPane card = createOrder(Integer.toString(numPlayers));
|
StackPane card = createOrder(Integer.toString(numPlayers));
|
||||||
@@ -613,6 +647,7 @@ public class MainFXMLController {
|
|||||||
board.getChildren().add(card);
|
board.getChildren().add(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Adds a clickable slot widget for each entry in the slot-player map. */
|
||||||
private void renderSlotMap() {
|
private void renderSlotMap() {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (Map.Entry<Slot, Player> entry : controller.miniModel.slotPlayerMap.entrySet()) {
|
for (Map.Entry<Slot, Player> entry : controller.miniModel.slotPlayerMap.entrySet()) {
|
||||||
@@ -625,6 +660,7 @@ public class MainFXMLController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Clears and re-renders all seven card-type piles in the local player's hand area. */
|
||||||
private void renderMyHand() {
|
private void renderMyHand() {
|
||||||
myHand.getChildren().clear();
|
myHand.getChildren().clear();
|
||||||
myHand.setSpacing(14);
|
myHand.setSpacing(14);
|
||||||
@@ -642,6 +678,7 @@ public class MainFXMLController {
|
|||||||
|
|
||||||
|
|
||||||
// ==== POPUP ====
|
// ==== POPUP ====
|
||||||
|
/** Creates and configures the shared card-preview popup container. */
|
||||||
private void initPopup() {
|
private void initPopup() {
|
||||||
popupCards = new HBox(10);
|
popupCards = new HBox(10);
|
||||||
popupCards.setAlignment(Pos.CENTER);
|
popupCards.setAlignment(Pos.CENTER);
|
||||||
@@ -663,6 +700,7 @@ public class MainFXMLController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Centers the popup over the current window and makes it visible. */
|
||||||
private void showPopup() {
|
private void showPopup() {
|
||||||
Window window = myHand.getScene().getWindow();
|
Window window = myHand.getScene().getWindow();
|
||||||
popup.show(window, 0, 0);
|
popup.show(window, 0, 0);
|
||||||
@@ -671,6 +709,7 @@ public class MainFXMLController {
|
|||||||
popup.setY(window.getY() + (window.getHeight() - popup.getHeight()) / 2);
|
popup.setY(window.getY() + (window.getHeight() - popup.getHeight()) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Populates the popup with non-clickable card images and shows it. */
|
||||||
private void openPopup(ArrayList<? extends PlayableCard> cardList) {
|
private void openPopup(ArrayList<? extends PlayableCard> cardList) {
|
||||||
popupCards.getChildren().clear();
|
popupCards.getChildren().clear();
|
||||||
for (PlayableCard card : cardList) {
|
for (PlayableCard card : cardList) {
|
||||||
@@ -679,6 +718,7 @@ public class MainFXMLController {
|
|||||||
showPopup();
|
showPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Populates the popup with clickable upper-building cards; clicking one draws it via the controller. */
|
||||||
private void openPopupUpperBuilding(ArrayList<BuildingCard> cardList) {
|
private void openPopupUpperBuilding(ArrayList<BuildingCard> cardList) {
|
||||||
popupCards.getChildren().clear();
|
popupCards.getChildren().clear();
|
||||||
for (int i = 0; i < cardList.size(); i++) {
|
for (int i = 0; i < cardList.size(); i++) {
|
||||||
@@ -693,6 +733,7 @@ public class MainFXMLController {
|
|||||||
showPopup();
|
showPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Populates the popup with clickable lower-building cards; clicking one draws it via the controller. */
|
||||||
private void openPopupLowerBuilding(ArrayList<BuildingCard> cardList) {
|
private void openPopupLowerBuilding(ArrayList<BuildingCard> cardList) {
|
||||||
popupCards.getChildren().clear();
|
popupCards.getChildren().clear();
|
||||||
for (int i = 0; i < cardList.size(); i++) {
|
for (int i = 0; i < cardList.size(); i++) {
|
||||||
@@ -707,6 +748,7 @@ public class MainFXMLController {
|
|||||||
showPopup();
|
showPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Shows the deck details/rules card in the popup. */
|
||||||
private void openDetailsPopup() {
|
private void openDetailsPopup() {
|
||||||
popupCards.getChildren().clear();
|
popupCards.getChildren().clear();
|
||||||
ImageView img = new ImageView(loadImage("/GUIImages/Backs/back-118.png"));
|
ImageView img = new ImageView(loadImage("/GUIImages/Backs/back-118.png"));
|
||||||
@@ -719,6 +761,7 @@ public class MainFXMLController {
|
|||||||
showPopup();
|
showPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a fixed-height {@link StackPane} card widget suitable for use inside the popup. */
|
||||||
private StackPane createCardPopup(PlayableCard card, boolean withZoom, boolean withShadow) {
|
private StackPane createCardPopup(PlayableCard card, boolean withZoom, boolean withShadow) {
|
||||||
ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + card.getIdIMG() + ".png"));
|
ImageView img = new ImageView(loadImage("/GUIImages/Fronts/card-" + card.getIdIMG() + ".png"));
|
||||||
img.setFitHeight(200);
|
img.setFitHeight(200);
|
||||||
|
|||||||
@@ -17,8 +17,15 @@ import javafx.util.Duration;
|
|||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FXML controller for the totem selection scene.
|
||||||
|
*
|
||||||
|
* <p>Displays the available totems and lets the current player choose one;
|
||||||
|
* other players see a waiting banner.
|
||||||
|
*/
|
||||||
public class TotemFXMLController {
|
public class TotemFXMLController {
|
||||||
|
|
||||||
|
/** Background image view injected via FXML. */
|
||||||
public ImageView backgroundImage;
|
public ImageView backgroundImage;
|
||||||
@FXML private HBox mainHBox;
|
@FXML private HBox mainHBox;
|
||||||
@FXML private Label turnBanner;
|
@FXML private Label turnBanner;
|
||||||
@@ -28,10 +35,16 @@ public class TotemFXMLController {
|
|||||||
private int selectedIndex = -1;
|
private int selectedIndex = -1;
|
||||||
private StackPane selectedFrame = null;
|
private StackPane selectedFrame = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the client controller into this FXML controller.
|
||||||
|
*
|
||||||
|
* @param controller the client controller to use.
|
||||||
|
*/
|
||||||
public void setController(ClientController controller) {
|
public void setController(ClientController controller) {
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Initializes the scene: sets the background image to fill the screen. */
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
Image img = new Image(getClass().getResourceAsStream("/GUIImages/Background.png"));
|
Image img = new Image(getClass().getResourceAsStream("/GUIImages/Background.png"));
|
||||||
@@ -41,6 +54,10 @@ public class TotemFXMLController {
|
|||||||
backgroundImage.setFitHeight(screenBounds.getHeight());
|
backgroundImage.setFitHeight(screenBounds.getHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the totem selection cards with entry animations.
|
||||||
|
* Disables interaction for players who are not the current chooser.
|
||||||
|
*/
|
||||||
public void render() {
|
public void render() {
|
||||||
mainHBox.getChildren().clear();
|
mainHBox.getChildren().clear();
|
||||||
selectedIndex = -1;
|
selectedIndex = -1;
|
||||||
@@ -71,6 +88,7 @@ public class TotemFXMLController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Updates the turn banner text and style based on whether it is the local player's turn. */
|
||||||
private void updateBanner(String chooser, boolean myTurn) {
|
private void updateBanner(String chooser, boolean myTurn) {
|
||||||
if (myTurn) {
|
if (myTurn) {
|
||||||
turnBanner.setText("✦ It's your turn to choose ✦");
|
turnBanner.setText("✦ It's your turn to choose ✦");
|
||||||
@@ -96,6 +114,7 @@ public class TotemFXMLController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Builds a totem card widget; if {@code interactive}, wires click/hover handlers and selection logic. */
|
||||||
private VBox buildCard(Totems totem, int idx, boolean interactive) {
|
private VBox buildCard(Totems totem, int idx, boolean interactive) {
|
||||||
ImageView img = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Totems/totem_" + String.valueOf(totem).toLowerCase(Locale.ROOT) + ".png")));
|
ImageView img = new ImageView(new Image(getClass().getResourceAsStream("/GUIImages/Totems/totem_" + String.valueOf(totem).toLowerCase(Locale.ROOT) + ".png")));
|
||||||
img.setFitHeight(150);
|
img.setFitHeight(150);
|
||||||
@@ -164,6 +183,7 @@ public class TotemFXMLController {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Enables or disables the confirm button and updates its visual opacity to reflect the state. */
|
||||||
private void updateConfirmButton(boolean enabled) {
|
private void updateConfirmButton(boolean enabled) {
|
||||||
confirmButton.setDisable(!enabled);
|
confirmButton.setDisable(!enabled);
|
||||||
confirmButton.setStyle(
|
confirmButton.setStyle(
|
||||||
@@ -176,6 +196,7 @@ public class TotemFXMLController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the CSS style string for the totem card frame, highlighting it when {@code selected}. */
|
||||||
private String frameStyle(boolean selected) {
|
private String frameStyle(boolean selected) {
|
||||||
return "-fx-border-color: " + (selected ? "#75FF79" : "rgba(200,120,20,0.28)") + ";" +
|
return "-fx-border-color: " + (selected ? "#75FF79" : "rgba(200,120,20,0.28)") + ";" +
|
||||||
"-fx-border-width: " + (selected ? "0" : "0") + ";" +
|
"-fx-border-width: " + (selected ? "0" : "0") + ";" +
|
||||||
@@ -183,17 +204,20 @@ public class TotemFXMLController {
|
|||||||
(selected ? "-fx-effect: dropshadow(gaussian, #ffffff, 20, 0.33, 0, 0);" : "");
|
(selected ? "-fx-effect: dropshadow(gaussian, #ffffff, 20, 0.33, 0, 0);" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the CSS style string for the totem name label, brightening it when {@code selected}. */
|
||||||
private String nameStyle(boolean selected) {
|
private String nameStyle(boolean selected) {
|
||||||
return "-fx-font-family: 'Cinzel'; -fx-font-size: 10; -fx-letter-spacing: 2;" +
|
return "-fx-font-family: 'Cinzel'; -fx-font-size: 10; -fx-letter-spacing: 2;" +
|
||||||
"-fx-text-fill: " + (selected ? "#ffffff" : "ffffff") + ";";
|
"-fx-text-fill: " + (selected ? "#ffffff" : "ffffff") + ";";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the totem name with only the first letter capitalised. */
|
||||||
private String capitalize(Totems t) {
|
private String capitalize(Totems t) {
|
||||||
String s = t.name().toLowerCase(Locale.ROOT);
|
String s = t.name().toLowerCase(Locale.ROOT);
|
||||||
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
|
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Submits the selected totem index to the controller when the confirm button is clicked. */
|
||||||
@FXML
|
@FXML
|
||||||
private void onConfirm() {
|
private void onConfirm() {
|
||||||
if (selectedIndex >= 0) {
|
if (selectedIndex >= 0) {
|
||||||
|
|||||||
@@ -5,10 +5,24 @@ import it.polimi.ingsw.gc14.Model.Game;
|
|||||||
import it.polimi.ingsw.gc14.Model.MiniModel;
|
import it.polimi.ingsw.gc14.Model.MiniModel;
|
||||||
import it.polimi.ingsw.gc14.Network.NetworkEvent;
|
import it.polimi.ingsw.gc14.Network.NetworkEvent;
|
||||||
|
|
||||||
|
/** View interface implemented by both the TUI and GUI. */
|
||||||
public interface IView {
|
public interface IView {
|
||||||
|
/**
|
||||||
|
* Updates the view's local copy of the game model.
|
||||||
|
*
|
||||||
|
* @param miniModel the latest mini model received from the server.
|
||||||
|
*/
|
||||||
public void setModel(MiniModel miniModel);
|
public void setModel(MiniModel miniModel);
|
||||||
|
|
||||||
|
/** Re-renders the view based on the current mini model state. */
|
||||||
public void render();
|
public void render();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an error message to the user.
|
||||||
|
*
|
||||||
|
* @param error the error type.
|
||||||
|
* @param message a human-readable description of the error.
|
||||||
|
*/
|
||||||
public void showError(ErrorType error,String message);
|
public void showError(ErrorType error,String message);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ public class TUI implements IView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Prints the final standings side-by-side in pairs, then announces the winner or game-over result. */
|
||||||
private void renderStanding() {
|
private void renderStanding() {
|
||||||
if(model.standingPlayers!=null)
|
if(model.standingPlayers!=null)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user