bugfixes in server and cli-client

main
Jonah Bauer 3 years ago
parent ce739933cb
commit e222ef6ce6

@ -86,3 +86,17 @@ object JavaWebSocket {
const val group = "org.java-websocket" const val group = "org.java-websocket"
const val id = "$group:Java-WebSocket:$version" 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"
}

@ -25,7 +25,14 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import static picocli.CommandLine.*;
@Command
public class Client extends TimeoutContext<ClientState, Client> implements Runnable { public class Client extends TimeoutContext<ClientState, Client> implements Runnable {
@Option(names = "-v")
@Getter
private boolean verbose;
private LineReader reader; private LineReader reader;
@Getter @Getter
@ -39,7 +46,7 @@ public class Client extends TimeoutContext<ClientState, Client> implements Runna
private CommandSpec spec; private CommandSpec spec;
public static void main(String[] args) { public static void main(String[] args) {
new Client().run(); new CommandLine(new Client()).execute(args);
} }
public Client() { public Client() {
@ -126,6 +133,9 @@ public class Client extends TimeoutContext<ClientState, Client> implements Runna
} }
public void onMessage(ServerMessage message) { public void onMessage(ServerMessage message) {
if (verbose) {
println(message.toString());
}
execute(s -> s.onMessage(this, message)); execute(s -> s.onMessage(this, message));
} }
@ -239,7 +249,7 @@ public class Client extends TimeoutContext<ClientState, Client> implements Runna
Exception exception = exceptionRef.get(); Exception exception = exceptionRef.get();
if (exception == null) { if (exception == null) {
Object result; Object result;
CommandLine.ParseResult parseResult = commandLine.getParseResult(); ParseResult parseResult = commandLine.getParseResult();
if (parseResult.subcommand() != null) { if (parseResult.subcommand() != null) {
CommandLine sub = parseResult.subcommand().commandSpec().commandLine(); CommandLine sub = parseResult.subcommand().commandSpec().commandLine();
result = sub.getExecutionResult(); result = sub.getExecutionResult();

@ -1,15 +1,12 @@
package eu.jonahbauer.wizard.client.cli.commands; package eu.jonahbauer.wizard.client.cli.commands;
import eu.jonahbauer.wizard.client.cli.Client; 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.Game;
import eu.jonahbauer.wizard.client.cli.state.Menu;
import eu.jonahbauer.wizard.common.messages.client.InteractionMessage; import eu.jonahbauer.wizard.common.messages.client.InteractionMessage;
import eu.jonahbauer.wizard.common.messages.player.JuggleMessage; import eu.jonahbauer.wizard.common.messages.player.JuggleMessage;
import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage; import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage; import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.common.messages.player.PredictMessage; import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -21,60 +18,41 @@ import static picocli.CommandLine.*;
@Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class, ShowCommand.class}) @Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class, ShowCommand.class})
public class GameCommand { public class GameCommand {
private final Client client; private final Client client;
private final Game game;
public GameCommand(Client client, Game game) { public GameCommand(Client client) {
this.client = client; this.client = client;
this.game = game;
} }
@Command(name = "play") @Command(name = "play")
public AwaitingAcknowledgement play( public void play(
@Parameters(index = "0", paramLabel = "<card>", completionCandidates = HandCompletion.class) Card card @Parameters(index = "0", paramLabel = "<card>", completionCandidates = HandCompletion.class) Card card
) { ) {
this.client.send(new InteractionMessage(new PlayCardMessage(card))); this.client.send(new InteractionMessage(new PlayCardMessage(card)));
return awaitAcknowledgement(); this.client.waitForReady();
} }
@Command(name = "predict") @Command(name = "predict")
public AwaitingAcknowledgement predict( public void predict(
@Parameters(index = "0", paramLabel = "<prediction>") int prediction @Parameters(index = "0", paramLabel = "<prediction>") int prediction
) { ) {
this.client.send(new InteractionMessage(new PredictMessage(prediction))); this.client.send(new InteractionMessage(new PredictMessage(prediction)));
return awaitAcknowledgement(); this.client.waitForReady();
} }
@Command(name = "trump") @Command(name = "trump")
public AwaitingAcknowledgement trump( public void trump(
@Parameters(index = "0", paramLabel = "<suit>") Card.Suit suit @Parameters(index = "0", paramLabel = "<suit>") Card.Suit suit
) { ) {
this.client.send(new InteractionMessage(new PickTrumpMessage(suit))); this.client.send(new InteractionMessage(new PickTrumpMessage(suit)));
return awaitAcknowledgement(); this.client.waitForReady();
} }
@Command(name = "juggle") @Command(name = "juggle")
public AwaitingAcknowledgement juggle( public void juggle(
@Parameters(index = "0", paramLabel = "<card>", completionCandidates = JuggleCompletion.class) Card card @Parameters(index = "0", paramLabel = "<card>", completionCandidates = JuggleCompletion.class) Card card
) { ) {
this.client.send(new InteractionMessage(new JuggleMessage(card))); this.client.send(new InteractionMessage(new JuggleMessage(card)));
return awaitAcknowledgement(); this.client.waitForReady();
}
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();
}
);
} }
public static class HandCompletion implements Iterable<String> { public static class HandCompletion implements Iterable<String> {

@ -1,7 +1,6 @@
package eu.jonahbauer.wizard.client.cli.commands; package eu.jonahbauer.wizard.client.cli.commands;
import eu.jonahbauer.wizard.client.cli.Client; 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.AwaitingJoinLobby;
import eu.jonahbauer.wizard.client.cli.state.Session; import eu.jonahbauer.wizard.client.cli.state.Session;
import eu.jonahbauer.wizard.common.messages.client.LeaveSessionMessage; import eu.jonahbauer.wizard.common.messages.client.LeaveSessionMessage;
@ -20,13 +19,12 @@ public class SessionCommand {
} }
@Command(name = "ready") @Command(name = "ready")
public AwaitingAcknowledgement ready( public void ready(
@Parameters(index = "0", paramLabel = "<ready>", defaultValue = "true") boolean ready @Parameters(index = "0", paramLabel = "<ready>", defaultValue = "true") boolean ready
) { ) {
session.setNextReady(ready);
client.send(new ReadyMessage(ready)); client.send(new ReadyMessage(ready));
return new AwaitingAcknowledgement( client.waitForReady();
() -> new Session(session.getSelf(), session.getSession(), ready, session.getPlayers())
);
} }
@Command(name = "leave", description = "Leaves the current session and returns to the lobby") @Command(name = "leave", description = "Leaves the current session and returns to the lobby")

@ -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<ClientState> success;
private final Function<ServerMessage, ClientState> failure;
public AwaitingAcknowledgement(Supplier<ClientState> success, Function<ServerMessage, ClientState> failure) {
this.success = success;
this.failure = failure;
}
public AwaitingAcknowledgement(Supplier<ClientState> success) {
this.success = success;
this.failure = null;
}
@Override
public Optional<ClientState> onEnter(Client client) {
client.println("Waiting for acknowledgment...");
return super.onEnter(client);
}
@Override
public Optional<ClientState> 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);
}
}
}

@ -1,9 +1,7 @@
package eu.jonahbauer.wizard.client.cli.state; package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client; import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.common.messages.server.NackMessage; import eu.jonahbauer.wizard.common.messages.server.*;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.messages.server.SessionJoinedMessage;
import java.util.Optional; import java.util.Optional;
@ -28,6 +26,9 @@ public final class AwaitingJoinSession extends Awaiting {
default -> { return super.onMessage(client, message); } default -> { return super.onMessage(client, message); }
} }
return Optional.of(new AwaitingJoinLobby()); return Optional.of(new AwaitingJoinLobby());
} else if (message instanceof SessionModifiedMessage || message instanceof SessionRemovedMessage) {
// drop
return Optional.empty();
} else { } else {
return super.onMessage(client, message); return super.onMessage(client, message);
} }

@ -31,6 +31,9 @@ public abstract class BaseState implements ClientState {
protected static Optional<ClientState> unexpectedMessage(Client client, ServerMessage message) { protected static Optional<ClientState> unexpectedMessage(Client client, ServerMessage message) {
// return to menu on unexpected message // return to menu on unexpected message
client.println("Fatal: Unexpected message " + message + ". Returning to menu."); client.println("Fatal: Unexpected message " + message + ". Returning to menu.");
if (client.isVerbose()) {
new Exception().printStackTrace();
}
return Optional.of(new Menu()); return Optional.of(new Menu());
} }
} }

@ -5,7 +5,9 @@ import eu.jonahbauer.wizard.client.cli.commands.GameCommand;
import eu.jonahbauer.wizard.client.cli.util.Pair; import eu.jonahbauer.wizard.client.cli.util.Pair;
import eu.jonahbauer.wizard.common.messages.data.PlayerData; import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.observer.*; 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.GameMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage; import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.common.model.Card;
import lombok.Getter; import lombok.Getter;
@ -149,6 +151,18 @@ public final class Game extends BaseState {
throw new AssertionError("Unknown observer message " + observerMessage.getClass().getSimpleName() + ""); throw new AssertionError("Unknown observer message " + observerMessage.getClass().getSimpleName() + "");
} }
return Optional.empty(); 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 { } else {
return unexpectedMessage(client, message); return unexpectedMessage(client, message);
} }

@ -23,7 +23,9 @@ public final class Menu extends BaseState {
@Override @Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) { public Optional<ClientState> 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 @Override

@ -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.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.server.*; import eu.jonahbauer.wizard.common.messages.server.*;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Model.CommandSpec;
import java.util.HashMap; import java.util.HashMap;
@ -19,9 +20,12 @@ public final class Session extends BaseState {
private final UUID self; private final UUID self;
private final UUID session; private final UUID session;
private final boolean ready;
private final Map<UUID, PlayerData> players = new HashMap<>(); private final Map<UUID, PlayerData> players = new HashMap<>();
private boolean ready;
@Setter
private Boolean nextReady;
public Session(SessionJoinedMessage joined) { public Session(SessionJoinedMessage joined) {
this.self = joined.getPlayer(); this.self = joined.getPlayer();
this.session = joined.getSession(); this.session = joined.getSession();
@ -68,6 +72,16 @@ public final class Session extends BaseState {
session, session,
players.values().stream().collect(Collectors.toMap(PlayerData::getUuid, PlayerData::getName)) 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 { } else {
return unexpectedMessage(client, message); return unexpectedMessage(client, message);
} }

@ -1,15 +1,16 @@
plugins { plugins {
id("org.springframework.boot").version("2.5.6") id(SpringBoot.plugin) version SpringBoot.version
//id("io.spring.dependency-management").version("1.0.7-RELEASE") id(SpringDependencyManagement.plugin) version SpringDependencyManagement.version
id("java") id("java")
} id("war")
repositories {
mavenCentral()
} }
dependencies { dependencies {
implementation(project(":wizard-core")) implementation(project(":wizard-core"))
implementation(project(":wizard-common")) 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)
} }

@ -9,19 +9,23 @@ import eu.jonahbauer.wizard.common.model.Configuration;
import eu.jonahbauer.wizard.core.machine.Game; import eu.jonahbauer.wizard.core.machine.Game;
import eu.jonahbauer.wizard.core.messages.Observer; import eu.jonahbauer.wizard.core.messages.Observer;
import eu.jonahbauer.wizard.core.model.Configurations; import eu.jonahbauer.wizard.core.model.Configurations;
import eu.jonahbauer.wizard.core.model.GameConfiguration;
import eu.jonahbauer.wizard.server.machine.Player; import eu.jonahbauer.wizard.server.machine.Player;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
@Getter @Getter
@Log4j2
@EqualsAndHashCode(of = "uuid") @EqualsAndHashCode(of = "uuid")
public class Session implements Observer { public class Session implements Observer {
private static final int MIN_PLAYERS = 3; 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 * session with a {@link PlayerJoinedMessage}, the joining player with a {@link SessionJoinedMessage} and all
* players in the lobby with a {@link SessionModifiedMessage}. * players in the lobby with a {@link SessionModifiedMessage}.
* *
@ -55,18 +59,19 @@ public class Session implements Observer {
* @return the players uuid * @return the players uuid
*/ */
public synchronized UUID join(Player player, String name) { 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."); throw new NackException(NackMessage.SESSION_FULL, "Session is full.");
} else if (players.values().stream().anyMatch(p -> p.getName().equalsIgnoreCase(name))) { } else if (players.values().stream().anyMatch(p -> p.getName().equalsIgnoreCase(name))) {
throw new NackException(NackMessage.NAME_TAKEN, "Name is already taken."); throw new NackException(NackMessage.NAME_TAKEN, "Name is already taken.");
} }
Lobby.getInstance().leave(player);
SessionPlayer sessionPlayer; SessionPlayer sessionPlayer;
do { do {
sessionPlayer = new SessionPlayer(UUID.randomUUID(), name, player); sessionPlayer = new SessionPlayer(UUID.randomUUID(), name);
} while (players.putIfAbsent(sessionPlayer.getUuid(), sessionPlayer) != null); } while (players.putIfAbsent(sessionPlayer.getUuid(), sessionPlayer) != null);
sessionPlayer.setPlayer(player);
notifyJoined(sessionPlayer.toData()); notifyJoined(sessionPlayer.toData());
Lobby.getInstance().notifyPlayers(new SessionModifiedMessage(toData())); Lobby.getInstance().notifyPlayers(new SessionModifiedMessage(toData()));
@ -75,54 +80,120 @@ public class Session implements Observer {
} }
public synchronized void leave(UUID player) { public synchronized void leave(UUID player) {
if (players.remove(player) != null) { if (game == null) {
if (players.size() == 0) { if (players.remove(player) != null) {
Lobby.getInstance().removeSession(uuid); if (players.size() == 0) {
} else { Lobby.getInstance().removeSession(uuid);
notifyPlayers(new PlayerLeftMessage(player)); } else {
Lobby.getInstance().notifyPlayers(new SessionModifiedMessage(toData())); 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) { public synchronized void ready(UUID uuid, boolean ready) {
var sessionPlayer = players.get(player); var player = players.get(uuid);
if (sessionPlayer == null) { if (player == null) {
throw new NackException(NackMessage.PLAYER_NOT_FOUND, "Who are you?"); 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) { player.send(new AckMessage());
sessionPlayer.setReady(ready); if (player.isReady() != ready) {
sessionPlayer.getPlayer().send(new AckMessage()); player.setReady(ready);
notifyPlayers(new PlayerModifiedMessage(sessionPlayer.toData())); notifyPlayers(new PlayerModifiedMessage(player.toData()));
} }
if (players.size() >= MIN_PLAYERS && players.values().stream().allMatch(SessionPlayer::isReady)) { if (players.size() >= MIN_PLAYERS && players.values().stream().allMatch(SessionPlayer::isReady)) {
game = new Game(Configurations.get(configuration), this); startGame();
notifyPlayers(new StartingGameMessage());
game.start(players.keySet().stream().toList());
} }
} }
/**
* 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) { private void notifyJoined(PlayerData joined) {
var message = new PlayerJoinedMessage(joined); var message = new PlayerJoinedMessage(joined);
for (SessionPlayer player : players.values()) { for (SessionPlayer player : players.values()) {
if (player.getUuid().equals(joined.getUuid())) { if (player.getUuid().equals(joined.getUuid())) {
player.getPlayer().send(new SessionJoinedMessage( player.send(new SessionJoinedMessage(
getUuid(), getUuid(),
player.getUuid(), player.getUuid(),
players.values().stream().map(SessionPlayer::toData).toList(), players.values().stream().map(SessionPlayer::toData).toList(),
player.getSecret() player.getSecret()
)); ));
} else { } else {
player.getPlayer().send(message); player.send(message);
} }
} }
} }
private void notifyPlayers(ServerMessage message) { private void notifyPlayers(ServerMessage message) {
for (SessionPlayer player : players.values()) { 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); 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 @Override
public void notify(ObserverMessage message) { public void notify(ObserverMessage message) {
notifyPlayers(new GameMessage(message)); notifyPlayers(new GameMessage(message));
@ -147,7 +208,7 @@ public class Session implements Observer {
@Override @Override
public void notify(UUID player, ObserverMessage message) { public void notify(UUID player, ObserverMessage message) {
players.get(player).getPlayer().send(new GameMessage(message)); players.get(player).send(new GameMessage(message));
} }
@Data @Data
@ -155,8 +216,8 @@ public class Session implements Observer {
private static class SessionPlayer { private static class SessionPlayer {
private final UUID uuid; private final UUID uuid;
private final String name; private final String name;
private final Player player;
private final String secret = generateSecret(); private final String secret = generateSecret();
private Player player;
private boolean ready; private boolean ready;
private static String generateSecret() { private static String generateSecret() {
@ -169,5 +230,21 @@ public class Session implements Observer {
public PlayerData toData() { public PlayerData toData() {
return new PlayerData(uuid, name, ready); 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;
}
} }
} }

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

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

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

@ -23,16 +23,33 @@ public class LobbyState implements ClientState {
public Optional<ClientState> onMessage(Player player, ClientMessage message) { public Optional<ClientState> onMessage(Player player, ClientMessage message) {
if (message instanceof CreateSessionMessage create) { if (message instanceof CreateSessionMessage create) {
Lobby.getInstance().leave(player); Lobby.getInstance().leave(player);
var session = Lobby.getInstance().createSession(create); try {
var uuid = session.join(player, create.getPlayerName()); player.buffer();
return Optional.of(new SessionState(session.getUuid(), uuid, create.getPlayerName())); 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) { } else if (message instanceof JoinSessionMessage join) {
var session = Lobby.getInstance().getSession(join.getSession()); Lobby.getInstance().leave(player);
if (session == null) { try {
throw new NackException(NackMessage.NOT_FOUND, "Session not found."); player.buffer();
} else { var session = Lobby.getInstance().getSession(join.getSession());
var uuid = session.join(player, join.getPlayerName()); if (session == null) {
return Optional.of(new SessionState(session.getUuid(), uuid, join.getPlayerName())); 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 { } else {
return ClientState.super.onMessage(player, message); return ClientState.super.onMessage(player, message);

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

@ -1,6 +1,9 @@
package eu.jonahbauer.wizard.server.socket; 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.client.ClientMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.server.NackException;
import eu.jonahbauer.wizard.server.machine.Player; import eu.jonahbauer.wizard.server.machine.Player;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -29,7 +32,14 @@ public class WizardSocketHandler extends TextWebSocketHandler {
@Override @Override
protected void handleTextMessage(@NotNull WebSocketSession session, @NotNull TextMessage text) { 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 @Override

Loading…
Cancel
Save