main
Alexander Hirmer 3 years ago
parent bb16c5fa03
commit 9c64f0397a

@ -2,12 +2,19 @@ package eu.jonahbauer.wizard.common.messages.server;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import org.intellij.lang.annotations.MagicConstant;
@Getter @Getter
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public final class NackMessage extends ServerMessage implements Response { public final class NackMessage extends ServerMessage implements Response {
public static final int BAD_REQUEST = 400;
public static final int NOT_FOUND = 404;
private final int code; private final int code;
private final String message; private final String message;
public NackMessage(@MagicConstant(valuesFromClass = NackMessage.class) int code, String message) {
this.code = code;
this.message = message;
}
} }

@ -1,4 +1,15 @@
plugins {
id("org.springframework.boot").version("2.5.6")
//id("io.spring.dependency-management").version("1.0.7-RELEASE")
id("java")
}
repositories {
mavenCentral()
}
dependencies { dependencies {
implementation(project(":wizard-core")) implementation(project(":wizard-core"))
implementation(project(":wizard-common"))
implementation("org.springframework.boot:spring-boot-starter-websocket:2.5.6")
} }

@ -0,0 +1,90 @@
package eu.jonahbauer.wizard.server;
import eu.jonahbauer.wizard.common.messages.client.CreateSessionMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.messages.server.SessionCreatedMessage;
import eu.jonahbauer.wizard.common.messages.server.SessionListMessage;
import eu.jonahbauer.wizard.common.messages.server.SessionRemovedMessage;
import eu.jonahbauer.wizard.server.machine.Player;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Lobby {
private static final Lobby INSTANCE = new Lobby();
public static Lobby getInstance() {
return INSTANCE;
}
private final Map<UUID, Session> sessions = new ConcurrentHashMap<>();
private final List<Player> players = new ArrayList<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private Lobby() {}
public Session createSession(CreateSessionMessage create) {
lock.readLock().lock();
try {
Session session;
do {
session = new Session(
UUID.randomUUID(),
create.getSessionName(),
create.getTimeout(),
create.getConfiguration()
);
} while (sessions.putIfAbsent(session.getUuid(), session) != null);
notifyPlayers(new SessionCreatedMessage(session.toData()));
return session;
} finally {
lock.readLock().unlock();
}
}
public Session getSession(UUID uuid) {
return sessions.get(uuid);
}
public void removeSession(UUID uuid) {
lock.readLock().lock();
try {
sessions.remove(uuid);
notifyPlayers(new SessionRemovedMessage(uuid));
} finally {
lock.readLock().unlock();
}
}
public void join(Player player) {
lock.writeLock().lock();
try {
players.add(player);
player.send(new SessionListMessage(sessions.values().stream().map(Session::toData).toList()));
} finally {
lock.writeLock().unlock();
}
}
public void leave(Player player) {
lock.writeLock().lock();
try {
players.remove(player);
} finally {
lock.writeLock().unlock();
}
}
public void notifyPlayers(ServerMessage message) {
lock.readLock().lock();
try {
for (Player player : players) {
player.send(message);
}
} finally {
lock.readLock().unlock();
}
}
}

@ -0,0 +1,22 @@
package eu.jonahbauer.wizard.server;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import org.intellij.lang.annotations.MagicConstant;
public class NackException extends RuntimeException {
private final int code;
public NackException(@MagicConstant(valuesFromClass = NackMessage.class) int code, String message) {
super(message);
this.code = code;
}
@MagicConstant(valuesFromClass = NackMessage.class)
public int getCode() {
return code;
}
public NackMessage toMessage() {
return new NackMessage(code, getMessage());
}
}

@ -0,0 +1,12 @@
package eu.jonahbauer.wizard.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Server {
public static void main(String[] args) {
SpringApplication.run(Server.class, args);
}
}

@ -0,0 +1,142 @@
package eu.jonahbauer.wizard.server;
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.data.SessionData;
import eu.jonahbauer.wizard.common.messages.server.*;
import eu.jonahbauer.wizard.common.model.Configuration;
import eu.jonahbauer.wizard.server.machine.Player;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@Getter
@EqualsAndHashCode(of = "uuid")
public class Session {
private static final int MIN_PLAYERS = 3;
private static final int MAX_PLAYERS = 6;
private final UUID uuid;
private final String name;
private final long timeout;
private final Configuration configuration;
@Getter(AccessLevel.NONE)
private final Map<UUID, SessionPlayer> players = new HashMap<>();
public Session(UUID uuid, String name, long timeout, Configuration configuration) {
this.uuid = uuid;
this.name = name;
this.timeout = timeout;
this.configuration = configuration;
}
/**
* Associates the given player with this session, removes him from the lobby, 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}.
*
* @param player the player
* @param name the players chosen name
* @return the players uuid
*/
public synchronized UUID join(Player player, String name) {
if (players.size() == MAX_PLAYERS) {
throw new NackException(NackMessage.BAD_REQUEST, "Session is full.");
} else if (players.values().stream().anyMatch(p -> p.getName().equalsIgnoreCase(name))) {
throw new NackException(NackMessage.BAD_REQUEST, "Name is already taken.");
}
Lobby.getInstance().leave(player);
SessionPlayer sessionPlayer;
do {
sessionPlayer = new SessionPlayer(UUID.randomUUID(), name, player);
} while (players.putIfAbsent(sessionPlayer.getUuid(), sessionPlayer) != null);
notifyJoined(sessionPlayer.toData());
Lobby.getInstance().notifyPlayers(new SessionModifiedMessage(toData()));
return sessionPlayer.getUuid();
}
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()));
}
}
}
public synchronized void ready(UUID player, boolean ready) {
var sessionPlayer = players.get(player);
if (sessionPlayer == null) {
throw new NackException(NackMessage.BAD_REQUEST, "Who are you?");
}
if (sessionPlayer.isReady() != ready) {
sessionPlayer.setReady(ready);
notifyPlayers(new PlayerModifiedMessage(sessionPlayer.toData()));
}
if (players.size() >= MIN_PLAYERS && players.values().stream().allMatch(SessionPlayer::isReady)) {
// TODO start game
}
}
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(
getUuid(),
player.getUuid(),
players.values().stream().map(SessionPlayer::toData).toList(),
player.getSecret()
));
} else {
player.getPlayer().send(message);
}
}
}
private void notifyPlayers(ServerMessage message) {
for (SessionPlayer player : players.values()) {
player.getPlayer().send(message);
}
}
public SessionData toData() {
return new SessionData(uuid, name, players.size(), configuration);
}
@Data
@EqualsAndHashCode(of = "uuid")
private static class SessionPlayer {
private final UUID uuid;
private final String name;
private final Player player;
private final String secret = generateSecret();
private boolean ready;
private static String generateSecret() {
return ThreadLocalRandom.current()
.ints(32, 'a', 'z' + 1)
.collect(StringBuilder::new, (sb, i) -> sb.append((char) i), StringBuilder::append)
.toString();
}
public PlayerData toData() {
return new PlayerData(uuid, name, ready);
}
}
}

@ -0,0 +1,21 @@
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 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
}
default Optional<ClientState> onMessage(Player player, ClientMessage message) {
player.send(new NackMessage(NackMessage.BAD_REQUEST, "Unexpected message."));
return Optional.empty();
}
Optional<ClientState> onClose(Player player, CloseStatus status);
}

@ -0,0 +1,43 @@
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.ServerMessage;
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;
public class Player extends Context<ClientState, Player> {
private final WebSocketSession session;
public Player(WebSocketSession session) {
super(new CreatedState());
this.session = session;
}
@Override
protected void handleError(Throwable t) {
}
public void onOpen() {
execute(s -> s.onOpen(this));
}
public void onMessage(ClientMessage message) {
execute(s -> s.onMessage(this, message));
}
public void onClose(CloseStatus status) {
execute(s -> s.onClose(this, status));
}
@SneakyThrows
public void send(ServerMessage message) {
synchronized (session) {
session.sendMessage(new TextMessage(message.toString()));
}
}
}

@ -0,0 +1,14 @@
package eu.jonahbauer.wizard.server.machine.states;
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 Closed implements ClientState {
@Override
public Optional<ClientState> onClose(Player player, CloseStatus status) {
return Optional.empty();
}
}

@ -0,0 +1,19 @@
package eu.jonahbauer.wizard.server.machine.states;
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 CreatedState implements ClientState {
@Override
public Optional<ClientState> onOpen(Player player) {
return Optional.of(new LobbyState());
}
@Override
public Optional<ClientState> onClose(Player player, CloseStatus status) {
throw new IllegalStateException();
}
}

@ -0,0 +1,46 @@
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();
}
}

@ -0,0 +1,51 @@
package eu.jonahbauer.wizard.server.machine.states;
import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
import eu.jonahbauer.wizard.common.messages.client.CreateSessionMessage;
import eu.jonahbauer.wizard.common.messages.client.JoinSessionMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.server.Lobby;
import eu.jonahbauer.wizard.server.NackException;
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 LobbyState implements ClientState {
@Override
public Optional<ClientState> onEnter(Player player) {
Lobby.getInstance().join(player);
return Optional.empty();
}
@Override
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()));
} 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()));
}
} else {
return ClientState.super.onMessage(player, message);
}
}
@Override
public Optional<ClientState> onClose(Player player, CloseStatus status) {
return Optional.of(new Closed());
}
@Override
public void onExit(Player player) {
Lobby.getInstance().leave(player);
}
}

@ -0,0 +1,55 @@
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.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;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.web.socket.CloseStatus;
import java.util.Optional;
import java.util.UUID;
@Getter
@RequiredArgsConstructor
public class SessionState implements ClientState {
private final UUID session;
private final UUID self;
private final String name;
private boolean ready;
@Override
public Optional<ClientState> onOpen(Player player) {
throw new IllegalStateException();
}
@Override
public Optional<ClientState> onMessage(Player player, ClientMessage message) {
if (message instanceof ReadyMessage ready) {
Lobby.getInstance().getSession(session).ready(self, ready.isReady());
this.ready = ready.isReady();
return Optional.empty();
} else if (message instanceof LeaveSessionMessage) {
Lobby.getInstance().getSession(session).leave(self);
return Optional.of(new LobbyState());
} else {
player.send(new NackMessage(0, "Error: Invalid Message!"));
return Optional.empty();
}
}
@Override
public Optional<ClientState> onClose(Player player, CloseStatus status) {
return Optional.of(new Closed());
}
@Override
public void onExit(Player context) {
var session = Lobby.getInstance().getSession(this.session);
if (session != null) session.leave(self);
}
}

@ -0,0 +1,26 @@
package eu.jonahbauer.wizard.server.socket;
import lombok.AccessLevel;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Getter(AccessLevel.PRIVATE)
private final WizardSocketHandler wizardSocketHandler;
public WebSocketConfig(WizardSocketHandler wizardSocketHandler) {
this.wizardSocketHandler = wizardSocketHandler;
}
@Override
public void registerWebSocketHandlers(@NotNull WebSocketHandlerRegistry registry) {
registry.addHandler(getWizardSocketHandler(), "/").setAllowedOrigins("*");
}
}

@ -0,0 +1,40 @@
package eu.jonahbauer.wizard.server.socket;
import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
import eu.jonahbauer.wizard.server.machine.Player;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Log4j2
@Component
public class WizardSocketHandler extends TextWebSocketHandler {
private final Map<String, Player> players = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(@NotNull WebSocketSession session) {
var player = new Player(session);
player.onOpen();
players.put(session.getId(), player);
log.info("Connection #{} from {}.", session.getId(), session.getRemoteAddress());
}
@Override
protected void handleTextMessage(@NotNull WebSocketSession session, @NotNull TextMessage text) {
players.get(session.getId()).onMessage(ClientMessage.parse(text.getPayload()));
}
@Override
public void afterConnectionClosed(@NotNull WebSocketSession session, @NotNull CloseStatus status) {
players.get(session.getId()).onClose(status);
log.info("Connection #{} closed {}.", session.getId(), status);
}
}
Loading…
Cancel
Save