[Feature]: Matchmaking Part1 #7

Merged
snoweuph merged 4 commits from story/td-18-matchmaking into trunk 2025-02-28 09:48:47 +00:00
24 changed files with 868 additions and 56 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Player, UnconfirmedMatch> 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<UnconfirmedMatch> optionalMatch = getPlayerMatch(player, matchId);
if (optionalMatch.isEmpty()) {
return;
}
UnconfirmedMatch match = optionalMatch.get();
Optional<PlayerMatchConfirmState> optionalPlayerState = match.getPlayerState(player);
if (optionalPlayerState.isEmpty()) {
unconfirmedMatch.remove(player);
return;
}
if (optionalPlayerState.get() != PlayerMatchConfirmState.UNKNOWN) {
return;
}
Optional<UnconfirmedMatchState> 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<UnconfirmedMatch> 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);
}
}

View file

@ -0,0 +1,7 @@
package de.towerdefence.server.match.confirmation;
public enum PlayerMatchConfirmState {
UNKNOWN,
CONFIRMED,
ABORTED
}

View file

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

View file

@ -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<UnconfirmedMatchState> setPlayerConfirmState(Player player, PlayerMatchConfirmState state) {
Optional<MatchPlayer> 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<PlayerMatchConfirmState> getPlayerState(Player player){
Optional<MatchPlayer> 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<MatchPlayer> 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);
}
}

View file

@ -0,0 +1,7 @@
package de.towerdefence.server.match.confirmation;
public enum UnconfirmedMatchState {
WAITING,
ABORTED,
CONFIRMED
}

View file

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

View file

@ -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<Player> queue = new ArrayList<>();
private final Map<Player, ConfirmationCallbacks> 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<Player> 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;
}
}

View file

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

View file

@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap;
@AllArgsConstructor @AllArgsConstructor
public abstract class JsonWebsocketHandler extends TextWebSocketHandler { public abstract class JsonWebsocketHandler extends TextWebSocketHandler {
private static final Logger logger = LoggerFactory.getLogger(JsonWebsocketHandler.class); private static final Logger logger = LoggerFactory.getLogger(JsonWebsocketHandler.class);
protected final Channel channel; public final Channel channel;
protected final SessionsService sessionsService; protected final SessionsService sessionsService;
protected final Map<WebSocketSession, Player> sessionPlayers = new ConcurrentHashMap<>(); protected final Map<WebSocketSession, Player> sessionPlayers = new ConcurrentHashMap<>();

View file

@ -0,0 +1,46 @@
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;
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;
@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,
this.matchQueueService,
this.matchConfirmationService
));
registerJsonChannel(registry, new TimeWebsocketHandler(this.sessionsService));
}
private void registerJsonChannel(WebSocketHandlerRegistry registry, JsonWebsocketHandler handler){
registry.addHandler(
handler,
CHANNEL_BASE_PATH + handler.channel.getJsonName()
).setAllowedOrigins("*");
}
}

View file

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

View file

@ -0,0 +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 {
protected final Map<Player, WebSocketSession> 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());
}
}
}

View file

@ -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<String, JsonNode> getData(JsonNodeFactory factory) {
return Map.of(
"searching", factory.booleanNode(this.searching)
);
}
}

View file

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

View file

@ -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<String, JsonNode> getData(JsonNodeFactory factory) {
return Map.of(
"matchId", factory.textNode(this.matchId)
);
}
}

View file

@ -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<String, JsonNode> getData(JsonNodeFactory factory) {
return Map.of(
"matchId", factory.textNode(this.matchId),
"opponentName", factory.textNode(this.opponentName)
);
}
}

View file

@ -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<String, JsonNode> getData(JsonNodeFactory factory) {
return Map.of(
"matchId", factory.textNode(this.matchId),
"created", factory.numberNode(this.created),
"ttl", factory.numberNode(this.ttl)
);
}
}

View file

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

View file

@ -8,6 +8,7 @@ import lombok.Getter;
@AllArgsConstructor @AllArgsConstructor
public enum Channel { public enum Channel {
CONNECTION("connection"), CONNECTION("connection"),
MATCHMAKING("matchmaking"),
TIME("time"); TIME("time");
private final String jsonName; private final String jsonName;

146
ws/ws.yml
View file

@ -5,7 +5,7 @@ info:
description: | description: |
This is the Websocket Specification for the Tower Defence Game. <br> This is the Websocket Specification for the Tower Defence Game. <br>
Because of the limitations of Async API, we expect that the actual json, 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`. <br> the corresponding `messageId`. <br>
The `messageId` should be handled case sensitive. The `messageId` should be handled case sensitive.
defaultContentType: application/json defaultContentType: application/json
@ -39,9 +39,7 @@ channels:
type: string type: string
format: messageId format: messageId
channel: channel:
type: string $ref: "#/components/schemas/Channel"
enum:
- time
required: required:
- $id - $id
- channel - channel
@ -58,15 +56,102 @@ channels:
type: string type: string
format: messageId format: messageId
channel: channel:
type: string $ref: "#/components/schemas/Channel"
enum:
- time
token: token:
$ref: "#/components/schemas/JWT" $ref: "#/components/schemas/JWT"
required: required:
- $id - $id
- channel - channel
- token - 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: time:
title: Time title: Time
description: | description: |
@ -102,6 +187,46 @@ operations:
$ref: "#/channels/connection" $ref: "#/channels/connection"
messages: messages:
- $ref: "#/channels/connection/messages/ProvidedConnectionToken" - $ref: "#/channels/connection/messages/ProvidedConnectionToken"
searchMatch:
title: SearchMatch
action: send
channel:
$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
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: updateTime:
title: Updates of the current Unix Time title: Updates of the current Unix Time
action: receive action: receive
@ -124,3 +249,10 @@ components:
JWT: JWT:
type: string type: string
format: jwt format: jwt
Channel:
type: string
enum:
- connection
- matchmaking
- time