refactor: switch more classes to singleton, simplify handling greatly
This commit is contained in:
parent
d9b5b3db2f
commit
522558bb16
2
Gamelib
2
Gamelib
@ -1 +1 @@
|
||||
Subproject commit 81cbec5348a444a1d9e394b8c2ae668ac85063a8
|
||||
Subproject commit 7a5d9dca76eca08e33fa78773508fdfa099a9708
|
@ -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");
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package uulm.teamname.marvelous.server.lobbymanager;
|
||||
|
||||
public enum LobbyConnectionState {
|
||||
Waiting,
|
||||
Started,
|
||||
Aborted
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package uulm.teamname.marvelous.server.lobbymanager;
|
||||
|
||||
public enum ParticipantState {
|
||||
Assigned,
|
||||
Selected,
|
||||
Playing
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package uulm.teamname.marvelous.server.netconnector;
|
||||
|
||||
public enum ClientState {
|
||||
Blank,
|
||||
Ready,
|
||||
Assigned,
|
||||
Playing
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user