From 522558bb169951f61398629cb579d903707c8bf9 Mon Sep 17 00:00:00 2001 From: punchready Date: Mon, 7 Jun 2021 07:36:51 +0200 Subject: [PATCH] refactor: switch more classes to singleton, simplify handling greatly --- Gamelib | 2 +- .../teamname/marvelous/server/Server.java | 3 +- .../marvelous/server/lobby/Lobby.java | 21 +- .../marvelous/server/lobby/TurnTimer.java | 14 +- .../server/lobbymanager/LobbyConnection.java | 639 +++++------------- .../lobbymanager/LobbyConnectionState.java | 7 + .../server/lobbymanager/LobbyManager.java | 312 ++++----- .../server/lobbymanager/LobbyRunner.java | 20 +- .../server/lobbymanager/Participant.java | 92 +-- .../server/lobbymanager/ParticipantState.java | 7 + .../marvelous/server/netconnector/Client.java | 38 ++ .../server/netconnector/ClientState.java | 8 + .../server/netconnector/MarvelousServer.java | 5 +- .../server/netconnector/UserManager.java | 424 ++++-------- 14 files changed, 532 insertions(+), 1060 deletions(-) create mode 100644 Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyConnectionState.java create mode 100644 Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/ParticipantState.java create mode 100644 Server/src/main/java/uulm/teamname/marvelous/server/netconnector/Client.java create mode 100644 Server/src/main/java/uulm/teamname/marvelous/server/netconnector/ClientState.java diff --git a/Gamelib b/Gamelib index 81cbec5..7a5d9dc 160000 --- a/Gamelib +++ b/Gamelib @@ -1 +1 @@ -Subproject commit 81cbec5348a444a1d9e394b8c2ae668ac85063a8 +Subproject commit 7a5d9dca76eca08e33fa78773508fdfa099a9708 diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/Server.java b/Server/src/main/java/uulm/teamname/marvelous/server/Server.java index 1dfb74a..54490b2 100644 --- a/Server/src/main/java/uulm/teamname/marvelous/server/Server.java +++ b/Server/src/main/java/uulm/teamname/marvelous/server/Server.java @@ -11,9 +11,7 @@ import uulm.teamname.marvelous.gamelibrary.config.ScenarioConfig; import uulm.teamname.marvelous.gamelibrary.json.JSON; import uulm.teamname.marvelous.gamelibrary.json.ValidationUtility; import uulm.teamname.marvelous.server.args.ServerArgs; -import uulm.teamname.marvelous.server.lobbymanager.LobbyManager; import uulm.teamname.marvelous.server.netconnector.MarvelousServer; -import uulm.teamname.marvelous.server.netconnector.UserManager; import java.io.*; import java.net.InetSocketAddress; @@ -112,6 +110,7 @@ public class Server { // Add log writer 1, a console writer (which logs to the console) map.put("writer1", "console"); map.put("writer1.level", logLevelDescriptor); + map.put("writer1.format", "[{thread}] {level}: {message}"); // Add log writer 2, a file writer logging to the file server.log map.put("writer2", "file"); diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/lobby/Lobby.java b/Server/src/main/java/uulm/teamname/marvelous/server/lobby/Lobby.java index b15d1d2..bfd66d2 100644 --- a/Server/src/main/java/uulm/teamname/marvelous/server/lobby/Lobby.java +++ b/Server/src/main/java/uulm/teamname/marvelous/server/lobby/Lobby.java @@ -1,7 +1,6 @@ package uulm.teamname.marvelous.server.lobby; import org.tinylog.Logger; -import uulm.teamname.marvelous.gamelibrary.config.CharacterProperties; import uulm.teamname.marvelous.gamelibrary.entities.EntityType; import uulm.teamname.marvelous.gamelibrary.events.Event; import uulm.teamname.marvelous.gamelibrary.events.EventBuilder; @@ -14,10 +13,10 @@ import uulm.teamname.marvelous.gamelibrary.config.ScenarioConfig; import uulm.teamname.marvelous.gamelibrary.requests.Request; import uulm.teamname.marvelous.server.lobby.pipelining.*; import uulm.teamname.marvelous.server.lobbymanager.LobbyConnection; +import uulm.teamname.marvelous.server.lobbymanager.LobbyManager; import uulm.teamname.marvelous.server.lobbymanager.Participant; import java.util.*; -import java.util.stream.Collectors; public class Lobby { private final String gameID; @@ -34,7 +33,7 @@ public class Lobby { * initialize the game it gets the following parameters * * @param gameID a String to identify the game - * @param connection the Connection to the {@link uulm.teamname.marvelous.server.lobbymanager.LobbyManager} + * @param connection the Connection to the {@link LobbyManager} * @param partyConfig declared in Editor * @param characterConfig declared in Editor * @param scenarioConfig declared in Editor @@ -46,13 +45,11 @@ public class Lobby { CharacterConfig characterConfig, ScenarioConfig scenarioConfig, List player1Characters, - List player2Characters) { - + List player2Characters + ) { this.gameID = gameID; this.connection = connection; - //partyConfig.maxRoundTime; - this.game = new GameInstance( partyConfig, characterConfig, @@ -91,7 +88,7 @@ public class Lobby { public synchronized void receiveRequests(Request[] requests, Participant source) { Logger.trace("Received {} requests from participant '{}' of type {}", requests.length, - source.name, + source.id.getName(), source.type); if (activePlayer != source && source.type != ParticipantType.Spectator) { Logger.trace("Resetting bad requests as new participant sent data"); @@ -101,7 +98,7 @@ public class Lobby { Logger.info("got {} requests from participant {}", requests.length, - source.name); + source.id.getName()); Logger.trace("Processing requests through pipeline"); Optional> resultingEvents = pipeline.processRequests(requests, source); @@ -110,7 +107,7 @@ public class Lobby { //resultingEvents isEmpty when a wrong request appeared Logger.trace("Checking whether resultingEvents (an optional) is empty"); if (resultingEvents.isEmpty()) { - Logger.debug("Rejecting requests from participant '{}'", source.name); + Logger.debug("Rejecting requests from participant '{}'", source.id.getName()); reject(source); } else { accept(source, resultingEvents.get()); @@ -141,7 +138,7 @@ public class Lobby { private void accept(Participant source, List accepted) { Logger.debug("Accepting requests from participant '{}', broadcasting events to all except source", - source.name); + source.id.getName()); connection.broadcastToAllExcept(source, accepted.toArray(new Event[0])); Logger.trace("Adding ack and sending back to originParticipant"); @@ -221,7 +218,7 @@ public class Lobby { .buildGameStateEvent(), new EventBuilder(EventType.DisconnectEvent) .buildGameStateEvent()); - connection.terminateConnection(); + connection.terminate(); } public PauseSegment getPauseSegment() { diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/lobby/TurnTimer.java b/Server/src/main/java/uulm/teamname/marvelous/server/lobby/TurnTimer.java index 8ca2847..9bffb28 100644 --- a/Server/src/main/java/uulm/teamname/marvelous/server/lobby/TurnTimer.java +++ b/Server/src/main/java/uulm/teamname/marvelous/server/lobby/TurnTimer.java @@ -4,8 +4,6 @@ import org.tinylog.Logger; import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType; import uulm.teamname.marvelous.server.lobbymanager.Participant; -import java.util.Timer; -import java.util.TimerTask; import java.util.concurrent.*; import java.util.function.Consumer; @@ -35,19 +33,19 @@ public class TurnTimer { /** * This method checks if the participant is not a spectator. Otherwise it won't start a timer. * - * @param participant the timer is for + * @param Participant the timer is for */ - public void startTurnTimer(Participant participant) { - if (participant.type == ParticipantType.Spectator) { + public void startTurnTimer(Participant Participant) { + if (Participant.type == ParticipantType.Spectator) { throw new IllegalStateException("Spectators don't have TurnTime"); } clear(); Logger.debug("Starting turn timer for participant '{}' with role {}", - participant.name, participant.type); + Participant.id.getName(), Participant.type); current = timer.schedule(() -> { - callback.accept(participant); - return participant; + callback.accept(Participant); + return Participant; }, maxRoundTime, TimeUnit.SECONDS); 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 2976ac7..7857a95 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,365 +1,186 @@ package uulm.teamname.marvelous.server.lobbymanager; -import org.java_websocket.WebSocket; import org.tinylog.Logger; +import uulm.teamname.marvelous.gamelibrary.ArrayTools; import uulm.teamname.marvelous.gamelibrary.Tuple; import uulm.teamname.marvelous.gamelibrary.config.CharacterProperties; import uulm.teamname.marvelous.gamelibrary.events.Event; 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.*; +import uulm.teamname.marvelous.gamelibrary.requests.Request; import uulm.teamname.marvelous.server.Server; import uulm.teamname.marvelous.server.lobby.Lobby; +import uulm.teamname.marvelous.server.netconnector.SUID; import uulm.teamname.marvelous.server.netconnector.UserManager; import java.util.*; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; -/** - * 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 lobbyID; public final String gameID; + public LobbyConnectionState state = LobbyConnectionState.Waiting; + private Participant player1, player2; + private final HashSet spectators = new HashSet<>(10); + private final HashMap> selection = new HashMap<>(); + public final HashMap options = new HashMap<>(); - /** Whether the character selection phase is active. True while active, false after done */ - private boolean characterSelectionActive; + private final BlockingQueue> requestQueue = new LinkedBlockingQueue<>(); - /** 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 + private Lobby lobby; /** Creates a new LobbyConnection */ - 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.characterSelectionActive = false; - this.inGame = false; - this.active = false; + public LobbyConnection(String lobbyID) { + this.lobbyID = lobbyID; + this.gameID = UUID.randomUUID().toString(); + + Tuple picked = Server.getCharacterConfig().getDisjointSetsOfPropertiesOfSize(12); + this.options.put(ParticipantType.PlayerOne, picked.item1); + this.options.put(ParticipantType.PlayerTwo, picked.item2); } - // Variables for Character Selection - Tuple selectionPossibilities; - CharacterProperties[] playerOneSelection = null; - CharacterProperties[] playerTwoSelection = null; + public boolean setSelection(Participant participant, Integer[] selection) { + this.selection.put(participant.id, ArrayTools.toArrayList(selection)); + return this.selection.size() == 2; + } + + public void addParticipant(Participant participant) { + if(participant.type == ParticipantType.Spectator) { + spectators.add(participant); + } + + if(participant.type == ParticipantType.PlayerOne) { + player1 = participant; + }else { + player2 = participant; + } + } + + public void removeParticipant(Participant participant) { + UserManager.getInstance().removeClient(participant.getClient(), ""); + + if(participant.type == ParticipantType.Spectator) { + spectators.remove(participant); + } + + if(participant.type == ParticipantType.PlayerOne) { + player1 = null; + }else { + player2 = null; + } + } + + public ParticipantType freeSlot() { + if(player1 == null) { + return ParticipantType.PlayerOne; + }else if(player2 == null) { + return ParticipantType.PlayerTwo; + }else { + return ParticipantType.Spectator; + } + } + + + public void handleMessage(Participant participant, Request[] requests) { + try { + this.requestQueue.put(Tuple.of(participant, requests)); + }catch (InterruptedException e) { + + } + } + + public void handleDisconnect(Participant participant) { + participant.disconnected = true; + } + + public void handleReconnect(Participant participant) { + participant.disconnected = false; + } + @Override public void run() { + state = LobbyConnectionState.Started; + + player1.state = ParticipantState.Playing; + player2.state = ParticipantState.Playing; + for(Participant spectator: spectators) { + spectator.state = ParticipantState.Playing; + } + Logger.info("Starting Lobby thread for lobby '{}'", gameID); - Logger.debug("Activating lobbyConnection"); - this.active = true; + sendGameStructure(); - Logger.debug("Activating characterSelection state"); - this.characterSelectionActive = true; - - - Logger.info("Starting character selection process"); - Logger.trace("Finding twenty-four random characters"); - selectionPossibilities = Server.getCharacterConfig().getDisjointSetsOfPropertiesOfSize(12); - - Logger.info("Sending GameAssignment message with random characters to players"); - var gameAssignmentMessage = new GameAssignmentMessage(); - gameAssignmentMessage.gameID = this.gameID; - - // Send to player one with characters for player one - gameAssignmentMessage.characterSelection = selectionPossibilities.item1; - sendMessage(player1, gameAssignmentMessage); - - // And send the others to player 2 - gameAssignmentMessage.characterSelection = selectionPossibilities.item2; - sendMessage(player2, gameAssignmentMessage); - - // 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 (characterSelectionActive && active) { - var currentMessage = getMessageAsync(1000); - - if (!active) { - Logger.info("Lobby '{}' is terminating. Exiting...", gameID); - return; - } else if (currentMessage == null) { - continue; - } else if (currentMessage.item2 instanceof CharacterSelectionMessage) { - var origin = currentMessage.item1; - var message = (CharacterSelectionMessage) currentMessage.item2; - - Logger.debug("CharacterSelectionMessage sent by Player '{}' during character selection phase", - origin.name); - characterSelectionActive = receiveCharacterSelection(origin, message); - - } 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"); - } - } - - Logger.trace("End of character selection phase reached. Lobby termination is '{}'", active); - if (!active) { - Logger.info("Lobby '{}' is terminating. Exiting... ", gameID); - return; - } else { - - 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(); - - // Sending GameStructure message with fitting assignment - gameStructureMessage.assignment = ParticipantType.PlayerOne; - sendMessage(player1, gameStructureMessage); - - gameStructureMessage.assignment = ParticipantType.PlayerTwo; - sendMessage(player2, gameStructureMessage); - - 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) { - var origin = currentMessage.item1; - // var message = (CharacterSelectionMessage) currentMessage.item2; - - Logger.debug("CharacterSelectionMessage sent by Player '{}' during ingame phase", origin.name); - sendError( - currentMessage.item1, - "CharacterSelection rejected as character selection phase is over already"); - - } else if (currentMessage.item2 instanceof RequestMessage) { - 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"); - } - } - } - - /** - * 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 { - currentMessage = incomingMessages.poll(timeoutMillis, TimeUnit.MILLISECONDS); - - } catch (InterruptedException e) { - Logger.warn("LobbyConnection thread got interrupted. Exception: " + e.getMessage()); - } - return currentMessage; - } - - /** 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); - try { - Logger.trace("Adding to queue..."); - this.incomingMessages.put(Tuple.of(origin, message)); - } catch (InterruptedException e) { - Logger.warn("Adding message to lobby was interrupted!"); - } - Logger.trace("Message placed inside queue. Current length is {}", incomingMessages.size()); - } - - private void receiveRequests(Participant origin, RequestMessage message) { - Logger.trace("Relaying requests through LobbyConnection"); - lobby.receiveRequests(message.messages, origin); - } - - /** - * 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 false if CharacterSelection is now completed, and true otherwise. If the method is called outside of the - * CharacterSelection phase, false 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"); - Logger.trace("Sending confirmSelectionMessage to player1"); - var replyMessage = new ConfirmSelectionMessage(); - replyMessage.selectionComplete = !characterSelectionInProgress(); - sendMessage(origin, replyMessage); - } 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) { - playerTwoSelection = getChosenCharacters( - selectionPossibilities.item1, - message.characters); - Logger.info("Player 2 has selected their characters"); - - Logger.trace("Sending confirmSelectionMessage to player2"); - var replyMessage = new ConfirmSelectionMessage(); - replyMessage.selectionComplete = !characterSelectionInProgress(); - sendMessage(origin, replyMessage); - } else { - Logger.debug("Player 2 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 characterSelectionInProgress(); - } - - /** Returns whether the character selection is not yet done */ - private boolean characterSelectionInProgress() { - return (playerOneSelection == null || playerTwoSelection == null) && characterSelectionActive; - } - - 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() { - Logger.trace("Transforming chosen characters into integer array of IDs"); - var player1CharacterChoiceIDs = Arrays.stream(playerOneSelection) - .map(properties -> properties.characterID) - .collect(Collectors.toList()); - - var player2CharacterChoiceIDs = Arrays.stream(playerOneSelection) - .map(properties -> properties.characterID) - .collect(Collectors.toList()); - - Logger.info("Initializing lobby..."); this.lobby = new Lobby( gameID, this, Server.getPartyConfig(), Server.getCharacterConfig(), Server.getScenarioConfig(), - player1CharacterChoiceIDs, - player2CharacterChoiceIDs); + selection.get(player1.id), + selection.get(player2.id) + ); + + while (state == LobbyConnectionState.Started) { + Tuple currentRequests = pollQueueAsync(1000); + + if(currentRequests != null) { + lobby.receiveRequests(currentRequests.item2, currentRequests.item1); + } + } } - /** - * @return whether there is a player1 - */ - public boolean hasPlayer1() { - return player1 != null; + public void terminate() { + state = LobbyConnectionState.Aborted; } - /** @return whether there is a player2 */ - public boolean hasPlayer2() { - return player2 != null; + private void sendGameStructure() { + GameStructureMessage gameStructureMessage = new GameStructureMessage(); + gameStructureMessage.playerOneName = player1.id.getName(); + gameStructureMessage.playerTwoName = player2.id.getName(); + + gameStructureMessage.playerOneCharacters = new CharacterProperties[6]; + gameStructureMessage.playerTwoCharacters = new CharacterProperties[6]; + int i = 0; + for(Integer id: selection.get(player1.id)) { + gameStructureMessage.playerOneCharacters[i++] = Server.getCharacterConfig().getIDMap().get(id); + } + i = 0; + for(Integer id: selection.get(player2.id)) { + gameStructureMessage.playerTwoCharacters[i++] = Server.getCharacterConfig().getIDMap().get(id); + } + + gameStructureMessage.matchconfig = Server.getPartyConfig(); + gameStructureMessage.scenarioconfig = Server.getScenarioConfig(); + + // Sending GameStructure message with fitting assignment + gameStructureMessage.assignment = ParticipantType.PlayerOne; + player1.sendMessage(gameStructureMessage); + + gameStructureMessage.assignment = ParticipantType.PlayerTwo; + player2.sendMessage(gameStructureMessage); + + gameStructureMessage.assignment = ParticipantType.Spectator; + broadcastToSpectators(gameStructureMessage); } - /** @return whether a player can join the lobby */ - public boolean isFull() { - return hasPlayer1() && hasPlayer2(); + private Tuple pollQueueAsync(int timeoutMillis) { + Tuple current = null; + try { + current = requestQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS); + }catch (InterruptedException e) { + + } + return current; } + public Participant getPlayer1() { return player1; } @@ -368,152 +189,42 @@ public class LobbyConnection implements Runnable { return player2; } - public Set getSpectators() { - return Collections.unmodifiableSet(spectators); + public boolean hasPlayer1() { + return player1 != null; } - /** - * Adds a new player into the player1 slot - * - * @param player is the websocket to be added - * @return true if added successfully, and false otherwise - */ - public boolean addPlayer1(Participant player) { - if (this.contains(player)) return false; - if (player1 == null) { - player1 = player; - return true; - } else { - return false; - } - } - - /** - * Adds a new player into the player1 slot - * - * @param player is the websocket to be added - * @return true if added successfully, and false otherwise - */ - public boolean addPlayer2(Participant player) { - if (this.contains(player)) return false; - if (player2 == null) { - player2 = player; - return true; - } else { - return false; - } - } - - /** - * 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 - */ - public boolean addPlayer(Participant player) { - if (!player.type.equals(ParticipantType.PlayerOne) && !player.type.equals(ParticipantType.PlayerTwo)) { - Logger.warn("addPlayer called with non-player. This is very probably a bug."); - return false; - } - if (!addPlayer1(player)) { - return addPlayer2(player); - } - return true; - } - - public boolean addSpectator(Participant spectator) { - if (!spectator.type.equals(ParticipantType.Spectator)) { - Logger.warn("addSpectator called with non-spectator. This is very probably a bug."); - return false; - } else { - return spectators.add(spectator); - } - } - - public boolean removeParticipant(Participant player) { - Logger.info("Removing participant '{}' with type {} from lobby '{}'", - player.name, player.type, gameID); - UserManager.getInstance().removeUser(player.getConnection()); - if (player1 == player) { - player1 = null; - return true; - } else if (player2 == player) { - player2 = null; - return true; - } else { - return spectators.remove(player); - } - } - - - public boolean contains(Participant participant) { - return player1 == participant || player2 == participant || spectators.contains(participant); - } - - // Methods to send messages - - private void sendMessage(Participant recipient, BasicMessage message) { - Logger.trace("Sending message to participant '{}'", recipient); - if (recipient == null) { - Logger.debug("Sent message to non-existent participant, ignoring..."); - } else { - sendMessageCallback.accept(recipient.getConnection(), message); - } + public boolean hasPlayer2() { + return player2 != null; } private void broadcast(BasicMessage message) { - Logger.trace("Broadcasting message to all participants"); - sendMessage(player1, message); - sendMessage(player2, message); - spectators.forEach(spectator -> sendMessage(spectator, message)); + player1.sendMessage(message); + player2.sendMessage(message); + spectators.forEach(spectator -> spectator.sendMessage(message)); } private void broadcastToSpectators(BasicMessage message) { - Logger.trace("Broadcasting message to all spectators"); - spectators.forEach(spectator -> sendMessage(spectator, message)); + spectators.forEach(spectator -> spectator.sendMessage(message)); } - private void broadcastToAllExcept(Participant except, BasicMessage message) { - Logger.trace("Broadcasting message to all participants except for '{}' with role {}", - except.name, except.type); - 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) { - Logger.trace("Sending error '{}' to participant '{}' with role {}", - errorMessage, recipient.name, recipient.type); - if (recipient == null) { - Logger.debug("Sent error to non-existent participant, ignoring..."); - } else { - sendErrorCallback.accept(recipient.getConnection(), errorMessage); - } - } - - // Methods to send events - public void sendEvents(Participant recipient, Event... events) { - Logger.trace("Sending {} events to participant '{}'", events.length, recipient.name); - var message = new EventMessage(); + EventMessage message = new EventMessage(); message.messages = events; - sendMessage(recipient, message); - } - - public void broadcastEvents(Event... events) { - Logger.trace("Broadcasting {} events to all participants", events.length); - var message = new EventMessage(); - message.messages = events; - - broadcast(message); + recipient.sendMessage(message); } public void broadcastEvents(List events) { broadcastEvents(events.toArray(new Event[0])); } + public void broadcastEvents(Event... events) { + EventMessage message = new EventMessage(); + message.messages = events; + + broadcast(message); + } + public void broadcastToAllExcept(Participant except, Event... events) { var message = new EventMessage(); message.messages = events; @@ -521,45 +232,11 @@ public class LobbyConnection implements Runnable { broadcastToAllExcept(except, message); } - /** Kills all connections to client, as well as the lobby. Notifying the thread has to be done separately. */ - public void terminateConnection() { - Logger.debug("Setting termination flag for lobby '{}'", gameID); - this.active = true; - } - - public boolean isActive() { - return active; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - LobbyConnection that = (LobbyConnection) o; - 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); - } - - private Integer hashCode; - - @Override - public int hashCode() { - if (hashCode == null) { - hashCode = Objects.hash(gameID, player1, player2, characterSelectionActive, inGame, active, spectators, incomingMessages, sendMessageCallback, sendErrorCallback, selectionPossibilities); - } - return hashCode; - } - - @Override - public String toString() { - return "LobbyConnection{" + - "lobby=" + lobby + - ", gameID='" + gameID + '\'' + - ", player1=" + player1 + - ", player2=" + player2 + - ", characterSelection=" + characterSelectionActive + - ", inGame=" + inGame + - ", spectators=" + spectators + - ", incomingMessages=" + incomingMessages + - '}'; + private void broadcastToAllExcept(Participant except, BasicMessage message) { + if (!except.equals(player1)) player1.sendMessage(message); + if (!except.equals(player2)) player2.sendMessage(message); + spectators.stream() + .filter(spectator -> !except.equals(spectator)) + .forEach(spectator -> spectator.sendMessage(message)); } } diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyConnectionState.java b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyConnectionState.java new file mode 100644 index 0000000..e4ce75c --- /dev/null +++ b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyConnectionState.java @@ -0,0 +1,7 @@ +package uulm.teamname.marvelous.server.lobbymanager; + +public enum LobbyConnectionState { + Waiting, + Started, + Aborted +} diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyManager.java b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyManager.java index cd1554f..18dfd59 100644 --- a/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyManager.java +++ b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyManager.java @@ -1,206 +1,192 @@ package uulm.teamname.marvelous.server.lobbymanager; -import org.java_websocket.WebSocket; -import org.tinylog.Logger; -import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage; +import uulm.teamname.marvelous.gamelibrary.config.CharacterProperties; import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType; import uulm.teamname.marvelous.gamelibrary.messages.RoleEnum; import uulm.teamname.marvelous.gamelibrary.messages.client.CharacterSelectionMessage; import uulm.teamname.marvelous.gamelibrary.messages.client.PlayerReadyMessage; import uulm.teamname.marvelous.gamelibrary.messages.client.RequestMessage; -import uulm.teamname.marvelous.server.lobby.Lobby; +import uulm.teamname.marvelous.gamelibrary.messages.server.ConfirmSelectionMessage; +import uulm.teamname.marvelous.gamelibrary.messages.server.GameAssignmentMessage; +import uulm.teamname.marvelous.gamelibrary.messages.server.GeneralAssignmentMessage; +import uulm.teamname.marvelous.server.netconnector.Client; +import uulm.teamname.marvelous.server.netconnector.SUID; +import uulm.teamname.marvelous.server.netconnector.UserManager; import java.util.HashMap; -import java.util.Map; -import java.util.function.BiConsumer; +import java.util.concurrent.atomic.AtomicBoolean; public class LobbyManager { - private final HashMap lobbies; - private final HashMap resourceDescriptorToLobby; - private final BiConsumer sendMessageCallback; - private final BiConsumer sendErrorCallback; - private String localResourceDescriptor; - - public LobbyManager(BiConsumer sendMessageCallback, - BiConsumer sendErrorCallback) { - this.lobbies = new HashMap<>(); - this.resourceDescriptorToLobby = new HashMap<>(); - this.sendMessageCallback = sendMessageCallback; - this.sendErrorCallback = sendErrorCallback; - } + private static LobbyManager instance; /** - * Assigns a lobby to the given participant. If there are no lobbies available, a new lobby will be created. The - * {@link WebSocket#getResourceDescriptor() ResourceDescriptor} is hereby preferred as the LobbyID, whereby - * spectators are always assigned to the lobby specified in said resourceDescriptor while players are connected to a - * lobby with a similar resourceDescriptor if the lobby they requested is already full - * - * @param playerName is the name of the player be assigned to a lobby - * @param message is the {@link PlayerReadyMessage} sent by the participant that triggered the LobbyAssignment - * @return the {@link Participant} that was actually assigned to the lobby + * @return the current instance of the UserManager */ - public Participant assignLobbyToConnection(WebSocket connection, String playerName, PlayerReadyMessage message) { - Logger.info("Assigning lobby to player '{}'", playerName); + public static LobbyManager getInstance() { + if (instance == null) { + instance = new LobbyManager(); + } + return instance; + } - // If no resourceDescriptor is given, generate new one - var resourceDescriptor = connection.getResourceDescriptor(); - if (resourceDescriptor == null || resourceDescriptor.length() == 0) { - Logger.trace("Resource descriptor is null, getting local one"); - resourceDescriptor = getLocalResourceDescriptor(); + private final HashMap lobbies = new HashMap<>(); + + private final HashMap participants = new HashMap<>(); + + + public boolean handleConnect(Client client, AtomicBoolean running) { + if(participants.containsKey(client.id)) { + running.set(true); + } + return true; + } + + public boolean handleReady(Client client, PlayerReadyMessage message) { + if(participants.containsKey(client.id)) { + return false; } - LobbyConnection targetedLobby = resourceDescriptorToLobby.get(resourceDescriptor); - Participant participant; + addParticipant(client, client.socket.getResourceDescriptor(), message.role); + return true; + } - // If lobby is filled, generate a new ResourceDescriptor - - if (targetedLobby == null) { - if (LobbyRunner.getInstance().canAddLobby()) { - Logger.info("Lobby '{}' is non-existent, initializing lobby...", resourceDescriptor); - targetedLobby = initializeNewLobby(resourceDescriptor); - } else { - Logger.info("No free lobby spot available, sending error and disconnecting Client"); - // TODO: Implement this! - } - - } else if (targetedLobby.isFull() && !message.role.equals(RoleEnum.SPECTATOR)) { - Logger.info("Lobby '{}' is already full, assigning player '{}' to new lobby", - resourceDescriptor, - playerName); - resourceDescriptor = getLocalResourceDescriptor(); - Logger.info("Lobby '{}' is non-existent, initializing lobby...", resourceDescriptor); - targetedLobby = initializeNewLobby(resourceDescriptor); + public boolean handleReconnect(Client client) { + if(!participants.containsKey(client.id)) { + return false; } - Logger.trace("Obtaining lock on targetedLobby '{}'", targetedLobby.gameID); - synchronized (targetedLobby) { - Logger.debug("Assigning participant to lobby"); - if (message.role.equals(RoleEnum.SPECTATOR)) { - Logger.trace("Generating new participant with type spectator"); - participant = new Participant(connection, ParticipantType.Spectator, playerName); - targetedLobby.addSpectator(participant); + Participant participant = participants.get(client.id); + participant.setClient(client); - } else { - Logger.trace("Checking whether Player1 or Player2 spot is free in lobby '{}'", - targetedLobby.gameID); - var participantType = - !targetedLobby.hasPlayer1() - ? ParticipantType.PlayerOne - : ParticipantType.PlayerTwo; + LobbyConnection lobby = lobbies.get(participant.lobby); - Logger.trace("Generating new participant with type {}", participantType); - participant = new Participant(connection, participantType, playerName); - if (!targetedLobby.addPlayer(participant)) { - Logger.warn("Participant could not be added to lobby. This is probably a bug."); + if(lobby == null) { + return false; + } - } - } - Logger.trace("Adding mapping from participant '{}' to lobby '{}'", - participant.name, targetedLobby.gameID); - synchronized (lobbies) { - lobbies.put(participant, targetedLobby); - } - if (targetedLobby.isFull()) { - Logger.trace("Lobby is full, checking whether to start lobby or not"); - startLobby(targetedLobby); + lobby.handleReconnect(participant); + + return true; + } + + public boolean handleSelection(Client client, CharacterSelectionMessage message) { + if(!participants.containsKey(client.id)) { + return false; + } + + Participant participant = participants.get(client.id); + + if(participant.state != ParticipantState.Assigned) { + return false; + } + + LobbyConnection lobby = lobbies.get(participant.lobby); + + if(lobby == null) { + return false; + } + + Integer[] selected = new Integer[6]; + + CharacterProperties[] options = lobby.options.get(participant.type); + + int n = 0; + for(int i = 0; i < 12; i++) { + if(message.characters[i]) { + selected[n++] = options[i].characterID; } } - return participant; - } - public void relayMessageToLobby(Participant origin, CharacterSelectionMessage message) { - var targetedLobby = lobbies.get(origin); - if (targetedLobby == null) { - Logger.warn("Tried to send character selection message to non-existent lobby. This is probably a bug."); - } else if (!targetedLobby.isActive()) { - Logger.info("Tried sending message to inactive lobby, sending error..."); - sendErrorCallback.accept(origin.getConnection(), "message could not be processed as " + - "lobby has not yet started as you are the only player in the lobby at the moment"); - } else { - targetedLobby.receiveMessage(origin, message); + if(n != 6) { + return false; } - } - public void relayMessageToLobby(Participant origin, RequestMessage message) { - var targetedLobby = lobbies.get(origin); - if (targetedLobby == null) { - Logger.warn("Tried to send event message to non-existent lobby. This is probably a bug."); - } else if (!targetedLobby.isActive()) { - Logger.info("Tried sending message to inactive lobby, sending error..."); - sendErrorCallback.accept(origin.getConnection(), "message could not be processed as " + - "lobby has not yet started as you are the only player in the lobby at the moment"); - } else { - targetedLobby.receiveMessage(origin, message); + participant.state = ParticipantState.Selected; + + boolean complete = lobby.setSelection(participant, selected); + + ConfirmSelectionMessage response = new ConfirmSelectionMessage(); + response.selectionComplete = complete; + participant.sendMessage(response); + + if(complete) { + LobbyRunner.getInstance().startLobby(lobby); } + + return true; } - public void restoreConnection(WebSocket connection, Participant participant) { - participant.setConnection(connection); - } - - /** - * Initializes a new {@link LobbyConnection} with a not yet initialized {@link Lobby}. The {@link LobbyConnection} - * gets some GameID, and also the sendMessageCallback and sendErrorCallback from the {@link - * uulm.teamname.marvelous.server.netconnector.UserManager}. - * - * @param gameID is the ID that the {@link LobbyConnection} is initialized with. This is normally the - * resourceDescriptor. - * @return the newly initialized LobbyConnection - */ - private LobbyConnection initializeNewLobby(String gameID) { - var lobby = new LobbyConnection(gameID, sendMessageCallback, sendErrorCallback); - Logger.debug("Adding mapping from gameID (resourceDescriptor) '{}' to new lobby...", gameID); - synchronized (resourceDescriptorToLobby) { - resourceDescriptorToLobby.put(gameID, lobby); + public boolean handleRequests(Client client, RequestMessage message) { + if(!participants.containsKey(client.id)) { + return false; } - return lobby; + + Participant participant = participants.get(client.id); + + if(participant.state != ParticipantState.Playing) { + return false; + } + + LobbyConnection lobby = lobbies.get(participant.lobby); + + if(lobby == null) { + return false; + } + + lobby.handleMessage(participant, message.messages); + + return true; } - /** - * Returns the local resource descriptor if it is pointing to a not yet filled lobby (joinable lobby), or generates - * a new one if the lobby described by the resourceDescriptor is full already - */ - private String getLocalResourceDescriptor() { - Logger.trace("Getting local resourceDescriptor. Currently this is '{}'", localResourceDescriptor); - if (localResourceDescriptor == null) { - Logger.trace("local resourceDescriptor is null. Initializing local resourceDescriptor..."); - localResourceDescriptor = RandomWordGenerator.generateTwoWords(); - Logger.debug("Local resoucrceDescriptor initialized as '{}'", localResourceDescriptor); + public void handleDisconnect(Client client, boolean byRemote) { + if(!participants.containsKey(client.id)) { + return; } - var lobby = resourceDescriptorToLobby.get(localResourceDescriptor); - if (lobby != null) { - if (lobby.isFull()) { - Logger.debug("Lobby is full, generating new local resourceDescriptor"); - while (resourceDescriptorToLobby.get(localResourceDescriptor) != null) { - localResourceDescriptor = RandomWordGenerator.generateTwoWords(); - } - Logger.debug("New resourceDescriptor is '{}'", localResourceDescriptor); + + Participant participant = participants.get(client.id); + + LobbyConnection lobby = lobbies.get(participant.lobby); + + if(lobby == null) { + return; + } + + lobby.handleDisconnect(participant); + } + + + private void addParticipant(Client client, String lobbyID, RoleEnum role) { + if(!lobbies.containsKey(lobbyID)) { + if(!LobbyRunner.getInstance().canAddLobby()) { + UserManager.getInstance().removeClient(client, "The server is currently full."); + return; } + lobbies.put(lobbyID, new LobbyConnection(lobbyID)); } - Logger.trace("Returning local resourceDescriptor"); - return localResourceDescriptor; - } - /** Checks whether the current lobby is already started, and if that is not the case, starts it */ - private void startLobby(LobbyConnection targetedLobby) { - if (!LobbyRunner.getInstance().isStarted(targetedLobby)) { - Logger.info("Starting Lobby '{}' ...", targetedLobby.gameID); - LobbyRunner.getInstance().startLobby(targetedLobby); + LobbyConnection lobby = lobbies.get(lobbyID); + + ParticipantType type = lobby.freeSlot(); + + if(type == ParticipantType.Spectator && role != RoleEnum.SPECTATOR) { + UserManager.getInstance().removeClient(client, "The game is already full. Please connect as a spectator instead."); + return; } - } - /** A method to obtain the lobbies HashMap. Meant for testing, and shouldn't be used anywhere else. */ - @Deprecated - Map getLobbies() { - return lobbies; - } + Participant participant = new Participant(client, lobbyID, type); + participants.put(client.id, participant); - /** - * A method to obtain the resourceDescriptorToLobby HashMap. Meant for testing, and shouldn't be used anywhere - * else. - */ - @Deprecated - Map getResourceDescriptorToLobby() { - return resourceDescriptorToLobby; + lobby.addParticipant(participant); + + if(type != ParticipantType.Spectator) { + GameAssignmentMessage response = new GameAssignmentMessage(); + response.gameID = lobby.gameID; + response.characterSelection = lobby.options.get(type); + participant.sendMessage(response); + }else { + GeneralAssignmentMessage response = new GeneralAssignmentMessage(); + response.gameID = lobby.gameID; + participant.sendMessage(response); + } } } diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyRunner.java b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyRunner.java index 2601127..8c61f86 100644 --- a/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyRunner.java +++ b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/LobbyRunner.java @@ -6,20 +6,10 @@ import uulm.teamname.marvelous.server.Server; import java.util.HashMap; /** - * Class meant for running lobbies. It manages said lobbys, creates threads for it, and moves it into an executor + * Class meant for running lobbies. It manages said lobbies, creates threads for it, and moves it into an executor */ public class LobbyRunner { - private static LobbyRunner instance; - private final HashMap activeLobbies; - - - /** - * Constructs a new LobbyRunner - */ - private LobbyRunner() { - this.activeLobbies = new HashMap<>(); - } static LobbyRunner getInstance() { if (instance == null) { @@ -28,6 +18,9 @@ public class LobbyRunner { return instance; } + private final HashMap activeLobbies = new HashMap<>(); + + boolean canAddLobby() { return activeLobbies.size() < Server.getMaxLobbies(); } @@ -58,7 +51,7 @@ public class LobbyRunner { Logger.warn("Tried to remove non-existent lobby thread. This is probably a bug."); } else { Logger.debug("Stopping and removing lobby '{}'", lobby.gameID); - lobby.terminateConnection(); + lobby.terminate(); activeLobbies.remove(lobby); } } @@ -71,11 +64,12 @@ public class LobbyRunner { /** Shutdown all threads, destroy the lobbies, and close everything up */ void shutdownAll() { Logger.info("Stopping and removing all LobbyThreads"); - activeLobbies.keySet().forEach(LobbyConnection::terminateConnection); + activeLobbies.keySet().forEach(LobbyConnection::terminate); Logger.debug("All lobby shutdown flags set"); } // later... void checkThreads() { + } } diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/Participant.java b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/Participant.java index a6f1ba3..db52ca2 100644 --- a/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/Participant.java +++ b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/Participant.java @@ -1,86 +1,44 @@ package uulm.teamname.marvelous.server.lobbymanager; -import org.java_websocket.WebSocket; -import org.tinylog.Logger; +import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage; import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType; - -import java.util.Objects; +import uulm.teamname.marvelous.server.netconnector.Client; +import uulm.teamname.marvelous.server.netconnector.SUID; public class Participant { - - /** The ID of the device that the client provided at connect */ - public final String deviceID; - public final String name; - /** The type (as in role) of participant */ + private Client client; + public final SUID id; + public final String lobby; + public ParticipantState state = ParticipantState.Assigned; public final ParticipantType type; + public boolean disconnected = false; - /** Persistent HashCode over the lifetime of the Participant */ - Integer hashCode; - - /* Whether the participant is an AI */ - // public final boolean AI; - - /** The Websocket to contact the participant with */ - private WebSocket connection; - - /** Creates a new {@link Participant} */ - public Participant(WebSocket connection, ParticipantType type, String deviceID, String name) { - this.connection = connection; + public Participant(Client client, String lobby, ParticipantType type) { + this.client = client; + this.id = client.id; + this.lobby = lobby; this.type = type; - this.deviceID = deviceID; - this.name = name; } - /** Creates a new {@link Participant} */ - public Participant(WebSocket connection, ParticipantType type, String name) { - this.connection = connection; - this.type = type; - this.deviceID = ""; - this.name = name; + public void setClient(Client client) { + this.client = client; } - /** Returns the {@link WebSocket} to contact the participant with */ - WebSocket getConnection() { - return connection; + public Client getClient() { + return client; } - /** Sets the connection {@link WebSocket} for the current participant */ - void setConnection(WebSocket connection) { - if (this.connection != null) { - Logger.warn("Overriding connection of active participant {}, which seems invalid", this.name); + public boolean sendError(String error) { + if(disconnected) { + return false; } - Logger.debug("Setting connection of participant {} to given Websocket {}", this.name, connection); - this.connection = connection; + return client.sendError(error); } - /** Removes reference to current connection from Participant */ - public void clearConnection() { - Logger.debug("Setting connection of participant {} to null", this.name); - this.connection = null; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Participant that = (Participant) o; - return Objects.equals(connection, that.connection) && Objects.equals(deviceID, that.deviceID) && Objects.equals(name, that.name) && type == that.type; - } - - @Override - public int hashCode() { - if (hashCode == null) hashCode = Objects.hash(connection, deviceID, name, type); - return hashCode; - } - - @Override - public String - toString() { - return "Participant{" + - "connection=" + connection + - ", deviceID='" + deviceID + '\'' + - ", name='" + name + '\'' + - ", type=" + type + - '}'; + public boolean sendMessage(BasicMessage message) { + if(disconnected) { + return false; + } + return client.sendMessage(message); } } diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/ParticipantState.java b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/ParticipantState.java new file mode 100644 index 0000000..9ff527c --- /dev/null +++ b/Server/src/main/java/uulm/teamname/marvelous/server/lobbymanager/ParticipantState.java @@ -0,0 +1,7 @@ +package uulm.teamname.marvelous.server.lobbymanager; + +public enum ParticipantState { + Assigned, + Selected, + Playing +} diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/Client.java b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/Client.java new file mode 100644 index 0000000..60f36d0 --- /dev/null +++ b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/Client.java @@ -0,0 +1,38 @@ +package uulm.teamname.marvelous.server.netconnector; + +import org.java_websocket.WebSocket; +import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage; +import uulm.teamname.marvelous.gamelibrary.messages.ErrorMessage; + +import java.util.Optional; + +public class Client { + public final WebSocket socket; + public SUID id; + public ClientState state = ClientState.Blank; + + public Client(WebSocket socket) { + this.socket = socket; + } + + public boolean sendError(String error) { + ErrorMessage errorMessage = new ErrorMessage(); + errorMessage.message = error; + return sendMessage(errorMessage); + } + + public boolean sendMessage(BasicMessage message) { + if(socket == null) { + return false; + } + + Optional data = UserManager.getInstance().json.stringify(message); + + if (data.isEmpty()) { + return false; + } + + socket.send(data.get()); + return true; + } +} diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/ClientState.java b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/ClientState.java new file mode 100644 index 0000000..3a99278 --- /dev/null +++ b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/ClientState.java @@ -0,0 +1,8 @@ +package uulm.teamname.marvelous.server.netconnector; + +public enum ClientState { + Blank, + Ready, + Assigned, + Playing +} diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/MarvelousServer.java b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/MarvelousServer.java index 1a761e8..574da86 100644 --- a/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/MarvelousServer.java +++ b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/MarvelousServer.java @@ -4,20 +4,19 @@ import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; import org.tinylog.Logger; -import uulm.teamname.marvelous.gamelibrary.json.JSON; import java.net.InetSocketAddress; public class MarvelousServer extends WebSocketServer { @Override public void onOpen(WebSocket conn, ClientHandshake handshake) { - Logger.info("New client connected. Adding new User."); + Logger.info("New client connected."); UserManager.getInstance().connectUser(conn); } @Override public void onClose(WebSocket conn, int code, String reason, boolean remote) { - Logger.info("Client disconnected"); + Logger.info("Client disconnected."); UserManager.getInstance().disconnectUser(conn, remote); } diff --git a/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserManager.java b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserManager.java index c085759..4ee7874 100644 --- a/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserManager.java +++ b/Server/src/main/java/uulm/teamname/marvelous/server/netconnector/UserManager.java @@ -5,26 +5,23 @@ import org.tinylog.Logger; import uulm.teamname.marvelous.gamelibrary.json.JSON; import uulm.teamname.marvelous.gamelibrary.json.ValidationUtility; import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage; -import uulm.teamname.marvelous.gamelibrary.messages.ErrorMessage; import uulm.teamname.marvelous.gamelibrary.messages.client.*; import uulm.teamname.marvelous.gamelibrary.messages.server.GoodbyeClientMessage; import uulm.teamname.marvelous.gamelibrary.messages.server.HelloClientMessage; import uulm.teamname.marvelous.server.Server; import uulm.teamname.marvelous.server.lobbymanager.LobbyManager; -import uulm.teamname.marvelous.server.lobbymanager.Participant; import org.java_websocket.WebSocket; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; /** * Class that manages users. It is meant as an extension to the {@link MarvelousServer} class. This is the first place * where messages are relayed to after they get received. This class is responsible for handshakes, reconnects and - * WebSocket-to-Participant matching. It is designed to be thread-safe. The Singleton of this class also contains a - * {@link LobbyManager}, which manages the lobbys. + * WebSocket-to-Participant matching. It is designed to be thread-safe. */ public class UserManager { - private static UserManager instance; /** @@ -32,352 +29,159 @@ public class UserManager { */ public static UserManager getInstance() { if (instance == null) { - Logger.debug("No instance of UserManager found. Creating new instance..."); instance = new UserManager(); } return instance; } - /** - * The {@link LobbyManager} associated with the {@link UserManager}. It is therefore a kind of extension to the - * current singleton, and may of course not be called from anywhere else - */ - private final LobbyManager lobbyManager; + /** A map of all connected clients. */ + private final HashMap clients = new HashMap<>(); - /** A set of users that aren't assigned to lobbies or character selection yet */ - private final HashSet newUsers; - - /** A set of users that can reconnect if they wish to do so, and their matching Participants */ - private final HashMap readyToReconnect; - - /** - * A set of users that only have to send the {@link uulm.teamname.marvelous.gamelibrary.messages.client.PlayerReadyMessage} - * to be assigned to a lobby, containing WebSockets mapped to their user's usernames - */ - private final HashMap readyToConnect; - - - /** A set of users that are already assigned to lobbies or character selection, mapped to their participants */ - private final HashMap inGame; - - /** - * A map mapping {@link SUID SUIDs} to {@link Participant Participants} to assert whether (and where to) reconnect - * an user - */ - private final HashMap activeParticipants; - - private final JSON json; + public final JSON json; /** Constructs a new, empty UserManager */ private UserManager() { - this.newUsers = new HashSet<>(); - this.readyToConnect = new HashMap<>(); - this.readyToReconnect = new HashMap<>(); - this.inGame = new HashMap<>(); - this.activeParticipants = new HashMap<>(); - - Logger.trace("Instantiating LobbyManager with message sending callbacks"); - this.lobbyManager = new LobbyManager(this::sendMessage, this::sendError); - - Logger.trace("Instantiating JSON with the server's CharacterConfig"); this.json = new JSON(Server.getCharacterConfig()); } /** Called on a new WebSocket connection. Places the WebSocket and its ResourceDescriptor in a HashMap. */ void connectUser(WebSocket conn) { - synchronized (newUsers) { - newUsers.add(conn); + synchronized(clients) { + clients.put(conn, new Client(conn)); } } /** * Called on any received messages. The method checks the message for validity, and then relays it accordingly. - * {@link HelloServerMessage HelloServerMessages} and {@link ReconnectMessage ReconnectMessages} are handled in this - * component, whereby a handshake or reconnect is performed, respectively. - * - * @param conn is the {@link WebSocket} that sent the message + * @param conn is the {@link WebSocket} that sent the message * @param message is the {@link String} sent by the connection */ void messageReceived(WebSocket conn, String message) { - Logger.trace("Parsing message..."); - var parsedMessageOptional = json.parse(message); - if (parsedMessageOptional.isEmpty()) { - Logger.debug("Message '{}' was invalid, sending error", message); - sendError(conn, "Message could not be parsed"); - } else { - var parsedMessage = parsedMessageOptional.get(); + Client client = clients.get(conn); - Logger.trace("Validating message..."); - var violations = ValidationUtility.validate(parsedMessage); - - if (violations.isPresent()) { - Logger.debug("Message '{}' was invalid: {}, sending error", message, violations.get()); - sendError(conn, violations.get()); - } else { - Logger.trace("Message was valid. Checking type of message..."); - - if (parsedMessage instanceof HelloServerMessage) { - Logger.trace("Message is instanceof HelloServerMessage, initiating handshake"); - handshake(conn, (HelloServerMessage) parsedMessage); - - } else if (parsedMessage instanceof ReconnectMessage) { - Logger.trace("Message is instanceof ReconnectMessage, initiating reconnect"); - reconnectClient(conn, (ReconnectMessage) parsedMessage); - - } else if (parsedMessage instanceof PlayerReadyMessage) { - Logger.trace("Message is instanceof PlayerReadyMessage, assigning lobby"); - playerReady(conn, (PlayerReadyMessage) parsedMessage); - - } else if (parsedMessage instanceof CharacterSelectionMessage) { - Logger.trace("Message is instanceof CharacterSelectionMessage, passing to LobbyConnection"); - charactersSelected(conn, (CharacterSelectionMessage) parsedMessage); - - } else if (parsedMessage instanceof RequestMessage) { - Logger.trace("Message is instanceof RequestMessage, passing to LobbyConnection"); - relayRequestMessage(conn, (RequestMessage) parsedMessage); - } else { - Logger.debug("Message '{}' has invalid type, Error will be sent"); - sendError(conn, String.format( - "Message '%s' has invalid type, and cannot be processed on the server", - message)); - } - } - } - } - - void handshake(WebSocket conn, HelloServerMessage message) { // TODO: Send helloClient - if (!newUsers.contains(conn)) { - Logger.debug("websocket {} sent HelloServerMessage outside of handshake", conn); - sendError(conn, "Invalid message, as Handshake is already completed"); + if(client == null) { return; } - Logger.info("Performing handshake with user '{}'", message.name); + Optional parsed = json.parse(message); - var answer = new HelloClientMessage(); - - SUID clientID = new SUID(message.name, message.deviceID); - - // check if client is reconnected - var participant = activeParticipants.get(clientID); - if (participant != null) { - synchronized (readyToReconnect) { - readyToReconnect.put(conn, clientID); - } - answer.runningGame = true; - } else { - Logger.trace("removing handshaking user from newUsers"); - synchronized (newUsers) { - newUsers.remove(conn); - } - Logger.trace("adding handshaking user to readyToConnect"); - synchronized (readyToConnect) { - readyToConnect.put(conn, clientID); - } - answer.runningGame = false; + if(parsed.isEmpty()) { + client.sendError("Message could not be parsed."); + return; } - // Send the answer message with the previously set runningGame - sendMessage(conn, answer); + + BasicMessage data = parsed.get(); + Optional violations = ValidationUtility.validate(data); + + if(violations.isPresent()) { + client.sendError(violations.get()); + return; + } + + handleMessage(client, data); } - void reconnectClient(WebSocket conn, ReconnectMessage message) { - if (!readyToReconnect.containsKey(conn)) { - Logger.debug("Non-reconnect-allowed client has sent reconnect message, sending error"); - sendError(conn, "Reconnect is not possible"); - } else if (message.reconnect) { - Logger.info("Reconnecting client {} to their lobby", conn); - var clientID = readyToReconnect.get(conn); - var participantToRestore = activeParticipants.get(clientID); - - lobbyManager.restoreConnection(conn, participantToRestore); - - synchronized (readyToReconnect) { - readyToReconnect.remove(conn); - } - synchronized (inGame) { - inGame.put(conn, participantToRestore); - } - lobbyManager.restoreConnection(conn, participantToRestore); - // activeParticipants remains the same, as no players have been removed from the game - } else { - Logger.debug("Client {} refused reconnection, will therefore be put into readyToConnect clients", - conn); - - var clientID = readyToReconnect.get(conn); - - synchronized (readyToConnect) { - readyToConnect.put(conn, clientID); - } - synchronized (readyToReconnect) { - readyToReconnect.remove(conn); - } - } - } - - void playerReady(WebSocket conn, PlayerReadyMessage message) { - if (!message.startGame) { - removeUser(conn); - } else if (readyToReconnect.containsKey(conn) || readyToConnect.containsKey(conn)) { - Logger.trace("Connecting client to server"); - SUID suid; - if (readyToConnect.containsKey(conn)) { - Logger.trace("Client in readyToConnect state, removing from list"); - synchronized (readyToConnect) { - suid = readyToConnect.get(conn); - readyToConnect.remove(conn); - } - } else { - Logger.trace("Client in readyToReconnect state, removing from list"); - synchronized (readyToReconnect) { - suid = readyToReconnect.get(conn); - readyToReconnect.remove(conn); - } - } - Participant participant; - Logger.trace("Letting LobbyManager assign lobby to client"); - synchronized (inGame) { - participant = lobbyManager.assignLobbyToConnection(conn, suid.getName(), message); - inGame.put(conn, participant); - } - Logger.trace("Adding participants to activeParticipants for reconnection possibilities"); - synchronized (activeParticipants) { - activeParticipants.put(suid, participant); - } - } else { - Logger.debug( - "WebSocket {} sent PlayerReadyMessage to server while not in connection ready state, " + - "sending error", conn); - sendError(conn, "Invalid message, as client is not in a connection-ready state"); - } - } - - void charactersSelected(WebSocket conn, CharacterSelectionMessage message) { - if (inGame.containsKey(conn)) { - lobbyManager.relayMessageToLobby(inGame.get(conn), message); - } else { - Logger.debug( - "WebSocket {} sent CharacterSelectionMessage to server while not ingame, sending error", - conn); - sendError(conn, "Invalid message, as client is not ingame"); - } - } - - void relayRequestMessage(WebSocket conn, RequestMessage message) { - if (inGame.containsKey(conn)) { - lobbyManager.relayMessageToLobby(inGame.get(conn), message); - } else { - Logger.debug( - "WebSocket {} sent RequestMessage to server while not ingame, sending error", - conn); - sendError(conn, "Invalid message, as client is not ingame"); - } - } - - /** - * Method to call when removing a user from the game, e.g. on player timeout, or end of match - * - * @param conn is the connection to close - */ - public void removeUser(WebSocket conn) { - Logger.debug("Removing user from game"); - if (conn == null) { - Logger.trace("Tried to remove user but connection was null, ignoring"); - } else if (!isUserConnected(conn)) { - Logger.warn("Tried to remove non-connected user. This is probably a bug."); - } else { - if (inGame.containsKey(conn)) { - var participant = inGame.get(conn); - Logger.debug("Removing reconnect possibility for participant '{}'", participant.name); - var suid = new SUID(participant.name, participant.deviceID); - activeParticipants.remove(suid); - } - var message = new GoodbyeClientMessage(); - message.message = "You got disconnected"; - sendMessage(conn, message); - conn.close(CloseFrame.NORMAL); - // this automatically calls disconnectUser through the MarvelousServer - } - } - - /** Method called exclusively from {@link MarvelousServer} on closed connection. */ + /** Called on closed connection. */ void disconnectUser(WebSocket conn, boolean closedByRemote) { - // TODO: notify clients and such if the connection was in fact closed by the remote. Also remove participant - synchronized (newUsers) { - newUsers.remove(conn); + Client client = clients.get(conn); + + if(client == null) { + return; } - synchronized (readyToConnect) { - readyToConnect.remove(conn); - } - synchronized (readyToReconnect) { - readyToReconnect.remove(conn); - } - synchronized (inGame) { - if (inGame.containsKey(conn)) inGame.get(conn).clearConnection(); - inGame.remove(conn); + + LobbyManager.getInstance().handleDisconnect(client, closedByRemote); + + synchronized(clients) { + clients.remove(conn); } } - void sendMessage(WebSocket conn, BasicMessage message) { - Logger.trace("Sending message to WebSocket {}", conn); - if (conn == null) { - Logger.debug("Message sent to non-existent websocket, ignoring"); - } else { - var jsonRepresentingMessage = json.stringify(message); - if (jsonRepresentingMessage.isEmpty()) { - Logger.warn("Message {} could not be serialized!", message); + + private void handleMessage(Client client, BasicMessage data) { + if(data instanceof HelloServerMessage) { + HelloServerMessage message = (HelloServerMessage) data; + if(client.state != ClientState.Blank) { + client.sendError("Invalid message."); + return; + } + + client.id = new SUID(message.name, message.deviceID); + + AtomicBoolean running = new AtomicBoolean(false); + if(LobbyManager.getInstance().handleConnect(client, running)) { + client.state = ClientState.Ready; + + HelloClientMessage response = new HelloClientMessage(); + response.runningGame = running.get(); + client.sendMessage(response); } else { - conn.send(jsonRepresentingMessage.get()); + client.sendError("Invalid message."); + } + } else if(data instanceof ReconnectMessage) { + ReconnectMessage message = (ReconnectMessage) data; + if(client.state != ClientState.Ready) { + client.sendError("Invalid message."); + return; + } + + if(message.reconnect) { + if(LobbyManager.getInstance().handleReconnect(client)) { + client.state = ClientState.Playing; + } else { + client.sendError("Invalid message."); + } + } else { + client.state = ClientState.Blank; + } + } else if(data instanceof PlayerReadyMessage) { + PlayerReadyMessage message = (PlayerReadyMessage) data; + if(client.state != ClientState.Ready) { + client.sendError("Invalid message."); + return; + } + + if(message.startGame) { + if(LobbyManager.getInstance().handleReady(client, message)) { + client.state = ClientState.Assigned; + } else { + client.sendError("Invalid message."); + } + } else { + removeClient(client, "You got disconnected."); + } + } else if(data instanceof CharacterSelectionMessage) { + CharacterSelectionMessage message = (CharacterSelectionMessage) data; + if(client.state != ClientState.Assigned) { + client.sendError("Invalid message."); + return; + } + + if(LobbyManager.getInstance().handleSelection(client, message)) { + client.state = ClientState.Playing; + } else { + client.sendError("Invalid message."); + } + } else if(data instanceof RequestMessage) { + RequestMessage message = (RequestMessage) data; + if(client.state != ClientState.Playing) { + client.sendError("Invalid message."); + return; + } + + if(LobbyManager.getInstance().handleRequests(client, message)) { + //"👍 i approve" - the server + } else { + client.sendError("Invalid message."); } } } - /** Sends an {@link uulm.teamname.marvelous.gamelibrary.messages.ErrorMessage} to the specified user. */ - void sendError(WebSocket conn, String error) { - Logger.debug("Sending error message '{}' to WebSocket {}", error, conn); - var errorMessage = new ErrorMessage(); - errorMessage.message = error; - sendMessage(conn, errorMessage); - } + public void removeClient(Client client, String message) { + GoodbyeClientMessage response = new GoodbyeClientMessage(); + response.message = message; - public boolean isUserConnected(WebSocket user) { - return newUsers.contains(user) || - readyToReconnect.containsKey(user) || - readyToConnect.containsKey(user) || - inGame.containsKey(user); - } + client.sendMessage(response); - public int getUserCount() { - // FIXME: This is bugged at the moment - return newUsers.size() + readyToConnect.size() + readyToReconnect.size() + inGame.size(); - } - - /** Package-private getter for mutable newUsers HashSet, meant for testing */ - @Deprecated - HashSet getNewUsers() { - return newUsers; - } - - /** Package-private getter for mutable readyToReconnect HashMap, meant for testing */ - @Deprecated - HashMap getReadyToReconnect() { - return readyToReconnect; - } - - /** Package-private getter for mutable readyToConnect HashMap, meant for testing */ - @Deprecated - HashMap getReadyToConnect() { - return readyToConnect; - } - - /** Package-private getter for mutable inGame HashMap, meant for testing */ - @Deprecated - HashMap getInGame() { - return inGame; - } - - /** Package-private getter for mutable activeParticipants HashMap, meant for testing */ - @Deprecated - HashMap getActiveParticipants() { - return activeParticipants; + client.socket.close(CloseFrame.NORMAL); } }