bugfixes in server and cli-client

This commit is contained in:
Jonah
2021-11-30 11:23:41 +01:00
parent 3179a17b36
commit 4cd5e90e2b
18 changed files with 262 additions and 201 deletions

View File

@@ -1,15 +1,16 @@
plugins {
id("org.springframework.boot").version("2.5.6")
//id("io.spring.dependency-management").version("1.0.7-RELEASE")
id(SpringBoot.plugin) version SpringBoot.version
id(SpringDependencyManagement.plugin) version SpringDependencyManagement.version
id("java")
}
repositories {
mavenCentral()
id("war")
}
dependencies {
implementation(project(":wizard-core"))
implementation(project(":wizard-common"))
implementation("org.springframework.boot:spring-boot-starter-websocket:2.5.6")
implementation(SpringBoot.starterWebsocket) {
exclude(group = SpringBoot.group, module = "spring-boot-starter-logging")
}
implementation(SpringBoot.starterLog4j2)
providedRuntime(SpringBoot.starterTomcat)
}

View File

@@ -9,19 +9,23 @@ import eu.jonahbauer.wizard.common.model.Configuration;
import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.messages.Observer;
import eu.jonahbauer.wizard.core.model.Configurations;
import eu.jonahbauer.wizard.core.model.GameConfiguration;
import eu.jonahbauer.wizard.server.machine.Player;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
@Getter
@Log4j2
@EqualsAndHashCode(of = "uuid")
public class Session implements Observer {
private static final int MIN_PLAYERS = 3;
@@ -46,7 +50,7 @@ public class Session implements Observer {
}
/**
* Associates the given player with this session, removes him from the lobby, notifies all other players in the
* Associates the given player with this session and notifies all other players in the
* session with a {@link PlayerJoinedMessage}, the joining player with a {@link SessionJoinedMessage} and all
* players in the lobby with a {@link SessionModifiedMessage}.
*
@@ -55,18 +59,19 @@ public class Session implements Observer {
* @return the players uuid
*/
public synchronized UUID join(Player player, String name) {
if (players.size() == MAX_PLAYERS) {
if (game != null) {
throw new NackException(NackMessage.GAME_ALREADY_STARTED, "Game has already started.");
} else if (players.size() == MAX_PLAYERS) {
throw new NackException(NackMessage.SESSION_FULL, "Session is full.");
} else if (players.values().stream().anyMatch(p -> p.getName().equalsIgnoreCase(name))) {
throw new NackException(NackMessage.NAME_TAKEN, "Name is already taken.");
}
Lobby.getInstance().leave(player);
SessionPlayer sessionPlayer;
do {
sessionPlayer = new SessionPlayer(UUID.randomUUID(), name, player);
sessionPlayer = new SessionPlayer(UUID.randomUUID(), name);
} while (players.putIfAbsent(sessionPlayer.getUuid(), sessionPlayer) != null);
sessionPlayer.setPlayer(player);
notifyJoined(sessionPlayer.toData());
Lobby.getInstance().notifyPlayers(new SessionModifiedMessage(toData()));
@@ -75,54 +80,120 @@ public class Session implements Observer {
}
public synchronized void leave(UUID player) {
if (players.remove(player) != null) {
if (players.size() == 0) {
Lobby.getInstance().removeSession(uuid);
} else {
notifyPlayers(new PlayerLeftMessage(player));
Lobby.getInstance().notifyPlayers(new SessionModifiedMessage(toData()));
if (game == null) {
if (players.remove(player) != null) {
if (players.size() == 0) {
Lobby.getInstance().removeSession(uuid);
} else {
notifyPlayers(new PlayerLeftMessage(player));
Lobby.getInstance().notifyPlayers(new SessionModifiedMessage(toData()));
}
}
} else {
var sessionPlayer = players.get(player);
if (sessionPlayer != null) {
sessionPlayer.setPlayer(null);
if (players.values().stream().noneMatch(SessionPlayer::isConnected)) {
Lobby.getInstance().removeSession(uuid);
}
}
}
}
public synchronized void ready(UUID player, boolean ready) {
var sessionPlayer = players.get(player);
if (sessionPlayer == null) {
public synchronized void ready(UUID uuid, boolean ready) {
var player = players.get(uuid);
if (player == null) {
throw new NackException(NackMessage.PLAYER_NOT_FOUND, "Who are you?");
} else if (game != null) {
throw new NackException(NackMessage.GAME_ALREADY_STARTED, "Game has already started.");
}
if (sessionPlayer.isReady() != ready) {
sessionPlayer.setReady(ready);
sessionPlayer.getPlayer().send(new AckMessage());
notifyPlayers(new PlayerModifiedMessage(sessionPlayer.toData()));
player.send(new AckMessage());
if (player.isReady() != ready) {
player.setReady(ready);
notifyPlayers(new PlayerModifiedMessage(player.toData()));
}
if (players.size() >= MIN_PLAYERS && players.values().stream().allMatch(SessionPlayer::isReady)) {
game = new Game(Configurations.get(configuration), this);
notifyPlayers(new StartingGameMessage());
game.start(players.keySet().stream().toList());
startGame();
}
}
/**
* Forwards the message sent by the player to the game.
*
* @param uuid the player
* @param message the players message
*/
public synchronized void handlePlayerMessage(UUID uuid, PlayerMessage message) {
var player = players.get(uuid);
if (player == null) {
throw new NackException(NackMessage.PLAYER_NOT_FOUND, "Who are you?");
} else if (game == null) {
throw new NackException(NackMessage.GAME_NOT_YET_STARTED, "Game hat not yet started.");
} else {
try {
game.execute(s -> {
// start buffering while game is locked
player.buffer();
return s.onMessage(game, uuid, message);
});
player.send(new AckMessage());
} catch (IllegalStateException e) {
throw new NackException(NackMessage.ILLEGAL_STATE, e.getMessage());
} catch (IllegalArgumentException e) {
throw new NackException(NackMessage.ILLEGAL_ARGUMENT, e.getMessage());
}
}
}
private void startGame() {
notifyPlayers(new StartingGameMessage());
game = new Game(Configurations.get(configuration).withTimeout(timeout), this);
game.start(List.copyOf(players.keySet()));
CompletableFuture.runAsync(() -> {
while (true) {
try {
game.await();
break;
} catch (InterruptedException e) {
// ignored
} catch (ExecutionException e) {
log.warn("Game completed with exception.", e);
break;
}
}
players.forEach((id, player) -> player.setReady(false));
synchronized (this) {
game = null;
for (SessionPlayer player : List.copyOf(players.values())) {
if (!player.isConnected()) {
leave(player.getUuid());
}
}
}
});
}
private void notifyJoined(PlayerData joined) {
var message = new PlayerJoinedMessage(joined);
for (SessionPlayer player : players.values()) {
if (player.getUuid().equals(joined.getUuid())) {
player.getPlayer().send(new SessionJoinedMessage(
player.send(new SessionJoinedMessage(
getUuid(),
player.getUuid(),
players.values().stream().map(SessionPlayer::toData).toList(),
player.getSecret()
));
} else {
player.getPlayer().send(message);
player.send(message);
}
}
}
private void notifyPlayers(ServerMessage message) {
for (SessionPlayer player : players.values()) {
player.getPlayer().send(message);
player.send(message);
}
}
@@ -130,16 +201,6 @@ public class Session implements Observer {
return new SessionData(uuid, name, players.size(), configuration);
}
/**
* Forwards the message sent by the player to the game.
*
* @param player the player
* @param message the players message
*/
public void handlePlayerMessage(UUID player, PlayerMessage message) {
game.onMessage(player, message);
}
@Override
public void notify(ObserverMessage message) {
notifyPlayers(new GameMessage(message));
@@ -147,7 +208,7 @@ public class Session implements Observer {
@Override
public void notify(UUID player, ObserverMessage message) {
players.get(player).getPlayer().send(new GameMessage(message));
players.get(player).send(new GameMessage(message));
}
@Data
@@ -155,8 +216,8 @@ public class Session implements Observer {
private static class SessionPlayer {
private final UUID uuid;
private final String name;
private final Player player;
private final String secret = generateSecret();
private Player player;
private boolean ready;
private static String generateSecret() {
@@ -169,5 +230,21 @@ public class Session implements Observer {
public PlayerData toData() {
return new PlayerData(uuid, name, ready);
}
public void send(ServerMessage message) {
if (player != null) {
player.send(message);
}
}
public void buffer() {
if (player != null) {
player.buffer();
}
}
public boolean isConnected() {
return player != null;
}
}
}

View File

@@ -3,18 +3,18 @@ package eu.jonahbauer.wizard.server.machine;
import eu.jonahbauer.wizard.common.machine.State;
import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.server.NackException;
import org.springframework.web.socket.CloseStatus;
import java.util.Optional;
public interface ClientState extends State<ClientState, Player> {
default Optional<ClientState> onOpen(Player player) {
throw new IllegalStateException(); // TODO nachdenken
throw new IllegalStateException();
}
default Optional<ClientState> onMessage(Player player, ClientMessage message) {
player.send(new NackMessage(NackMessage.BAD_REQUEST, "Unexpected message."));
return Optional.empty();
throw new NackException(NackMessage.UNEXPECTED_MESSAGE, "Don't know what to do with " + message + ".");
}
Optional<ClientState> onClose(Player player, CloseStatus status);

View File

@@ -2,17 +2,20 @@ package eu.jonahbauer.wizard.server.machine;
import eu.jonahbauer.wizard.common.machine.Context;
import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.common.messages.server.Response;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.server.NackException;
import eu.jonahbauer.wizard.server.machine.states.CreatedState;
import lombok.SneakyThrows;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.util.ArrayList;
public class Player extends Context<ClientState, Player> {
private final WebSocketSession session;
private final ArrayList<ServerMessage> buffer = new ArrayList<>();
private Class<?> shouldBuffer = null;
public Player(WebSocketSession session) {
super(new CreatedState());
@@ -29,22 +32,32 @@ public class Player extends Context<ClientState, Player> {
}
public void onMessage(ClientMessage message) {
try {
execute(s -> s.onMessage(this, message));
} catch (NackException e) {
send(new NackMessage(e.getCode(), e.getMessage()));
}
execute(s -> s.onMessage(this, message));
}
public void onClose(CloseStatus status) {
execute(s -> s.onClose(this, status));
}
public void buffer() {
shouldBuffer = Response.class;
}
@SneakyThrows
public void send(ServerMessage message) {
synchronized (session) {
session.sendMessage(new TextMessage(message.toString()));
if (shouldBuffer != null && shouldBuffer.isInstance(message)) {
session.sendMessage(new TextMessage(message.toString()));
shouldBuffer = null;
for (ServerMessage serverMessage : buffer) {
session.sendMessage(new TextMessage(serverMessage.toString()));
}
buffer.clear();
} else if (shouldBuffer != null) {
buffer.add(message);
} else {
session.sendMessage(new TextMessage(message.toString()));
}
}
}
}

View File

@@ -1,46 +0,0 @@
package eu.jonahbauer.wizard.server.machine.states;
import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
import eu.jonahbauer.wizard.common.messages.client.LeaveSessionMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.common.messages.server.StartingGameMessage;
import eu.jonahbauer.wizard.server.machine.ClientState;
import eu.jonahbauer.wizard.server.machine.Player;
import org.springframework.web.socket.CloseStatus;
import java.util.Optional;
public class InGame implements ClientState {
@Override
public Optional<ClientState> onEnter(Player context) {
context.send(new StartingGameMessage());
return ClientState.super.onEnter(context);
}
@Override
public void onExit(Player context) {
ClientState.super.onExit(context);
}
@Override
public Optional<ClientState> onOpen(Player player) {
return Optional.empty();
}
@Override
public Optional<ClientState> onMessage(Player player, ClientMessage message) {
if(message instanceof LeaveSessionMessage) {
//?
return Optional.empty();
} else {
player.send(new NackMessage(0, "Error: Invalid Message!"));
return Optional.empty();
}
}
@Override
public Optional<ClientState> onClose(Player player, CloseStatus status) {
return Optional.empty();
}
}

View File

@@ -23,16 +23,33 @@ public class LobbyState implements ClientState {
public Optional<ClientState> onMessage(Player player, ClientMessage message) {
if (message instanceof CreateSessionMessage create) {
Lobby.getInstance().leave(player);
var session = Lobby.getInstance().createSession(create);
var uuid = session.join(player, create.getPlayerName());
return Optional.of(new SessionState(session.getUuid(), uuid, create.getPlayerName()));
try {
player.buffer();
var session = Lobby.getInstance().createSession(create);
var uuid = session.join(player, create.getPlayerName());
return Optional.of(new SessionState(session.getUuid(), uuid, create.getPlayerName()));
} catch (NackException nack) {
// nack must be sent before joinlobby
player.send(nack.toMessage());
Lobby.getInstance().join(player);
return Optional.empty();
}
} else if (message instanceof JoinSessionMessage join) {
var session = Lobby.getInstance().getSession(join.getSession());
if (session == null) {
throw new NackException(NackMessage.NOT_FOUND, "Session not found.");
} else {
var uuid = session.join(player, join.getPlayerName());
return Optional.of(new SessionState(session.getUuid(), uuid, join.getPlayerName()));
Lobby.getInstance().leave(player);
try {
player.buffer();
var session = Lobby.getInstance().getSession(join.getSession());
if (session == null) {
throw new NackException(NackMessage.NOT_FOUND, "Session not found.");
} else {
var uuid = session.join(player, join.getPlayerName());
return Optional.of(new SessionState(session.getUuid(), uuid, join.getPlayerName()));
}
} catch (NackException nack) {
// nack must be sent before joinlobby
player.send(nack.toMessage());
Lobby.getInstance().join(player);
return Optional.empty();
}
} else {
return ClientState.super.onMessage(player, message);

View File

@@ -4,7 +4,6 @@ import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
import eu.jonahbauer.wizard.common.messages.client.InteractionMessage;
import eu.jonahbauer.wizard.common.messages.client.LeaveSessionMessage;
import eu.jonahbauer.wizard.common.messages.client.ReadyMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.server.Lobby;
import eu.jonahbauer.wizard.server.machine.ClientState;
import eu.jonahbauer.wizard.server.machine.Player;
@@ -42,8 +41,7 @@ public class SessionState implements ClientState {
Lobby.getInstance().getSession(session).handlePlayerMessage(self, playerMessage);
return Optional.empty();
} else {
player.send(new NackMessage(NackMessage.UNEXPECTED_MESSAGE, "Invalid Message!"));
return Optional.empty();
return ClientState.super.onMessage(player, message);
}
}

View File

@@ -1,6 +1,9 @@
package eu.jonahbauer.wizard.server.socket;
import com.google.gson.JsonParseException;
import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.server.NackException;
import eu.jonahbauer.wizard.server.machine.Player;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
@@ -29,7 +32,14 @@ public class WizardSocketHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(@NotNull WebSocketSession session, @NotNull TextMessage text) {
players.get(session.getId()).onMessage(ClientMessage.parse(text.getPayload()));
var player = players.get(session.getId());
try {
player.onMessage(ClientMessage.parse(text.getPayload()));
} catch (NackException e) {
player.send(e.toMessage());
} catch (JsonParseException e) {
player.send(new NackMessage(NackMessage.MALFORMED_MESSAGE, "Could not parse " + text.getPayload() + "."));
}
}
@Override