Compare commits

..

No commits in common. "ac65dd2f29023e9c12e514290a128db308870453" and "server" have entirely different histories.

59 changed files with 2905 additions and 4184 deletions

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -3,15 +3,15 @@ RUN mkdir logs
COPY Server/build/libs/Server.jar Server.jar COPY Server/build/libs/Server.jar Server.jar
COPY configs/testmapwithportals.scenario.json default/testmapwithportals.scenario.json COPY configs/asgard.scenario.json default/asgard.scenario.json
COPY configs/characterConfig.character.json default/characterConfig.character.json COPY configs/marvelheros.character.json default/marvelheroes.character.json
COPY configs/gameConfig.game.json default/gameConfig.game.json COPY configs/matchconfig_1.game.json default/matchconfig.game.json
ARG MMU_LOG_LEVEL=3 ARG MMU_LOG_LEVEL=3
ARG MMU_CONF_MATCH=default/gameConfig.game.json ARG MMU_CONF_MATCH=default/matchconfig.game.json
ARG MMU_CONF_CHARS=default/characterConfig.character.json ARG MMU_CONF_CHARS=default/marvelheroes.character.json
ARG MMU_CONF_SCENARIO=default/testmapwithportals.scenario.json ARG MMU_CONF_SCENARIO=default/asgard.scenario.json
ARG MMU_REPLAY_DIR=/replays ARG MMU_REPLAY_DIR=/replays

View File

@ -80,7 +80,7 @@ sonarqube {
} }
} }
var mainClassName = "uulm.teamname.marvelous.server.ServerApplication" var mainClassName = "uulm.teamname.marvelous.server.Server"
jar { jar {
manifest { manifest {

View File

@ -11,8 +11,7 @@ import uulm.teamname.marvelous.gamelibrary.config.ScenarioConfig;
import uulm.teamname.marvelous.gamelibrary.json.JSON; import uulm.teamname.marvelous.gamelibrary.json.JSON;
import uulm.teamname.marvelous.gamelibrary.json.ValidationUtility; import uulm.teamname.marvelous.gamelibrary.json.ValidationUtility;
import uulm.teamname.marvelous.server.args.ServerArgs; import uulm.teamname.marvelous.server.args.ServerArgs;
import uulm.teamname.marvelous.server.net.MarvelousServer; import uulm.teamname.marvelous.server.netconnector.MarvelousServer;
import uulm.teamname.marvelous.server.net.ServerSession;
import java.io.*; import java.io.*;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@ -26,22 +25,20 @@ import java.util.Map;
* {@code -c .\configs\marvelheros.character.json -m .\configs\matchconfig_1.game.json * {@code -c .\configs\marvelheros.character.json -m .\configs\matchconfig_1.game.json
* -s .\configs\asgard.scenario.json -v} into the arguments field. * -s .\configs\asgard.scenario.json -v} into the arguments field.
*/ */
public class ServerApplication { public class Server {
private static PartyConfig partyConfig; private static PartyConfig partyConfig;
private static ScenarioConfig scenarioConfig; private static ScenarioConfig scenarioConfig;
private static CharacterConfig characterConfig; private static CharacterConfig characterConfig;
private static ServerSession session; private static Integer maxLobbies;
public static void main(String[] args) { public static void main(String[] args) {
Thread.currentThread().setName("Main");
ServerArgs serverArgs = new ServerArgs(); ServerArgs serverArgs = new ServerArgs();
JCommander jc = JCommander.newBuilder().addObject(serverArgs).build(); JCommander jc = JCommander.newBuilder().addObject(serverArgs).build();
try { try {
jc.parse(args); jc.parse(args);
}catch(ParameterException e) { } catch (ParameterException e) {
Logger.error("Invalid parameters: {}", e.getMessage()); Logger.error("Invalid parameters: {}", e.getMessage());
System.exit(1); System.exit(1);
return; return;
@ -53,11 +50,13 @@ public class ServerApplication {
return; return;
} }
if(serverArgs.isVerbose() || serverArgs.isCheckConfig()) { maxLobbies = serverArgs.getMaxLobbies();
if (serverArgs.isVerbose() || serverArgs.isCheckConfig()) {
// If checkConfig, the LogLevel is also set to max, because more information // If checkConfig, the LogLevel is also set to max, because more information
// is exactly what checking the requirements means // is exactly what checking the requirements means
setLogLevel(5); setLogLevel(5);
}else { } else {
setLogLevel(serverArgs.getLogLevel()); setLogLevel(serverArgs.getLogLevel());
} }
@ -73,33 +72,25 @@ public class ServerApplication {
} }
Logger.info("populating static Server variables with config objects"); Logger.info("populating static Server variables with config objects");
ServerApplication.scenarioConfig = scenarioConfig; Server.scenarioConfig = scenarioConfig;
ServerApplication.characterConfig = characterConfig; Server.characterConfig = characterConfig;
ServerApplication.partyConfig = partyConfig; Server.partyConfig = partyConfig;
InetSocketAddress address = new InetSocketAddress(serverArgs.getPort()); InetSocketAddress address = new InetSocketAddress(serverArgs.getPort());
Logger.trace("Inet address {} created", address); Logger.trace("Inet address {} created", address);
Logger.trace("Instantiating ServerSession...");
session = new ServerSession();
Logger.trace("Instantiating MarvelousServer..."); Logger.trace("Instantiating MarvelousServer...");
MarvelousServer server = new MarvelousServer(address); MarvelousServer netConnector = new MarvelousServer(address);
Logger.trace("Starting MarvelousServer..."); Logger.trace("Starting MarvelousServer...");
server.start(); netConnector.start();
Logger.trace("Starting ServerSession...");
getSession().run();
Logger.trace("End of Main reached. Exiting main thread."); Logger.trace("End of Main reached. Exiting main thread.");
} }
/** /** Function that sets the log level for {@link Logger Tinylog}.
* Function that sets the log level for {@link Logger Tinylog}.
* It has to be executed <b>BEFORE ANY LOGGING OPERATIONS</b> . * It has to be executed <b>BEFORE ANY LOGGING OPERATIONS</b> .
*/ */
@SuppressWarnings("DuplicateBranchesInSwitch")
private static void setLogLevel(int logLevel) { private static void setLogLevel(int logLevel) {
Map<String, String> map = new HashMap<>(); Map<String, String> map = new HashMap<>();
@ -153,21 +144,18 @@ public class ServerApplication {
} }
int grassFields = 0; int grassFields = 0;
int portals = 0;
for (FieldType[] row: config.get().scenario) { for (FieldType[] row: config.get().scenario) {
for (FieldType type: row) { for (FieldType type: row) {
if (type == FieldType.GRASS) grassFields++; if (type == FieldType.GRASS) grassFields++;
if (type == FieldType.PORTAL) portals++;
} }
if (grassFields > 18) break;
} }
if (grassFields < 20) { if (grassFields <= 18) {
Logger.error("Scenario Configuration vas invalid: Only {} grass fields found, which is less than 20", grassFields); Logger.error(
System.exit(1); "Scenario Configuration vas invalid: Only {} grass fields found, which is less than 18"
} , grassFields);
if (portals < 2) {
Logger.error("Scenario Configuration vas invalid: Only {} portals found, which is less than 2", portals);
System.exit(1); System.exit(1);
} }
@ -243,7 +231,8 @@ public class ServerApplication {
return characterConfig; return characterConfig;
} }
public static ServerSession getSession() { /** Returns the maximum amount of lobbies the server should create */
return session; public static Integer getMaxLobbies() {
return maxLobbies;
} }
} }

View File

@ -4,7 +4,6 @@ import com.beust.jcommander.Parameter;
import java.io.File; import java.io.File;
@SuppressWarnings({"FieldMayBeFinal", "unused"})
public class ServerArgs { public class ServerArgs {
/** Whether help is requested */ /** Whether help is requested */
@Parameter(names = {"-h", "--help", "-?", "--?", "-H"}, help = true) @Parameter(names = {"-h", "--help", "-?", "--?", "-H"}, help = true)
@ -42,6 +41,9 @@ public class ServerArgs {
@Parameter(names = {"--replay", "-r"}, description = "Path for replay files to be saved at") @Parameter(names = {"--replay", "-r"}, description = "Path for replay files to be saved at")
private String folderPath; private String folderPath;
@Parameter(names = {"--max-lobbies", "-L", "--team25-max-lobbies"}, description = "Maximum amount of lobbies that can be created")
private int maxLobbies = 8;
/** Whether help is requested */ /** Whether help is requested */
public boolean isHelp() { public boolean isHelp() {
return help; return help;
@ -87,6 +89,11 @@ public class ServerArgs {
return folderPath; return folderPath;
} }
/** The maximum mount of lobbies automatically created */
public int getMaxLobbies() {
return maxLobbies;
}
@Override @Override
public String toString() { public String toString() {

View File

@ -1,275 +0,0 @@
package uulm.teamname.marvelous.server.game;
import uulm.teamname.marvelous.gamelibrary.ArrayTools;
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;
import uulm.teamname.marvelous.gamelibrary.events.EventType;
import uulm.teamname.marvelous.gamelibrary.gamelogic.GameInstance;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.messages.server.EventMessage;
import uulm.teamname.marvelous.gamelibrary.messages.server.GameStructureMessage;
import uulm.teamname.marvelous.gamelibrary.messages.server.GeneralAssignmentMessage;
import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.server.ServerApplication;
import uulm.teamname.marvelous.server.game.pipelining.*;
import uulm.teamname.marvelous.server.net.Client;
import uulm.teamname.marvelous.server.net.ClientState;
import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class GameSession {
public final String id = UUID.randomUUID().toString();
private final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(3);
public final HashMap<ParticipantType, CharacterProperties[]> characterChoices = new HashMap<>();
private final HashMap<ParticipantType, Integer[]> characterIndices = new HashMap<>();
private final HashMap<ParticipantType, List<Integer>> characterSelection = new HashMap<>();
private final HashMap<ParticipantType, Integer> badRequests = new HashMap<>();
private ParticipantType turnWaiting;
private ScheduledFuture<?> turnTimer;
private boolean started = false;
private GameInstance instance;
private boolean paused = false;
private Pipeline pipeline;
public GameSession() {
List<CharacterProperties> characters = ArrayTools.toArrayList(ServerApplication.getCharacterConfig().characters);
characterChoices.put(ParticipantType.PlayerOne, new CharacterProperties[characters.size() / 2]);
characterChoices.put(ParticipantType.PlayerTwo, new CharacterProperties[characters.size() / 2]);
characterIndices.put(ParticipantType.PlayerOne, new Integer[characters.size() / 2]);
characterIndices.put(ParticipantType.PlayerTwo, new Integer[characters.size() / 2]);
int n1 = 0;
int n2 = 0;
int i = 0;
Collections.shuffle(characters);
for(CharacterProperties character: characters) {
if(i < characters.size() / 2) {
characterChoices.get(ParticipantType.PlayerOne)[n1] = character;
characterIndices.get(ParticipantType.PlayerOne)[n1] = character.characterID;
n1++;
}else {
characterChoices.get(ParticipantType.PlayerTwo)[n2] = character;
characterIndices.get(ParticipantType.PlayerTwo)[n2] = character.characterID;
n2++;
}
i++;
}
}
public void setPaused(boolean paused) {
this.paused = paused;
}
public boolean getPaused() {
return paused;
}
public GameInstance getInstance() {
return instance;
}
public void addSpectator(Client client) {
GeneralAssignmentMessage response = new GeneralAssignmentMessage();
response.gameID = id;
client.sendMessage(response);
client.sendMessage(getGameStructure(client.getType()));
}
public void handleReconnect(Client client) {
GeneralAssignmentMessage response = new GeneralAssignmentMessage();
response.gameID = id;
client.sendMessage(response);
client.sendMessage(getGameStructure(client.getType()));
}
public void handleSelection(Client client, boolean[] selection) {
ArrayList<Integer> indices = new ArrayList<>();
for(int i = 0; i < selection.length; i++) {
if(selection[i]) {
indices.add(characterIndices.get(client.getType())[i]);
}
}
characterSelection.put(client.getType(), indices);
}
public void handleRequests(Client client, Request[] messages) {
if(client.getType() == ParticipantType.Spectator || paused) {
return;
}
Optional<List<Event>> resultingEvents = pipeline.processRequests(client, messages);
if(resultingEvents.isEmpty()) {
reject(client);
}else {
accept(client, resultingEvents.get());
if(instance.state.isWon()) {
ServerApplication.getSession().reset();
}
}
}
public void handleDisconnect(Client client) {
if(started && client.getType() != ParticipantType.Spectator) {
winner(client.getType() == ParticipantType.PlayerOne ? ParticipantType.PlayerTwo : ParticipantType.PlayerOne);
}
}
public void start() {
initialize();
List<Event> start = instance.startGame(characterSelection.get(ParticipantType.PlayerOne), characterSelection.get(ParticipantType.PlayerTwo));
EventMessage message = new EventMessage();
message.messages = start.toArray(new Event[0]);
ServerApplication.getSession().broadcast(message);
started = true;
turnWaiting = instance.state.getActiveCharacter().type == EntityType.P1 ? ParticipantType.PlayerOne : ParticipantType.PlayerTwo;
turnTimer = scheduler.schedule(this::timeout, ServerApplication.getPartyConfig().maxRoundTime, TimeUnit.SECONDS);
}
private void initialize() {
pipeline = new Pipeline();
var reqSegment = new RequestGameStateSegment(this);
var pauseSegment = new PauseSegment(this);
var filterEndRoundRequestSegment = new FilterEndRoundRequestSegment(this);
var disconnectSegment = new DisconnectSegment();
var playerFilterSegment = new PlayerFilterSegment();
var gameLogicSegment = new GameLogicSegment(this);
pipeline.addSegment(reqSegment)
.addSegment(pauseSegment)
.addSegment(filterEndRoundRequestSegment)
.addSegment(disconnectSegment)
.addSegment(playerFilterSegment)
.addSegment(gameLogicSegment);
instance = new GameInstance(ServerApplication.getPartyConfig(), ServerApplication.getCharacterConfig(), ServerApplication.getScenarioConfig());
for(Client player: ServerApplication.getSession().getPlayers()) {
player.setState(ClientState.Playing);
player.sendMessage(getGameStructure(player.getType()));
}
GameStructureMessage spectators = getGameStructure(ParticipantType.Spectator);
for(Client spectator: ServerApplication.getSession().getSpectators()) {
spectator.setState(ClientState.Playing);
spectator.sendMessage(spectators);
}
}
private void accept(Client source, List<Event> accepted) {
if(accepted.isEmpty()) {
return;
}
accepted.add(instance.getGameStateEvent());
EventMessage message = new EventMessage();
message.messages = accepted.toArray(new Event[0]);
ServerApplication.getSession().broadcast(message, source.getID());
accepted.add(0, new EventBuilder(EventType.Ack).buildGameEvent());
EventMessage response = new EventMessage();
response.messages = accepted.toArray(new Event[0]);
source.sendMessage(response);
badRequests.put(source.getType(), 0);
if(turnTimer != null) {
turnTimer.cancel(true);
}
turnWaiting = source.getType();
turnTimer = scheduler.schedule(this::timeout, ServerApplication.getPartyConfig().maxRoundTime, TimeUnit.SECONDS);
}
private void reject(Client source) {
EventMessage response = new EventMessage();
response.messages = new Event[]{
new EventBuilder(EventType.Nack).buildGameEvent(),
instance.getGameStateEvent()
};
source.sendMessage(response);
int bad = badRequests.getOrDefault(source.getType(), 0) + 1;
badRequests.put(source.getType(), bad);
if(bad >= 5) {
winner(source.getType() == ParticipantType.PlayerOne ? ParticipantType.PlayerTwo : ParticipantType.PlayerOne);
}
}
private synchronized void winner(ParticipantType winner) {
EventMessage message = new EventMessage();
message.messages = new Event[] {
new EventBuilder(EventType.WinEvent)
.withPlayerWon(winner.equals(ParticipantType.PlayerOne) ? 1 : 2)
.buildGameEvent(),
new EventBuilder(EventType.DisconnectEvent)
.buildGameEvent()
};
ServerApplication.getSession().broadcast(message);
ServerApplication.getSession().reset();
}
private void timeout() {
for(Client player: ServerApplication.getSession().getPlayers()) {
if(player != null && player.getType() != turnWaiting) {
EventMessage notification = new EventMessage();
notification.messages = new Event[] { new EventBuilder(EventType.TurnTimeoutEvent).buildGameEvent() };
player.sendMessage(notification);
break;
}
}
EventMessage message = new EventMessage();
message.messages = instance.endTurn().toArray(new Event[0]);
ServerApplication.getSession().broadcast(message);
}
private GameStructureMessage getGameStructure(ParticipantType assignment) {
GameStructureMessage message = new GameStructureMessage();
message.assignment = assignment;
message.matchconfig = ServerApplication.getPartyConfig();
message.scenarioconfig = ServerApplication.getScenarioConfig();
var characters = ServerApplication.getCharacterConfig().getIDMap();
for(Client player: ServerApplication.getSession().getPlayers()) {
int n = 0;
CharacterProperties[] selected = new CharacterProperties[characters.size() / 4];
for(Integer i: characterSelection.get(player.getType())) {
selected[n++] = characters.get(i);
}
if(player.getType() == ParticipantType.PlayerOne) {
message.playerOneName = player.getID().getName();
message.playerOneCharacters = selected;
}else {
message.playerTwoName = player.getID().getName();
message.playerTwoCharacters = selected;
}
}
return message;
}
}

View File

@ -1,23 +0,0 @@
package uulm.teamname.marvelous.server.game.pipelining;
import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.game.GameSession;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* The {@link DisconnectSegment} handles requests of {@link RequestType} DisconnectRequest.
*/
public class DisconnectSegment implements Segment {
@Override
public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) {
Logger.trace("DisconnectSegment received {} requests.", packet.size());
if(packet.containsRequestOfType(RequestType.DisconnectRequest)) {
packet.getOrigin().disconnect();
packet.clear();
}
}
}

View File

@ -1,41 +0,0 @@
package uulm.teamname.marvelous.server.game.pipelining;
import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.entities.EntityType;
import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.gamelogic.GameInstance;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.game.GameSession;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* {@link Segment} that checks for an {@link RequestType#EndRoundRequest}, and
* if it exists, checks for the player that has sent it. If the player sending
* this request is not the active player, it flags the message as an error.
*/
public class FilterEndRoundRequestSegment implements Segment {
private final GameSession parent;
public FilterEndRoundRequestSegment(GameSession parent) {
this.parent = parent;
}
@Override
public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) {
Logger.trace("FilterEndRoundSegment has received {} requests", packet.size());
if(packet.containsRequestOfType(RequestType.EndRoundRequest)) {
Logger.trace("Packet contains EndRoundRequest");
var active = parent.getInstance().state.getActiveCharacter().type;
var from = packet.getOrigin().getType() == ParticipantType.PlayerOne ? EntityType.P1 : EntityType.P2;
if(active != from) {
Logger.trace("Invalid endRoundRequest. Expected {} but got {}. Aborting...", active, from);
abort.set(true);
}
}
}
}

View File

@ -0,0 +1,30 @@
package uulm.teamname.marvelous.server.lobby;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
/**
* A timer meant to time the entire Lifetime of the lobby, and if it is higher than the maximum permitted value,
* call a callback given by the lobby to compute a winner, and quit the lobby.
*/
public class LifetimeTimer {
private final ScheduledExecutorService timer;
private final int maxGameTime;
private final Runnable callback;
LifetimeTimer(int maxGameTime, Runnable callback) {
String lobbyThreadName = Thread.currentThread().getName();
ThreadFactory threadFactory = r -> new Thread(r, lobbyThreadName + "-LifetimeTimerThread");
this.timer = Executors.newSingleThreadScheduledExecutor(threadFactory);
this.maxGameTime = maxGameTime;
this.callback = callback;
}
void startTimer() {
timer.schedule(callback, maxGameTime, TimeUnit.SECONDS);
}
}

View File

@ -0,0 +1,364 @@
package uulm.teamname.marvelous.server.lobby;
import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.entities.EntityID;
import uulm.teamname.marvelous.gamelibrary.entities.EntityType;
import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.events.EventBuilder;
import uulm.teamname.marvelous.gamelibrary.events.EventType;
import uulm.teamname.marvelous.gamelibrary.gamelogic.GameInstance;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.config.CharacterConfig;
import uulm.teamname.marvelous.gamelibrary.config.PartyConfig;
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.*;
public class Lobby {
private final String gameID;
private final LobbyConnection connection;
private final GameInstance game;
private final Pipeline pipeline;
private Participant activePlayer;
private int badRequests;
private PauseSegment pauseSegment;
private final TurnTimeoutTimer turnTimeoutTimer;
private final TimeoutTimer timeoutTimer;
private final LifetimeTimer lifetimeTimer;
/**
* The {@link Lobby} is where the magic happens. In this class is a whole {@link GameInstance game} is processed. To
* initialize the game it gets the following parameters
*
* @param gameID a String to identify the game
* @param connection the Connection to the {@link LobbyManager}
* @param partyConfig declared in Editor
* @param characterConfig declared in Editor
* @param scenarioConfig declared in Editor
*/
public Lobby(
String gameID,
LobbyConnection connection,
PartyConfig partyConfig,
CharacterConfig characterConfig,
ScenarioConfig scenarioConfig,
List<Integer> player1Characters,
List<Integer> player2Characters
) {
this.gameID = gameID;
this.connection = connection;
this.game = new GameInstance(
partyConfig,
characterConfig,
scenarioConfig
);
this.pipeline = new Pipeline();
var reqSegment = new RequestGameStateSegment(this.game);
this.pauseSegment = new PauseSegment();
var filterEndRoundRequestSegment = new FilterEndRoundRequestSegment(game.state::getActiveCharacter);
var disconnectSegment = new DisconnectSegment(this);
var playerFilterSegment = new PlayerFilterSegment();
var gameLogicSegment = new GameLogicSegment(this.game);
pipeline.addSegment(reqSegment)
.addSegment(pauseSegment)
.addSegment(filterEndRoundRequestSegment)
.addSegment(disconnectSegment)
.addSegment(playerFilterSegment)
.addSegment(gameLogicSegment);
Logger.trace("Instantiating timers...");
this.turnTimeoutTimer = new TurnTimeoutTimer(partyConfig.maxRoundTime, this::turnTimeout);
updateTurnTimer();
this.timeoutTimer = new TimeoutTimer(partyConfig.maxResponseTime, this::soonTimeout, this::timeout);
refreshTimeoutTimer(connection.getPlayer1());
refreshTimeoutTimer(connection.getPlayer2());
this.lifetimeTimer = new LifetimeTimer(
partyConfig.maxGameTime,
() -> triggerWin(getParticipantForEntityType(game.state.getCurrentOvertimeWinner()).get()));
this.connection.broadcastEvents(this.game.startGame(player1Characters, player2Characters));
}
/**
* Called by {@link LobbyConnection} to handle requests
*
* @param requests to be processed
* @param source the player executing the requests
*/
public synchronized void receiveRequests(Request[] requests, Participant source) {
Logger.trace("Received {} requests from participant '{}' of type {}",
requests.length,
source.id.getName(),
source.type);
refreshTimeoutTimer(source);
if (activePlayer != source && source.type != ParticipantType.Spectator) {
Logger.trace("Resetting bad requests as new participant sent data");
activePlayer = source;
badRequests = 0;
}
Logger.info("got {} requests from participant {}",
requests.length,
source.id.getName());
Logger.trace("Processing requests through pipeline");
Optional<List<Event>> resultingEvents = pipeline.processRequests(requests, source);
Logger.debug("generated {} events from the pipeline", resultingEvents.map(List::size).orElse(0));
//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.id.getName());
reject(source);
} else {
if (game.state.isWon()) { // If game was won in the current turn
Logger.info("Game is won, terminating lobby");
var events = resultingEvents.get();
events.add(new EventBuilder(EventType.DisconnectEvent).buildGameEvent());
accept(source, events);
connection.terminate();
return;
} else { // If not (normally)
accept(source, resultingEvents.get());
}
}
updateTurnTimer();
}
/**
* Called by {@link LobbyConnection} when a client disconnects
*
* @param source the player disconnecting
*/
public synchronized void handleDisconnect(Participant source) {
}
/**
* Called by {@link LobbyConnection} when a client reconnects
*
* @param source the player reconnecting
*/
public synchronized void handleReconnect(Participant source) {
}
/**
* This method is called at the end of receiveRequests, to start a timer. The active player has now a specific
* amount of time to do his moves.
*/
void updateTurnTimer() {
var currentlyActiveParticipant =
getParticipantForEntityType(game.state.getActiveCharacter());
Logger.trace("Updating turnTimer...");
if (pauseSegment.isPaused()) {
Logger.trace("Game is paused, clearing turnTimer");
turnTimeoutTimer.clear();
} else if (currentlyActiveParticipant.isPresent()) {
var participant = currentlyActiveParticipant.get();
Logger.trace("Scheduling turnTimer for Player1");
turnTimeoutTimer.startTurnTimer(participant);
} else {
Logger.trace("Currently active participant was NPC, clearing TurnTimer");
turnTimeoutTimer.clear();
}
}
/**
* Returns an {@link Optional} of the {@link Participant} for the given {@link EntityType}, or an empty {@link
* Optional} if it was an NPC
*/
Optional<Participant> getParticipantForEntityType(EntityID entityID) {
if (entityID == null) {
Logger.trace("cannot get participant for empty EntityType, returning empty Optional");
return Optional.empty();
} else {
return getParticipantForEntityType(entityID.type);
}
}
Optional<Participant> getParticipantForEntityType(EntityType type) {
if (type == EntityType.P1) {
return Optional.ofNullable(connection.getPlayer1());
} else if (type == EntityType.P2) {
return Optional.ofNullable(connection.getPlayer2());
} else {
return Optional.empty();
}
}
/** Method meant for updating a TurnTimer. Said TurnTimer will be refreshed with the given participant. */
void refreshTimeoutTimer(Participant participant) {
if (participant.type == ParticipantType.Spectator) {
Logger.trace("Tried to refresh timeoutTimer for Spectator, ignoring...");
} else {
Logger.debug("Refreshing timeoutTimer for Participant '{}'", participant.id.getName());
timeoutTimer.refreshTimer(participant);
}
}
private void accept(Participant source, List<Event> accepted) {
if (!accepted.isEmpty()) {
Logger.debug("Accepting requests from participant '{}', broadcasting events to all except source",
source.id.getName());
accepted.add(game.getGameStateEvent());
connection.broadcastToAllExcept(source, accepted.toArray(new Event[0]));
Logger.trace("Adding ack and sending back to originParticipant");
accepted.add(0, new EventBuilder(EventType.Ack).buildGameEvent());
connection.sendEvents(source, accepted.toArray(new Event[0]));
}
badRequests = 0;
}
/**
* If the player executed a false request the request gets rejected.
*
* @param source the executing player
*/
private void reject(Participant source) {
connection.sendEvents(source, new EventBuilder(EventType.Nack).buildGameEvent(), game.getGameStateEvent());
badRequests++;
//if the player sends 2 bad messages after one another, the player gets kicked out of the lobby.
if (badRequests >= 5) {
Logger.info("Participant '{}' has sent too many bad requests, disconnecting...", source.id.getName());
connection.removeParticipant(source);
if (connection.hasPlayer1()) {
Logger.debug("Triggering win for Player 1");
triggerWin(connection.getPlayer1());
} else if (connection.hasPlayer2()) {
Logger.debug("Triggering win for Player 2");
triggerWin(connection.getPlayer2());
}
}
}
/**
* Warns the player he get timeouted soon.
*
* @param source soon to be timeouted player
*/
public synchronized void soonTimeout(Participant source, int timeLeft) {
connection.sendEvents(
source,
new EventBuilder(EventType.TimeoutWarningEvent)
.withTimeLeft(timeLeft)
.withMessage("If you do not send a message soon, you will be time-outed")
.buildGameEvent());
}
/**
* If a player times out the other player automatically wins.
*
* @param source is the timeouted player
*/
public synchronized void timeout(Participant source) {
connection.sendEvents(
source,
new EventBuilder(EventType.TimeoutEvent).buildGameEvent());
connection.removeParticipant(source);
if (connection.hasPlayer1() && !connection.hasPlayer2()) {
triggerWin(connection.getPlayer1());
} else if (!connection.hasPlayer1() && connection.hasPlayer2()) {
triggerWin(connection.getPlayer2());
} else {
throw new IllegalStateException("Spectator was time-outed which is impossible");
}
}
/** Skips the current turn, and starts a new one. Exclusively called in the {@link TurnTimeoutTimer}. */
private synchronized void turnTimeout(Participant source) {
var nextTurnEvents = game.endTurn();
nextTurnEvents.add(game.getGameStateEvent());
connection.broadcastEvents(nextTurnEvents.toArray(new Event[0]));
updateTurnTimer();
}
/**
* The method triggers a winEvent for a {@link Participant}, and broadcasts said event. Afterwards it sends a
* DisconnectRequest to everyone, and terminates the connection.
*
* @param winner is the {@link Participant} that won
*/
public synchronized void triggerWin(Participant winner) {
Logger.debug("Triggering win. Building events and broadcasting for winner of type '{}'", winner);
connection.broadcastEvents(
new EventBuilder(EventType.WinEvent)
.withPlayerWon(winner.type.equals(ParticipantType.PlayerOne) ? 1 : 2)
.buildGameEvent(),
new EventBuilder(EventType.DisconnectEvent)
.buildGameEvent());
connection.terminate();
}
public PauseSegment getPauseSegment() {
return pauseSegment;
}
public String getGameID() {
return gameID;
}
public LobbyConnection getConnection() {
return connection;
}
public GameInstance getGame() {
return game;
}
public Pipeline getPipeline() {
return pipeline;
}
public Participant getActivePlayer() {
return activePlayer;
}
// Note: DO NOT ADD the connection to the equals and hashcode here, otherwise they recursively call each other
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Lobby lobby = (Lobby) o;
return badRequests == lobby.badRequests && Objects.equals(gameID, lobby.gameID) && Objects.equals(game, lobby.game) && Objects.equals(pipeline, lobby.pipeline) && Objects.equals(activePlayer, lobby.activePlayer) && Objects.equals(pauseSegment, lobby.pauseSegment) && Objects.equals(turnTimeoutTimer, lobby.turnTimeoutTimer);
}
@Override
public int hashCode() {
return Objects.hash(gameID, game, pipeline, activePlayer, badRequests, pauseSegment, turnTimeoutTimer);
}
@Override
public String toString() {
return "Lobby{" +
"gameID='" + gameID + '\'' +
", game=" + game +
", pipeline=" + pipeline +
", activePlayer=" + activePlayer +
", badRequests=" + badRequests +
", pauseSegment=" + pauseSegment +
", turnTimer=" + turnTimeoutTimer +
'}';
}
}

View File

@ -0,0 +1,91 @@
package uulm.teamname.marvelous.server.lobby;
import org.tinylog.Logger;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import java.util.concurrent.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class TimeoutTimer {
private final ScheduledExecutorService timer;
private ScheduledFuture<Participant> player1AlmostTimeout;
private ScheduledFuture<Participant> player1Timeout;
private ScheduledFuture<Participant> player2AlmostTimeout;
private ScheduledFuture<Participant> player2Timeout;
private final BiConsumer<Participant, Integer> almostTimeoutCallback;
private final Consumer<Participant> timeoutCallback;
private final int almostTimeoutTime;
private final int timeoutTime;
/**
* Class that manages timeouts of players after not sending a message for a long time.
*
* @param timeoutTime is the time that a player has to send any message
* @param almostTimeoutCallback is the callback that is called if the timeoutTime is almost over
* @param timeoutCallback is the callback that is called when the timeoutTime is over
*/
public TimeoutTimer(int timeoutTime,
BiConsumer<Participant, Integer> almostTimeoutCallback,
Consumer<Participant> timeoutCallback) {
this.almostTimeoutCallback = almostTimeoutCallback;
this.timeoutCallback = timeoutCallback;
this.timeoutTime = timeoutTime;
this.almostTimeoutTime = Math.max(timeoutTime - 15, Math.min(timeoutTime, 5));
String lobbyThreadName = Thread.currentThread().getName();
ThreadFactory threadFactory = r -> new Thread(r, lobbyThreadName + "-timeoutTimer");
timer = Executors.newSingleThreadScheduledExecutor(threadFactory);
}
/** Refreshes the timeout timer for the given Participant. This is meant to be used for Players, not Spectators. */
public void refreshTimer(Participant participant) {
Logger.debug("Refreshing turnTimer for participant of type '{}'", participant.type);
switch (participant.type) {
case PlayerOne -> {
Logger.trace("Was playerOne, refreshing...");
if (player1AlmostTimeout != null) player1AlmostTimeout.cancel(false);
player1AlmostTimeout =
timer.schedule(() -> {
almostTimeoutCallback.accept(participant, timeoutTime - almostTimeoutTime);
return participant;
}, this.almostTimeoutTime, TimeUnit.SECONDS);
if (player1Timeout != null) player1Timeout.cancel(false);
player1Timeout =
timer.schedule(() -> {
timeoutCallback.accept(participant);
return participant;
}, this.timeoutTime, TimeUnit.SECONDS);
}
case PlayerTwo -> {
Logger.trace("Was playerOne, refreshing...");
if (player2AlmostTimeout != null) player2AlmostTimeout.cancel(false);
player2AlmostTimeout =
timer.schedule(() -> {
almostTimeoutCallback.accept(participant, timeoutTime - almostTimeoutTime);
return participant;
}, this.almostTimeoutTime, TimeUnit.SECONDS);
if (player2Timeout != null) player2Timeout.cancel(false);
player2Timeout =
timer.schedule(() -> {
timeoutCallback.accept(participant);
return participant;
}, this.timeoutTime, TimeUnit.SECONDS);
}
case Spectator -> Logger.warn("Timeout called on spectator '{}'. This is probably a bug.",
participant.id.getName());
}
}
}

View File

@ -0,0 +1,56 @@
package uulm.teamname.marvelous.server.lobby;
import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import java.util.concurrent.*;
import java.util.function.Consumer;
/**
* The {@link TurnTimeoutTimer} class is called by the {@link Lobby} to limit the amount of time a player has per round.
*/
public class TurnTimeoutTimer {
private final ScheduledExecutorService timer;
private final Consumer<Participant> callback;
private final int maxRoundTime;
private ScheduledFuture<Participant> current;
public TurnTimeoutTimer(int maxRoundTime, Consumer<Participant> callback) {
String lobbyThreadName = Thread.currentThread().getName();
ThreadFactory threadFactory = r -> new Thread(r, lobbyThreadName + "-TurnTimerThread");
this.timer = Executors.newSingleThreadScheduledExecutor(threadFactory);
this.maxRoundTime = maxRoundTime;
this.callback = callback;
}
/**
* This method checks if the participant is not a spectator. Otherwise it won't start a timer.
*
* @param participant the timer is for
*/
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.id.getName(), participant.type);
current = timer.schedule(() -> {
callback.accept(participant);
return participant;
},
maxRoundTime,
TimeUnit.SECONDS);
}
/** cancels all currently running timers */
public void clear() {
Logger.trace("Clearing timer");
if (this.current != null) {
current.cancel(true);
}
}
}

View File

@ -0,0 +1,41 @@
package uulm.teamname.marvelous.server.lobby.pipelining;
import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.lobby.Lobby;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* The {@link DisconnectSegment} handles requests of {@link RequestType} DisconnectRequest. Therefore it removes the
* disconnecting player-connection from the {@link Lobby}.
*/
public class DisconnectSegment implements Segment {
private final Lobby parent;
public DisconnectSegment(Lobby parent) {
this.parent = parent;
}
@Override
public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) {
Logger.trace("DisconnectSegment received {} requests.", packet.size());
if (packet.containsRequestOfType(RequestType.DisconnectRequest)) {
Logger.debug("Player of Type {} sent DisconnectRequest", packet.getOrigin().type);
parent.getConnection().removeParticipant(packet.getOrigin());
if (packet.getOrigin().type != ParticipantType.Spectator) {
if (parent.getConnection().hasPlayer1()) {
parent.triggerWin(parent.getConnection().getPlayer1());
} else if (parent.getConnection().hasPlayer2()) {
parent.triggerWin(parent.getConnection().getPlayer2());
}
}
packet.clear();
}
}
}

View File

@ -0,0 +1,54 @@
package uulm.teamname.marvelous.server.lobby.pipelining;
import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.entities.EntityID;
import uulm.teamname.marvelous.gamelibrary.entities.EntityType;
import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.gamelogic.GameStateView;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import uulm.teamname.marvelous.server.lobby.Lobby;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
/**
* {@link Segment} that checks for an {@link RequestType#EndRoundRequest}, and
* if it exists, checks for the player that has sent it. If the player sending
* this request is not the active player, it flags the message as an error.
*/
public class FilterEndRoundRequestSegment implements Segment {
private final Supplier<EntityID> getActiveCharacterCallback;
/**
* Creates a new {@link FilterEndRoundRequestSegment}
* @param getActiveCharacterCallback is a {@link Supplier}<{@link Participant}>
* that supplies the currently active participant
* in a {@link Lobby}. It should normally be bound
* to {@link GameStateView#getActiveCharacter()}, by executing
* {@code new Filter...Segment(game.state::getActiveCharacter)}.
*/
public FilterEndRoundRequestSegment(Supplier<EntityID> getActiveCharacterCallback) {
this.getActiveCharacterCallback = getActiveCharacterCallback;
}
@Override
public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) {
Logger.trace("FilterEndRoundSegment has received {} requests", packet.size());
if (packet.containsRequestOfType(RequestType.EndRoundRequest)) {
Logger.trace("Packet contains EndRoundRequest");
var active = getActiveCharacterCallback.get().type;
var from = packet.getOrigin().type == ParticipantType.PlayerOne ? EntityType.P1 : EntityType.P2;
if (packet.containsRequestOfType(RequestType.EndRoundRequest) && active != from) {
Logger.trace("Invalid endRoundRequest. Expected {} but got {}. Aborting...", active, from);
abort.set(true);
}
}
}
}

View File

@ -1,36 +1,35 @@
package uulm.teamname.marvelous.server.game.pipelining; package uulm.teamname.marvelous.server.lobby.pipelining;
import org.tinylog.Logger; import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.events.Event; import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.gamelogic.GameInstance; import uulm.teamname.marvelous.gamelibrary.gamelogic.GameInstance;
import uulm.teamname.marvelous.gamelibrary.requests.Request; import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.server.game.GameSession;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* The {@link GameLogicSegment} handles all {@link GameInstance game} relevant {@link Request Requests}. * The {@link GameLogicSegment} handles all {@link GameInstance game} relevant {@link Request Requests}. Therefore it
* updates the game with the matching request.
*/ */
public class GameLogicSegment implements Segment { public class GameLogicSegment implements Segment {
private GameInstance game;
private final GameSession parent;
public GameLogicSegment(GameSession parent) { public GameLogicSegment(GameInstance game) {
this.parent = parent; this.game = game;
} }
@Override @Override
public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) { public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) {
Logger.trace("GameStateSegment received {} requests.", packet.size()); Logger.trace("GameStateSegment received {} requests.", packet.size());
var result = game.checkRequestsAndApply(packet);
var result = parent.getInstance().checkRequestsAndApply(packet);
Logger.trace("GameLogic generated {} events", result.map(List::size).orElse(0)); Logger.trace("GameLogic generated {} events", result.map(List::size).orElse(0));
if(result.isPresent()) { if (result.isPresent()) {
Logger.trace("Result from GameLogic is present. Adding requests to carrier."); Logger.trace("Result from GameLogic is present. Adding requests to carrier.");
carrier.addAll(result.get()); carrier.addAll(result.get());
packet.clear(); packet.clear();
}else { } else {
Logger.debug("Result from GameLogic is invalid. Triggering error."); Logger.debug("Result from GameLogic is invalid. Triggering error.");
abort.set(true); abort.set(true);
} }

View File

@ -1,20 +1,20 @@
package uulm.teamname.marvelous.server.game.pipelining; package uulm.teamname.marvelous.server.lobby.pipelining;
import uulm.teamname.marvelous.gamelibrary.requests.Request; import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType; import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.net.Client; import uulm.teamname.marvelous.server.lobbymanager.Participant;
import java.util.*; import java.util.*;
/** /**
* The {@link Packet} contains all {@link Request Requests} and the belonging {@link Client}. You can add and * The {@link Packet} contains all {@link Request Requests} and the belonging {@link Participant}. You can add and
* remove {@link Request Requests} at will. * remove {@link Request Requests} at will.
*/ */
public class Packet extends ArrayList<Request> { public class Packet extends ArrayList<Request> {
private final Client origin; private final Participant origin;
public Packet(Request[] requests, Client origin) { public Packet(Request[] requests, Participant origin) {
this.origin = origin; this.origin = origin;
addAll(Arrays.asList(requests)); addAll(Arrays.asList(requests));
} }
@ -55,7 +55,7 @@ public class Packet extends ArrayList<Request> {
this.removeIf(request -> !listOfTypes.contains(request.type)); this.removeIf(request -> !listOfTypes.contains(request.type));
} }
public Client getOrigin() { public Participant getOrigin() {
return origin; return origin;
} }
@ -69,7 +69,6 @@ public class Packet extends ArrayList<Request> {
} }
@Override @Override
@SuppressWarnings("MethodDoesntCallSuperMethod")
public Object clone() { public Object clone() {
return new Packet(this.toArray(new Request[0]), this.origin); return new Packet(this.toArray(new Request[0]), this.origin);
} }

View File

@ -1,4 +1,4 @@
package uulm.teamname.marvelous.server.game.pipelining; package uulm.teamname.marvelous.server.lobby.pipelining;
import org.tinylog.Logger; import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.events.Event; import uulm.teamname.marvelous.gamelibrary.events.Event;
@ -7,7 +7,6 @@ import uulm.teamname.marvelous.gamelibrary.events.EventType;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType; import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder; import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType; import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.game.GameSession;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -17,12 +16,35 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/ */
public class PauseSegment implements Segment { public class PauseSegment implements Segment {
private final GameSession parent; private boolean paused;
public PauseSegment(GameSession parent) { public PauseSegment() {
this.parent = parent; paused = false;
} }
/** This method pauses the game if it is not already paused.*/
public void pauseGame() {
if (!paused) {
paused = true;
Logger.debug("Game paused.");
}
}
/**
* This method ends a paused game.
*/
public void pauseEnd() {
if (paused) {
paused = false;
Logger.debug("Game unpaused.");
}
}
public boolean isPaused() {
return paused;
}
/** /**
* Pipelining method to process a set of requests. The list of requests will be processed according to the following * Pipelining method to process a set of requests. The list of requests will be processed according to the following
* rules: * rules:
@ -46,46 +68,47 @@ public class PauseSegment implements Segment {
public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) { public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) {
Logger.trace("PauseSegment received {} requests. PausedState is {}", Logger.trace("PauseSegment received {} requests. PausedState is {}",
packet.size(), packet.size(),
parent.getPaused()); paused);
// check if there is a pause request (either start or stop) // check if there is a pause request (either start or stop)
if(packet.contains(new RequestBuilder(RequestType.PauseStartRequest).buildGameRequest())) { if (packet.contains(new RequestBuilder(RequestType.PauseStartRequest).buildGameRequest())) {
Logger.trace("PauseStartRequest found"); Logger.trace("PauseStartRequest found");
if(packet.getOrigin().getType() == ParticipantType.Spectator || packet.getOrigin().isAI()) { if(packet.getOrigin().type == ParticipantType.Spectator || packet.getOrigin().isAI) {
Logger.trace("Invalid pause start request. Aborting"); Logger.trace("Invalid pause start request. Aborting");
abort.set(true); abort.set(true);
return; return;
} }
if(!parent.getPaused()) { if (!paused) {
parent.setPaused(true); // pause the game
pauseGame();
// create a new PauseStartEvent // create a new PauseStartEvent
carrier.add(new EventBuilder(EventType.PauseStartEvent).buildGameEvent()); carrier.add(new EventBuilder(EventType.PauseStartEvent).buildGameEvent());
Logger.trace("Added PauseStartEvent to pipeline carrier"); Logger.trace("Added PauseStartEvent to pipeline carrier");
}else { // if the game is already paused } else { // if the game is already paused
Logger.info("PauseStartRequest sent even though the game wasn't paused. Error triggered."); Logger.info("PauseStartRequest sent even though the game wasn't paused. Error triggered.");
abort.set(true); abort.set(true);
return; return;
} }
}else if(packet.contains(new RequestBuilder(RequestType.PauseStopRequest).buildGameRequest())) { } else if (packet.contains(new RequestBuilder(RequestType.PauseStopRequest).buildGameRequest())) {
Logger.trace("PauseStopRequest found"); Logger.trace("PauseStopRequest found");
if(packet.getOrigin().getType() == ParticipantType.Spectator || packet.getOrigin().isAI()) { if(packet.getOrigin().type == ParticipantType.Spectator || packet.getOrigin().isAI) {
Logger.trace("Invalid pause stop request. Aborting"); Logger.trace("Invalid pause stop request. Aborting");
abort.set(true); abort.set(true);
return; return;
} }
if(parent.getPaused()) { if (paused) {
parent.setPaused(false); pauseEnd();
Logger.debug("Game unpaused.");
// create a new PauseStartRequest // create a new PauseStartRequest
carrier.add(new EventBuilder(EventType.PauseStopEvent).buildGameEvent()); carrier.add(new EventBuilder(EventType.PauseStopEvent).buildGameEvent());
Logger.trace("Added PauseStopEvent to pipeline carrier"); Logger.trace("Added PauseStopEvent to pipeline carrier");
}else { // if the game is not paused } else { // if the game is not paused
Logger.info("PauseStopRequest sent even though the game wasn't paused. Error triggered."); Logger.info("PauseStopRequest sent even though the game wasn't paused. Error triggered.");
abort.set(true); abort.set(true);
return; return;
} }
} }
if(parent.getPaused()) { if (paused) {
Logger.trace("As the game is paused, Requests are removed."); Logger.trace("As the game is paused, Requests are removed.");
packet.removeRequestsOfTypes( packet.removeRequestsOfTypes(
RequestType.MeleeAttackRequest, RequestType.MeleeAttackRequest,

View File

@ -1,9 +1,9 @@
package uulm.teamname.marvelous.server.game.pipelining; package uulm.teamname.marvelous.server.lobby.pipelining;
import org.tinylog.Logger; import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.events.Event; import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.requests.Request; import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.server.net.Client; import uulm.teamname.marvelous.server.lobbymanager.Participant;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -37,7 +37,7 @@ public class Pipeline {
* first check whether the {@link Optional} is empty by doing {@link Optional#isEmpty()} or {@link * first check whether the {@link Optional} is empty by doing {@link Optional#isEmpty()} or {@link
* Optional#isPresent()}, and act accordingly. * Optional#isPresent()}, and act accordingly.
*/ */
public Optional<List<Event>> processRequests(Client origin, Request[] requests) { public Optional<List<Event>> processRequests(Request[] requests, Participant origin) {
Logger.trace("Pipeline started RequestProcessing"); Logger.trace("Pipeline started RequestProcessing");
// The packet carries the requests, and gets smaller per segment // The packet carries the requests, and gets smaller per segment
Packet packet = new Packet(requests, origin); Packet packet = new Packet(requests, origin);

View File

@ -1,11 +1,11 @@
package uulm.teamname.marvelous.server.game.pipelining; package uulm.teamname.marvelous.server.lobby.pipelining;
import org.tinylog.Logger; import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.entities.EntityType; import uulm.teamname.marvelous.gamelibrary.entities.EntityType;
import uulm.teamname.marvelous.gamelibrary.events.Event; import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.requests.CharacterRequest; import uulm.teamname.marvelous.gamelibrary.requests.CharacterRequest;
import uulm.teamname.marvelous.gamelibrary.requests.Request; import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.server.net.Client; import uulm.teamname.marvelous.server.lobbymanager.Participant;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -14,9 +14,8 @@ public class PlayerFilterSegment implements Segment {
@Override @Override
public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) { public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) {
Logger.trace("PlayerFilterSegment received {} requests", packet.size()); Logger.trace("PlayerFilterSegment received {} requests", packet.size());
for (Request request: packet) {
for(Request request: packet) { boolean valid = switch (request.type) {
boolean valid = switch(request.type) {
case MeleeAttackRequest, case MeleeAttackRequest,
RangedAttackRequest, RangedAttackRequest,
MoveRequest, MoveRequest,
@ -24,7 +23,7 @@ public class PlayerFilterSegment implements Segment {
UseInfinityStoneRequest -> isValid((CharacterRequest) request, packet.getOrigin()); UseInfinityStoneRequest -> isValid((CharacterRequest) request, packet.getOrigin());
default -> true; default -> true;
}; };
if(!valid) { if (!valid) {
Logger.debug("Invalid request of type {} of enemy player found, setting abort flag", request.type); Logger.debug("Invalid request of type {} of enemy player found, setting abort flag", request.type);
abort.set(true); abort.set(true);
break; break;
@ -32,17 +31,21 @@ public class PlayerFilterSegment implements Segment {
} }
} }
private boolean isValid(CharacterRequest request, Client client) { private boolean isValid(CharacterRequest request, Participant participant) {
EntityType type = switch(client.getType()) { EntityType type = switch(participant.type) {
case PlayerOne -> EntityType.P1; case PlayerOne -> EntityType.P1;
case PlayerTwo -> EntityType.P2; case PlayerTwo -> EntityType.P2;
case Spectator -> null; case Spectator -> null;
}; };
if(type == null) { if (type == null) {
Logger.warn("Some spectator-sent movement requests arrived in the PlayerFilterSegment.\n" +
"Have you ordered your segments properly?");
return false; return false;
}else { } else {
return request.originEntity.type.equals(type); return request.originEntity.type.equals(type);
} }
} }
} }

View File

@ -1,36 +1,35 @@
package uulm.teamname.marvelous.server.game.pipelining; package uulm.teamname.marvelous.server.lobby.pipelining;
import org.tinylog.Logger; import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.events.Event; import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.gamelogic.GameInstance; import uulm.teamname.marvelous.gamelibrary.gamelogic.GameInstance;
import uulm.teamname.marvelous.gamelibrary.messages.server.EventMessage; import uulm.teamname.marvelous.gamelibrary.messages.server.EventMessage;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType; import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.game.GameSession;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* The {@link RequestGameStateSegment} handles requests of {@link RequestType} Req. * The {@link RequestGameStateSegment} handles requests of {@link RequestType} Req. Therefore it sends the active
* gamestate and clears the {@link Packet} afterwards.
*/ */
public class RequestGameStateSegment implements Segment { public class RequestGameStateSegment implements Segment {
private final GameSession parent; private final GameInstance game;
public RequestGameStateSegment(GameSession parent) { public RequestGameStateSegment(GameInstance game) {
this.parent = parent; this.game = game;
} }
@Override @Override
public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) { public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort) {
Logger.trace("RequestGameStateSegment received {} requests", packet.size()); Logger.trace("RequestGameStateSegment received {} requests", packet.size());
if (packet.containsRequestOfType(RequestType.Req)) {
if(packet.containsRequestOfType(RequestType.Req)) { Logger.trace("Req event found. Returning Gamestate, and clearing entire RequestList");
Logger.trace("Req event found. Returning GameState, and clearing entire RequestList"); var gamestateEventMessage = new EventMessage();
EventMessage message = new EventMessage(); gamestateEventMessage.messages = new Event[] {game.getGameStateEvent()};
message.messages = new Event[] {parent.getInstance().getGameStateEvent()}; packet.getOrigin().sendMessage(gamestateEventMessage);
packet.getOrigin().sendMessage(message);
carrier.clear(); carrier.clear();
packet.clear(); packet.clear();
} }

View File

@ -1,4 +1,4 @@
package uulm.teamname.marvelous.server.game.pipelining; package uulm.teamname.marvelous.server.lobby.pipelining;
import uulm.teamname.marvelous.gamelibrary.events.Event; import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.requests.Request; import uulm.teamname.marvelous.gamelibrary.requests.Request;
@ -21,5 +21,5 @@ public interface Segment {
* segment, but instead an error in the events passed to it, like for example moving into a Rock. The * segment, but instead an error in the events passed to it, like for example moving into a Rock. The
* conventional way of setting this boolean is to write {@code abort.set(true); return;} * conventional way of setting this boolean is to write {@code abort.set(true); return;}
*/ */
void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort); public void processRequests(Packet packet, List<Event> carrier, AtomicBoolean abort);
} }

View File

@ -0,0 +1,326 @@
package uulm.teamname.marvelous.server.lobbymanager;
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.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.ClientState;
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;
public class LobbyConnection implements Runnable {
private static boolean synchronous = false;
public final String gameID;
public LobbyConnectionState state = LobbyConnectionState.Waiting;
private Participant player1;
private Participant player2;
private final Set<Participant> spectators = new HashSet<>(10);
private final Map<SUID, List<Integer>> selection = new HashMap<>(2);
public final Map<ParticipantType, CharacterProperties[]> options = new HashMap<>(2);
private final BlockingQueue<Tuple<Participant, Request[]>> requestQueue = new LinkedBlockingQueue<>();
private Lobby lobby;
/** Creates a new LobbyConnection */
public LobbyConnection(String gameID) {
this.gameID = gameID;
Tuple<CharacterProperties[], CharacterProperties[]> picked = Server.getCharacterConfig().getDisjointSetsOfPropertiesOfSize(12);
this.options.put(ParticipantType.PlayerOne, picked.item1);
this.options.put(ParticipantType.PlayerTwo, picked.item2);
}
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 (this.state == LobbyConnectionState.Started) {
Logger.trace("Set client state to playing");
participant.getClient().setState(ClientState.Playing);
participant.state = ParticipantState.Playing;
participant.sendMessage(generateGameStructure(participant.type));
}
if (participant.type == ParticipantType.Spectator) {
Logger.trace("Adding spectator");
spectators.add(participant);
} else if (participant.type == ParticipantType.PlayerOne) {
player1 = participant;
} else {
player2 = participant;
}
}
/** Disconnects a participant from the LobbyConnection */
public void removeParticipant(Participant participant) {
removeParticipant(participant, "You have been disconnected.");
}
/** Disconnects a participant from the LobbyConnection */
public void removeParticipant(Participant participant, String reason) {
if (participant != null) {
Logger.debug("Removing participant '{}' with role {} from lobby",
participant.id.getName(), participant.type);
LobbyManager.getInstance().removeParticipant(participant);
UserManager.getInstance().removeClient(participant.getClient(), reason);
if (participant.type == ParticipantType.Spectator) {
spectators.remove(participant);
} else if (participant.type == ParticipantType.PlayerOne) {
player1 = null;
} else {
player2 = null;
}
}
}
/** Returns the next free slot in the lobby as a {@link ParticipantType} */
public ParticipantType freeSlot() {
if (player1 == null) {
return ParticipantType.PlayerOne;
} else if (player2 == null) {
return ParticipantType.PlayerTwo;
} else {
return ParticipantType.Spectator;
}
}
/** Returns whether there is a player slot available in the lobby */
public boolean hasFreePlayerSlot() {
return player1 == null || player2 == null;
}
public Participant getPlayer1() {
return player1;
}
public Participant getPlayer2() {
return player2;
}
public Set<Participant> getSpectators() {
return spectators;
}
public boolean hasPlayer1() {
return player1 != null;
}
public boolean hasPlayer2() {
return player2 != null;
}
public void handleMessage(Participant participant, Request[] requests) {
if(synchronous) {
lobby.receiveRequests(requests, participant);
return;
}
try {
this.requestQueue.put(Tuple.of(participant, requests));
} catch (InterruptedException ignored) {
}
}
/** Handles disconnect of a Participant. Hereby, the participant is made ready for reconnection */
public void handleDisconnect(Participant participant) {
participant.disconnected = true;
if (state == LobbyConnectionState.Started) {
lobby.handleDisconnect(participant);
}
}
/** Handles reconnect of a Participant. Hereby, the participant is made ready for reconnection */
public void handleReconnect(Participant participant) {
participant.disconnected = false;
if (state == LobbyConnectionState.Started) {
GeneralAssignmentMessage response = new GeneralAssignmentMessage();
response.gameID = gameID;
participant.sendMessage(response);
participant.sendMessage(generateGameStructure(participant.type));
lobby.handleReconnect(participant);
}
}
public void runSynchronous() {
synchronous = true;
run();
}
@Override
public void run() {
state = LobbyConnectionState.Started;
player1.state = ParticipantState.Playing;
player2.state = ParticipantState.Playing;
for (Participant spectator : spectators) {
spectator.state = ParticipantState.Playing;
}
if(!synchronous) {
Logger.info("Starting Lobby thread for lobby '{}'", gameID);
}else {
Logger.info("Starting Lobby in main thread for lobby '{}'", gameID);
}
broadcastGameStructure();
this.lobby = new Lobby(
gameID,
this,
Server.getPartyConfig(),
Server.getCharacterConfig(),
Server.getScenarioConfig(),
selection.get(player1.id),
selection.get(player2.id)
);
if(synchronous) {
return;
}
while (state == LobbyConnectionState.Started) {
Tuple<Participant, Request[]> currentRequests = pollQueueAsync();
if (currentRequests != null) {
lobby.receiveRequests(currentRequests.item2, currentRequests.item1);
}
}
}
public void terminate() {
LobbyRunner.getInstance().removeLobby(this);
LobbyManager.getInstance().removeLobby(gameID);
state = LobbyConnectionState.Aborted;
removeParticipant(player1);
removeParticipant(player2);
spectators.forEach(this::removeParticipant);
}
private void broadcastGameStructure() {
// Sending GameStructure message with fitting assignment
player1.sendMessage(generateGameStructure(ParticipantType.PlayerOne));
player2.sendMessage(generateGameStructure(ParticipantType.PlayerTwo));
broadcastToSpectators(generateGameStructure(ParticipantType.Spectator));
}
private GameStructureMessage generateGameStructure(ParticipantType assignment) {
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();
gameStructureMessage.assignment = assignment;
return gameStructureMessage;
}
private Tuple<Participant, Request[]> pollQueueAsync() {
Tuple<Participant, Request[]> current = null;
try {
current = requestQueue.poll(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
}
return current;
}
private void broadcast(BasicMessage message) {
Logger.trace("Broadcasting message of type {} to all members of lobby", message.messageType);
if (player1 != null) player1.sendMessage(message);
if (player2 != null) player2.sendMessage(message);
spectators.forEach(spectator -> spectator.sendMessage(message));
}
private void broadcastToSpectators(BasicMessage message) {
Logger.trace("Broadcasting message of type {} to all spectators", message.messageType);
spectators.forEach(spectator -> spectator.sendMessage(message));
}
private void broadcastToAllExcept(Participant except, BasicMessage message) {
Logger.trace("Sending message of type {} to all except participant with role {}",
message.messageType, except == null ? "NONE" : except.type);
if (except != null) {
if (!except.equals(player1)) player1.sendMessage(message);
if (!except.equals(player2)) player2.sendMessage(message);
for (Participant spectator : spectators) {
if (!except.equals(spectator)) {
spectator.sendMessage(message);
}
}
} else {
broadcast(message);
}
}
public void sendEvents(Participant recipient, Event... events) {
Logger.trace("Sending {} events to participant with role {}",
events.length, recipient == null ? "NONE" : recipient.type);
if (recipient != null) {
EventMessage message = new EventMessage();
message.messages = events;
recipient.sendMessage(message);
}
}
public void broadcastEvents(List<Event> events) {
broadcastEvents(events.toArray(new Event[0]));
}
public void broadcastEvents(Event... events) {
Logger.trace("Broadcasting {} events", events.length);
EventMessage message = new EventMessage();
message.messages = events;
broadcast(message);
}
public void broadcastToAllExcept(Participant except, Event... events) {
Logger.trace("Broadcasting {} events to all except participant with role {}",
events.length, except == null ? "NONE" : except.type);
var message = new EventMessage();
message.messages = events;
broadcastToAllExcept(except, message);
}
}

View File

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

View File

@ -0,0 +1,273 @@
package uulm.teamname.marvelous.server.lobbymanager;
import org.tinylog.Logger;
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.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.ClientState;
import uulm.teamname.marvelous.server.netconnector.SUID;
import uulm.teamname.marvelous.server.netconnector.UserManager;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class LobbyManager {
private static LobbyManager instance;
/**
* @return the current instance of the LobbyManager
*/
public static LobbyManager getInstance() {
if (instance == null) {
instance = new LobbyManager();
}
return instance;
}
private final HashMap<String, LobbyConnection> lobbies = new HashMap<>();
private final HashMap<SUID, Participant> participants = new HashMap<>();
/**
* Handles a newly connected Client, and returns whether the game is already running in the given {@link
* AtomicBoolean}.
*/
public boolean handleConnect(Client client, AtomicBoolean running) {
Logger.debug("Connecting new client");
Participant participant = participants.get(client.getId());
if (participant != null) {
LobbyConnection lobby = lobbies.get(participant.lobby);
if (lobby != null) {
running.set(lobby.state == LobbyConnectionState.Started);
}
}
return true;
}
public boolean handleReady(Client client, PlayerReadyMessage message) {
if (participants.containsKey(client.getId())) {
return false;
}
addParticipant(client, client.socket.getResourceDescriptor(), message.role);
return true;
}
public boolean handleReconnect(Client client) {
if (!participants.containsKey(client.getId())) {
return false;
}
Participant participant = participants.get(client.getId());
participant.setClient(client);
LobbyConnection lobby = lobbies.get(participant.lobby);
if (lobby == null) {
return false;
}
lobby.handleReconnect(participant);
return true;
}
/**
* Handles a {@link CharacterSelectionMessage}, computes the characters that have been selected and relays that
* information to the {@link LobbyConnection} concerned by this information.
*
* @param client is the client that sent the message
* @param message is the message sent by the client
* @return true if handled successfully, and false otherwise
*/
public boolean handleSelection(Client client, CharacterSelectionMessage message) {
Logger.debug("Handling characterSelection...");
if (!participants.containsKey(client.getId())) {
Logger.trace("Participant didn't exist, returning...");
return false;
}
Participant participant = participants.get(client.getId());
if (participant.state != ParticipantState.Assigned) {
Logger.trace("Participant wasn't assigned, exiting...");
return false;
} else if (participant.type == ParticipantType.Spectator) {
Logger.trace("Spectator sent message, returning...");
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 (Boolean.TRUE.equals(message.characters[i])) {
selected[n++] = options[i].characterID;
}
}
if (n != 6) {
return false;
}
participant.state = ParticipantState.Selected;
boolean complete = lobby.setSelection(participant, selected);
if (complete) {
lobby.getPlayer1().getClient().setState(ClientState.Playing);
lobby.getPlayer2().getClient().setState(ClientState.Playing);
lobby.getSpectators().forEach(spectator -> spectator.getClient().setState(ClientState.Playing));
LobbyRunner.getInstance().startLobby(lobby);
} else {
ConfirmSelectionMessage response = new ConfirmSelectionMessage();
response.selectionComplete = false;
participant.sendMessage(response);
}
return true;
}
/**
* Handles a requestMessage, and relays it to the Lobby if valid
*
* @param client is the client that sent the message
* @param message is the message sent by the client
* @return true if handled successfully, and false otherwise
*/
public boolean handleRequests(Client client, RequestMessage message) {
if (!participants.containsKey(client.getId())) {
return false;
}
Participant participant = participants.get(client.getId());
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;
}
/**
* Handles the disconnect of a WebSocket.
*/
public void handleDisconnect(Client client, boolean byRemote) {
Logger.trace("Handling disconnect of Client");
if (!participants.containsKey(client.getId())) {
return;
}
Participant participant = participants.get(client.getId());
LobbyConnection lobby = lobbies.get(participant.lobby);
if (lobby == null) {
return;
}
if(lobby.state == LobbyConnectionState.Started) {
lobby.handleDisconnect(participant);
}else {
Logger.debug("Deleting participant after leaving a non-started lobby");
participants.remove(client.getId());
if(lobby.hasFreePlayerSlot()) {
Logger.debug("Destroying lobby after last player left");
lobby.terminate();
lobbies.remove(participant.lobby);
}
}
}
public void removeLobby(String lobbyID) {
lobbies.remove(lobbyID);
}
/**
* Adds a participant to a lobby. If the maximum amount of lobbies is already filled, or if the lobby requested
* isn't free, the participant is disconnected.
*/
private void addParticipant(Client client, String lobbyID, RoleEnum role) {
Logger.trace("Adding participant '{}' to the lobby '{}'", client.getId(), lobbyID);
if (!lobbies.containsKey(lobbyID)) {
if (!LobbyRunner.getInstance().canAddLobby()) {
Logger.info("Rejecting participant '{}' as server is already full", client.getId());
UserManager.getInstance().removeClient(client, "The server is currently full. Please connect as a spectator instead.");
return;
}
Logger.info("Lobby '{}' didn't exist yet, initializing", lobbyID);
lobbies.put(lobbyID, new LobbyConnection(lobbyID));
}
LobbyConnection lobby = lobbies.get(lobbyID);
if (!lobby.hasFreePlayerSlot() && role != RoleEnum.SPECTATOR) {
Logger.debug("No free player slots available, disconnecting client '{}'", client.getId());
UserManager.getInstance().removeClient(client, "The lobby your requested is already full. Please connect as a spectator instead.");
return;
}
ParticipantType type = lobby.freeSlot();
Logger.trace("New participant '{}' has the role '{}'", client.getId(), type);
Participant participant = new Participant(client, lobbyID, type, role == RoleEnum.KI);
participants.put(client.getId(), participant);
lobby.addParticipant(participant);
if (type != ParticipantType.Spectator) {
Logger.debug("Sending GameAssignment message to user '{}'", client.getId());
GameAssignmentMessage response = new GameAssignmentMessage();
response.gameID = lobby.gameID;
response.characterSelection = lobby.options.get(type);
participant.sendMessage(response);
} else {
Logger.debug("Sending GeneralAssignment message to user '{}'", client.getId());
GeneralAssignmentMessage response = new GeneralAssignmentMessage();
response.gameID = lobby.gameID;
participant.sendMessage(response);
}
}
/**
* Removes a participant from the game entirely. This is done when for example a player gets removed from the
* Lobby because of a timeout.
*/
public void removeParticipant(Participant participant) {
if (participant != null) {
participants.remove(participant.id);
}
}
}

View File

@ -0,0 +1,76 @@
package uulm.teamname.marvelous.server.lobbymanager;
import org.tinylog.Logger;
import uulm.teamname.marvelous.server.Server;
import java.util.HashMap;
/**
* Class meant for running lobbies. It manages said lobbies, creates threads for it, and moves it into an executor
*/
public class LobbyRunner {
private static boolean synchronous = false;
private static LobbyRunner instance;
static LobbyRunner getInstance() {
if (instance == null) {
instance = new LobbyRunner();
}
return instance;
}
private final HashMap<LobbyConnection, Thread> activeLobbies = new HashMap<>();
boolean canAddLobby() {
return activeLobbies.size() < Server.getMaxLobbies();
}
/** Starts a new thread for the current LobbyConnection, and adds it to the currently active lobbies */
void startLobby(LobbyConnection connection) {
Logger.trace("Starting lobby connection thread '{}'", connection.gameID);
synchronized (activeLobbies) {
if (activeLobbies.containsKey(connection)) {
Logger.warn("Already active lobby was started again. This is probably a bug.");
} else {
if(!synchronous) {
Logger.trace("Executing LobbyThread 'Lobby-{}'...", connection.gameID);
Thread lobbyThread = new Thread(connection, "Lobby-" + connection.gameID);
activeLobbies.put(connection, lobbyThread);
lobbyThread.start();
}else {
Logger.trace("Executing Lobby 'Lobby-{}'...", connection.gameID);
connection.runSynchronous();
}
}
}
}
void removeLobby(LobbyConnection lobby) {
synchronized (activeLobbies) {
if (!activeLobbies.containsKey(lobby)) {
Logger.warn("Tried to remove non-existent lobby thread. This is probably a bug.");
} else {
Logger.debug("Stopping and removing lobby '{}'", lobby.gameID);
activeLobbies.remove(lobby);
}
}
}
boolean isStarted(LobbyConnection connection) {
return activeLobbies.containsKey(connection);
}
/** Shutdown all threads, destroy the lobbies, and close everything up */
void shutdownAll() {
Logger.info("Stopping and removing all LobbyThreads");
activeLobbies.keySet().forEach(LobbyConnection::terminate);
Logger.debug("All lobby shutdown flags set");
}
// later...
void checkThreads() {
}
}

View File

@ -0,0 +1,62 @@
package uulm.teamname.marvelous.server.lobbymanager;
import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.server.netconnector.Client;
import uulm.teamname.marvelous.server.netconnector.SUID;
public class 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;
public final boolean isAI;
public Participant(Client client, String lobby, ParticipantType type, boolean ai) {
this.client = client;
this.id = client.getId();
this.lobby = lobby;
this.type = type;
this.isAI = ai;
}
public void setClient(Client client) {
this.client = client;
}
public Client getClient() {
return client;
}
public boolean sendError(String error) {
if(disconnected) {
return false;
}
return client.sendError(error);
}
public boolean sendMessage(BasicMessage message) {
if(disconnected) {
return false;
}
return client.sendMessage(message);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Participant other = (Participant) o;
return other.id.equals(id);
}
@Override
public int hashCode(){
final int prime = 31;
int result = 1;
result = prime * result + ((client == null) ? 0 : client.hashCode()) + ((id == null) ? 0 : id.hashCode());
return result;
}
}

View File

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

View File

@ -0,0 +1,139 @@
package uulm.teamname.marvelous.server.lobbymanager;
import java.util.Random;
public class RandomWordGenerator {
private static Random random = new Random();
public static String generateTwoWords() {
var firstWord = randomWords[random.nextInt(randomWords.length)];
var secondWord = randomWords[random.nextInt(randomWords.length)];
while (firstWord.equals(secondWord)) {
secondWord = randomWords[random.nextInt(randomWords.length)];
}
firstWord = firstWord.substring(0, 1).toUpperCase() + firstWord.substring(1).toLowerCase();
secondWord = secondWord.substring(0, 1).toUpperCase() + secondWord.substring(1).toLowerCase();
return firstWord + secondWord;
}
private static final String[] randomWords = new String[]{
"wait",
"release",
"river",
"important",
"mark",
"electric",
"defective",
"poke",
"blue",
"beef",
"spring",
"hurt",
"orange",
"happy",
"zealous",
"flowery",
"accurate",
"brake",
"title",
"x-ray",
"festive",
"wrathful",
"scissors",
"peaceful",
"finicky",
"shape",
"soothe",
"head",
"spotted",
"needless",
"time",
"abundant",
"humdrum",
"mouth",
"trot",
"bounce",
"thank",
"avoid",
"shocking",
"minor",
"secret",
"rabbit",
"protect",
"honey",
"business",
"worthless",
"suggest",
"splendid",
"drab",
"safe",
"gigantic",
"arrive",
"drum",
"hate",
"dinosaurs",
"bore",
"tired",
"regret",
"fit",
"potato",
"confuse",
"childlike",
"vein",
"sound",
"attack",
"exchange",
"back",
"check",
"damaged",
"grandmother",
"division",
"groovy",
"throat",
"office",
"pin",
"stare",
"meddle",
"shivering",
"interfere",
"occur",
"hole",
"sugar",
"test",
"blind",
"free",
"perform",
"cherries",
"flavor",
"stupendous",
"purpose",
"extend",
"risk",
"fanatical",
"grubby",
"beg",
"romantic",
"outrageous",
"swift",
"bath",
"room",
"pocket",
"front",
"flower",
"quicksand",
"mark",
"sturdy",
"resolute",
"letters",
"expert",
"hapless",
"bloody",
"blue-eyed",
"hope",
"chew",
};
}

View File

@ -1,104 +0,0 @@
package uulm.teamname.marvelous.server.net;
import org.java_websocket.WebSocket;
import org.java_websocket.framing.CloseFrame;
import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage;
import uulm.teamname.marvelous.gamelibrary.messages.ErrorMessage;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import java.util.Optional;
@SuppressWarnings("UnusedReturnValue")
public class Client {
private final WebSocket socket;
private ClientState state = ClientState.Blank;
private SUID id = null;
private ParticipantType type = null;
private boolean ai = false;
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 = MarvelousServer.json.stringify(message);
if(data.isEmpty()) {
return false;
}
Logger.debug("Sending message to " + this + ": " + data.get()); //i hate java so much
try {
socket.send(data.get());
return true;
}catch (Exception ignored) {
return false;
}
}
public boolean disconnect() {
if(socket == null) {
return false;
}
socket.close(CloseFrame.NORMAL);
return true;
}
public ClientState getState() {
return state;
}
public void setState(ClientState state) {
this.state = state;
}
public SUID getID() {
return id;
}
public void setID(SUID id) {
this.id = id;
}
public ParticipantType getType() {
return type;
}
public void setType(ParticipantType type) {
this.type = type;
}
public boolean isAI() {
return ai;
}
public void isAI(boolean ai) {
this.ai = ai;
}
public WebSocket getSocket() {
return socket;
}
@Override
public String toString() {
if(id != null) {
return id.toString();
}else {
return "Client@"+Integer.toHexString(hashCode());
}
}
}

View File

@ -1,88 +0,0 @@
package uulm.teamname.marvelous.server.net;
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 uulm.teamname.marvelous.server.ServerApplication;
import java.net.InetSocketAddress;
import java.util.HashMap;
public class MarvelousServer extends WebSocketServer {
public static final JSON json;
//static initializers are executed the first time the class is referenced
static {
json = new JSON(ServerApplication.getCharacterConfig());
}
public MarvelousServer(InetSocketAddress address) {
super(address);
Thread.currentThread().setName("WebSocketServer");
}
/** A map of all connected clients. */
private final HashMap<WebSocket, Client> clients = new HashMap<>();
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
Logger.debug("Connected new user");
Client client = new Client(conn);
synchronized(clients) {
clients.put(conn, client);
}
Logger.trace("Queueing event...");
ServerApplication.getSession().addEvent(new SocketEvent(SocketEventType.Connect, client));
}
@Override
public void onMessage(WebSocket conn, String message) {
Logger.debug("Message received from {}", conn);
Client client = clients.get(conn);
if(client == null) {
return;
}
if(message.length() == 0) {
return;
}
Logger.trace("Queueing event...");
ServerApplication.getSession().addEvent(new SocketEvent(SocketEventType.Message, client, message));
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
Logger.info("Disconnecting client '{}'", conn);
Client client = clients.get(conn);
if(client == null) {
return;
}
Logger.trace("Queueing event...");
ServerApplication.getSession().addEvent(new SocketEvent(SocketEventType.Disconnect, client));
synchronized(clients) {
clients.remove(conn);
}
}
@Override
public void onStart() {
Logger.info("MarvelousServer started on address {}", this.getAddress().toString());
}
@Override
public void onError(WebSocket conn, Exception ex) {
Logger.warn("WebSocket-Error occurred: {}", ex.getMessage());
}
}

View File

@ -1,393 +0,0 @@
package uulm.teamname.marvelous.server.net;
import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.json.ValidationUtility;
import uulm.teamname.marvelous.gamelibrary.messages.BasicMessage;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.messages.RoleEnum;
import uulm.teamname.marvelous.gamelibrary.messages.client.*;
import uulm.teamname.marvelous.gamelibrary.messages.server.*;
import uulm.teamname.marvelous.server.ServerApplication;
import uulm.teamname.marvelous.server.game.GameSession;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class ServerSession implements Runnable {
/** Set to true when the thread is asked to stop. */
private volatile boolean stopping = false;
/** Queue of socket events to handle. */
private final BlockingQueue<SocketEvent> events = new LinkedBlockingQueue<>();
public void addEvent(SocketEvent event) {
try {
this.events.put(event);
}catch(InterruptedException ignored) { }
}
@Override
public void run() {
Thread.currentThread().setName("Session");
Logger.trace("Started session thread.");
while(!stopping) {
try {
SocketEvent event = events.poll(1000, TimeUnit.MILLISECONDS);
if(event != null) {
handleSocketEvent(event);
}else {
Thread.onSpinWait();
}
}catch (InterruptedException ignored) { }
}
Logger.trace("Stopped session thread.");
}
/**
* Handles {@link SocketEvent SocketEvents} from the queue.
* @param event The event to handle
*/
private void handleSocketEvent(SocketEvent event) {
switch(event.type) {
case Connect -> onConnect(event.client);
case Message -> onMessage(event.client, event.data);
case Disconnect -> onDisconnect(event.client);
}
}
/** Requests the session thread to stop. */
public void stop() {
Logger.trace("Session stop requested.");
stopping = true;
}
public void broadcast(BasicMessage message, SUID... except) {
if(state != SessionState.Running) {
return;
}
HashSet<SUID> filter = new HashSet<>(List.of(except));
for(Client player: players) {
if(player == null || filter.contains(player.getID())) {
continue;
}
player.sendMessage(message);
}
for(Client spectator: spectators) {
if(filter.contains(spectator.getID())) {
continue;
}
spectator.sendMessage(message);
}
}
public void reset() {
if(state != SessionState.Running) {
return;
}
state = SessionState.Pending;
for(int i = 0; i < 2; i++) {
if(players[i] != null) {
players[i].disconnect();
players[i] = null;
}
}
for(Client spectator: spectators) {
spectator.disconnect();
}
spectators.clear();
game = null;
}
private SessionState state = SessionState.Pending;
private GameSession game = null;
private final Client[] players = new Client[2];
private final ArrayList<Client> spectators = new ArrayList<>();
public Client[] getPlayers() {
return players;
}
public ArrayList<Client> getSpectators() {
return spectators;
}
private void onConnect(Client client) {
//do nothing ?
}
private void onMessage(Client client, String message) {
Logger.trace("Parsing message...");
Optional<BasicMessage> parsed = MarvelousServer.json.parse(message);
if(parsed.isEmpty()) {
Logger.debug("Message couldn't be parsed, sending error...");
client.sendError("Message could not be parsed.");
return;
}
BasicMessage data = parsed.get();
Optional<String> violations = ValidationUtility.validate(data);
if(violations.isPresent()) {
Logger.debug("The message that client sent had structural violations: '{}'. Sending error...", violations.get());
client.sendError(violations.get());
}else {
HandleMessage(client, data);
}
}
private void onDisconnect(Client client) {
if(client.getID() == null) {
return;
}
Logger.debug("Client " + client + " disconnected");
if(game != null) {
game.handleDisconnect(client);
}
for(int i = 0; i < 2; i++) {
if(players[i] != null && players[i].getID().equals(client.getID())) {
players[i] = null;
return;
}
}
for(int i = 0; i < spectators.size(); i++) {
if(spectators.get(i).getID().equals(client.getID())) {
spectators.remove(i);
return;
}
}
}
private final String errorInvalidMessage = "This message is invalid right now.";
private void HandleMessage(Client client, BasicMessage data) {
Logger.debug("Received message from " + client + ": " + data);
Logger.trace("Handling message...");
if(data instanceof HelloServerMessage) {
Logger.trace("Message was instanceof HelloServerMessage");
HelloServerMessage message = (HelloServerMessage) data;
handleHelloServerMessage(client, message);
} else if(data instanceof ReconnectMessage) {
ReconnectMessage message = (ReconnectMessage) data;
handleReconnectMessage(client, message);
} else if(data instanceof PlayerReadyMessage) {
Logger.trace("Message was instanceof PlayerReadyMessage");
PlayerReadyMessage message = (PlayerReadyMessage) data;
handlePlayerReadyMessage(client, message);
} else if(data instanceof CharacterSelectionMessage) {
Logger.trace("Message was instanceof CharacterSelectionMessage");
CharacterSelectionMessage message = (CharacterSelectionMessage) data;
handleCharacterSelectionMessage(client, message);
} else if(data instanceof RequestMessage) {
Logger.trace("Message was instanceof RequestMessage");
RequestMessage message = (RequestMessage) data;
handleRequestsMessage(client, message);
}
}
/** Handles a HelloServerMessage */
private void handleHelloServerMessage(Client client, HelloServerMessage message) {
if(client.getState() != ClientState.Blank) {
client.sendError(errorInvalidMessage);
return;
}
SUID id = new SUID(message.name, message.deviceID);
boolean known = false;
for(int i = 0; i < 2; i++) {
if(players[i] != null && players[i].getID().equals(id)) {
players[i] = client;
known = true;
break;
}
}
for(int i = 0; i < spectators.size(); i++) {
if(known) {
break;
}
if(spectators.get(i).getID().equals(id)) {
spectators.set(i, client);
known = true;
break;
}
}
if(!known) {
client.setID(id);
}
boolean running = false;
if(state == SessionState.Running && known) {
client.setState(ClientState.Reconnect);
running = true;
}else {
client.setState(ClientState.Ready);
}
HelloClientMessage response = new HelloClientMessage();
response.runningGame = running;
client.sendMessage(response);
}
/** Handles a reconnectMessage */
private void handleReconnectMessage(Client client, ReconnectMessage message) {
if(client.getState() != ClientState.Reconnect) {
client.sendError(errorInvalidMessage);
return;
}
if(message.reconnect.equals(Boolean.TRUE)) {
client.setState(ClientState.Playing);
game.handleReconnect(client);
}else {
client.setState(ClientState.Ready);
}
}
/** Handles a PlayerReadyMessage */
private void handlePlayerReadyMessage(Client client, PlayerReadyMessage message) {
if(client.getState() != ClientState.Ready) {
client.sendError(errorInvalidMessage);
return;
}
if(message.startGame.equals(Boolean.FALSE)) {
GoodbyeClientMessage response = new GoodbyeClientMessage();
response.message = "No game requested.";
client.sendMessage(response);
client.disconnect();
return;
}
ParticipantType participant = null;
if(message.role == RoleEnum.SPECTATOR) {
spectators.add(client);
participant = ParticipantType.Spectator;
}else {
for(int i = 0; i < 2; i++) {
if(players[i] == null) {
players[i] = client;
participant = i == 0 ? ParticipantType.PlayerOne : ParticipantType.PlayerTwo;
break;
}
}
}
if(participant != null) {
if(game == null) {
game = new GameSession();
}
client.setType(participant);
client.isAI(message.role == RoleEnum.KI);
client.setState(ClientState.Assigned);
if(participant != ParticipantType.Spectator) {
GameAssignmentMessage response = new GameAssignmentMessage();
response.gameID = game.id;
response.characterSelection = game.characterChoices.get(participant);
client.sendMessage(response);
}else {
if(state == SessionState.Running) {
game.addSpectator(client);
}
}
}else {
client.sendError("The game is already full. Please connect as a spectator instead.");
client.disconnect();
}
}
/** Handles a characterSelectionMessage */
private void handleCharacterSelectionMessage(Client client, CharacterSelectionMessage message) {
if(client.getState() != ClientState.Assigned) {
client.sendError(errorInvalidMessage);
return;
}
int total = ServerApplication.getCharacterConfig().characters.length;
int received = message.characters.length;
if(received != total / 2) {
client.sendError("Your character selection size is invalid.");
return;
}
int selected = 0;
boolean[] selection = new boolean[total / 2];
for(int i = 0; i < received; i++) {
if(message.characters[i].equals(Boolean.TRUE)) {
selected++;
selection[i] = true;
}else {
selection[i] = false;
}
}
if(selected != total / 4) {
client.sendError("Your character selection is incomplete.");
return;
}
client.setState(ClientState.Selected);
int complete = 0;
for(int i = 0; i < 2; i++) {
if(players[i] != null && players[i].getState() == ClientState.Selected) {
complete++;
}
}
game.handleSelection(client, selection);
if(complete <= 1) {
ConfirmSelectionMessage response = new ConfirmSelectionMessage();
response.selectionComplete = true;
client.sendMessage(response);
}else {
state = SessionState.Running;
game.start();
}
}
/** Handles a RequestMessage */
private void handleRequestsMessage(Client client, RequestMessage message) {
if(client.getState() != ClientState.Playing) {
client.sendError(errorInvalidMessage);
return;
}
game.handleRequests(client, message.messages);
}
}

View File

@ -1,6 +0,0 @@
package uulm.teamname.marvelous.server.net;
public enum SessionState {
Pending,
Running
}

View File

@ -1,19 +0,0 @@
package uulm.teamname.marvelous.server.net;
public class SocketEvent {
public final SocketEventType type;
public final Client client;
public final String data;
public SocketEvent(SocketEventType type, Client client, String data) {
this.type = type;
this.client = client;
this.data = data;
}
public SocketEvent(SocketEventType type, Client client) {
this.type = type;
this.client = client;
this.data = null;
}
}

View File

@ -1,7 +0,0 @@
package uulm.teamname.marvelous.server.net;
public enum SocketEventType {
Connect,
Message,
Disconnect
}

View File

@ -0,0 +1,58 @@
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;
private SUID id;
private 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;
}
public WebSocket getSocket() {
return socket;
}
public ClientState getState() {
return state;
}
public SUID getId() {
return id;
}
public void setId(SUID id) {
this.id = id;
}
public void setState(ClientState state) {
this.state = state;
}
}

View File

@ -1,10 +1,9 @@
package uulm.teamname.marvelous.server.net; package uulm.teamname.marvelous.server.netconnector;
public enum ClientState { public enum ClientState {
Blank, Blank,
Ready, Ready,
Assigned, Assigned,
Selected,
Reconnect, Reconnect,
Playing Playing
} }

View File

@ -0,0 +1,42 @@
package uulm.teamname.marvelous.server.netconnector;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import org.tinylog.Logger;
import java.net.InetSocketAddress;
public class MarvelousServer extends WebSocketServer {
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
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.");
UserManager.getInstance().disconnectUser(conn, remote);
}
@Override
public void onMessage(WebSocket conn, String message) {
Logger.debug("Message received: {}", message);
UserManager.getInstance().messageReceived(conn, message);
}
@Override
public void onError(WebSocket conn, Exception ex) {
Logger.warn("WebSocket-Error occurred: {}", ex.getMessage());
}
@Override
public void onStart() {
Logger.info("MarvelousServer started on Address {}", this.getAddress().toString());
}
public MarvelousServer(InetSocketAddress address) {
super(address);
}
}

View File

@ -1,4 +1,4 @@
package uulm.teamname.marvelous.server.net; package uulm.teamname.marvelous.server.netconnector;
import java.util.Objects; import java.util.Objects;

View File

@ -0,0 +1,278 @@
package uulm.teamname.marvelous.server.netconnector;
import org.java_websocket.framing.CloseFrame;
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.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 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.
*/
public class UserManager {
private static UserManager instance;
private static final String errorInvalidMessage = "Invalid message.";
/**
* @return the current instance of the UserManager
*/
public static UserManager getInstance() {
if (instance == null) {
instance = new UserManager();
}
return instance;
}
/** A map of all connected clients. */
private final HashMap<WebSocket, Client> clients = new HashMap<>();
public final JSON json;
/** Constructs a new, empty UserManager */
private UserManager() {
this.json = new JSON(Server.getCharacterConfig());
}
/** Called on a new WebSocket connection. */
public void connectUser(WebSocket conn) {
Logger.debug("Connected new user");
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.
* @param conn is the {@link WebSocket} that sent the message
* @param message is the {@link String} sent by the connection
*/
public void messageReceived(WebSocket conn, String message) {
Logger.debug("Message received from {}", conn);
Client client = clients.get(conn);
if(client == null) {
Logger.warn("messageReceived called with null-valued client. This is probably a bug.");
return;
}
Logger.trace("Parsing message...");
Optional<BasicMessage> parsed = json.parse(message);
if(parsed.isEmpty()) {
Logger.debug("Message couldn't be parsed, sending error...");
client.sendError("Message could not be parsed.");
return;
}
BasicMessage data = parsed.get();
Optional<String> violations = ValidationUtility.validate(data);
if(violations.isPresent()) {
Logger.debug("The message that client sent had structural violations: '{}'. Sending error...",
violations.get());
client.sendError(violations.get());
return;
}
Logger.trace("Handling message...");
handleMessage(client, data);
}
/** Called on closed connection. */
public void disconnectUser(WebSocket conn, boolean closedByRemote) {
Logger.info("Disconnecting client '{}'", conn);
Client client = clients.get(conn);
if(client == null) {
Logger.warn("disconnect called with null-valued client. This is probably a bug.");
return;
}
Logger.trace("Calling handleDisconnect on LobbyManager");
LobbyManager.getInstance().handleDisconnect(client, closedByRemote);
synchronized(clients) {
Logger.debug("Removing connections from clients HashMap");
clients.remove(conn);
}
}
/** Checks type of a given message, and delegates its execution to the respective handler method */
private void handleMessage(Client client, BasicMessage data) {
Logger.debug("Received message from client '{}'", client);
if(data instanceof HelloServerMessage) {
Logger.trace("Message was instanceof HelloServerMessage");
HelloServerMessage message = (HelloServerMessage) data;
handleHelloServerMessage(client, message);
} else if(data instanceof ReconnectMessage) {
ReconnectMessage message = (ReconnectMessage) data;
handleReconnectMessage(client, message);
} else if(data instanceof PlayerReadyMessage) {
Logger.trace("Message was instanceof PlayerReadyMessage");
PlayerReadyMessage message = (PlayerReadyMessage) data;
handlePlayerReadyMessage(client, message);
} else if(data instanceof CharacterSelectionMessage) {
Logger.trace("Message was instanceof CharacterSelectionMessage");
CharacterSelectionMessage message = (CharacterSelectionMessage) data;
handleCharacterSelectionMessage(client, message);
} else if(data instanceof RequestMessage) {
Logger.trace("Message was instanceof RequestMessage");
RequestMessage message = (RequestMessage) data;
handleRequestsMessage(client, message);
}
}
/** Handles a HelloServerMessage */
private void handleHelloServerMessage(Client client, HelloServerMessage message) {
if(client.getState() != ClientState.Blank) {
Logger.debug("Disconnecting client as ClientState isn't Blank but {}", client.getState());
client.sendError("Invalid message, as handshake already completed.");
return;
}
client.setId(new SUID(message.name, message.deviceID));
Logger.trace("forwarding message to the LobbyManager");
AtomicBoolean running = new AtomicBoolean(false);
if(LobbyManager.getInstance().handleConnect(client, running)) {
var clientHasRunningGame = running.get();
if (clientHasRunningGame) {
client.setState(ClientState.Reconnect);
} else {
client.setState(ClientState.Ready);
}
HelloClientMessage response = new HelloClientMessage();
response.runningGame = running.get();
client.sendMessage(response);
} else {
client.sendError("Message could not be processed.");
}
}
/** Handles a reconnectMessage, and reconnects the client if needed */
private void handleReconnectMessage(Client client, ReconnectMessage message) {
if(client.getState() != ClientState.Reconnect) {
client.sendError("Invalid message, as client is not in reconnect-ready state");
return;
}
if(Boolean.TRUE.equals(message.reconnect)) {
Logger.trace("Reconnecting to lobby. Forwarding reconnect instruction to the LobbyManager");
if(LobbyManager.getInstance().handleReconnect(client)) {
Logger.trace("Successfully reconnected client, changing state to Playing...");
client.setState(ClientState.Playing);
} else {
Logger.debug("The client couldn't be reconnected properly");
client.sendError("You could not be reconnected to the Lobby");
}
} else {
Logger.trace("No reconnect requested, setting client to ready to connect state");
client.setState(ClientState.Ready);
}
}
/** Handles a PlayerReadyMessage, and connects a client to a lobby if valid */
private void handlePlayerReadyMessage(Client client, PlayerReadyMessage message) {
Logger.trace("Handing PlayerReadyMessage...");
if(client.getState() != ClientState.Ready) {
Logger.debug("Client wasn't in Ready state but instead in {} state, sending error...", client.getState());
client.sendError("Invalid message, as client is not in Ready state");
return;
}
Logger.trace("Relaying message to LobbyManager");
if(Boolean.TRUE.equals(message.startGame)) {
if(LobbyManager.getInstance().handleReady(client, message)) {
client.setState(ClientState.Assigned);
} else {
Logger.trace("Sending error to client as message couldn't be processed properly");
client.sendError(errorInvalidMessage);
}
} else {
Logger.debug("Disconnecting client as game couldn't be started");
removeClient(client, "You got disconnected.");
}
}
/** Handles a characterSelectionMessage, and forwards it to the LobbyManager if valid */
private void handleCharacterSelectionMessage(Client client, CharacterSelectionMessage message) {
Logger.trace("Handling CharacterSelectionMessage");
if(client.getState() != ClientState.Assigned) {
Logger.debug("Couldn't handle CharacterSelectionMessage as client wasn't in assignedState but in {}",
client.getState());
client.sendError("Cannot select character, as client is not in the Character Selection phase");
return;
}
Logger.trace("relaying message to be handled by the LobbyManager...");
if(LobbyManager.getInstance().handleSelection(client, message)) {
Logger.trace("Handled successfully");
} else {
client.sendError(errorInvalidMessage);
}
}
/** Handles a RequestMessage, and forwards it to the LobbyManager if valid */
private void handleRequestsMessage(Client client, RequestMessage message) {
Logger.trace("Handling RequestMessage");
if(client.getState() != ClientState.Playing) {
Logger.debug("Couldn't handle RequestMessage as client wasn't in playingState but in {}",
client.getState());
client.sendError("Invalid message, as client is not ingame");
return;
}
Logger.trace("relaying message to be handled by the LobbyManager...");
if(LobbyManager.getInstance().handleRequests(client, message)) {
Logger.trace("Handled successfully");
//"i approve" - the server
} else {
Logger.debug("Message couldn't be handled, sending error to client");
client.sendError(errorInvalidMessage);
}
}
/**
* Called when a client should be removed from the game.
* @param client is the client to be removed
* @param message is the message that is sent to the client in the accompanying {@link GoodbyeClientMessage}
*/
public void removeClient(Client client, String message) {
GoodbyeClientMessage response = new GoodbyeClientMessage();
response.message = message;
client.sendMessage(response);
client.socket.close(CloseFrame.NORMAL);
}
public int getUserCount() {
return clients.size();
}
public boolean containsConnection(WebSocket conn) {
return clients.containsKey(conn);
}
}

View File

@ -1,7 +1,9 @@
package uulm.teamname.marvelous.server; package uulm.teamname.marvelous.server;
import uulm.teamname.marvelous.gamelibrary.config.*; import uulm.teamname.marvelous.gamelibrary.config.*;
import uulm.teamname.marvelous.server.lobbymanager.LobbyRunner;
import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
@ -84,4 +86,16 @@ public class BaseGameLogicTest {
return props; return props;
} }
static void setPrivateFinalBoolean(Class c, String name, boolean value) throws NoSuchFieldException, IllegalAccessException {
Field field = LobbyRunner.class.getDeclaredField(name);
field.setAccessible(true);
//this is broken somehow
//Field modifiersField = Field.class.getDeclaredField("modifiers");
//modifiersField.setAccessible(true);
//modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, value);
}
} }

View File

@ -1,5 +1,6 @@
package uulm.teamname.marvelous.server; package uulm.teamname.marvelous.server;
import org.java_websocket.WebSocket;
import org.junit.jupiter.api.*; import org.junit.jupiter.api.*;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import org.mockito.Mockito; import org.mockito.Mockito;
@ -14,72 +15,87 @@ import uulm.teamname.marvelous.gamelibrary.messages.client.HelloServerMessage;
import uulm.teamname.marvelous.gamelibrary.messages.client.PlayerReadyMessage; import uulm.teamname.marvelous.gamelibrary.messages.client.PlayerReadyMessage;
import uulm.teamname.marvelous.gamelibrary.messages.client.ReconnectMessage; import uulm.teamname.marvelous.gamelibrary.messages.client.ReconnectMessage;
import uulm.teamname.marvelous.gamelibrary.messages.server.*; import uulm.teamname.marvelous.gamelibrary.messages.server.*;
import uulm.teamname.marvelous.server.net.Client; import uulm.teamname.marvelous.server.lobbymanager.LobbyConnection;
import uulm.teamname.marvelous.server.net.ServerSession; import uulm.teamname.marvelous.server.lobbymanager.LobbyManager;
import uulm.teamname.marvelous.server.net.SocketEvent; import uulm.teamname.marvelous.server.lobbymanager.LobbyRunner;
import uulm.teamname.marvelous.server.net.SocketEventType; import uulm.teamname.marvelous.server.netconnector.UserManager;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@SuppressWarnings({"ResultOfMethodCallIgnored", "SameParameterValue", "unchecked"}) class MarvelousServerTest extends BaseGameLogicTest {
class MarvelousServerApplicationTest extends BaseGameLogicTest { private static MockedStatic<Server> serverMock;
private static ServerSession session;
private static MockedStatic<ServerApplication> serverMock;
private static JSON json; private static JSON json;
@BeforeAll @BeforeAll
static void start() { static void start() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
session = new ServerSession(); Constructor<UserManager> um = UserManager.class.getDeclaredConstructor();
um.setAccessible(true);
um.newInstance();
Constructor<LobbyManager> lm = LobbyManager.class.getDeclaredConstructor();
lm.setAccessible(true);
lm.newInstance();
serverMock = Mockito.mockStatic(ServerApplication.class); serverMock = Mockito.mockStatic(Server.class);
generate(); generate();
serverMock.when(ServerApplication::getSession).thenReturn(session); setPrivateFinalBoolean(LobbyConnection.class, "synchronous", true);
serverMock.when(ServerApplication::getPartyConfig).thenReturn(partyConfig); setPrivateFinalBoolean(LobbyRunner.class, "synchronous", true);
serverMock.when(ServerApplication::getScenarioConfig).thenReturn(scenarioConfig);
serverMock.when(ServerApplication::getCharacterConfig).thenReturn(characterConfig); serverMock.when(Server::getMaxLobbies).thenReturn(10);
serverMock.when(Server::getPartyConfig).thenReturn(partyConfig);
serverMock.when(Server::getScenarioConfig).thenReturn(scenarioConfig);
serverMock.when(Server::getCharacterConfig).thenReturn(characterConfig);
json = new JSON(characterConfig); json = new JSON(characterConfig);
if(!Configuration.isFrozen()) { if (!Configuration.isFrozen()) {
Configuration.set("writer1", "console"); Configuration.set("writer1", "console");
Configuration.set("writer1.level", "trace"); Configuration.set("writer1.level", "trace");
Configuration.set("writer1.format", "[{thread}] {level}: <TEST> {message}"); Configuration.set("writer1.format", "[{thread}] {level}: <TEST> {message}");
} }
} }
@Test @Test
@Disabled
void main() { void main() {
session.run(); UserManager m = UserManager.getInstance();
Client p1 = mock(Client.class); WebSocket p1 = mock(WebSocket.class);
Client p2 = mock(Client.class); WebSocket p2 = mock(WebSocket.class);
Client s1 = mock(Client.class); WebSocket s1 = mock(WebSocket.class);
Client s2 = mock(Client.class); WebSocket s2 = mock(WebSocket.class);
session.addEvent(new SocketEvent(SocketEventType.Connect, p1)); when(p1.getResourceDescriptor()).thenReturn("/");
ensureHandshake(p1, "Player 1", "1234", false); when(p2.getResourceDescriptor()).thenReturn("/");
when(s1.getResourceDescriptor()).thenReturn("/");
when(s2.getResourceDescriptor()).thenReturn("/");
session.addEvent(new SocketEvent(SocketEventType.Connect, p2)); m.connectUser(p1);
ensureHandshake(p2, "Player 2", "4321", false); ensureHandshake(m, p1, "Player 1", "1234", false);
session.addEvent(new SocketEvent(SocketEventType.Connect, s1)); m.connectUser(p2);
ensureHandshake(s1, "Spectator 1", "3333", false); ensureHandshake(m, p2, "Player 2", "4321", false);
session.addEvent(new SocketEvent(SocketEventType.Connect, s2)); m.connectUser(s1);
ensureHandshake(s2, "Spectator 2", "4444", false); ensureHandshake(m, s1, "Spectator 1", "3333", false);
ensurePlayerReady(p1, true, RoleEnum.PLAYER); m.connectUser(s2);
ensurePlayerReady(p2, true, RoleEnum.PLAYER); ensureHandshake(m, s2, "Spectator 2", "4444", false);
ensureSpectatorReady(s1, true, RoleEnum.SPECTATOR);
ensureCharacterSelection(p1, true); ensurePlayerReady(m, p1, true, RoleEnum.PLAYER);
ensureCharacterSelection(p2, false); ensurePlayerReady(m, p2, true, RoleEnum.PLAYER);
ensureSpectatorReady(m, s1, true, RoleEnum.SPECTATOR);
ensureCharacterSelection(m, p1, true);
ensureCharacterSelection(m, p2, false);
GameStructureMessage game = new GameStructureMessage(); GameStructureMessage game = new GameStructureMessage();
game.playerOneName = "Player 1"; game.playerOneName = "Player 1";
@ -103,14 +119,14 @@ class MarvelousServerApplicationTest extends BaseGameLogicTest {
clearInvocations(p1, p2, s1); clearInvocations(p1, p2, s1);
session.addEvent(new SocketEvent(SocketEventType.Disconnect, p1)); m.disconnectUser(p1, true);
session.addEvent(new SocketEvent(SocketEventType.Connect, p1)); m.connectUser(p1);
ensureHandshake(p1, "Player 1", "1234", true); ensureHandshake(m, p1, "Player 1", "1234", true);
ReconnectMessage message = new ReconnectMessage(); ReconnectMessage message = new ReconnectMessage();
message.reconnect = true; message.reconnect = true;
sendMessage(p1, message); sendMessage(m, p1, message);
ensureReceived(p1, new GeneralAssignmentMessage()); ensureReceived(p1, new GeneralAssignmentMessage());
ensureReceived(p1, new GameStructureMessage()); ensureReceived(p1, new GameStructureMessage());
@ -118,7 +134,7 @@ class MarvelousServerApplicationTest extends BaseGameLogicTest {
System.out.println("Test Completed"); System.out.println("Test Completed");
} }
private void ensureHandshake(Client c, String name, String deviceID, boolean runningGame) { private void ensureHandshake(UserManager m, WebSocket c, String name, String deviceID, boolean runningGame) {
HelloServerMessage message = new HelloServerMessage(); HelloServerMessage message = new HelloServerMessage();
message.name = name; message.name = name;
message.deviceID = deviceID; message.deviceID = deviceID;
@ -126,10 +142,10 @@ class MarvelousServerApplicationTest extends BaseGameLogicTest {
HelloClientMessage response = new HelloClientMessage(); HelloClientMessage response = new HelloClientMessage();
response.runningGame = runningGame; response.runningGame = runningGame;
ensureResponse(c, message, response); ensureResponse(m, c, message, response);
} }
private void ensurePlayerReady(Client c, boolean startGame, RoleEnum role) { private void ensurePlayerReady(UserManager m, WebSocket c, boolean startGame, RoleEnum role) {
PlayerReadyMessage message = new PlayerReadyMessage(); PlayerReadyMessage message = new PlayerReadyMessage();
message.startGame = startGame; message.startGame = startGame;
message.role = role; message.role = role;
@ -137,10 +153,10 @@ class MarvelousServerApplicationTest extends BaseGameLogicTest {
GameAssignmentMessage response = new GameAssignmentMessage(); GameAssignmentMessage response = new GameAssignmentMessage();
//properties are left null because we can't test their content (they are random) //properties are left null because we can't test their content (they are random)
ensureResponse(c, message, response); ensureResponse(m, c, message, response);
} }
private void ensureSpectatorReady(Client c, boolean startGame, RoleEnum role) { private void ensureSpectatorReady(UserManager m, WebSocket c, boolean startGame, RoleEnum role) {
PlayerReadyMessage message = new PlayerReadyMessage(); PlayerReadyMessage message = new PlayerReadyMessage();
message.startGame = startGame; message.startGame = startGame;
message.role = role; message.role = role;
@ -148,10 +164,10 @@ class MarvelousServerApplicationTest extends BaseGameLogicTest {
GeneralAssignmentMessage response = new GeneralAssignmentMessage(); GeneralAssignmentMessage response = new GeneralAssignmentMessage();
//properties are left null because we can't test their content (they are random) //properties are left null because we can't test their content (they are random)
ensureResponse(c, message, response); ensureResponse(m, c, message, response);
} }
private void ensureCharacterSelection(Client c, boolean confirmSelection) { private void ensureCharacterSelection(UserManager m, WebSocket c, boolean confirmSelection) {
CharacterSelectionMessage message = new CharacterSelectionMessage(); CharacterSelectionMessage message = new CharacterSelectionMessage();
message.characters = new Boolean[12]; message.characters = new Boolean[12];
for(int i = 0; i < 6; i++) { for(int i = 0; i < 6; i++) {
@ -164,44 +180,44 @@ class MarvelousServerApplicationTest extends BaseGameLogicTest {
if(confirmSelection) { if(confirmSelection) {
ConfirmSelectionMessage response = new ConfirmSelectionMessage(); ConfirmSelectionMessage response = new ConfirmSelectionMessage();
response.selectionComplete = false; response.selectionComplete = false;
ensureResponse(c, message, response); ensureResponse(m, c, message, response);
} else { } else {
sendMessage(c, message); sendMessage(m, c, message);
} }
} }
/** Sends a message from the socket. */ /** Sends a message from the socket. */
private void sendMessage(Client c, BasicMessage message) { private void sendMessage(UserManager m, WebSocket c, BasicMessage message) {
Optional<String> in = json.stringify(message); Optional<String> in = json.stringify(message);
if(in.isPresent()) { if(in.isPresent()) {
session.addEvent(new SocketEvent(SocketEventType.Message, c, in.get())); m.messageReceived(c, in.get());
}else { }else {
throw new IllegalArgumentException("[TEST] Message in test call could not be serialized!"); throw new IllegalArgumentException("[TEST] Message in test call could not be serialized!");
} }
} }
/** Ensures that the given socket received the given response in response to the given message. */ /** Ensures that the given socket received the given response in response to the given message. */
private void ensureResponse(Client c, BasicMessage message, BasicMessage response) { private void ensureResponse(UserManager m, WebSocket c, BasicMessage message, BasicMessage response) {
Optional<String> in = json.stringify(message); Optional<String> in = json.stringify(message);
if(in.isPresent()) { if(in.isPresent()) {
session.addEvent(new SocketEvent(SocketEventType.Message, c, in.get())); m.messageReceived(c, in.get());
verify(c.getSocket()).send((String)argThat( verify(c).send((String)argThat(
(out)->verifyResponse((String)out, response) (out)->verifyResponse((String)out, response)
)); ));
clearInvocations(c.getSocket()); clearInvocations(c);
}else { }else {
throw new IllegalArgumentException("[TEST] Message in test call could not be serialized!"); throw new IllegalArgumentException("[TEST] Message in test call could not be serialized!");
} }
} }
/** Ensures that the given socket received the given response. */ /** Ensures that the given socket received the given response. */
private <T extends BasicMessage> void ensureReceived(Client c, T response) { private <T extends BasicMessage> void ensureReceived(WebSocket c, T response) {
boolean found = false; boolean found = false;
for(Invocation i: mockingDetails(c.getSocket()).getInvocations()) { for(Invocation i: mockingDetails(c).getInvocations()) {
Optional<BasicMessage> received = json.parse(i.getArgument(0, String.class)); Optional<BasicMessage> received = json.parse(i.getArgument(0, String.class));
if(received.isPresent()) { if(received.isPresent()) {
@ -211,7 +227,7 @@ class MarvelousServerApplicationTest extends BaseGameLogicTest {
continue; //message is not of the right type continue; //message is not of the right type
} }
T message = (T)parsed; T message = (T) parsed;
for(Field field: response.getClass().getDeclaredFields()) { for(Field field: response.getClass().getDeclaredFields()) {
try { try {
@ -249,7 +265,7 @@ class MarvelousServerApplicationTest extends BaseGameLogicTest {
Optional<BasicMessage> in = json.parse(actual); Optional<BasicMessage> in = json.parse(actual);
if(in.isPresent()) { if(in.isPresent()) {
T message = (T)in.get(); T message = (T) in.get();
for(Field field: expected.getClass().getDeclaredFields()) { for(Field field: expected.getClass().getDeclaredFields()) {
try { try {
@ -274,7 +290,6 @@ class MarvelousServerApplicationTest extends BaseGameLogicTest {
@AfterAll @AfterAll
static void stop() { static void stop() {
session.stop();
serverMock.close(); serverMock.close();
} }
} }

View File

@ -0,0 +1,31 @@
package uulm.teamname.marvelous.server.lobby;
import org.java_websocket.WebSocket;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import uulm.teamname.marvelous.server.netconnector.Client;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
class TurnTimeoutTimerTest {
TurnTimeoutTimer turnTimeoutTimer;
@BeforeEach
void beforeEach(){
var callback = mock(Consumer.class);
turnTimeoutTimer = new TurnTimeoutTimer(20, callback);
}
@Test
void startTurnTimerTest(){
var connection = mock(WebSocket.class);
var participant = new Participant(new Client(connection), "lobby", ParticipantType.Spectator, false);
assertThatIllegalStateException().describedAs("Spectators don't have TurnTime").isThrownBy(() -> turnTimeoutTimer.startTurnTimer(participant));
}
}

View File

@ -0,0 +1,103 @@
package uulm.teamname.marvelous.server.lobby.pipelining;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.config.FieldType;
import uulm.teamname.marvelous.gamelibrary.config.ScenarioConfig;
import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.lobby.Lobby;
import uulm.teamname.marvelous.server.lobbymanager.LobbyConnection;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
class DisconnectSegmentTest {
Participant player1;
Participant player2;
Participant spectator;
DisconnectSegment disconnectSegment;
Lobby lobby;
LobbyConnection connection;
@BeforeEach
void beforeEach(){
connection = mock(LobbyConnection.class);
player1 = mock(Participant.class);
player2 = mock(Participant.class);
spectator = mock(Participant.class);
lobby = mock(Lobby.class);
disconnectSegment = new DisconnectSegment(lobby);
when(lobby.getConnection()).thenReturn(connection);
when(connection.getPlayer1()).thenReturn(player1);
when(connection.getPlayer2()).thenReturn(player2);
}
@Test
void noDisconnectRequestTest(){
var requests = new Request[] {
new RequestBuilder(RequestType.Req).buildGameRequest(),
new RequestBuilder(RequestType.MoveRequest).buildGameRequest()
};
Packet packet = new Packet(requests, player1);
AtomicBoolean abort = new AtomicBoolean(false);
disconnectSegment.processRequests(packet, new ArrayList<>(), abort);
assertThat(packet).containsOnly(requests);
}
@Test
void disconnectRequestBySpectatorTest(){
var requests = new Request[] {
new RequestBuilder(RequestType.DisconnectRequest).buildGameRequest(),
new RequestBuilder(RequestType.Req).buildGameRequest(),
new RequestBuilder(RequestType.MoveRequest).buildGameRequest()
};
Packet packet = new Packet(requests, spectator);
AtomicBoolean abort = new AtomicBoolean(false);
disconnectSegment.processRequests(packet, new ArrayList<>(), abort);
assertThat(packet).doesNotContain(requests);
verify(connection).hasPlayer1();
verify(connection).hasPlayer2();
}
@Test
void disconnectRequestByPlayer1Test(){
var requests = new Request[] {
new RequestBuilder(RequestType.DisconnectRequest).buildGameRequest(),
new RequestBuilder(RequestType.Req).buildGameRequest(),
new RequestBuilder(RequestType.MoveRequest).buildGameRequest()
};
Packet packet = new Packet(requests, player1);
AtomicBoolean abort = new AtomicBoolean(false);
disconnectSegment.processRequests(packet, new ArrayList<>(), abort);
assertThat(packet).doesNotContain(requests);
verify(connection).removeParticipant(player1);
assertThat(verify(connection).hasPlayer1()).isFalse();
verify(connection).hasPlayer2();
}
@Test
void disconnectRequestByPlayer2Test(){
var requests = new Request[] {
new RequestBuilder(RequestType.DisconnectRequest).buildGameRequest(),
new RequestBuilder(RequestType.Req).buildGameRequest(),
new RequestBuilder(RequestType.MoveRequest).buildGameRequest()
};
Packet packet = new Packet(requests, player2);
AtomicBoolean abort = new AtomicBoolean(false);
disconnectSegment.processRequests(packet, new ArrayList<>(), abort);
assertThat(packet).doesNotContain(requests);
verify(connection).removeParticipant(player2);
verify(connection).hasPlayer1();
assertThat(verify(connection).hasPlayer2()).isFalse();
}
}

View File

@ -0,0 +1,76 @@
package uulm.teamname.marvelous.server.lobby.pipelining;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.entities.EntityID;
import uulm.teamname.marvelous.gamelibrary.entities.EntityType;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import uulm.teamname.marvelous.server.netconnector.Client;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
class FilterEndRoundRequestSegmentTest {
FilterEndRoundRequestSegment segment;
EntityID activeCharacter;
Participant activeParticipant, inactiveParticipant;
Request[] requests;
@BeforeEach
void beforeEach() {
this.segment = new FilterEndRoundRequestSegment(this::getActiveCharacter);
this.activeCharacter = new EntityID(EntityType.P1, 2);
this.activeParticipant = new Participant(mock(Client.class), null, ParticipantType.PlayerOne, false);
this.inactiveParticipant = new Participant(mock(Client.class), null, ParticipantType.PlayerTwo, false);
requests = new Request[] {
new RequestBuilder(RequestType.EndRoundRequest).buildGameRequest()
};
}
private EntityID getActiveCharacter() {
return activeCharacter;
}
@Test
@DisplayName("Request from active participant doesn't get filtered")
void packetFromActiveParticipantTest() {
var packet = new Packet(requests, activeParticipant);
var atomicBoolean = new AtomicBoolean(false);
var processedPacket = (Packet) packet.clone();
segment.processRequests(processedPacket, new ArrayList<>(), atomicBoolean);
assertThat(processedPacket).isEqualTo(packet);
assertThat(atomicBoolean.get()).isFalse();
}
@Test
@DisplayName("Request from non-active participant gets flagged as an error")
void packetFromNonActiveParticipantTest() {
var packet = new Packet(requests, inactiveParticipant);
var atomicBoolean = new AtomicBoolean(false);
var processedPacket = (Packet) packet.clone();
segment.processRequests(processedPacket, new ArrayList<>(), atomicBoolean);
// assertThat(processedPacket).isEqualTo(packet); is not necessary as there's no actual filtering going on
assertThat(atomicBoolean.get()).isTrue();
}
}

View File

@ -0,0 +1,46 @@
package uulm.teamname.marvelous.server.lobby.pipelining;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.events.EventBuilder;
import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder;
import uulm.teamname.marvelous.gamelibrary.events.EventType;
import uulm.teamname.marvelous.gamelibrary.gamelogic.GameInstance;
import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
class GameLogicSegmentTest {
@Test
void processRequests() {
var game = mock(GameInstance.class);
var request = new RequestBuilder(RequestType.DisconnectRequest).buildGameRequest();
var event = new EventBuilder(EventType.DisconnectEvent).buildGameEvent();
when(game.checkRequestsAndApply(any(ArrayList.class))).thenReturn(Optional.of(List.of(event)));
var abort = new AtomicBoolean(false);
var segment = new GameLogicSegment(game);
// note that DisconnectRequests are actually never passed to the GameLogic, ever.
var packet = new Packet(
new Request[] {request},
null);
var carrier = new ArrayList<Event>(1);
segment.processRequests(packet, carrier, abort);
assertThat(packet).isEmpty();
assertThat(carrier).contains(event);
verify(game).checkRequestsAndApply(any(Packet.class));
assertThat(abort.get()).isFalse();
}
}

View File

@ -0,0 +1,57 @@
package uulm.teamname.marvelous.server.lobby.pipelining;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import uulm.teamname.marvelous.server.netconnector.Client;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;
class PacketTest {
Packet packet;
@BeforeEach
void beforeEach(){
var requests = new Request[] {
new RequestBuilder(RequestType.Req).buildGameRequest(),
new RequestBuilder(RequestType.MoveRequest).buildGameRequest()
};
packet = new Packet(requests, null);
}
@Test
void containsRequestTest(){
assertThat(packet.containsRequestOfType(RequestType.Req)).isTrue();
assertThat(packet.containsRequestOfType(RequestType.DisconnectRequest)).isFalse();
}
@Test
void removeRequestsOfTypesTest(){
packet.removeRequestsOfTypes(RequestType.Req);
assertThat(packet).containsOnly(new RequestBuilder(RequestType.MoveRequest).buildGameRequest());
}
@Test
void removeRequestsNotOfTypesTest(){
packet.removeRequestsNotOfTypes(RequestType.Req);
assertThat(packet).containsOnly(new RequestBuilder(RequestType.Req).buildGameRequest());
}
@Test
void getOriginTest(){
var requests = new Request[] {
new RequestBuilder(RequestType.Req).buildGameRequest(),
new RequestBuilder(RequestType.MoveRequest).buildGameRequest()
};
var participant = new Participant(new Client(null), "SomeLobby", ParticipantType.PlayerOne, false);
packet = new Packet(requests, participant);
assertThat(packet.getOrigin()).isEqualTo(participant);
}
}

View File

@ -0,0 +1,106 @@
package uulm.teamname.marvelous.server.lobby.pipelining;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.lobbymanager.LobbyConnection;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.mock;
class PauseSegmentTest {
PauseSegment pauseSegment;
@BeforeEach
void setUp() {
pauseSegment = new PauseSegment();
}
@Test
void pauseGame() {
assertThat(pauseSegment.isPaused()).isFalse();
pauseSegment.pauseGame();
assertThat(pauseSegment.isPaused()).isTrue();
pauseSegment.pauseGame();
assertThat(pauseSegment.isPaused()).isTrue();
}
@Test
void pauseEnd() {
assertThat(pauseSegment.isPaused()).isFalse();
pauseSegment.pauseEnd();
assertThat(pauseSegment.isPaused()).isFalse();
pauseSegment.pauseGame();
assertThat(pauseSegment.isPaused()).isTrue();
pauseSegment.pauseEnd();
assertThat(pauseSegment.isPaused()).isFalse();
}
@Test
void doNotProcessEventsIfStopped() {
var requests = new Request[]{
new RequestBuilder(RequestType.PauseStartRequest).buildGameRequest(),
new RequestBuilder(RequestType.Req).buildGameRequest(),
new RequestBuilder(RequestType.DisconnectRequest).buildGameRequest(),
new RequestBuilder(RequestType.EndRoundRequest).buildGameRequest(),
new RequestBuilder(RequestType.MoveRequest).buildGameRequest()
};
var participant = mock(Participant.class);
var packet = new Packet(requests, participant);
AtomicBoolean abort = new AtomicBoolean(false);
pauseSegment.processRequests(packet, new ArrayList<>(), abort);
assertThat(packet).containsOnly(new RequestBuilder(RequestType.Req).buildGameRequest(), new RequestBuilder(RequestType.DisconnectRequest).buildGameRequest());
}
@Test
void doProcessEventsIfNotStopped(){
var requests = new Request[]{
new RequestBuilder(RequestType.PauseStopRequest).buildGameRequest(),
new RequestBuilder(RequestType.EndRoundRequest).buildGameRequest(),
new RequestBuilder(RequestType.MoveRequest).buildGameRequest()
};
var participant = mock(Participant.class);
var packet = new Packet(requests, participant);
AtomicBoolean abort = new AtomicBoolean(false);
pauseSegment.pauseGame();
pauseSegment.processRequests(packet, new ArrayList<>(), abort);
assertThat(packet).containsOnly(
new RequestBuilder(RequestType.EndRoundRequest).buildGameRequest(),
new RequestBuilder(RequestType.MoveRequest).buildGameRequest()
);
}
@Test
void pauseRequestWhilePaused(){
var requests = new Request[]{
new RequestBuilder(RequestType.PauseStartRequest).buildGameRequest(),
};
var participant = mock(Participant.class);
var packet = new Packet(requests, participant);
AtomicBoolean abort = new AtomicBoolean(false);
pauseSegment.pauseGame();
pauseSegment.processRequests(packet, new ArrayList<>(), abort);
assertThat(abort.get()).isTrue();
}
@Test
void unpauseRequestWhenNotPaused(){
var requests = new Request[]{
new RequestBuilder(RequestType.PauseStopRequest).buildGameRequest(),
};
var participant = mock(Participant.class);
var packet = new Packet(requests, participant);
AtomicBoolean abort = new AtomicBoolean(false);
pauseSegment.pauseEnd();
pauseSegment.processRequests(packet, new ArrayList<>(), abort);
assertThat(abort.get()).isTrue();
}
}

View File

@ -0,0 +1,66 @@
package uulm.teamname.marvelous.server.lobby.pipelining;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.lobby.Lobby;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
class PipelineTest {
Pipeline pipeline;
@BeforeEach
void beforeEach(){
pipeline = new Pipeline();
}
@Test
void addSegmentsTest(){
Lobby lobby = mock(Lobby.class);
PauseSegment pauseSegment = new PauseSegment();
DisconnectSegment disconnectSegment = new DisconnectSegment(lobby);
pipeline.addSegment(pauseSegment);
assertThat(pipeline.contains(pauseSegment)).isTrue();
assertThat(pipeline.contains(disconnectSegment)).isFalse();
pipeline.addSegment(disconnectSegment);
assertThat(pipeline.contains(pauseSegment)).isTrue();
assertThat(pipeline.contains(disconnectSegment)).isTrue();
}
@Test
void processRequestTest(){
var segment = mock(Segment.class);
var segment2 = mock(Segment.class);
var segment3 = mock(Segment.class);
var requests = new Request[]{
new RequestBuilder(RequestType.PauseStartRequest).buildGameRequest(),
new RequestBuilder(RequestType.Req).buildGameRequest()
};
Participant participant = mock(Participant.class);
var abort = new AtomicBoolean(false);
Packet packet = new Packet(requests, participant);
pipeline.addSegment(segment)
.addSegment(segment2)
.addSegment(segment3);
pipeline.processRequests(requests, participant);
verify(segment).processRequests(eq(packet), eq(new ArrayList<>()), any(AtomicBoolean.class));
verify(segment2).processRequests(eq(packet), eq(new ArrayList<>()), any(AtomicBoolean.class));
verify(segment3).processRequests(eq(packet), eq(new ArrayList<>()), any(AtomicBoolean.class));
}
}

View File

@ -0,0 +1,118 @@
package uulm.teamname.marvelous.server.lobby.pipelining;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.IntVector2;
import uulm.teamname.marvelous.gamelibrary.entities.EntityID;
import uulm.teamname.marvelous.gamelibrary.entities.EntityType;
import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import uulm.teamname.marvelous.server.netconnector.Client;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;
class PlayerFilterSegmentTest {
PlayerFilterSegment segment;
AtomicBoolean abort;
@BeforeEach
void beforeEach() {
segment = new PlayerFilterSegment();
abort = new AtomicBoolean(false);
}
@Test
void validRequestsRemainUntouched() {
var participant = new Participant(mock(Client.class), null, ParticipantType.PlayerOne, false);
var requests = new Request[] {
new RequestBuilder(RequestType.MeleeAttackRequest)
.withOriginField(new IntVector2(1, 4))
.withTargetField(new IntVector2(2, 4))
.withOriginEntity(new EntityID(EntityType.P1, 3))
.buildCharacterRequest(),
new RequestBuilder(RequestType.MeleeAttackRequest)
.withOriginField(new IntVector2(2, 4))
.withTargetField(new IntVector2(3, 5))
.withOriginEntity(new EntityID(EntityType.P1, 3))
.withTargetEntity(new EntityID(EntityType.P2, 3))
.withValue(14)
.buildCharacterRequest()
};
var packet = new Packet(requests, participant);
var carrier = new ArrayList<Event>();
assertThatNoException().isThrownBy(() -> segment.processRequests(packet, carrier, abort));
assertThat(packet.toArray(new Request[0]))
.isEqualTo(requests);
assertThat(carrier).isEmpty();
assertThat(abort).isFalse();
}
@Test
void invalidRequestsTriggerAbort() {
var participant = new Participant(mock(Client.class), null, ParticipantType.PlayerOne, false);
var requests = new Request[] {
new RequestBuilder(RequestType.MeleeAttackRequest)
.withOriginField(new IntVector2(1, 4))
.withTargetField(new IntVector2(2, 4))
.withOriginEntity(new EntityID(EntityType.P2, 3))
.buildCharacterRequest(),
new RequestBuilder(RequestType.MeleeAttackRequest)
.withOriginField(new IntVector2(2, 4))
.withTargetField(new IntVector2(3, 5))
.withOriginEntity(new EntityID(EntityType.P2, 3))
.withTargetEntity(new EntityID(EntityType.P1, 3))
.withValue(14)
.buildCharacterRequest()
};
var packet = new Packet(requests, participant);
var carrier = new ArrayList<Event>();
assertThatNoException().isThrownBy(() -> segment.processRequests(packet, carrier, abort));
assertThat(packet.toArray(new Request[0]))
.isEqualTo(requests);
assertThat(carrier).isEmpty();
assertThat(abort).isTrue();
}
@Test
void gameRequestsRemainUntouched() {
var participant = new Participant(mock(Client.class), null, ParticipantType.PlayerOne, false);
var requests = new Request[] {
new RequestBuilder(RequestType.Req).buildGameRequest(),
new RequestBuilder(RequestType.EndRoundRequest).buildGameRequest(),
new RequestBuilder(RequestType.DisconnectRequest).buildGameRequest()
};
var packet = new Packet(requests, participant);
var carrier = new ArrayList<Event>();
assertThatNoException().isThrownBy(() -> segment.processRequests(packet, carrier, abort));
assertThat(packet.toArray(new Request[0]))
.isEqualTo(requests);
assertThat(carrier).isEmpty();
assertThat(abort).isFalse();
}
}

View File

@ -0,0 +1,51 @@
package uulm.teamname.marvelous.server.lobby.pipelining;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.events.GamestateEvent;
import uulm.teamname.marvelous.gamelibrary.gamelogic.GameInstance;
import uulm.teamname.marvelous.gamelibrary.messages.client.RequestMessage;
import uulm.teamname.marvelous.gamelibrary.messages.server.EventMessage;
import uulm.teamname.marvelous.gamelibrary.requests.Request;
import uulm.teamname.marvelous.gamelibrary.requests.RequestBuilder;
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class RequestGameLogicSegmentTest {
@Test
void requestGamestateTest(){
var game = mock(GameInstance.class);
var gamestateEvent = mock(GamestateEvent.class);
var segment = new RequestGameStateSegment(game);
var requests = new Request[]{
new RequestBuilder(RequestType.Req).buildGameRequest(),
new RequestBuilder(RequestType.DisconnectRequest).buildGameRequest()
};
var participant = mock(Participant.class);
var packet = new Packet(requests, participant);
var message = new EventMessage();
message.messages = new Event[] {gamestateEvent};
when(game.getGameStateEvent()).thenReturn(gamestateEvent);
AtomicBoolean abort = new AtomicBoolean(false);
List<Event> carrier = new ArrayList<>();
segment.processRequests(packet, carrier, abort);
assertThat(packet).isEmpty();
assertThat(carrier).isEmpty();
verify(game).getGameStateEvent();
verify(participant).sendMessage(message);
}
}

View File

@ -0,0 +1,17 @@
package uulm.teamname.marvelous.server.lobbymanager;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;
class RandomWordGeneratorTest {
@Test
void generatesAString() {
// System.out.println(RandomWordGenerator.generateTwoWords());
assertThat(RandomWordGenerator.generateTwoWords()).isInstanceOf(String.class);
}
}

View File

@ -0,0 +1,53 @@
package uulm.teamname.marvelous.server.netconnector;
import org.java_websocket.WebSocket;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.json.JSON;
import uulm.teamname.marvelous.gamelibrary.messages.ErrorMessage;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;
class ClientTest {
Client client;
WebSocket socket;
SUID suid;
@BeforeEach
void setUp() {
socket = mock(WebSocket.class);
suid = new SUID("ClientName", "DeviceID");
client = new Client(socket);
}
@Test
void clientGetsCreatedEmpty() {
assertThat(client.getState()).isEqualTo(ClientState.Blank);
assertThat(client.getId()).isNull();
assertThat(client.getSocket()).isEqualTo(socket);
}
@Test
void sendError() {
client.sendError("SomeMessage");
verify(socket).send("{\"messageType\":\"ERROR\",\"message\":\"SomeMessage\",\"type\":0}");
}
@Test
void sendMessage() {
var stringRepresentingErrorMessage = "{\"messageType\":\"ERROR\",\"message\":\"SomeMessage\",\"type\":0}";
var errorMessage = new ErrorMessage();
errorMessage.message = "SomeMessage";
errorMessage.type = 0;
client.sendMessage(errorMessage);
verify(socket).send(stringRepresentingErrorMessage);
}
}

View File

@ -0,0 +1,63 @@
package uulm.teamname.marvelous.server.netconnector;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
import uulm.teamname.marvelous.gamelibrary.messages.client.*;
import uulm.teamname.marvelous.server.lobbymanager.Participant;
import java.lang.reflect.InvocationTargetException;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
class UserManagerTest {
UserManager manager;
Client client1, client2;
WebSocket socket1, socket2;
SUID suid1, suid2;
@BeforeEach
void beforeEach()
throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
socket1 = mock(WebSocket.class);
client1 = spy(new Client(socket1));
suid1 = new SUID("name1", "devID1");
when(client1.getId()).thenReturn(suid1);
when(client1.getSocket()).thenReturn(socket1);
socket2 = mock(WebSocket.class);
client2 = spy(new Client(socket2));
suid2 = new SUID("name2", "devID2");
when(client2.getId()).thenReturn(suid2);
when(client2.getSocket()).thenReturn(socket2);
var c = UserManager.class.getDeclaredConstructor();
c.setAccessible(true);
manager = spy(c.newInstance());
}
@Test
void userIsConnectedTest() {
assertThat(manager.getUserCount()).isZero();
assertThat(manager.containsConnection(socket1)).isFalse();
manager.connectUser(socket1);
assertThat(manager.getUserCount()).isEqualTo(1);
assertThat(manager.containsConnection(socket1)).isTrue();
verify(socket1, never()).send(any(String.class));
}
@Test
void helloServerMessagesGetAssignedProperly() {
manager.messageReceived(
socket1,
"{\"messageType\":\"HELLO_SERVER\",\"name\":\"SomeAwesomeName\",\"deviceID\":\"YAY\"}");
// TODO: test this
}
}

View File

@ -1,244 +0,0 @@
{
"characters": [
{
"characterID": 1,
"name": "Rocket Raccoon",
"HP": 54,
"MP": 8,
"AP": 12,
"meleeDamage": 9,
"rangeCombatDamage": 3,
"rangeCombatReach": 4
},
{
"characterID": 2,
"name": "Quicksilver",
"HP": 138,
"MP": 12,
"AP": 5,
"meleeDamage": 6,
"rangeCombatDamage": 3,
"rangeCombatReach": 12
},
{
"characterID": 3,
"name": "Hulk",
"HP": 69,
"MP": 11,
"AP": 5,
"meleeDamage": 6,
"rangeCombatDamage": 5,
"rangeCombatReach": 1
},
{
"characterID": 4,
"name": "Black Widow",
"HP": 54,
"MP": 1,
"AP": 13,
"meleeDamage": 14,
"rangeCombatDamage": 5,
"rangeCombatReach": 5
},
{
"characterID": 5,
"name": "Hawkeye",
"HP": 98,
"MP": 16,
"AP": 5,
"meleeDamage": 9,
"rangeCombatDamage": 3,
"rangeCombatReach": 1
},
{
"characterID": 6,
"name": "Captain America",
"HP": 82,
"MP": 4,
"AP": 6,
"meleeDamage": 6,
"rangeCombatDamage": 6,
"rangeCombatReach": 1
},
{
"characterID": 7,
"name": "Spiderman",
"HP": 133,
"MP": 6,
"AP": 5,
"meleeDamage": 7,
"rangeCombatDamage": 3,
"rangeCombatReach": 2
},
{
"characterID": 8,
"name": "Dr. Strange",
"HP": 51,
"MP": 5,
"AP": 7,
"meleeDamage": 6,
"rangeCombatDamage": 2,
"rangeCombatReach": 5
},
{
"characterID": 9,
"name": "Iron Man",
"HP": 89,
"MP": 2,
"AP": 1,
"meleeDamage": 16,
"rangeCombatDamage": 1,
"rangeCombatReach": 18
},
{
"characterID": 10,
"name": "Black Panther",
"HP": 62,
"MP": 14,
"AP": 1,
"meleeDamage": 9,
"rangeCombatDamage": 6,
"rangeCombatReach": 9
},
{
"characterID": 11,
"name": "Thor",
"HP": 72,
"MP": 9,
"AP": 3,
"meleeDamage": 8,
"rangeCombatDamage": 1,
"rangeCombatReach": 1
},
{
"characterID": 12,
"name": "Captain Marvel",
"HP": 100,
"MP": 1,
"AP": 1,
"meleeDamage": 12,
"rangeCombatDamage": 11,
"rangeCombatReach": 6
},
{
"characterID": 13,
"name": "Groot",
"HP": 112,
"MP": 4,
"AP": 6,
"meleeDamage": 15,
"rangeCombatDamage": 9,
"rangeCombatReach": 6
},
{
"characterID": 14,
"name": "Starlord",
"HP": 104,
"MP": 8,
"AP": 3,
"meleeDamage": 9,
"rangeCombatDamage": 3,
"rangeCombatReach": 13
},
{
"characterID": 15,
"name": "Gamora",
"HP": 94,
"MP": 8,
"AP": 8,
"meleeDamage": 7,
"rangeCombatDamage": 1,
"rangeCombatReach": 15
},
{
"characterID": 16,
"name": "Ant Man",
"HP": 71,
"MP": 5,
"AP": 6,
"meleeDamage": 7,
"rangeCombatDamage": 3,
"rangeCombatReach": 1
},
{
"characterID": 17,
"name": "Vision",
"HP": 66,
"MP": 8,
"AP": 4,
"meleeDamage": 11,
"rangeCombatDamage": 5,
"rangeCombatReach": 3
},
{
"characterID": 18,
"name": "Deadpool",
"HP": 118,
"MP": 1,
"AP": 21,
"meleeDamage": 3,
"rangeCombatDamage": 2,
"rangeCombatReach": 8
},
{
"characterID": 19,
"name": "Loki",
"HP": 142,
"MP": 1,
"AP": 11,
"meleeDamage": 2,
"rangeCombatDamage": 1,
"rangeCombatReach": 7
},
{
"characterID": 20,
"name": "Silver Surfer",
"HP": 79,
"MP": 10,
"AP": 5,
"meleeDamage": 2,
"rangeCombatDamage": 1,
"rangeCombatReach": 17
},
{
"characterID": 21,
"name": "Mantis",
"HP": 149,
"MP": 3,
"AP": 10,
"meleeDamage": 9,
"rangeCombatDamage": 4,
"rangeCombatReach": 5
},
{
"characterID": 22,
"name": "Ghost Rider",
"HP": 74,
"MP": 15,
"AP": 5,
"meleeDamage": 12,
"rangeCombatDamage": 4,
"rangeCombatReach": 4
},
{
"characterID": 23,
"name": "Jessica Jones",
"HP": 92,
"MP": 10,
"AP": 1,
"meleeDamage": 15,
"rangeCombatDamage": 2,
"rangeCombatReach": 7
},
{
"characterID": 24,
"name": "Scarlet Witch",
"HP": 51,
"MP": 9,
"AP": 1,
"meleeDamage": 14,
"rangeCombatDamage": 1,
"rangeCombatReach": 11
}
]
}

View File

@ -1,15 +0,0 @@
{
"maxRounds": 18,
"maxRoundTime": 384,
"maxGameTime": 1347,
"maxAnimationTime": 260,
"spaceStoneCD": 5,
"mindStoneCD": 2,
"realityStoneCD": 5,
"powerStoneCD": 2,
"timeStoneCD": 6,
"soulStoneCD": 1,
"mindStoneDMG": 29,
"maxPauseTime": 698,
"maxResponseTime": 142
}

View File

@ -1,12 +0,0 @@
{
"scenario":[
["GRASS","GRASS", "GRASS", "GRASS", "GRASS", "GRASS", "GRASS"],
["GRASS","PORTAL", "GRASS", "ROCK", "GRASS", "PORTAL", "GRASS"],
["GRASS","GRASS", "GRASS", "ROCK", "GRASS", "GRASS", "GRASS"],
["GRASS","GRASS", "GRASS", "ROCK", "GRASS", "GRASS", "GRASS"],
["GRASS","GRASS", "ROCK", "GRASS", "ROCK", "GRASS", "GRASS"],
["GRASS","ROCK", "ROCK", "GRASS", "GRASS", "ROCK", "GRASS"]
],
"author": "jakobmh",
"name": "asgard"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,174 +0,0 @@
{
"scenario": [
[
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"ROCK",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS"
],
[
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"ROCK",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"PORTAL",
"GRASS",
"GRASS"
],
[
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"ROCK",
"ROCK",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"ROCK"
],
[
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"ROCK",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"ROCK",
"ROCK"
],
[
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"ROCK",
"ROCK",
"ROCK",
"GRASS",
"GRASS",
"GRASS",
"ROCK"
],
[
"GRASS",
"GRASS",
"ROCK",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS"
],
[
"GRASS",
"PORTAL",
"ROCK",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS"
],
[
"GRASS",
"GRASS",
"ROCK",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS"
],
[
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"ROCK",
"GRASS",
"GRASS",
"GRASS"
],
[
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"ROCK",
"ROCK",
"ROCK",
"ROCK",
"GRASS",
"GRASS",
"GRASS"
],
[
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"ROCK",
"ROCK",
"GRASS",
"GRASS",
"GRASS",
"PORTAL",
"GRASS",
"GRASS"
],
[
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS",
"GRASS"
]
],
"name": "TestMapWithPortals",
"author": "Lenard Hermann"
}

View File

@ -45,7 +45,7 @@ try {
java -jar Server/build/libs/Server.jar ` java -jar Server/build/libs/Server.jar `
-c .\configs\marvelheros.character.json ` -c .\configs\marvelheros.character.json `
-m.\configs\matchconfig_1.game.json ` -m.\configs\matchconfig_1.game.json `
-s .\configs\newAsgard.scenario.json ` -s .\configs\asgard.scenario.json `
-v ` -v `
-p $port -p $port
} catch { } catch {