diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyConnection.java b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyConnection.java index d474a51..93873e0 100644 --- a/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyConnection.java +++ b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyConnection.java @@ -1,5 +1,6 @@ package uulm.teamname.marvelous.server.lobbymanager; +import org.java_websocket.WebSocket; import org.tinylog.Logger; import uulm.teamname.marvelous.gamelibrary.Tuple; import uulm.teamname.marvelous.gamelibrary.config.CharacterProperties; @@ -8,10 +9,9 @@ import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage; import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType; import uulm.teamname.marvelous.gamelibrary.messages.client.CharacterSelectionMessage; import uulm.teamname.marvelous.gamelibrary.messages.client.RequestMessage; -import uulm.teamname.marvelous.gamelibrary.messages.server.GameAssignmentMessage; +import uulm.teamname.marvelous.gamelibrary.messages.server.*; import uulm.teamname.marvelous.server.Server; import uulm.teamname.marvelous.server.lobby.Lobby; -import uulm.teamname.marvelous.server.netconnector.UserManager; import java.util.*; import java.util.concurrent.BlockingQueue; @@ -20,44 +20,65 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; /** - * A class that handles the connection to the lobby. It contains the participants inside of the lobby. - * The class is meant to be used in conjecture with {@link MessageRelay}. + * A class that handles the connection to the lobby. It contains the participants inside of the lobby. The class + * implements runnable and harbors a thread, but should exclusively be run from the {@link LobbyRunner} class. */ public class LobbyConnection implements Runnable { private Lobby lobby; public final String gameID; private Participant player1, player2; - private boolean characterSelection; - /** Whether the character selection phase is reached */ + /** Whether the character selection phase is active. True while active, false after done */ + private boolean characterSelectionActive; + + /** Whether the ingame phase is reached */ private boolean inGame; + + /** A boolean set true if the thread should be closed. Basically a terminator. */ + private boolean active; + private final HashSet spectators; private final BlockingQueue> incomingMessages; + private final BiConsumer sendMessageCallback; + private final BiConsumer sendErrorCallback; + // TODO: FIX THIS JAVADOC + /** Creates a new LobbyConnection */ - public LobbyConnection(String gameID) { + public LobbyConnection(String gameID, + BiConsumer sendMessageCallback, + BiConsumer sendErrorCallback) { this.gameID = gameID; + this.sendMessageCallback = sendMessageCallback; + this.sendErrorCallback = sendErrorCallback; this.spectators = new HashSet<>(10); this.incomingMessages = new LinkedBlockingQueue<>(); - this.characterSelection = false; + this.characterSelectionActive = false; this.inGame = false; + this.active = false; } + // Variables for Character Selection + Tuple selectionPossibilities; + + CharacterProperties[] playerOneSelection = null; + CharacterProperties[] playerTwoSelection = null; + @Override public void run() { Logger.info("Starting Lobby thread for lobby '{}'", gameID); - Logger.trace("Initializing lobby..."); - initializeLobby(); - Logger.trace("Activating characterSelection state"); - this.characterSelection = true; + Logger.debug("Activating lobbyConnection"); + this.active = true; + + Logger.debug("Activating characterSelection state"); + this.characterSelectionActive = true; Logger.info("Starting character selection process"); Logger.trace("Finding twenty-four random characters"); - Tuple selectionPossibilities = - Server.getCharacterConfig().getDisjointSetsOfPropertiesOfSize(12); + selectionPossibilities = Server.getCharacterConfig().getDisjointSetsOfPropertiesOfSize(12); Logger.info("Sending GameAssignment message with random characters to players"); var gameAssignmentMessage = new GameAssignmentMessage(); @@ -65,115 +86,226 @@ public class LobbyConnection implements Runnable { // Send to player one with characters for player one gameAssignmentMessage.characterSelection = selectionPossibilities.item1; - UserManager.getInstance().sendMessage(player1.getConnection(), gameAssignmentMessage); + sendMessage(player1, gameAssignmentMessage); // And send the others to player 2 gameAssignmentMessage.characterSelection = selectionPossibilities.item2; - UserManager.getInstance().sendMessage(player2.getConnection(), gameAssignmentMessage); + sendMessage(player2, gameAssignmentMessage); - CharacterProperties[] playerOneSelection = null; - CharacterProperties[] playerTwoSelection = null; + // Also, send the GeneralAssignment message to all spectators + var generalAssignmentMessage = new GeneralAssignmentMessage(); + generalAssignmentMessage.gameID = gameID; + broadcastToSpectators(generalAssignmentMessage); Logger.info("Entering GameAssignment state. Waiting for answer about selected characters from players."); - while (characterSelection) { - Tuple currentMessage = null; + while (characterSelectionActive && active) { + var currentMessage = getMessageAsync(1000); - try { // TODO: Exact duplication. Maybe extract? - Logger.trace("Checking for messages. Currently the amount of messages is {}", - incomingMessages.size()); - - if (incomingMessages.isEmpty()) { - Logger.trace("LobbyConnection thread waiting for new messages..."); - Thread.currentThread().wait(); - Logger.trace("Lobby '{}' woken up", gameID); - } - - Logger.trace("Polling incoming message queue"); - currentMessage = incomingMessages.poll(100, TimeUnit.MILLISECONDS); - - } catch (InterruptedException e) { - Logger.warn("LobbyConnection thread got interrupted. Exception: " + e.getMessage()); - } - - if (currentMessage == null) { + if (!active) { + Logger.info("Lobby '{}' is terminating. Exiting...", gameID); + return; + } else if (currentMessage == null) { + Logger.trace("Message was null, continuing"); // TODO: remove for production continue; } else if (currentMessage.item2 instanceof CharacterSelectionMessage) { var origin = currentMessage.item1; var message = (CharacterSelectionMessage) currentMessage.item2; - if (origin.type.equals(ParticipantType.Spectator)) { - Logger.info("Spectator sent CharacterSelectionMessage. Sending error..."); - UserManager.getInstance().sendError( - origin.getConnection(), - "Spectators can't select characters"); - } else if (origin.type.equals(ParticipantType.PlayerOne)) { - if (playerOneSelection == null) { // TODO: Extract this. This isn't beautiful at all. - ArrayList chosenCharacters = new ArrayList<>(); - for (int i = 0; i < 12; i++) { - if (message.characters[i]) { - chosenCharacters.add(selectionPossibilities.item1[i]); - } - } - playerOneSelection = chosenCharacters.toArray(new CharacterProperties[0]); - } - Logger.info("Player 1 has selected their characters"); - } else { - if (playerTwoSelection == null) { - ArrayList chosenCharacters = new ArrayList<>(); - for (int i = 0; i < 12; i++) { - if (message.characters[i]) { - chosenCharacters.add(selectionPossibilities.item2[i]); - } - } - playerTwoSelection = chosenCharacters.toArray(new CharacterProperties[0]); - } - Logger.info("Player 2 has selected their characters"); - } - } + Logger.debug("CharacterSelectionMessage sent by Player '{}' during character selection phase", + origin.name); + characterSelectionActive = receiveCharacterSelection(origin, message); + + Logger.trace("Sending confirmSelectionMessage to player1"); + var replyMessage = new ConfirmSelectionMessage(); + replyMessage.selectionComplete = !characterSelectionActive; + sendMessage(origin, replyMessage); + } else if (currentMessage.item2 instanceof RequestMessage) { + var origin = currentMessage.item1; + // var message = (RequestMessage) currentMessage.item2; this is ignored here + + Logger.debug("RequestMessage sent by Player '{}' during character selection phase", + origin.name); + sendError( + currentMessage.item1, + "Requests rejected as ingame phase not reached yet"); + } else { + Logger.warn("Message that isn't of type RequestMessage or CharacacterSelectionMessage" + + "received in Lobby. This is probably a bug."); + sendError( + currentMessage.item1, + "Message couldn't be processed by the lobby, as its type was invalid"); + } } - while(inGame) { - Tuple currentMessage = null; - try { - Logger.trace("Checking for messages. Currently the amount of messages is {}", - incomingMessages.size()); + Logger.trace("End of character selection phase reached. Lobby termination is '{}'", active); + if (!active) { + Logger.info("Lobby '{}' is terminating. Exiting... ", gameID); + return; + } else { - if (incomingMessages.isEmpty()) { - Logger.trace("LobbyConnection thread waiting for new messages..."); - Thread.currentThread().wait(); - Logger.trace("Lobby '{}' woken up", gameID); - } + Logger.info("Sending GameStructure message to clients as the game is starting"); + // Building GameStructure message + var gameStructureMessage = new GameStructureMessage(); + gameStructureMessage.playerOneName = player1.name; + gameStructureMessage.playerTwoName = player2.name; + gameStructureMessage.playerOneCharacters = playerOneSelection; + gameStructureMessage.playerTwoCharacters = playerTwoSelection; + gameStructureMessage.matchconfig = Server.getPartyConfig(); + gameStructureMessage.scenarioconfig = Server.getScenarioConfig(); - Logger.trace("Polling incoming message queue"); - currentMessage = incomingMessages.poll(100, TimeUnit.MILLISECONDS); + // Sending GameStructure message with fitting assignment + gameStructureMessage.assignment = ParticipantType.PlayerOne; + sendMessage(player1, gameStructureMessage); - } catch (InterruptedException e) { - Logger.warn("LobbyConnection thread got interrupted. Exception: " + e.getMessage()); - } + gameStructureMessage.assignment = ParticipantType.PlayerTwo; + sendMessage(player2, gameStructureMessage); - if (currentMessage == null) { - Logger.trace("Message was null, continuing"); + gameStructureMessage.assignment = ParticipantType.Spectator; + broadcastToSpectators(gameStructureMessage); + } + + + Logger.info("Entering Ingame phase"); + inGame = true; + Logger.trace("Initializing lobby..."); + initializeLobby(); + while (inGame && active) { + Tuple currentMessage = getMessageAsync(1000); + + if (!active) { + Logger.info("Lobby '{}' is terminating. Exiting...", gameID); + return; + } else if (currentMessage == null) { + Logger.trace("Message was null, continuing"); // TODO: remove for production continue; } else if (currentMessage.item2 instanceof CharacterSelectionMessage) { - receiveCharacterSelection((CharacterSelectionMessage) currentMessage.item2); + var origin = currentMessage.item1; + var message = (CharacterSelectionMessage) currentMessage.item2; + Logger.debug("CharacterSelectionMessage sent by Player '{}' during ingame phase", origin.name); + receiveCharacterSelection(origin, message); } else if (currentMessage.item2 instanceof RequestMessage) { - receiveRequests((RequestMessage) currentMessage.item2); + var origin = currentMessage.item1; + var message = (RequestMessage) currentMessage.item2; + Logger.debug("RequestMessage sent by Player '{}' during ingame phase", + origin.name); + receiveRequests(origin, message); } else { + Logger.warn("Message that isn't of type RequestMessage or CharacacterSelectionMessage" + + "received in Lobby. This is probably a bug."); + sendError( + currentMessage.item1, + "Message couldn't be processed by the lobby, as its type was invalid"); } } } - /** Send messages to the lobbyConnection */ - public void receiveMessage(Participant participant, BasicMessage message) { - this.incomingMessages.add(Tuple.of(participant, message)); + /** + * Tries to read the next message in the messageQueue. waits for timeoutMillis + * for a queue item + * + * @param timeoutMillis is the amount of time the method waits until continuing even though not being notified. + */ + private Tuple getMessageAsync(int timeoutMillis) { + Tuple currentMessage = null; + try { + Logger.trace("Checking for messages. Currently the amount of messages is {}", + incomingMessages.size()); + + Logger.trace("Polling incoming message queue"); + currentMessage = incomingMessages.poll(timeoutMillis, TimeUnit.MILLISECONDS); + + } catch (InterruptedException e) { + Logger.warn("LobbyConnection thread got interrupted. Exception: " + e.getMessage()); + } + return currentMessage; } - private void receiveRequests(RequestMessage message) { - // TODO: implement this + /** Send messages to the lobbyConnection. This is thread-safe, and meant to be called from the outside. */ + public void receiveMessage(Participant origin, BasicMessage message) { + Logger.trace("Lobby '{}' received message from participant '{}'", gameID, origin.name); + this.incomingMessages.add(Tuple.of(origin, message)); } - private void receiveCharacterSelection(CharacterSelectionMessage message) { - // TODO: Implement proper character selection + private void receiveRequests(Participant origin, RequestMessage message) { + System.out.println("REMOVE THIS PRINTLN: received request message " + message.toString()); + } + + /** + * Method executed on Character selection. Hereby the local fields {@link LobbyConnection#selectionPossibilities}, + * {@link LobbyConnection#playerOneSelection} and {@link LobbyConnection#playerTwoSelection} are checked and + * populated + * + * @param origin is the participant that sent the message + * @param message is the message triggering the CharacterSelection method + * @return true if CharacterSelection is now completed, and false otherwise. If the method is called outside of the + * CharacterSelection phase, true is returned, as the characterSelection is after all done + */ + private boolean receiveCharacterSelection(Participant origin, CharacterSelectionMessage message) { + if (this.characterSelectionActive) { + if (!hasRightNumberOfTrues(message.characters, 6)) { + Logger.debug("Player chose non-six number of characters, which is invalid. Sending error"); + sendError(origin, "Cannot select characters as character choices wasn't 6"); + } else { + switch (origin.type) { + case PlayerOne -> { + if (playerOneSelection == null) { + playerOneSelection = getChosenCharacters( + selectionPossibilities.item1, + message.characters); + Logger.info("Player 1 has selected their characters"); + } else { + Logger.debug("Player 1 tried to select characters twice, sending error"); + sendError(origin, + "Cannot select characters as characters were already selected" + ); + } + } + case PlayerTwo -> { + if (playerTwoSelection == null) { + Logger.trace("Checking whether there are actually 6 choices"); + playerTwoSelection = getChosenCharacters( + selectionPossibilities.item1, + message.characters); + Logger.info("Player 1 has selected their characters"); + } else { + Logger.debug("Player 1 tried to select characters twice, sending error"); + sendError(origin, + "Cannot select characters as characters were already selected"); + } + } + case Spectator -> { + Logger.info("Spectator sent CharacterSelectionMessage. Sending error..."); + sendError(origin, "Spectators can't select characters"); + } + } + } + } else { + Logger.debug("Participant '{}' sent a CharacterSelectionMessage outside of " + + "the CharacterSelectionPhase, sending error", origin.name); + sendError(origin, "The character selection phase is already over"); + } + return !characterSelectionActive || (playerOneSelection != null && playerTwoSelection != null); + } + + private CharacterProperties[] getChosenCharacters(CharacterProperties[] possibleChoices, Boolean[] choices) { + ArrayList chosenCharacters = new ArrayList<>(); + for (int i = 0; i < 12; i++) { + if (choices[i]) { + chosenCharacters.add(possibleChoices[i]); + } + } + return chosenCharacters.toArray(new CharacterProperties[0]); + } + + private boolean hasRightNumberOfTrues(Boolean[] values, int expected) { + int choices = Arrays.stream(values) + .mapToInt(choice -> choice ? 1 : 0) + .sum(); + if (choices == expected) { + return true; + } else { + return false; + } } private void initializeLobby() { @@ -199,8 +331,8 @@ public class LobbyConnection implements Runnable { } /** @return whether a player can join the lobby */ - public boolean hasFreePlayerSpot() { - return !(hasPlayer1() && hasPlayer2()); + public boolean isFull() { + return hasPlayer1() && hasPlayer2(); } public Participant getPlayer1() { @@ -217,6 +349,7 @@ public class LobbyConnection implements Runnable { /** * Adds a new player into the player1 slot + * * @param player is the websocket to be added * @return true if added successfully, and false otherwise */ @@ -232,6 +365,7 @@ public class LobbyConnection implements Runnable { /** * Adds a new player into the player1 slot + * * @param player is the websocket to be added * @return true if added successfully, and false otherwise */ @@ -247,6 +381,7 @@ public class LobbyConnection implements Runnable { /** * Adds a new player into either the player1, or if full, player2 slot + * * @param player is the websocket to be added * @return true if added successfully, and false otherwise */ @@ -287,35 +422,65 @@ public class LobbyConnection implements Runnable { return player1 == participant || player2 == participant || spectators.contains(participant); } + // Methods to send messages + + private void sendMessage(Participant recipient, BasicMessage message) { + sendMessageCallback.accept(recipient.getConnection(), message); + } + + private void broadcast(BasicMessage message) { + sendMessage(player1, message); + sendMessage(player2, message); + spectators.forEach(spectator -> sendMessage(spectator, message)); + } + + private void broadcastToSpectators(BasicMessage message) { + spectators.forEach(spectator -> sendMessage(spectator, message)); + } + + private void broadcastToAllExcept(Participant except, BasicMessage message) { + + if (!except.equals(player1)) sendMessage(player1, message); + if (!except.equals(player2)) sendMessage(player2, message); + spectators.stream().filter(spectator -> !except.equals(spectator)) + .forEach(spectator -> sendMessage(spectator, message)); + } + + private void sendError(Participant recipient, String errorMessage) { + sendErrorCallback.accept(recipient.getConnection(), errorMessage); + } + // Methods to send events public void sendEvents(Participant recipient, Event... events) { - MessageRelay.getInstance().sendMessage(this, recipient, events); + var message = new EventMessage(); + message.messages = events; + + sendMessage(recipient, message); } public void broadcastEvents(Event... events) { - // TODO: implement - MessageRelay.getInstance().broadcastEvents(this, events); + var message = new EventMessage(); + message.messages = events; + + broadcast(message); } public void broadcastToAllExcept(Participant except, Event... events) { - // TODO: implement - var messageRelayInstance = MessageRelay.getInstance(); - if (except.type == ParticipantType.Spectator) { - spectators.stream() - .filter(spectator -> !spectator.equals(except)) - .forEach(spectator -> messageRelayInstance.sendMessage(this, spectator, events)); - messageRelayInstance.sendMessage(this, player1, events); - messageRelayInstance.sendMessage(this, player2, events); - } else { - messageRelayInstance.sendMessage(this, except.equals(player1) ? player2 : player1, events); - spectators.forEach(spectator -> messageRelayInstance.sendMessage(this, spectator, events)); - } + var message = new EventMessage(); + message.messages = events; + + broadcastToAllExcept(except, message); } - /** Kills all connections to client, as well as the lobby */ + /** Kills all connections to client, as well as the lobby. Notifying the thread has to be done separately. */ public void terminateConnection() { - // TODO: implement this + Logger.debug("Setting termination flag for lobby '{}'", gameID); + this.active = true; + } + + public boolean isActive() { + return active; } @Override @@ -323,12 +488,12 @@ public class LobbyConnection implements Runnable { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LobbyConnection that = (LobbyConnection) o; - return characterSelection == that.characterSelection && inGame == that.inGame && Objects.equals(lobby, that.lobby) && Objects.equals(gameID, that.gameID) && Objects.equals(player1, that.player1) && Objects.equals(player2, that.player2) && Objects.equals(spectators, that.spectators) && Objects.equals(incomingMessages, that.incomingMessages); + return characterSelectionActive == that.characterSelectionActive && inGame == that.inGame && Objects.equals(lobby, that.lobby) && Objects.equals(gameID, that.gameID) && Objects.equals(player1, that.player1) && Objects.equals(player2, that.player2) && Objects.equals(spectators, that.spectators) && Objects.equals(incomingMessages, that.incomingMessages); } @Override public int hashCode() { - return Objects.hash(lobby, gameID, player1, player2, characterSelection, inGame, spectators, incomingMessages); + return Objects.hash(lobby, gameID, player1, player2, characterSelectionActive, inGame, spectators, incomingMessages); } @Override @@ -338,7 +503,7 @@ public class LobbyConnection implements Runnable { ", gameID='" + gameID + '\'' + ", player1=" + player1 + ", player2=" + player2 + - ", characterSelection=" + characterSelection + + ", characterSelection=" + characterSelectionActive + ", inGame=" + inGame + ", spectators=" + spectators + ", incomingMessages=" + incomingMessages +