diff --git a/src/main/java/de/towerdefence/server/match/Match.java b/src/main/java/de/towerdefence/server/match/Match.java index b36f0ef..491ec9e 100644 --- a/src/main/java/de/towerdefence/server/match/Match.java +++ b/src/main/java/de/towerdefence/server/match/Match.java @@ -1,5 +1,6 @@ package de.towerdefence.server.match; +import de.towerdefence.server.match.confirmation.ConfirmationCallbacks; import de.towerdefence.server.player.Player; import lombok.AllArgsConstructor; import lombok.Getter; @@ -7,7 +8,8 @@ import lombok.Getter; @AllArgsConstructor @Getter public class Match { + private final String matchId; - private final Player playerOne; - private final Player playerTwo; + private final Player player1; + private final Player player2; } diff --git a/src/main/java/de/towerdefence/server/match/MatchService.java b/src/main/java/de/towerdefence/server/match/MatchService.java new file mode 100644 index 0000000..3c55d9f --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/MatchService.java @@ -0,0 +1,22 @@ +package de.towerdefence.server.match; + +import de.towerdefence.server.player.Player; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class MatchService { + private final Map playerMatches = new HashMap<>(); + + public void createMatch(String matchId, Player player1, Player player2) { + Match match = new Match(matchId, player1, player2); + playerMatches.put(player1, match); + playerMatches.put(player2, match); + } + + public Match get(Player player) { + return playerMatches.get(player); + } +} diff --git a/src/main/java/de/towerdefence/server/match/confirmation/MatchConfirmationService.java b/src/main/java/de/towerdefence/server/match/confirmation/MatchConfirmationService.java index d48c93a..377e2d8 100644 --- a/src/main/java/de/towerdefence/server/match/confirmation/MatchConfirmationService.java +++ b/src/main/java/de/towerdefence/server/match/confirmation/MatchConfirmationService.java @@ -1,28 +1,76 @@ package de.towerdefence.server.match.confirmation; +import de.towerdefence.server.match.MatchService; import de.towerdefence.server.player.Player; +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.*; +@AllArgsConstructor @Service public class MatchConfirmationService { - + private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final Map unconfirmedMatch = new HashMap<>(); + final Map> matchAbortTasks = new ConcurrentHashMap<>(); + + @Autowired + private final MatchService matchService; public UnconfirmedMatch createMatch(Player player1, - Player player2, - ConfirmationCallbacks player1Callbacks, - ConfirmationCallbacks player2callbacks) { + Player player2, + ConfirmationCallbacks player1Callbacks, + ConfirmationCallbacks player2Callbacks) { UnconfirmedMatch match = new UnconfirmedMatch( - player1, - player2, - player1Callbacks, - player2callbacks); + player1, + player2, + player1Callbacks, + player2Callbacks); unconfirmedMatch.put(player1, match); unconfirmedMatch.put(player2, match); + ScheduledFuture scheduledTask = scheduler.schedule( + () -> { + matchAbortTasks.remove(match); + unconfirmedMatch.remove(match.getPlayer1()); + unconfirmedMatch.remove(match.getPlayer2()); + if (match.getPlayer1State() == PlayerMatchConfirmState.CONFIRMED ) { + player1Callbacks.getRequeueCallback().call( + match.getPlayer1(), + match.getMatchId(), + player1Callbacks.getFoundCallback(), + player1Callbacks.getQueuedCallback(), + player1Callbacks.getAbortCallback(), + player1Callbacks.getEstablishedCallback()); + } else { + player1Callbacks.getAbortCallback().call( + match.getPlayer1(), + match.getMatchId() + ); + } + if (match.getPlayer2State() == PlayerMatchConfirmState.CONFIRMED) { + player2Callbacks.getRequeueCallback().call( + match.getPlayer2(), + match.getMatchId(), + player2Callbacks.getFoundCallback(), + player2Callbacks.getQueuedCallback(), + player2Callbacks.getAbortCallback(), + player2Callbacks.getEstablishedCallback()); + } else { + player2Callbacks.getAbortCallback().call( + match.getPlayer2(), + match.getMatchId() + ); + } + }, + UnconfirmedMatch.TTL, + TimeUnit.MILLISECONDS + ); + matchAbortTasks.put(match, scheduledTask); return match; } @@ -63,6 +111,8 @@ public class MatchConfirmationService { if (state == UnconfirmedMatchState.WAITING) { return; } + matchAbortTasks.get(match).cancel(true); + matchAbortTasks.remove(match); unconfirmedMatch.remove(match.getPlayer1()); unconfirmedMatch.remove(match.getPlayer2()); @@ -71,30 +121,91 @@ public class MatchConfirmationService { ConfirmationCallbacks player2Callbacks = match.getPlayer2Callbacks(); switch (state) { - case ABORTED -> { - if (match.getPlayer1State() == PlayerMatchConfirmState.CONFIRMED) { - player1Callbacks.getRequeueCallback().call( - match.getPlayer1(), - match.getMatchId(), - player1Callbacks.getFoundCallback(), - player1Callbacks.getQueuedCallback(), - player1Callbacks.getAbortCallback(), - player1Callbacks.getEstablishedCallback()); - } - if (match.getPlayer2State() == PlayerMatchConfirmState.CONFIRMED) { - player2Callbacks.getRequeueCallback().call( - match.getPlayer2(), - match.getMatchId(), - player2Callbacks.getFoundCallback(), - player2Callbacks.getQueuedCallback(), - player2Callbacks.getAbortCallback(), - player2Callbacks.getEstablishedCallback()); - } + case ABORTED -> { + if (match.getPlayer1State() != PlayerMatchConfirmState.ABORTED ) { + player1Callbacks.getRequeueCallback().call( + match.getPlayer1(), + match.getMatchId(), + player1Callbacks.getFoundCallback(), + player1Callbacks.getQueuedCallback(), + player1Callbacks.getAbortCallback(), + player1Callbacks.getEstablishedCallback()); + } else { + player1Callbacks.getAbortCallback().call( + match.getPlayer1(), + match.getMatchId() + ); } - case CONFIRMED -> { - // TODO: Create Match and Send Players the info that the Match is created + if (match.getPlayer2State() != PlayerMatchConfirmState.ABORTED) { + player2Callbacks.getRequeueCallback().call( + match.getPlayer2(), + match.getMatchId(), + player2Callbacks.getFoundCallback(), + player2Callbacks.getQueuedCallback(), + player2Callbacks.getAbortCallback(), + player2Callbacks.getEstablishedCallback()); + } else { + player2Callbacks.getAbortCallback().call( + match.getPlayer2(), + match.getMatchId() + ); } } + case CONFIRMED -> { + + boolean player1successful = sendPlayerEstablished( + match.getMatchId(), + player1Callbacks.getEstablishedCallback(), + match.getPlayer1(), + match.getPlayer2() + ); + boolean player2successful = sendPlayerEstablished( + match.getMatchId(), + player2Callbacks.getEstablishedCallback(), + match.getPlayer2(), + match.getPlayer1() + ); + if (!player1successful || !player2successful) { + if (player1successful) { + player1Callbacks.getRequeueCallback().call( + match.getPlayer1(), + match.getMatchId(), + player1Callbacks.getFoundCallback(), + player1Callbacks.getQueuedCallback(), + player1Callbacks.getAbortCallback(), + player1Callbacks.getEstablishedCallback()); + } + if (player2successful) { + player2Callbacks.getRequeueCallback().call( + match.getPlayer2(), + match.getMatchId(), + player2Callbacks.getFoundCallback(), + player2Callbacks.getQueuedCallback(), + player2Callbacks.getAbortCallback(), + player2Callbacks.getEstablishedCallback()); + } + return; + } + matchService.createMatch(match.getMatchId(), match.getPlayer1(), match.getPlayer2()); + } + } + } + + /** + * @return if successful + */ + private boolean sendPlayerEstablished( + String matchId, + EstablishedCallback callback, + Player player, + Player opponent + ) { + try { + callback.call(player, matchId, opponent); + } catch (IOException ignored) { + return false; + } + return true; } private Optional getPlayerMatch(Player player, String matchId) { diff --git a/src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatch.java b/src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatch.java index eceef0c..a802907 100644 --- a/src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatch.java +++ b/src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatch.java @@ -18,10 +18,10 @@ public class UnconfirmedMatch { private final ConfirmationCallbacks player2Callbacks; public UnconfirmedMatch( - Player player1, - Player player2, - ConfirmationCallbacks player1Callbacks, - ConfirmationCallbacks player2Callbacks) { + Player player1, + Player player2, + ConfirmationCallbacks player1Callbacks, + ConfirmationCallbacks player2Callbacks) { this.player1 = player1; this.player2 = player2; this.player1Callbacks = player1Callbacks; @@ -35,16 +35,21 @@ public class UnconfirmedMatch { public Optional setPlayerConfirmState(Player player, PlayerMatchConfirmState state) { Optional matchPlayer = getMatchPlayer(player); - if(matchPlayer.isEmpty()){ + if (matchPlayer.isEmpty()) { return Optional.empty(); } - switch (matchPlayer.get()){ - case ONE -> player1State = state; - case TWO -> player2State = state; + switch (matchPlayer.get()) { + case ONE -> player1State = state; + case TWO -> player2State = state; } - if (player1State == PlayerMatchConfirmState.ABORTED || player2State == PlayerMatchConfirmState.ABORTED) { + boolean timedOut = Instant.now().toEpochMilli() > created + TTL; + if ( + timedOut + || player1State == PlayerMatchConfirmState.ABORTED + || player2State == PlayerMatchConfirmState.ABORTED + ) { return Optional.of(UnconfirmedMatchState.ABORTED); } if (player1State == PlayerMatchConfirmState.UNKNOWN || player2State == PlayerMatchConfirmState.UNKNOWN) { @@ -53,31 +58,31 @@ public class UnconfirmedMatch { return Optional.of(UnconfirmedMatchState.CONFIRMED); } - public Optional getPlayerState(Player player){ + public Optional getPlayerState(Player player) { Optional matchPlayer = getMatchPlayer(player); - if(matchPlayer.isEmpty()){ + if (matchPlayer.isEmpty()) { return Optional.empty(); } - return switch (matchPlayer.get()){ + return switch (matchPlayer.get()) { case ONE -> Optional.of(player1State); case TWO -> Optional.of(player2State); }; } - private enum MatchPlayer{ + private enum MatchPlayer { ONE, TWO } - private Optional getMatchPlayer(Player player){ + private Optional getMatchPlayer(Player player) { boolean isPlayerOne = player.equals(player1); boolean isPlayerTwo = player.equals(player2); if (!isPlayerOne && !isPlayerTwo) { return Optional.empty(); } - if (isPlayerOne){ + if (isPlayerOne) { return Optional.of(MatchPlayer.ONE); } return Optional.of(MatchPlayer.TWO); diff --git a/src/main/java/de/towerdefence/server/server/channels/WebsocketConfig.java b/src/main/java/de/towerdefence/server/server/channels/WebsocketConfig.java index 6a41a8d..aa3c14a 100644 --- a/src/main/java/de/towerdefence/server/server/channels/WebsocketConfig.java +++ b/src/main/java/de/towerdefence/server/server/channels/WebsocketConfig.java @@ -1,11 +1,12 @@ package de.towerdefence.server.server.channels; +import de.towerdefence.server.match.MatchService; import de.towerdefence.server.match.confirmation.MatchConfirmationService; import de.towerdefence.server.match.queue.MatchQueueService; import de.towerdefence.server.server.JsonWebsocketHandler; import de.towerdefence.server.server.channels.connection.ConnectionWebsocketHandler; +import de.towerdefence.server.server.channels.match.MatchWebsocketHandler; import de.towerdefence.server.server.channels.matchmaking.MatchmakingWebsocketHandler; -import de.towerdefence.server.server.channels.time.TimeWebsocketHandler; import de.towerdefence.server.session.SessionsService; import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; @@ -25,6 +26,8 @@ public class WebsocketConfig implements WebSocketConfigurer { private final MatchQueueService matchQueueService; @Autowired private final MatchConfirmationService matchConfirmationService; + @Autowired + private final MatchService matchService; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { @@ -34,7 +37,7 @@ public class WebsocketConfig implements WebSocketConfigurer { this.matchQueueService, this.matchConfirmationService )); - registerJsonChannel(registry, new TimeWebsocketHandler(this.sessionsService)); + registerJsonChannel(registry, new MatchWebsocketHandler(this.sessionsService, this.matchService)); } private void registerJsonChannel(WebSocketHandlerRegistry registry, JsonWebsocketHandler handler){ diff --git a/src/main/java/de/towerdefence/server/server/channels/connection/ConnectionWebsocketHandler.java b/src/main/java/de/towerdefence/server/server/channels/connection/ConnectionWebsocketHandler.java index 688e1ec..fa6236e 100644 --- a/src/main/java/de/towerdefence/server/server/channels/connection/ConnectionWebsocketHandler.java +++ b/src/main/java/de/towerdefence/server/server/channels/connection/ConnectionWebsocketHandler.java @@ -34,6 +34,9 @@ public class ConnectionWebsocketHandler extends JsonWebsocketHandler { private void handleRequestConnectionToken(WebSocketSession session, String payload) throws Exception { RequestConnectionTokenMessage msg = objectMapper.readValue(payload, RequestConnectionTokenMessage.class); Player player = this.sessionPlayers.get(session); + if (msg.getChannel() != Channel.MATCHMAKING) { + return; + } String jwt = this.sessionsService.createSession(player, msg.getChannel()); new ProvidedConnectionTokenMessage(msg.getChannel(), jwt).send(session); } diff --git a/src/main/java/de/towerdefence/server/server/channels/time/TimeWebsocketHandler.java b/src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java similarity index 56% rename from src/main/java/de/towerdefence/server/server/channels/time/TimeWebsocketHandler.java rename to src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java index 4af9371..59986d7 100644 --- a/src/main/java/de/towerdefence/server/server/channels/time/TimeWebsocketHandler.java +++ b/src/main/java/de/towerdefence/server/server/channels/match/MatchWebsocketHandler.java @@ -1,5 +1,6 @@ -package de.towerdefence.server.server.channels.time; +package de.towerdefence.server.server.channels.match; +import de.towerdefence.server.match.MatchService; import de.towerdefence.server.server.JsonWebsocketHandler; import de.towerdefence.server.session.Channel; import de.towerdefence.server.session.SessionsService; @@ -9,24 +10,25 @@ import java.io.IOException; import java.util.Map; import java.util.concurrent.*; -public class TimeWebsocketHandler extends JsonWebsocketHandler { +public class MatchWebsocketHandler extends JsonWebsocketHandler { - private final Map> sessionTaskMap = new ConcurrentHashMap<>(); + final Map> sessionTaskMap = new ConcurrentHashMap<>(); private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private final MatchService matchService; - public TimeWebsocketHandler(SessionsService sessionsService) { - super(Channel.TIME, sessionsService); + public MatchWebsocketHandler(SessionsService sessionsService, MatchService matchService) { + super(Channel.MATCH, sessionsService); + this.matchService = matchService; } @Override public void afterConnectionEstablished(WebSocketSession session) { super.afterConnectionEstablished(session); - ScheduledFuture scheduledTask = scheduler.scheduleAtFixedRate( - () -> sendCurrentTime(session), - 0, - 1, - TimeUnit.MILLISECONDS + () -> sendCurrentTime(session), + 0, + 1, + TimeUnit.SECONDS ); sessionTaskMap.put(session, scheduledTask); } @@ -34,10 +36,13 @@ public class TimeWebsocketHandler extends JsonWebsocketHandler { private void sendCurrentTime(WebSocketSession session) { ScheduledFuture task = sessionTaskMap.get(session); try { - if(!session.isOpen()){ + if (!session.isOpen()) { throw new RuntimeException("Session is not open"); } - new TimeMessage(System.currentTimeMillis()).send(session); + new TimeMessage( + System.currentTimeMillis(), + matchService.get(this.sessionPlayers.get(session)).getMatchId() + ).send(session); } catch (RuntimeException | IOException e) { task.cancel(true); sessionTaskMap.remove(session); diff --git a/src/main/java/de/towerdefence/server/server/channels/time/TimeMessage.java b/src/main/java/de/towerdefence/server/server/channels/match/TimeMessage.java similarity index 68% rename from src/main/java/de/towerdefence/server/server/channels/time/TimeMessage.java rename to src/main/java/de/towerdefence/server/server/channels/match/TimeMessage.java index 9093194..6072dbd 100644 --- a/src/main/java/de/towerdefence/server/server/channels/time/TimeMessage.java +++ b/src/main/java/de/towerdefence/server/server/channels/match/TimeMessage.java @@ -1,4 +1,4 @@ -package de.towerdefence.server.server.channels.time; +package de.towerdefence.server.server.channels.match; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; @@ -10,6 +10,7 @@ import java.util.Map; @AllArgsConstructor public class TimeMessage extends JsonMessage { private final long time; + private final String matchId; @Override protected String getMessageId() { @@ -18,6 +19,9 @@ public class TimeMessage extends JsonMessage { @Override protected Map getData(JsonNodeFactory factory) { - return Map.of("time", factory.numberNode(this.time)); + return Map.of( + "time", factory.numberNode(this.time), + "matchId", factory.textNode(this.matchId) + ); } } diff --git a/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketHandler.java b/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketHandler.java index a54713c..719b7a5 100644 --- a/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketHandler.java +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketHandler.java @@ -46,6 +46,7 @@ public class MatchmakingWebsocketHandler extends JsonWebsocketHandler { @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { try { + String payload = message.getPayload(); switch (objectMapper.readTree(payload).get("$id").asText()) { case MatchSetSearchStateMessage.MESSAGE_ID -> handleMatchSetSearchStateMessage(session, payload); @@ -96,7 +97,8 @@ public class MatchmakingWebsocketHandler extends JsonWebsocketHandler { private void onEstablished(Player player, String matchId, Player opponent) throws IOException { WebSocketSession session = playerSessions.get(player); - MatchEstablishedMessage msg = new MatchEstablishedMessage(matchId, opponent.getUsername()); + String token = this.sessionsService.createSession(player, Channel.MATCH); + MatchEstablishedMessage msg = new MatchEstablishedMessage(matchId, opponent.getUsername(), token); msg.send(session); } diff --git a/src/main/java/de/towerdefence/server/server/channels/matchmaking/bi/MatchSetSearchStateMessage.java b/src/main/java/de/towerdefence/server/server/channels/matchmaking/bi/MatchSetSearchStateMessage.java index c47427d..7049457 100644 --- a/src/main/java/de/towerdefence/server/server/channels/matchmaking/bi/MatchSetSearchStateMessage.java +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/bi/MatchSetSearchStateMessage.java @@ -1,5 +1,6 @@ package de.towerdefence.server.server.channels.matchmaking.bi; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; @@ -12,7 +13,6 @@ import java.util.Map; @Getter @NotNull -@AllArgsConstructor public class MatchSetSearchStateMessage extends JsonMessage { public static final String MESSAGE_ID = "MatchSetSearchState"; @@ -26,6 +26,15 @@ public class MatchSetSearchStateMessage extends JsonMessage { this(MESSAGE_ID, searching); } + @JsonCreator + public MatchSetSearchStateMessage( + @JsonProperty("$id") String messageId, + @JsonProperty("searching") boolean searching + ) { + this.messageId = messageId; + this.searching = searching; + } + @Override protected Map getData(JsonNodeFactory factory) { return Map.of( diff --git a/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchEstablishedMessage.java b/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchEstablishedMessage.java index 01d2980..00531c2 100644 --- a/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchEstablishedMessage.java +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchEstablishedMessage.java @@ -11,6 +11,7 @@ import java.util.Map; public class MatchEstablishedMessage extends JsonMessage { private String matchId; private String opponentName; + private String token; @Override protected String getMessageId() { @@ -20,8 +21,9 @@ public class MatchEstablishedMessage extends JsonMessage { @Override protected Map getData(JsonNodeFactory factory) { return Map.of( - "matchId", factory.textNode(this.matchId), - "opponentName", factory.textNode(this.opponentName) + "matchId", factory.textNode(this.matchId), + "opponentName", factory.textNode(this.opponentName), + "token", factory.textNode(this.token) ); } } diff --git a/src/main/java/de/towerdefence/server/session/Channel.java b/src/main/java/de/towerdefence/server/session/Channel.java index 74704d0..b68f108 100644 --- a/src/main/java/de/towerdefence/server/session/Channel.java +++ b/src/main/java/de/towerdefence/server/session/Channel.java @@ -9,7 +9,7 @@ import lombok.Getter; public enum Channel { CONNECTION("connection"), MATCHMAKING("matchmaking"), - TIME("time"); + MATCH("match"); private final String jsonName; diff --git a/src/main/resources/spotbugs-exclude.xml b/src/main/resources/spotbugs-exclude.xml index a574128..033d812 100644 --- a/src/main/resources/spotbugs-exclude.xml +++ b/src/main/resources/spotbugs-exclude.xml @@ -14,4 +14,9 @@ + + + + + diff --git a/ws/ws.yml b/ws/ws.yml index f73ddb6..36972e6 100644 --- a/ws/ws.yml +++ b/ws/ws.yml @@ -66,7 +66,7 @@ channels: matchmaking: title: Matchmaking - description: | + description: | A Channel used to search for a match and to receive one messages: @@ -147,16 +147,18 @@ channels: type: string opponentName: type: string + token: + $ref: "#/components/schemas/JWT" required: - $id - matchId - opponentName + - token - time: - title: Time + match: + title: Match description: | - A Simple example channel for receiving - the current Unix time + Channel for managing an active match messages: CurrentUnixTime: description: The Current time in Unix Time @@ -170,9 +172,14 @@ channels: time: type: integer format: int64 + matchId: + type: string required: - $id - time + - matchId + + operations: requestConnectionToken: @@ -227,13 +234,13 @@ operations: $ref: "#/channels/matchmaking" messages: - $ref: "#/channels/matchmaking/messages/MatchEstablished" - updateTime: - title: Updates of the current Unix Time + matchUpdate: + title: MatchUpdate action: receive channel: - $ref: "#/channels/time" + $ref: "#/channels/match" messages: - - $ref: "#/channels/time/messages/CurrentUnixTime" + - $ref: "#/channels/match/messages/CurrentUnixTime" components: securitySchemes: @@ -252,7 +259,4 @@ components: Channel: type: string enum: - - connection - matchmaking - - time -