From ada41d334050789cd528034689a5cf37db5acee6 Mon Sep 17 00:00:00 2001 From: Snoweuph Date: Wed, 19 Feb 2025 11:38:00 +0100 Subject: [PATCH 1/4] TD-18: Add Specification of Matchmaking Channel and Creation of Type --- .../matchmaking/in/MatchAcceptedMessage.java | 15 ++ .../in/MatchSetSearchStateMessage.java | 14 ++ .../matchmaking/out/MatchAbortedMessage.java | 25 ++++ .../out/MatchEstablishedMessage.java | 27 ++++ .../matchmaking/out/MatchFoundMessage.java | 29 ++++ ws/ws.yml | 137 +++++++++++++++++- 6 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchAcceptedMessage.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchSetSearchStateMessage.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchAbortedMessage.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchEstablishedMessage.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchFoundMessage.java diff --git a/src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchAcceptedMessage.java b/src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchAcceptedMessage.java new file mode 100644 index 0000000..a870392 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchAcceptedMessage.java @@ -0,0 +1,15 @@ +package de.towerdefence.server.server.channels.matchmaking.in; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@NotNull +public class MatchAcceptedMessage { + public static final String MESSAGE_ID = "MatchAccepted"; + @JsonProperty("$id") + private String messageId; + private String matchId; + private boolean accepted; +} diff --git a/src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchSetSearchStateMessage.java b/src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchSetSearchStateMessage.java new file mode 100644 index 0000000..9e53b09 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchSetSearchStateMessage.java @@ -0,0 +1,14 @@ +package de.towerdefence.server.server.channels.matchmaking.in; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@NotNull +public class MatchSetSearchStateMessage { + public static final String MESSAGE_ID = "MatchSetSearchState"; + @JsonProperty("$id") + private String messageId; + private boolean searching; +} \ No newline at end of file diff --git a/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchAbortedMessage.java b/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchAbortedMessage.java new file mode 100644 index 0000000..731c2b0 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchAbortedMessage.java @@ -0,0 +1,25 @@ +package de.towerdefence.server.server.channels.matchmaking.out; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import de.towerdefence.server.server.JsonMessage; +import lombok.AllArgsConstructor; + +import java.util.Map; + +@AllArgsConstructor +public class MatchAbortedMessage extends JsonMessage { + private String matchId; + + @Override + protected String getMessageId() { + return "MatchAborted"; + } + + @Override + protected Map getData(JsonNodeFactory factory) { + return Map.of( + "matchId", factory.textNode(this.matchId) + ); + } +} 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 new file mode 100644 index 0000000..01d2980 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchEstablishedMessage.java @@ -0,0 +1,27 @@ +package de.towerdefence.server.server.channels.matchmaking.out; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import de.towerdefence.server.server.JsonMessage; +import lombok.AllArgsConstructor; + +import java.util.Map; + +@AllArgsConstructor +public class MatchEstablishedMessage extends JsonMessage { + private String matchId; + private String opponentName; + + @Override + protected String getMessageId() { + return "MatchEstablished"; + } + + @Override + protected Map getData(JsonNodeFactory factory) { + return Map.of( + "matchId", factory.textNode(this.matchId), + "opponentName", factory.textNode(this.opponentName) + ); + } +} diff --git a/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchFoundMessage.java b/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchFoundMessage.java new file mode 100644 index 0000000..4869295 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/out/MatchFoundMessage.java @@ -0,0 +1,29 @@ +package de.towerdefence.server.server.channels.matchmaking.out; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import de.towerdefence.server.server.JsonMessage; +import lombok.AllArgsConstructor; + +import java.util.Map; + +@AllArgsConstructor +public class MatchFoundMessage extends JsonMessage { + private String matchId; + private long created; + private long ttl; + + @Override + protected String getMessageId() { + return "MatchFound"; + } + + @Override + protected Map getData(JsonNodeFactory factory) { + return Map.of( + "matchId", factory.textNode(this.matchId), + "created", factory.numberNode(this.created), + "ttl", factory.numberNode(this.ttl) + ); + } +} diff --git a/ws/ws.yml b/ws/ws.yml index af35b47..5503e26 100644 --- a/ws/ws.yml +++ b/ws/ws.yml @@ -39,9 +39,7 @@ channels: type: string format: messageId channel: - type: string - enum: - - time + $ref: "#/components/schemas/Channel" required: - $id - channel @@ -58,15 +56,102 @@ channels: type: string format: messageId channel: - type: string - enum: - - time + $ref: "#/components/schemas/Channel" token: $ref: "#/components/schemas/JWT" required: - $id - channel - token + + matchmaking: + title: Matchmaking + description: | + A Channel used to search for a match and + to receive one + messages: + MatchSetSearchState: + payload: + type: object + additionalProperties: false + properties: + $id: + type: string + format: messageId + searching: + type: boolean + required: + - $id + - searching + MatchFound: + payload: + type: object + additionalProperties: false + properties: + $id: + type: string + format: messageId + matchId: + type: string + created: + description: "Unix Timestamp describing when this Match was found" + type: integer + format: int64 + ttl: + description: "Time in Milliseconds, how long this Match is open for accepting" + type: integer + format: int64 + required: + - $id + - matchId + - created + - ttl + MatchAccepted: + payload: + type: object + additionalProperties: false + properties: + $id: + type: string + format: messageId + matchId: + type: string + accepted: + type: boolean + required: + - $id + - matchId + - accepted + MatchAborted: + payload: + type: object + additionalProperties: false + properties: + $id: + type: string + format: messageId + matchId: + type: string + required: + - $id + - matchId + MatchEstablished: + payload: + type: object + additionalProperties: false + properties: + $id: + type: string + format: messageId + matchId: + type: string + opponentName: + type: string + required: + - $id + - matchId + - opponentName + time: title: Time description: | @@ -102,6 +187,39 @@ operations: $ref: "#/channels/connection" messages: - $ref: "#/channels/connection/messages/ProvidedConnectionToken" + searchMatch: + title: SearchMatch + action: send + channel: + $ref: "#/channels/matchmaking" + messages: + - $ref: "#/channels/matchmaking/messages/MatchSetSearchState" + foundMatch: + title: FoundGame + action: receive + channel: + $ref: "#/channels/matchmaking" + messages: + - $ref: "#/channels/matchmaking/messages/MatchFound" + reply: + channel: + $ref: "#/channels/matchmaking" + messages: + - $ref: "#/channels/matchmaking/messages/MatchAccepted" + abortedMatch: + title: AbortedMatch + action: receive + channel: + $ref: "#/channels/matchmaking" + messages: + - $ref: "#/channels/matchmaking/messages/MatchAborted" + establishedMatch: + title: EstablishedMatch + action: receive + channel: + $ref: "#/channels/matchmaking" + messages: + - $ref: "#/channels/matchmaking/messages/MatchEstablished" updateTime: title: Updates of the current Unix Time action: receive @@ -124,3 +242,10 @@ components: JWT: type: string format: jwt + Channel: + type: string + enum: + - connection + - matchmaking + - time + From 2681ecf35d14ab1e80f3c6afd781ab450444e9a1 Mon Sep 17 00:00:00 2001 From: Snoweuph Date: Wed, 19 Feb 2025 12:25:55 +0100 Subject: [PATCH 2/4] TD-18: Creating Websocket Handler --- .../MatchmakingWebsocketConfig.java | 25 +++++++++++++++++++ .../MatchmakingWebsocketHandler.java | 11 ++++++++ .../towerdefence/server/session/Channel.java | 1 + 3 files changed, 37 insertions(+) create mode 100644 src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketConfig.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketHandler.java diff --git a/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketConfig.java b/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketConfig.java new file mode 100644 index 0000000..53930ed --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketConfig.java @@ -0,0 +1,25 @@ +package de.towerdefence.server.server.channels.matchmaking; + +import de.towerdefence.server.server.channels.connection.ConnectionWebsocketHandler; +import de.towerdefence.server.session.SessionsService; +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +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; + +@AllArgsConstructor +@Configuration +@EnableWebSocket +public class MatchmakingWebsocketConfig implements WebSocketConfigurer { + @Autowired + private final SessionsService sessionsService; + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler( + new MatchmakingWebsocketHandler(this.sessionsService), + "/ws/matchmaking" + ).setAllowedOrigins("*"); + } +} 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 new file mode 100644 index 0000000..46b0ac4 --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketHandler.java @@ -0,0 +1,11 @@ +package de.towerdefence.server.server.channels.matchmaking; + +import de.towerdefence.server.server.JsonWebsocketHandler; +import de.towerdefence.server.session.Channel; +import de.towerdefence.server.session.SessionsService; + +public class MatchmakingWebsocketHandler extends JsonWebsocketHandler { + public MatchmakingWebsocketHandler(SessionsService sessionsService) { + super(Channel.MATCHMAKING, sessionsService); + } +} diff --git a/src/main/java/de/towerdefence/server/session/Channel.java b/src/main/java/de/towerdefence/server/session/Channel.java index 3e4aece..74704d0 100644 --- a/src/main/java/de/towerdefence/server/session/Channel.java +++ b/src/main/java/de/towerdefence/server/session/Channel.java @@ -8,6 +8,7 @@ import lombok.Getter; @AllArgsConstructor public enum Channel { CONNECTION("connection"), + MATCHMAKING("matchmaking"), TIME("time"); private final String jsonName; From 41509b62424c19ffeb050bfea9a24442eebc016b Mon Sep 17 00:00:00 2001 From: Snoweuph Date: Wed, 19 Feb 2025 12:31:33 +0100 Subject: [PATCH 3/4] TD-18: Refactor Channel Registration --- .../server/server/JsonWebsocketHandler.java | 2 +- .../server/channels/WebsocketConfig.java | 35 +++++++++++++++++++ .../connection/ConnectionWebsocketConfig.java | 24 ------------- .../MatchmakingWebsocketConfig.java | 25 ------------- .../channels/time/TimeWebsocketConfig.java | 24 ------------- 5 files changed, 36 insertions(+), 74 deletions(-) create mode 100644 src/main/java/de/towerdefence/server/server/channels/WebsocketConfig.java delete mode 100644 src/main/java/de/towerdefence/server/server/channels/connection/ConnectionWebsocketConfig.java delete mode 100644 src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketConfig.java delete mode 100644 src/main/java/de/towerdefence/server/server/channels/time/TimeWebsocketConfig.java diff --git a/src/main/java/de/towerdefence/server/server/JsonWebsocketHandler.java b/src/main/java/de/towerdefence/server/server/JsonWebsocketHandler.java index 5d351cb..29fd481 100644 --- a/src/main/java/de/towerdefence/server/server/JsonWebsocketHandler.java +++ b/src/main/java/de/towerdefence/server/server/JsonWebsocketHandler.java @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap; @AllArgsConstructor public abstract class JsonWebsocketHandler extends TextWebSocketHandler { private static final Logger logger = LoggerFactory.getLogger(JsonWebsocketHandler.class); - protected final Channel channel; + public final Channel channel; protected final SessionsService sessionsService; protected final Map sessionPlayers = new ConcurrentHashMap<>(); diff --git a/src/main/java/de/towerdefence/server/server/channels/WebsocketConfig.java b/src/main/java/de/towerdefence/server/server/channels/WebsocketConfig.java new file mode 100644 index 0000000..d242e1e --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/WebsocketConfig.java @@ -0,0 +1,35 @@ +package de.towerdefence.server.server.channels; + +import de.towerdefence.server.server.JsonWebsocketHandler; +import de.towerdefence.server.server.channels.connection.ConnectionWebsocketHandler; +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; +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; + +@AllArgsConstructor +@Configuration +@EnableWebSocket +public class WebsocketConfig implements WebSocketConfigurer { + private static final String CHANNEL_BASE_PATH = "/ws/"; + @Autowired + private final SessionsService sessionsService; + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registerJsonChannel(registry, new ConnectionWebsocketHandler(this.sessionsService)); + registerJsonChannel(registry, new MatchmakingWebsocketHandler(this.sessionsService)); + registerJsonChannel(registry, new TimeWebsocketHandler(this.sessionsService)); + } + + private void registerJsonChannel(WebSocketHandlerRegistry registry, JsonWebsocketHandler handler){ + registry.addHandler( + handler, + CHANNEL_BASE_PATH + handler.channel.getJsonName() + ).setAllowedOrigins("*"); + } +} diff --git a/src/main/java/de/towerdefence/server/server/channels/connection/ConnectionWebsocketConfig.java b/src/main/java/de/towerdefence/server/server/channels/connection/ConnectionWebsocketConfig.java deleted file mode 100644 index 0bd5a96..0000000 --- a/src/main/java/de/towerdefence/server/server/channels/connection/ConnectionWebsocketConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.towerdefence.server.server.channels.connection; - -import de.towerdefence.server.session.SessionsService; -import lombok.AllArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; -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; - -@AllArgsConstructor -@Configuration -@EnableWebSocket -public class ConnectionWebsocketConfig implements WebSocketConfigurer { - @Autowired - private final SessionsService sessionsService; - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler( - new ConnectionWebsocketHandler(this.sessionsService), - "/ws/connection" - ).setAllowedOrigins("*"); - } -} diff --git a/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketConfig.java b/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketConfig.java deleted file mode 100644 index 53930ed..0000000 --- a/src/main/java/de/towerdefence/server/server/channels/matchmaking/MatchmakingWebsocketConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.towerdefence.server.server.channels.matchmaking; - -import de.towerdefence.server.server.channels.connection.ConnectionWebsocketHandler; -import de.towerdefence.server.session.SessionsService; -import lombok.AllArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; -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; - -@AllArgsConstructor -@Configuration -@EnableWebSocket -public class MatchmakingWebsocketConfig implements WebSocketConfigurer { - @Autowired - private final SessionsService sessionsService; - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler( - new MatchmakingWebsocketHandler(this.sessionsService), - "/ws/matchmaking" - ).setAllowedOrigins("*"); - } -} diff --git a/src/main/java/de/towerdefence/server/server/channels/time/TimeWebsocketConfig.java b/src/main/java/de/towerdefence/server/server/channels/time/TimeWebsocketConfig.java deleted file mode 100644 index 2bb7421..0000000 --- a/src/main/java/de/towerdefence/server/server/channels/time/TimeWebsocketConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.towerdefence.server.server.channels.time; - -import de.towerdefence.server.session.SessionsService; -import lombok.AllArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; -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; - -@AllArgsConstructor -@Configuration -@EnableWebSocket -public class TimeWebsocketConfig implements WebSocketConfigurer { - @Autowired - private final SessionsService sessionsService; - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler( - new TimeWebsocketHandler(this.sessionsService), - "/ws/time" - ).setAllowedOrigins("*"); - } -} From 160dbff816e8b48a7f9c46cc5559bed4ddc822dc Mon Sep 17 00:00:00 2001 From: Snoweuph Date: Wed, 26 Feb 2025 13:26:58 +0100 Subject: [PATCH 4/4] TD-18: Implement Queue System --- .../de/towerdefence/server/match/Match.java | 13 ++ .../match/confirmation/AbortCallback.java | 8 + .../confirmation/ConfirmationCallbacks.java | 16 ++ .../confirmation/EstablishedCallback.java | 10 ++ .../MatchConfirmationService.java | 111 ++++++++++++++ .../confirmation/PlayerMatchConfirmState.java | 7 + .../match/confirmation/RequeueCallback.java | 18 +++ .../match/confirmation/UnconfirmedMatch.java | 85 +++++++++++ .../confirmation/UnconfirmedMatchState.java | 7 + .../server/match/queue/FoundCallback.java | 10 ++ .../server/match/queue/MatchQueueService.java | 143 ++++++++++++++++++ .../server/match/queue/QueuedCallback.java | 10 ++ .../server/channels/WebsocketConfig.java | 13 +- .../MatchmakingWebsocketHandler.java | 103 ++++++++++++- .../bi/MatchSetSearchStateMessage.java | 35 +++++ .../in/MatchSetSearchStateMessage.java | 14 -- ws/ws.yml | 9 +- 17 files changed, 595 insertions(+), 17 deletions(-) create mode 100644 src/main/java/de/towerdefence/server/match/Match.java create mode 100644 src/main/java/de/towerdefence/server/match/confirmation/AbortCallback.java create mode 100644 src/main/java/de/towerdefence/server/match/confirmation/ConfirmationCallbacks.java create mode 100644 src/main/java/de/towerdefence/server/match/confirmation/EstablishedCallback.java create mode 100644 src/main/java/de/towerdefence/server/match/confirmation/MatchConfirmationService.java create mode 100644 src/main/java/de/towerdefence/server/match/confirmation/PlayerMatchConfirmState.java create mode 100644 src/main/java/de/towerdefence/server/match/confirmation/RequeueCallback.java create mode 100644 src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatch.java create mode 100644 src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatchState.java create mode 100644 src/main/java/de/towerdefence/server/match/queue/FoundCallback.java create mode 100644 src/main/java/de/towerdefence/server/match/queue/MatchQueueService.java create mode 100644 src/main/java/de/towerdefence/server/match/queue/QueuedCallback.java create mode 100644 src/main/java/de/towerdefence/server/server/channels/matchmaking/bi/MatchSetSearchStateMessage.java delete mode 100644 src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchSetSearchStateMessage.java diff --git a/src/main/java/de/towerdefence/server/match/Match.java b/src/main/java/de/towerdefence/server/match/Match.java new file mode 100644 index 0000000..b36f0ef --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/Match.java @@ -0,0 +1,13 @@ +package de.towerdefence.server.match; + +import de.towerdefence.server.player.Player; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class Match { + private final String matchId; + private final Player playerOne; + private final Player playerTwo; +} diff --git a/src/main/java/de/towerdefence/server/match/confirmation/AbortCallback.java b/src/main/java/de/towerdefence/server/match/confirmation/AbortCallback.java new file mode 100644 index 0000000..ce55341 --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/confirmation/AbortCallback.java @@ -0,0 +1,8 @@ +package de.towerdefence.server.match.confirmation; + +import de.towerdefence.server.player.Player; + +@FunctionalInterface +public interface AbortCallback { + void call(Player player, String matchId); +} diff --git a/src/main/java/de/towerdefence/server/match/confirmation/ConfirmationCallbacks.java b/src/main/java/de/towerdefence/server/match/confirmation/ConfirmationCallbacks.java new file mode 100644 index 0000000..23fabe7 --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/confirmation/ConfirmationCallbacks.java @@ -0,0 +1,16 @@ +package de.towerdefence.server.match.confirmation; + +import de.towerdefence.server.match.queue.FoundCallback; +import de.towerdefence.server.match.queue.QueuedCallback; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ConfirmationCallbacks { + private final FoundCallback foundCallback; + private final QueuedCallback queuedCallback; + private final AbortCallback abortCallback; + private final EstablishedCallback establishedCallback; + private final RequeueCallback requeueCallback; +} diff --git a/src/main/java/de/towerdefence/server/match/confirmation/EstablishedCallback.java b/src/main/java/de/towerdefence/server/match/confirmation/EstablishedCallback.java new file mode 100644 index 0000000..2b9173d --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/confirmation/EstablishedCallback.java @@ -0,0 +1,10 @@ +package de.towerdefence.server.match.confirmation; + +import de.towerdefence.server.player.Player; + +import java.io.IOException; + +@FunctionalInterface +public interface EstablishedCallback { + void call(Player player, String matchId, Player opponent) throws IOException; +} diff --git a/src/main/java/de/towerdefence/server/match/confirmation/MatchConfirmationService.java b/src/main/java/de/towerdefence/server/match/confirmation/MatchConfirmationService.java new file mode 100644 index 0000000..d48c93a --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/confirmation/MatchConfirmationService.java @@ -0,0 +1,111 @@ +package de.towerdefence.server.match.confirmation; + +import de.towerdefence.server.player.Player; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Service +public class MatchConfirmationService { + + private final Map unconfirmedMatch = new HashMap<>(); + + public UnconfirmedMatch createMatch(Player player1, + Player player2, + ConfirmationCallbacks player1Callbacks, + ConfirmationCallbacks player2callbacks) { + UnconfirmedMatch match = new UnconfirmedMatch( + player1, + player2, + player1Callbacks, + player2callbacks); + unconfirmedMatch.put(player1, match); + unconfirmedMatch.put(player2, match); + return match; + } + + public void accept(Player player, String matchId) { + setPlayerAcceptState(player, matchId, PlayerMatchConfirmState.CONFIRMED); + } + + public void decline(Player player, String matchId) { + setPlayerAcceptState(player, matchId, PlayerMatchConfirmState.ABORTED); + } + + private void setPlayerAcceptState(Player player, String matchId, PlayerMatchConfirmState state) { + Optional optionalMatch = getPlayerMatch(player, matchId); + if (optionalMatch.isEmpty()) { + return; + } + UnconfirmedMatch match = optionalMatch.get(); + + Optional optionalPlayerState = match.getPlayerState(player); + if (optionalPlayerState.isEmpty()) { + unconfirmedMatch.remove(player); + return; + } + + if (optionalPlayerState.get() != PlayerMatchConfirmState.UNKNOWN) { + return; + } + + Optional matchState = match.setPlayerConfirmState(player, state); + if (matchState.isEmpty()) { + unconfirmedMatch.remove(player); + return; + } + handleMatchConfirmation(match, matchState.get()); + } + + private void handleMatchConfirmation(UnconfirmedMatch match, UnconfirmedMatchState state) { + if (state == UnconfirmedMatchState.WAITING) { + return; + } + + unconfirmedMatch.remove(match.getPlayer1()); + unconfirmedMatch.remove(match.getPlayer2()); + + ConfirmationCallbacks player1Callbacks = match.getPlayer1Callbacks(); + 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 CONFIRMED -> { + // TODO: Create Match and Send Players the info that the Match is created + } + } + } + + private Optional getPlayerMatch(Player player, String matchId) { + UnconfirmedMatch match = unconfirmedMatch.get(player); + if (match == null) { + return Optional.empty(); + } + if (!match.getMatchId().equals(matchId)) { + unconfirmedMatch.remove(player); + return Optional.empty(); + } + return Optional.of(match); + } +} diff --git a/src/main/java/de/towerdefence/server/match/confirmation/PlayerMatchConfirmState.java b/src/main/java/de/towerdefence/server/match/confirmation/PlayerMatchConfirmState.java new file mode 100644 index 0000000..f8ac863 --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/confirmation/PlayerMatchConfirmState.java @@ -0,0 +1,7 @@ +package de.towerdefence.server.match.confirmation; + +public enum PlayerMatchConfirmState { + UNKNOWN, + CONFIRMED, + ABORTED +} diff --git a/src/main/java/de/towerdefence/server/match/confirmation/RequeueCallback.java b/src/main/java/de/towerdefence/server/match/confirmation/RequeueCallback.java new file mode 100644 index 0000000..25936ad --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/confirmation/RequeueCallback.java @@ -0,0 +1,18 @@ +package de.towerdefence.server.match.confirmation; + +import de.towerdefence.server.match.queue.FoundCallback; +import de.towerdefence.server.match.queue.QueuedCallback; +import de.towerdefence.server.player.Player; + +@FunctionalInterface +public interface RequeueCallback { + void call( + Player player, + String matchId, + FoundCallback foundCallback, + QueuedCallback queuedCallback, + AbortCallback abortCallback, + EstablishedCallback establishedCallback + ); +} + diff --git a/src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatch.java b/src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatch.java new file mode 100644 index 0000000..eceef0c --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatch.java @@ -0,0 +1,85 @@ +package de.towerdefence.server.match.confirmation; + +import de.towerdefence.server.player.Player; +import lombok.Getter; + +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +@Getter +public class UnconfirmedMatch { + private final String matchId; + private final long created; + public static final long TTL = 30 * 1000; + private final Player player1; + private final Player player2; + private final ConfirmationCallbacks player1Callbacks; + private final ConfirmationCallbacks player2Callbacks; + + public UnconfirmedMatch( + Player player1, + Player player2, + ConfirmationCallbacks player1Callbacks, + ConfirmationCallbacks player2Callbacks) { + this.player1 = player1; + this.player2 = player2; + this.player1Callbacks = player1Callbacks; + this.player2Callbacks = player2Callbacks; + this.created = Instant.now().toEpochMilli(); + this.matchId = UUID.randomUUID().toString(); + } + + private PlayerMatchConfirmState player1State = PlayerMatchConfirmState.UNKNOWN; + private PlayerMatchConfirmState player2State = PlayerMatchConfirmState.UNKNOWN; + + public Optional setPlayerConfirmState(Player player, PlayerMatchConfirmState state) { + Optional matchPlayer = getMatchPlayer(player); + if(matchPlayer.isEmpty()){ + return Optional.empty(); + } + + switch (matchPlayer.get()){ + case ONE -> player1State = state; + case TWO -> player2State = state; + } + + if (player1State == PlayerMatchConfirmState.ABORTED || player2State == PlayerMatchConfirmState.ABORTED) { + return Optional.of(UnconfirmedMatchState.ABORTED); + } + if (player1State == PlayerMatchConfirmState.UNKNOWN || player2State == PlayerMatchConfirmState.UNKNOWN) { + return Optional.of(UnconfirmedMatchState.WAITING); + } + return Optional.of(UnconfirmedMatchState.CONFIRMED); + } + + public Optional getPlayerState(Player player){ + Optional matchPlayer = getMatchPlayer(player); + if(matchPlayer.isEmpty()){ + return Optional.empty(); + } + + return switch (matchPlayer.get()){ + case ONE -> Optional.of(player1State); + case TWO -> Optional.of(player2State); + }; + } + + private enum MatchPlayer{ + ONE, + TWO + } + + private Optional getMatchPlayer(Player player){ + boolean isPlayerOne = player.equals(player1); + boolean isPlayerTwo = player.equals(player2); + if (!isPlayerOne && !isPlayerTwo) { + return Optional.empty(); + } + + if (isPlayerOne){ + return Optional.of(MatchPlayer.ONE); + } + return Optional.of(MatchPlayer.TWO); + } +} diff --git a/src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatchState.java b/src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatchState.java new file mode 100644 index 0000000..0dc880b --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/confirmation/UnconfirmedMatchState.java @@ -0,0 +1,7 @@ +package de.towerdefence.server.match.confirmation; + +public enum UnconfirmedMatchState { + WAITING, + ABORTED, + CONFIRMED +} diff --git a/src/main/java/de/towerdefence/server/match/queue/FoundCallback.java b/src/main/java/de/towerdefence/server/match/queue/FoundCallback.java new file mode 100644 index 0000000..52d5362 --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/queue/FoundCallback.java @@ -0,0 +1,10 @@ +package de.towerdefence.server.match.queue; + +import de.towerdefence.server.player.Player; + +import java.io.IOException; + +@FunctionalInterface +public interface FoundCallback { + void call(Player player, String matchId, long created, long ttl) throws IOException; +} diff --git a/src/main/java/de/towerdefence/server/match/queue/MatchQueueService.java b/src/main/java/de/towerdefence/server/match/queue/MatchQueueService.java new file mode 100644 index 0000000..4c822fd --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/queue/MatchQueueService.java @@ -0,0 +1,143 @@ +package de.towerdefence.server.match.queue; + +import de.towerdefence.server.match.confirmation.*; +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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@AllArgsConstructor +public class MatchQueueService { + private final static int REQUIRED_PLAYER_COUNT = 2; + + @Autowired + private final MatchConfirmationService matchConfirmationService; + + private final List queue = new ArrayList<>(); + private final Map confirmationCallbacks = new HashMap<>(); + + public void queuePlayer( + Player player, + FoundCallback foundCallback, + QueuedCallback queuedCallback, + AbortCallback abortCallback, + EstablishedCallback establishedCallback) { + queue.add(player); + confirmationCallbacks.put(player, new ConfirmationCallbacks( + foundCallback, + queuedCallback, + abortCallback, + establishedCallback, + this::onRequeue)); + tryMatching(); + } + + public void onRequeue( + Player player, + String matchId, + FoundCallback foundCallback, + QueuedCallback queuedCallback, + AbortCallback abortCallback, + EstablishedCallback establishedCallback) { + abortCallback.call(player, matchId); + try { + queuedCallback.call(player); + } catch (IOException ignored) { + return; + } + queuePlayer(player, foundCallback, queuedCallback, abortCallback, establishedCallback); + } + + public void unQueuePlayer(Player player) { + queue.remove(player); + confirmationCallbacks.remove(player); + } + + private void tryMatching() { + if (queue.size() < REQUIRED_PLAYER_COUNT) { + return; + } + + List loopQueue = new ArrayList<>(queue); + for (int i = 0; i < loopQueue.size() / REQUIRED_PLAYER_COUNT; i++) { + Player player1 = loopQueue.get(REQUIRED_PLAYER_COUNT * i); + Player player2 = loopQueue.get(REQUIRED_PLAYER_COUNT * i + 1); + + ConfirmationCallbacks player1Callbacks = confirmationCallbacks.get(player1); + ConfirmationCallbacks player2Callbacks = confirmationCallbacks.get(player2); + + UnconfirmedMatch match = this.matchConfirmationService.createMatch( + player1, + player2, + player1Callbacks, + player2Callbacks); + sentMatchFound( + match, + player1, + player2, + player1Callbacks, + player2Callbacks); + + } + if (queue.size() > REQUIRED_PLAYER_COUNT) { + tryMatching(); + } + } + + private void sentMatchFound( + UnconfirmedMatch match, + Player player1, + Player player2, + ConfirmationCallbacks player1Callbacks, + ConfirmationCallbacks player2Callbacks) { + boolean player1disconnected = setMatchFoundToPlayer(player1, player1Callbacks.getFoundCallback(), match); + boolean player2disconnected = setMatchFoundToPlayer(player2, player2Callbacks.getFoundCallback(), match); + + queue.remove(player1); + queue.remove(player2); + + if (!player1disconnected && !player2disconnected) { + return; + } + + if (player1disconnected && match.getPlayer2State() != PlayerMatchConfirmState.ABORTED) { + player2Callbacks.getRequeueCallback().call( + player2, + match.getMatchId(), + player2Callbacks.getFoundCallback(), + player2Callbacks.getQueuedCallback(), + player2Callbacks.getAbortCallback(), + player2Callbacks.getEstablishedCallback()); + } + + if (player2disconnected && match.getPlayer1State() != PlayerMatchConfirmState.ABORTED) { + player1Callbacks.getRequeueCallback().call( + player1, + match.getMatchId(), + player1Callbacks.getFoundCallback(), + player1Callbacks.getQueuedCallback(), + player1Callbacks.getAbortCallback(), + player1Callbacks.getEstablishedCallback()); + } + } + + private boolean setMatchFoundToPlayer(Player player, FoundCallback callback, UnconfirmedMatch match) { + try { + callback.call( + player, + match.getMatchId(), + match.getCreated(), + UnconfirmedMatch.TTL); + } catch (IOException e) { + return true; + } + return false; + } +} diff --git a/src/main/java/de/towerdefence/server/match/queue/QueuedCallback.java b/src/main/java/de/towerdefence/server/match/queue/QueuedCallback.java new file mode 100644 index 0000000..48558e1 --- /dev/null +++ b/src/main/java/de/towerdefence/server/match/queue/QueuedCallback.java @@ -0,0 +1,10 @@ +package de.towerdefence.server.match.queue; + +import de.towerdefence.server.player.Player; + +import java.io.IOException; + +@FunctionalInterface +public interface QueuedCallback { + void call(Player player) throws IOException; +} 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 d242e1e..6a41a8d 100644 --- a/src/main/java/de/towerdefence/server/server/channels/WebsocketConfig.java +++ b/src/main/java/de/towerdefence/server/server/channels/WebsocketConfig.java @@ -1,5 +1,7 @@ package de.towerdefence.server.server.channels; +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.matchmaking.MatchmakingWebsocketHandler; @@ -19,10 +21,19 @@ public class WebsocketConfig implements WebSocketConfigurer { private static final String CHANNEL_BASE_PATH = "/ws/"; @Autowired private final SessionsService sessionsService; + @Autowired + private final MatchQueueService matchQueueService; + @Autowired + private final MatchConfirmationService matchConfirmationService; + @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registerJsonChannel(registry, new ConnectionWebsocketHandler(this.sessionsService)); - registerJsonChannel(registry, new MatchmakingWebsocketHandler(this.sessionsService)); + registerJsonChannel(registry, new MatchmakingWebsocketHandler( + this.sessionsService, + this.matchQueueService, + this.matchConfirmationService + )); registerJsonChannel(registry, new TimeWebsocketHandler(this.sessionsService)); } 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 46b0ac4..a54713c 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 @@ -1,11 +1,112 @@ package de.towerdefence.server.server.channels.matchmaking; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.towerdefence.server.match.confirmation.MatchConfirmationService; +import de.towerdefence.server.match.queue.MatchQueueService; +import de.towerdefence.server.player.Player; import de.towerdefence.server.server.JsonWebsocketHandler; +import de.towerdefence.server.server.channels.matchmaking.bi.MatchSetSearchStateMessage; +import de.towerdefence.server.server.channels.matchmaking.in.MatchAcceptedMessage; +import de.towerdefence.server.server.channels.matchmaking.out.MatchAbortedMessage; +import de.towerdefence.server.server.channels.matchmaking.out.MatchEstablishedMessage; +import de.towerdefence.server.server.channels.matchmaking.out.MatchFoundMessage; import de.towerdefence.server.session.Channel; import de.towerdefence.server.session.SessionsService; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public class MatchmakingWebsocketHandler extends JsonWebsocketHandler { - public MatchmakingWebsocketHandler(SessionsService sessionsService) { + protected final Map playerSessions = new ConcurrentHashMap<>(); + private final ObjectMapper objectMapper = new ObjectMapper(); + private final MatchQueueService matchQueueService; + private final MatchConfirmationService matchConfirmationService; + + + public MatchmakingWebsocketHandler( + SessionsService sessionsService, + MatchQueueService matchQueueService, + MatchConfirmationService matchConfirmationService + ) { super(Channel.MATCHMAKING, sessionsService); + this.matchQueueService = matchQueueService; + this.matchConfirmationService = matchConfirmationService; + } + + @Override + public void afterConnectionEstablished(WebSocketSession session) { + super.afterConnectionEstablished(session); + playerSessions.put(sessionPlayers.get(session), session); + } + + @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); + case MatchAcceptedMessage.MESSAGE_ID -> handleMatchAcceptedMessage(session, payload); + default -> this.closeSession(session, CloseStatus.BAD_DATA); + } + } catch (Exception ignored) { + this.closeSession(session, CloseStatus.BAD_DATA); + } + } + + private void handleMatchSetSearchStateMessage(WebSocketSession session, String payload) throws Exception { + MatchSetSearchStateMessage msg = objectMapper.readValue(payload, MatchSetSearchStateMessage.class); + Player player = sessionPlayers.get(session); + if (!msg.isSearching()) { + this.matchQueueService.unQueuePlayer(player); + return; + } + this.matchQueueService.queuePlayer( + player, + this::onFound, + this::onQueued, + this::onAbort, + this::onEstablished + ); + } + + private void onFound(Player player, String matchId, long created, long ttl) throws IOException { + WebSocketSession session = playerSessions.get(player); + MatchFoundMessage msg = new MatchFoundMessage(matchId, created, ttl); + msg.send(session); + } + + private void onQueued(Player player) throws IOException{ + WebSocketSession session = playerSessions.get(player); + MatchSetSearchStateMessage msg = new MatchSetSearchStateMessage(true); + msg.send(session); + } + + private void onAbort(Player player, String matchId) { + WebSocketSession session = playerSessions.get(player); + MatchAbortedMessage msg = new MatchAbortedMessage(matchId); + try { + msg.send(session); + } catch (IOException ignored) { + } + } + + private void onEstablished(Player player, String matchId, Player opponent) throws IOException { + WebSocketSession session = playerSessions.get(player); + MatchEstablishedMessage msg = new MatchEstablishedMessage(matchId, opponent.getUsername()); + msg.send(session); + } + + private void handleMatchAcceptedMessage(WebSocketSession session, String payload) throws Exception { + MatchAcceptedMessage msg = objectMapper.readValue(payload, MatchAcceptedMessage.class); + Player player = sessionPlayers.get(session); + if (msg.isAccepted()) { + this.matchConfirmationService.accept(player, msg.getMatchId()); + } else { + this.matchConfirmationService.decline(player, msg.getMatchId()); + } } } 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 new file mode 100644 index 0000000..c47427d --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/channels/matchmaking/bi/MatchSetSearchStateMessage.java @@ -0,0 +1,35 @@ +package de.towerdefence.server.server.channels.matchmaking.bi; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import de.towerdefence.server.server.JsonMessage; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Map; + +@Getter +@NotNull +@AllArgsConstructor +public class MatchSetSearchStateMessage extends JsonMessage { + public static final String MESSAGE_ID = "MatchSetSearchState"; + + @Getter + @JsonProperty("$id") + private String messageId; + + private boolean searching; + + public MatchSetSearchStateMessage(boolean searching) { + this(MESSAGE_ID, searching); + } + + @Override + protected Map getData(JsonNodeFactory factory) { + return Map.of( + "searching", factory.booleanNode(this.searching) + ); + } +} diff --git a/src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchSetSearchStateMessage.java b/src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchSetSearchStateMessage.java deleted file mode 100644 index 9e53b09..0000000 --- a/src/main/java/de/towerdefence/server/server/channels/matchmaking/in/MatchSetSearchStateMessage.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.towerdefence.server.server.channels.matchmaking.in; - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Data -@NotNull -public class MatchSetSearchStateMessage { - public static final String MESSAGE_ID = "MatchSetSearchState"; - @JsonProperty("$id") - private String messageId; - private boolean searching; -} \ No newline at end of file diff --git a/ws/ws.yml b/ws/ws.yml index 5503e26..f73ddb6 100644 --- a/ws/ws.yml +++ b/ws/ws.yml @@ -5,7 +5,7 @@ info: description: | This is the Websocket Specification for the Tower Defence Game.
Because of the limitations of Async API, we expect that the actual json, - which is send as payload to always contain a field called `$id` with + which is send as payload to always contain a field called `$id` with the corresponding `messageId`.
The `messageId` should be handled case sensitive. defaultContentType: application/json @@ -194,6 +194,13 @@ operations: $ref: "#/channels/matchmaking" messages: - $ref: "#/channels/matchmaking/messages/MatchSetSearchState" + setPlayerMatchSearching: + title: SetPlayerMatchSearching + action: receive + channel: + $ref: "#/channels/matchmaking" + messages: + - $ref: "#/channels/matchmaking/messages/MatchSetSearchState" foundMatch: title: FoundGame action: receive