Workaround to get JavaFX multiplayer working

This commit is contained in:
Charles Gould 2017-10-22 13:37:09 -04:00
parent 075b19cede
commit 394f75cd6e
8 changed files with 169 additions and 42 deletions

View File

@ -1,7 +1,9 @@
# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
# Development # Development
web.socket.url: ws://localhost:8080/stomp web.base.url: http://localhost:8080
web.socket.url: ws://localhost:8080/sockjs
# Production # Production
#web.socket.url: ws://lingo.charego.com/stomp #web.base.url: http://lingo.charego.com
#web.socket.url: ws://lingo.charego.com/sockjs

View File

@ -3,14 +3,17 @@ package lingo.client.multiplayer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.springframework.boot.web.client.RootUriTemplateHandler;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.messaging.converter.ByteArrayMessageConverter; import org.springframework.messaging.converter.ByteArrayMessageConverter;
import org.springframework.messaging.converter.CompositeMessageConverter; import org.springframework.messaging.converter.CompositeMessageConverter;
import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.converter.StringMessageConverter;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.socket.client.WebSocketClient; import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.standard.StandardWebSocketClient; import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.messaging.WebSocketStompClient; import org.springframework.web.socket.messaging.WebSocketStompClient;
@ -47,4 +50,11 @@ public class MultiplayerConfig {
return new CompositeMessageConverter(converters); return new CompositeMessageConverter(converters);
} }
@Bean
public RestTemplate restTemplate(Environment env) {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new RootUriTemplateHandler(env.getProperty("web.base.url")));
return restTemplate;
}
} }

View File

@ -2,6 +2,8 @@ package lingo.client.multiplayer;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -10,9 +12,12 @@ import javax.annotation.PostConstruct;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.messaging.simp.stomp.StompFrameHandler; import org.springframework.messaging.simp.stomp.StompFrameHandler;
import org.springframework.messaging.simp.stomp.StompHeaders; import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
@ -35,6 +40,7 @@ import lingo.client.view.Board;
import lingo.client.view.OpponentBoard; import lingo.client.view.OpponentBoard;
import lingo.client.view.PlayerBoard; import lingo.client.view.PlayerBoard;
import lingo.common.Game; import lingo.common.Game;
import lingo.common.GameLeftMessage;
import lingo.common.Report; import lingo.common.Report;
@Component @Component
@ -59,6 +65,9 @@ public class MultiplayerPresenter implements FxmlController {
@Autowired @Autowired
private ExecutorService executorService; private ExecutorService executorService;
@Autowired
private RestTemplate restTemplate;
@Autowired @Autowired
private StompTemplate stompTemplate; private StompTemplate stompTemplate;
@ -72,7 +81,11 @@ public class MultiplayerPresenter implements FxmlController {
private OpponentBoard opponentBoard; private OpponentBoard opponentBoard;
private final CountDownLatch subscriptionsLatch = new CountDownLatch(4); private final CountDownLatch subscriptionsLatch = new CountDownLatch(7);
private String username;
private String opponentUsername;
private void clearBoards(boolean clearScore) { private void clearBoards(boolean clearScore) {
playerBoard.clearBoard(); playerBoard.clearBoard();
@ -123,7 +136,22 @@ public class MultiplayerPresenter implements FxmlController {
ok.printStackTrace(); ok.printStackTrace();
} }
} }
stompTemplate.getSession().send("/app/lingo/join", null);
username = UUID.randomUUID().toString().substring(0, 8);
stompTemplate.getSession().send("/app/setUsername", username);
Collection<Game> games = restTemplate.exchange("/games", HttpMethod.GET, null, new GameList()).getBody();
boolean joinedGame = false;
for (Game game : games) {
if (game.getPlayerTwo() == null) {
stompTemplate.getSession().send("/app/joinGame", game.getId());
joinedGame = true;
break;
}
}
if (!joinedGame) {
stompTemplate.getSession().send("/app/hostGame", null);
}
}); });
} }
@ -137,7 +165,7 @@ public class MultiplayerPresenter implements FxmlController {
} else if (keyCode == KeyCode.ENTER) { } else if (keyCode == KeyCode.ENTER) {
final String guess = playerBoard.handleEnter(); final String guess = playerBoard.handleEnter();
if (guess != null) { if (guess != null) {
executorService.execute(() -> stompTemplate.getSession().send("/app/lingo/guess", guess)); executorService.execute(() -> stompTemplate.getSession().send("/app/guess", guess));
repaint(); repaint();
} }
} else if (keyCode.isLetterKey()) { } else if (keyCode.isLetterKey()) {
@ -154,10 +182,16 @@ public class MultiplayerPresenter implements FxmlController {
@PostConstruct @PostConstruct
private void postConstruct() { private void postConstruct() {
executorService.execute(() -> { executorService.execute(() -> {
stompTemplate.subscribe(Destinations.GAME_CLOSED, new GameClosedHandler(),
subscription -> subscriptionsLatch.countDown());
stompTemplate.subscribe(Destinations.GAME_HOSTED, new GameHostedHandler(),
subscription -> subscriptionsLatch.countDown());
stompTemplate.subscribe(Destinations.GAME_JOINED, new GameJoinedHandler(),
subscription -> subscriptionsLatch.countDown());
stompTemplate.subscribe(Destinations.GAME_LEFT, new GameLeftHandler(),
subscription -> subscriptionsLatch.countDown());
stompTemplate.subscribe("/user" + Destinations.OPPONENT_JOINED, new OpponentJoinedHandler(), stompTemplate.subscribe("/user" + Destinations.OPPONENT_JOINED, new OpponentJoinedHandler(),
subscription -> subscriptionsLatch.countDown()); subscription -> subscriptionsLatch.countDown());
//stompTemplate.subscribe("/user" + Destinations.OPPONENT_LEFT, new OpponentLeftHandler(),
// subscription -> subscriptionsLatch.countDown());
stompTemplate.subscribe("/user" + Destinations.OPPONENT_REPORTS, new OpponentReportHandler(), stompTemplate.subscribe("/user" + Destinations.OPPONENT_REPORTS, new OpponentReportHandler(),
subscription -> subscriptionsLatch.countDown()); subscription -> subscriptionsLatch.countDown());
stompTemplate.subscribe("/user" + Destinations.PLAYER_REPORTS, new PlayerReportHandler(), stompTemplate.subscribe("/user" + Destinations.PLAYER_REPORTS, new PlayerReportHandler(),
@ -185,16 +219,100 @@ public class MultiplayerPresenter implements FxmlController {
} }
} }
private class GameClosedHandler implements StompFrameHandler {
@Override
public Type getPayloadType(StompHeaders headers) {
return Game.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
handleMessage((Game) payload);
}
private void handleMessage(Game game) {
log.debug("{} closed Game {}", game.getPlayerOne(), game.getId());
}
}
private class GameHostedHandler implements StompFrameHandler {
@Override
public Type getPayloadType(StompHeaders headers) {
return Game.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
handleMessage((Game) payload);
}
private void handleMessage(Game game) {
log.debug("{} hosted Game {}", game.getPlayerOne(), game.getId());
}
}
private class GameJoinedHandler implements StompFrameHandler {
@Override
public Type getPayloadType(StompHeaders headers) {
return Game.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
handleMessage((Game) payload);
}
private void handleMessage(Game game) {
log.debug("{} joined {}'s game", game.getPlayerTwo(), game.getPlayerOne());
}
}
private class GameLeftHandler implements StompFrameHandler {
@Override
public Type getPayloadType(StompHeaders headers) {
return GameLeftMessage.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
handleMessage((GameLeftMessage) payload);
}
private void handleMessage(GameLeftMessage message) {
final Game game = message.getGame();
final String gameLeaver = message.getGameLeaver().getUsername();
log.debug("{} left {}'s game", gameLeaver, game.getPlayerOne());
if (gameLeaver.equals(username) || gameLeaver.equals(opponentUsername)) {
Platform.runLater(() -> {
clearBoards(true);
showWaitingAnimation(true);
opponentUsername = null;
lastWord = null;
repaint();
});
}
}
}
private class OpponentJoinedHandler implements StompFrameHandler { private class OpponentJoinedHandler implements StompFrameHandler {
@Override @Override
public Type getPayloadType(StompHeaders headers) { public Type getPayloadType(StompHeaders headers) {
return String.class; return String[].class;
} }
@Override @Override
public void handleFrame(StompHeaders headers, Object payload) { public void handleFrame(StompHeaders headers, Object payload) {
final String firstLetter = payload.toString(); handleMessage((String[]) payload);
}
private void handleMessage(String[] message) {
final String firstLetter = message[0];
opponentUsername = message[1];
Platform.runLater(() -> { Platform.runLater(() -> {
clearBoards(true); clearBoards(true);
newWord(firstLetter); newWord(firstLetter);
@ -204,24 +322,6 @@ public class MultiplayerPresenter implements FxmlController {
} }
} }
private class OpponentLeftHandler implements StompFrameHandler {
@Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
Platform.runLater(() -> {
clearBoards(true);
showWaitingAnimation(true);
lastWord = null;
repaint();
});
}
}
private class OpponentReportHandler implements StompFrameHandler { private class OpponentReportHandler implements StompFrameHandler {
@Override @Override
@ -319,4 +419,8 @@ public class MultiplayerPresenter implements FxmlController {
} }
} }
private class GameList extends ParameterizedTypeReference<Collection<Game>> {
// intentionally left empty
}
} }

View File

@ -18,7 +18,7 @@
<VBox spacing="20" alignment="CENTER"> <VBox spacing="20" alignment="CENTER">
<children> <children>
<Button text="Practice" onAction="#showSinglePlayer" prefWidth="350" styleClass="game-mode" /> <Button text="Practice" onAction="#showSinglePlayer" prefWidth="350" styleClass="game-mode" />
<Button text="Multiplayer" onAction="#showMultiplayer" prefWidth="350" styleClass="game-mode" disable="true" /> <Button text="Multiplayer" onAction="#showMultiplayer" prefWidth="350" styleClass="game-mode" />
</children> </children>
</VBox> </VBox>
</center> </center>

View File

@ -18,7 +18,7 @@ public class Game {
private static final AtomicInteger idCounter = new AtomicInteger(0); private static final AtomicInteger idCounter = new AtomicInteger(0);
public final int id; private int id;
private Player playerOne; private Player playerOne;
@ -32,6 +32,10 @@ public class Game {
private int wordIndex = 0; private int wordIndex = 0;
public Game() {
// Empty constructor required for serialization
}
public Game(Player host) { public Game(Player host) {
this.id = idCounter.incrementAndGet(); this.id = idCounter.incrementAndGet();
this.playerOne = host; this.playerOne = host;
@ -87,6 +91,10 @@ public class Game {
return result; return result;
} }
public int getId() {
return id;
}
public Player getPlayerOne() { public Player getPlayerOne() {
return playerOne; return playerOne;
} }
@ -110,6 +118,10 @@ public class Game {
this.acceptableGuesses = value; this.acceptableGuesses = value;
} }
public void setId(int value) {
this.id = value;
}
public void setPlayerOne(Player value) { public void setPlayerOne(Player value) {
this.playerOne = value; this.playerOne = value;
} }

View File

@ -5,10 +5,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
public class Player { public class Player {
@JsonIgnore @JsonIgnore
private final String sessionId; private String sessionId;
private String username; private String username;
public Player() {
// Empty constructor required for serialization
}
public Player(String sessionId) { public Player(String sessionId) {
this.sessionId = sessionId; this.sessionId = sessionId;
} }

View File

@ -121,9 +121,9 @@ public class LingoController {
return; return;
} }
final Game game = new Game(player); final Game game = new Game(player);
gameById.put(game.id, game); gameById.put(game.getId(), game);
gameByPlayer.put(player, game); gameByPlayer.put(player, game);
log.info("{} hosted Game {}", player, game.id); log.info("{} hosted Game {}", player, game.getId());
send(Destinations.GAME_HOSTED, game); send(Destinations.GAME_HOSTED, game);
} }
@ -198,8 +198,8 @@ public class LingoController {
if (playerOne == player) { if (playerOne == player) {
if (playerTwo == null) { if (playerTwo == null) {
// Close the game // Close the game
log.info("{} closed Game {}", player, game.id); log.info("{} closed Game {}", player, game.getId());
gameById.remove(game.id); gameById.remove(game.getId());
send(Destinations.GAME_CLOSED, game); send(Destinations.GAME_CLOSED, game);
} else { } else {
// Leave the game // Leave the game

View File

@ -357,7 +357,6 @@ function start() {
client.subscribe('/topic/gameJoined', onGameJoined); client.subscribe('/topic/gameJoined', onGameJoined);
client.subscribe('/topic/gameLeft', onGameLeft); client.subscribe('/topic/gameLeft', onGameLeft);
client.subscribe('/user/topic/opponentJoined', onOpponentJoined); client.subscribe('/user/topic/opponentJoined', onOpponentJoined);
client.subscribe('/user/topic/opponentLeft', onOpponentLeft);
client.subscribe('/user/topic/opponentReports', onOpponentReport); client.subscribe('/user/topic/opponentReports', onOpponentReport);
client.subscribe('/user/topic/playerReports', onPlayerReport); client.subscribe('/user/topic/playerReports', onPlayerReport);
} }
@ -495,7 +494,9 @@ function onGameLeft(message) {
vm.gameId = null; vm.gameId = null;
} }
if (previousPlayers.indexOf(vm.username) != -1) { if (previousPlayers.indexOf(vm.username) != -1) {
onOpponentLeft(); vm.opponentUsername = null;
vm.lastWord = null;
vm.repaint();
} }
} }
@ -508,12 +509,6 @@ function onOpponentJoined(message) {
vm.repaint(); vm.repaint();
} }
function onOpponentLeft(message) {
vm.opponentUsername = null;
vm.lastWord = null;
vm.repaint();
}
function onOpponentReport(message) { function onOpponentReport(message) {
var report = JSON.parse(message.body); var report = JSON.parse(message.body);
if (report.correct === true) { if (report.correct === true) {