diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index dce112f..a0aae7a 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -85,4 +85,18 @@ object JavaWebSocket { const val version = "1.5.2" const val group = "org.java-websocket" const val id = "$group:Java-WebSocket:$version" +} + +object SpringBoot { + const val version = "2.5.6" + const val group = "org.springframework.boot" + const val plugin = group + const val starterWebsocket = "$group:spring-boot-starter-websocket" + const val starterTomcat = "$group:spring-boot-starter-tomcat" + const val starterLog4j2 = "$group:spring-boot-starter-log4j2" +} + +object SpringDependencyManagement { + const val version = "1.0.11.RELEASE" + const val plugin = "io.spring.dependency-management" } \ No newline at end of file diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/Client.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/Client.java index 21a181c..c8155e7 100644 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/Client.java +++ b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/Client.java @@ -25,7 +25,14 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import java.util.stream.IntStream; +import static picocli.CommandLine.*; + +@Command public class Client extends TimeoutContext implements Runnable { + @Option(names = "-v") + @Getter + private boolean verbose; + private LineReader reader; @Getter @@ -39,7 +46,7 @@ public class Client extends TimeoutContext implements Runna private CommandSpec spec; public static void main(String[] args) { - new Client().run(); + new CommandLine(new Client()).execute(args); } public Client() { @@ -126,6 +133,9 @@ public class Client extends TimeoutContext implements Runna } public void onMessage(ServerMessage message) { + if (verbose) { + println(message.toString()); + } execute(s -> s.onMessage(this, message)); } @@ -239,7 +249,7 @@ public class Client extends TimeoutContext implements Runna Exception exception = exceptionRef.get(); if (exception == null) { Object result; - CommandLine.ParseResult parseResult = commandLine.getParseResult(); + ParseResult parseResult = commandLine.getParseResult(); if (parseResult.subcommand() != null) { CommandLine sub = parseResult.subcommand().commandSpec().commandLine(); result = sub.getExecutionResult(); diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/commands/GameCommand.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/commands/GameCommand.java index e2b13be..695b440 100644 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/commands/GameCommand.java +++ b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/commands/GameCommand.java @@ -1,15 +1,12 @@ package eu.jonahbauer.wizard.client.cli.commands; import eu.jonahbauer.wizard.client.cli.Client; -import eu.jonahbauer.wizard.client.cli.state.AwaitingAcknowledgement; import eu.jonahbauer.wizard.client.cli.state.Game; -import eu.jonahbauer.wizard.client.cli.state.Menu; import eu.jonahbauer.wizard.common.messages.client.InteractionMessage; import eu.jonahbauer.wizard.common.messages.player.JuggleMessage; import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage; import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage; import eu.jonahbauer.wizard.common.messages.player.PredictMessage; -import eu.jonahbauer.wizard.common.messages.server.NackMessage; import eu.jonahbauer.wizard.common.model.Card; import org.jetbrains.annotations.NotNull; @@ -21,60 +18,41 @@ import static picocli.CommandLine.*; @Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class, ShowCommand.class}) public class GameCommand { private final Client client; - private final Game game; - public GameCommand(Client client, Game game) { + public GameCommand(Client client) { this.client = client; - this.game = game; } @Command(name = "play") - public AwaitingAcknowledgement play( + public void play( @Parameters(index = "0", paramLabel = "", completionCandidates = HandCompletion.class) Card card ) { this.client.send(new InteractionMessage(new PlayCardMessage(card))); - return awaitAcknowledgement(); + this.client.waitForReady(); } @Command(name = "predict") - public AwaitingAcknowledgement predict( + public void predict( @Parameters(index = "0", paramLabel = "") int prediction ) { this.client.send(new InteractionMessage(new PredictMessage(prediction))); - return awaitAcknowledgement(); + this.client.waitForReady(); } @Command(name = "trump") - public AwaitingAcknowledgement trump( + public void trump( @Parameters(index = "0", paramLabel = "") Card.Suit suit ) { this.client.send(new InteractionMessage(new PickTrumpMessage(suit))); - return awaitAcknowledgement(); + this.client.waitForReady(); } @Command(name = "juggle") - public AwaitingAcknowledgement juggle( + public void juggle( @Parameters(index = "0", paramLabel = "", completionCandidates = JuggleCompletion.class) Card card ) { this.client.send(new InteractionMessage(new JuggleMessage(card))); - return awaitAcknowledgement(); - } - - private AwaitingAcknowledgement awaitAcknowledgement() { - return new AwaitingAcknowledgement( - () -> game, - message -> { - if (message instanceof NackMessage nack) { - int code = nack.getCode(); - if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) { - client.println("Error: " + nack.getMessage()); - return game; - } - } - client.println("Fatal: Unexpected message " + message + ". Returning to menu."); - return new Menu(); - } - ); + this.client.waitForReady(); } public static class HandCompletion implements Iterable { diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/commands/SessionCommand.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/commands/SessionCommand.java index ccb23c4..b64b3ba 100644 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/commands/SessionCommand.java +++ b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/commands/SessionCommand.java @@ -1,7 +1,6 @@ package eu.jonahbauer.wizard.client.cli.commands; import eu.jonahbauer.wizard.client.cli.Client; -import eu.jonahbauer.wizard.client.cli.state.AwaitingAcknowledgement; import eu.jonahbauer.wizard.client.cli.state.AwaitingJoinLobby; import eu.jonahbauer.wizard.client.cli.state.Session; import eu.jonahbauer.wizard.common.messages.client.LeaveSessionMessage; @@ -20,13 +19,12 @@ public class SessionCommand { } @Command(name = "ready") - public AwaitingAcknowledgement ready( + public void ready( @Parameters(index = "0", paramLabel = "", defaultValue = "true") boolean ready ) { + session.setNextReady(ready); client.send(new ReadyMessage(ready)); - return new AwaitingAcknowledgement( - () -> new Session(session.getSelf(), session.getSession(), ready, session.getPlayers()) - ); + client.waitForReady(); } @Command(name = "leave", description = "Leaves the current session and returns to the lobby") diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/AwaitingAcknowledgement.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/AwaitingAcknowledgement.java deleted file mode 100644 index 2ad9cbf..0000000 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/AwaitingAcknowledgement.java +++ /dev/null @@ -1,43 +0,0 @@ -package eu.jonahbauer.wizard.client.cli.state; - -import eu.jonahbauer.wizard.client.cli.Client; -import eu.jonahbauer.wizard.common.messages.server.AckMessage; -import eu.jonahbauer.wizard.common.messages.server.Response; -import eu.jonahbauer.wizard.common.messages.server.ServerMessage; - -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; - -public final class AwaitingAcknowledgement extends Awaiting { - private final Supplier success; - private final Function failure; - - public AwaitingAcknowledgement(Supplier success, Function failure) { - this.success = success; - this.failure = failure; - } - - public AwaitingAcknowledgement(Supplier success) { - this.success = success; - this.failure = null; - } - - @Override - public Optional onEnter(Client client) { - client.println("Waiting for acknowledgment..."); - return super.onEnter(client); - } - - @Override - public Optional onMessage(Client client, ServerMessage message) { - if (message instanceof AckMessage) { - return Optional.of(success.get()); - } else if (failure != null) { - return Optional.of(failure.apply(message)); - } else { - return super.onMessage(client, message); - } - } -} diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/AwaitingJoinSession.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/AwaitingJoinSession.java index 46d4aac..7e04d4b 100644 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/AwaitingJoinSession.java +++ b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/AwaitingJoinSession.java @@ -1,9 +1,7 @@ package eu.jonahbauer.wizard.client.cli.state; import eu.jonahbauer.wizard.client.cli.Client; -import eu.jonahbauer.wizard.common.messages.server.NackMessage; -import eu.jonahbauer.wizard.common.messages.server.ServerMessage; -import eu.jonahbauer.wizard.common.messages.server.SessionJoinedMessage; +import eu.jonahbauer.wizard.common.messages.server.*; import java.util.Optional; @@ -28,6 +26,9 @@ public final class AwaitingJoinSession extends Awaiting { default -> { return super.onMessage(client, message); } } return Optional.of(new AwaitingJoinLobby()); + } else if (message instanceof SessionModifiedMessage || message instanceof SessionRemovedMessage) { + // drop + return Optional.empty(); } else { return super.onMessage(client, message); } diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/BaseState.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/BaseState.java index 65cf3c2..7c55048 100644 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/BaseState.java +++ b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/BaseState.java @@ -31,6 +31,9 @@ public abstract class BaseState implements ClientState { protected static Optional unexpectedMessage(Client client, ServerMessage message) { // return to menu on unexpected message client.println("Fatal: Unexpected message " + message + ". Returning to menu."); + if (client.isVerbose()) { + new Exception().printStackTrace(); + } return Optional.of(new Menu()); } } diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java index dbdf922..961c853 100644 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java +++ b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java @@ -5,7 +5,9 @@ import eu.jonahbauer.wizard.client.cli.commands.GameCommand; import eu.jonahbauer.wizard.client.cli.util.Pair; import eu.jonahbauer.wizard.common.messages.data.PlayerData; import eu.jonahbauer.wizard.common.messages.observer.*; +import eu.jonahbauer.wizard.common.messages.server.AckMessage; import eu.jonahbauer.wizard.common.messages.server.GameMessage; +import eu.jonahbauer.wizard.common.messages.server.NackMessage; import eu.jonahbauer.wizard.common.messages.server.ServerMessage; import eu.jonahbauer.wizard.common.model.Card; import lombok.Getter; @@ -149,6 +151,18 @@ public final class Game extends BaseState { throw new AssertionError("Unknown observer message " + observerMessage.getClass().getSimpleName() + ""); } return Optional.empty(); + } else if (message instanceof NackMessage nack) { + int code = nack.getCode(); + if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) { + client.println("Error: " + nack.getMessage()); + client.ready(); + return Optional.empty(); + } else { + return unexpectedMessage(client, message); + } + } else if (message instanceof AckMessage) { + client.ready(); + return Optional.empty(); } else { return unexpectedMessage(client, message); } diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Menu.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Menu.java index c5a89d3..161e974 100644 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Menu.java +++ b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Menu.java @@ -23,7 +23,9 @@ public final class Menu extends BaseState { @Override public Optional onMessage(Client client, ServerMessage message) { - throw new AssertionError("Received a ServerMessage while not connected."); + // it is possible that there are messages still queued after + // returning to the menu as a result of a previous message + return Optional.empty(); } @Override diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Session.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Session.java index b5767b7..8e1bf68 100644 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Session.java +++ b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Session.java @@ -5,6 +5,7 @@ import eu.jonahbauer.wizard.client.cli.commands.SessionCommand; import eu.jonahbauer.wizard.common.messages.data.PlayerData; import eu.jonahbauer.wizard.common.messages.server.*; import lombok.Getter; +import lombok.Setter; import picocli.CommandLine.Model.CommandSpec; import java.util.HashMap; @@ -19,9 +20,12 @@ public final class Session extends BaseState { private final UUID self; private final UUID session; - private final boolean ready; private final Map players = new HashMap<>(); + private boolean ready; + @Setter + private Boolean nextReady; + public Session(SessionJoinedMessage joined) { this.self = joined.getPlayer(); this.session = joined.getSession(); @@ -68,6 +72,16 @@ public final class Session extends BaseState { session, players.values().stream().collect(Collectors.toMap(PlayerData::getUuid, PlayerData::getName)) )); + } else if (nextReady != null && message instanceof NackMessage nack) { + client.println("Error: " + nack.getMessage()); + nextReady = null; + client.ready(); + return Optional.empty(); + } else if (nextReady != null && message instanceof AckMessage) { + ready = nextReady; + nextReady = null; + client.ready(); + return Optional.empty(); } else { return unexpectedMessage(client, message); } diff --git a/wizard-server/build.gradle.kts b/wizard-server/build.gradle.kts index 128a8dc..0263dd9 100644 --- a/wizard-server/build.gradle.kts +++ b/wizard-server/build.gradle.kts @@ -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) } diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java index 648775c..e6449cb 100644 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java +++ b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java @@ -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; + } } } diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/ClientState.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/ClientState.java index 7fa4c36..fd68086 100644 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/ClientState.java +++ b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/ClientState.java @@ -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 { default Optional onOpen(Player player) { - throw new IllegalStateException(); // TODO nachdenken + throw new IllegalStateException(); } default Optional 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 onClose(Player player, CloseStatus status); diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/Player.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/Player.java index 382ce0a..f4d82bf 100644 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/Player.java +++ b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/Player.java @@ -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 { private final WebSocketSession session; + private final ArrayList buffer = new ArrayList<>(); + private Class shouldBuffer = null; public Player(WebSocketSession session) { super(new CreatedState()); @@ -29,22 +32,32 @@ public class Player extends Context { } 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())); + } } } } diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/InGame.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/InGame.java deleted file mode 100644 index 40db728..0000000 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/InGame.java +++ /dev/null @@ -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 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 onOpen(Player player) { - return Optional.empty(); - } - - @Override - public Optional 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 onClose(Player player, CloseStatus status) { - return Optional.empty(); - } -} diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/LobbyState.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/LobbyState.java index 09e3bfc..2d6b26c 100644 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/LobbyState.java +++ b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/LobbyState.java @@ -23,16 +23,33 @@ public class LobbyState implements ClientState { public Optional 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); diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/SessionState.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/SessionState.java index 95edd69..00334c6 100644 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/SessionState.java +++ b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/SessionState.java @@ -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); } } diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/socket/WizardSocketHandler.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/socket/WizardSocketHandler.java index 42c1176..7b54f22 100644 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/socket/WizardSocketHandler.java +++ b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/socket/WizardSocketHandler.java @@ -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