refactor: switch more classes to singleton, simplify handling greatly

This commit is contained in:
punchready 2021-06-07 07:36:51 +02:00
parent d9b5b3db2f
commit 522558bb16
14 changed files with 532 additions and 1060 deletions

@ -1 +1 @@
Subproject commit 81cbec5348a444a1d9e394b8c2ae668ac85063a8
Subproject commit 7a5d9dca76eca08e33fa78773508fdfa099a9708

View File

@ -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");

View File

@ -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<Integer> player1Characters,
List<Integer> player2Characters) {
List<Integer> 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<List<Event>> 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<Event> 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() {

View File

@ -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);

View File

@ -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<Participant> spectators = new HashSet<>(10);
private final HashMap<SUID, List<Integer>> selection = new HashMap<>();
public final HashMap<ParticipantType, CharacterProperties[]> options = new HashMap<>();
/** Whether the character selection phase is active. True while active, false after done */
private boolean characterSelectionActive;
private final BlockingQueue<Tuple<Participant, Request[]>> 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<Participant> spectators;
private final BlockingQueue<Tuple<Participant, BasicMessage>> incomingMessages;
private final BiConsumer<WebSocket, BasicMessage> sendMessageCallback;
private final BiConsumer<WebSocket, String> sendErrorCallback;
// TODO: FIX THIS JAVADOC
private Lobby lobby;
/** Creates a new LobbyConnection */
public LobbyConnection(String gameID,
BiConsumer<WebSocket, BasicMessage> sendMessageCallback,
BiConsumer<WebSocket, String> 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<CharacterProperties[], CharacterProperties[]> picked = Server.getCharacterConfig().getDisjointSetsOfPropertiesOfSize(12);
this.options.put(ParticipantType.PlayerOne, picked.item1);
this.options.put(ParticipantType.PlayerTwo, picked.item2);
}
// Variables for Character Selection
Tuple<CharacterProperties[], CharacterProperties[]> 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<Participant, BasicMessage> 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<Participant, BasicMessage> getMessageAsync(int timeoutMillis) {
Tuple<Participant, BasicMessage> 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<CharacterProperties> 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<Participant, Request[]> 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<Participant, Request[]> pollQueueAsync(int timeoutMillis) {
Tuple<Participant, Request[]> 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<Participant> 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<Event> 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));
}
}

View File

@ -0,0 +1,7 @@
package uulm.teamname.marvelous.server.lobbymanager;
public enum LobbyConnectionState {
Waiting,
Started,
Aborted
}

View File

@ -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<Participant, LobbyConnection> lobbies;
private final HashMap<String, LobbyConnection> resourceDescriptorToLobby;
private final BiConsumer<WebSocket, BasicMessage> sendMessageCallback;
private final BiConsumer<WebSocket, String> sendErrorCallback;
private String localResourceDescriptor;
public LobbyManager(BiConsumer<WebSocket, BasicMessage> sendMessageCallback,
BiConsumer<WebSocket, String> 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<String, LobbyConnection> lobbies = new HashMap<>();
private final HashMap<SUID, Participant> 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<Participant, LobbyConnection> 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<String, LobbyConnection> 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);
}
}
}

View File

@ -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<LobbyConnection, Thread> 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<LobbyConnection, Thread> 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() {
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package uulm.teamname.marvelous.server.lobbymanager;
public enum ParticipantState {
Assigned,
Selected,
Playing
}

View File

@ -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<String> data = UserManager.getInstance().json.stringify(message);
if (data.isEmpty()) {
return false;
}
socket.send(data.get());
return true;
}
}

View File

@ -0,0 +1,8 @@
package uulm.teamname.marvelous.server.netconnector;
public enum ClientState {
Blank,
Ready,
Assigned,
Playing
}

View File

@ -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);
}

View File

@ -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<WebSocket, Client> clients = new HashMap<>();
/** A set of users that aren't assigned to lobbies or character selection yet */
private final HashSet<WebSocket> newUsers;
/** A set of users that can reconnect if they wish to do so, and their matching Participants */
private final HashMap<WebSocket, SUID> 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<WebSocket, SUID> readyToConnect;
/** A set of users that are already assigned to lobbies or character selection, mapped to their participants */
private final HashMap<WebSocket, Participant> inGame;
/**
* A map mapping {@link SUID SUIDs} to {@link Participant Participants} to assert whether (and where to) reconnect
* an user
*/
private final HashMap<SUID, Participant> 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<BasicMessage> 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<String> 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<WebSocket> getNewUsers() {
return newUsers;
}
/** Package-private getter for mutable readyToReconnect HashMap, meant for testing */
@Deprecated
HashMap<WebSocket, SUID> getReadyToReconnect() {
return readyToReconnect;
}
/** Package-private getter for mutable readyToConnect HashMap, meant for testing */
@Deprecated
HashMap<WebSocket, SUID> getReadyToConnect() {
return readyToConnect;
}
/** Package-private getter for mutable inGame HashMap, meant for testing */
@Deprecated
HashMap<WebSocket, Participant> getInGame() {
return inGame;
}
/** Package-private getter for mutable activeParticipants HashMap, meant for testing */
@Deprecated
HashMap<SUID, Participant> getActiveParticipants() {
return activeParticipants;
client.socket.close(CloseFrame.NORMAL);
}
}